You're Invited:Meet the Socket Team at BlackHat and DEF CON in Las Vegas, Aug 4-6.RSVP
Socket
Book a DemoInstallSign in
Socket

posthtml-component

Package Overview
Dependencies
Maintainers
2
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

posthtml-component - npm Package Compare versions

Comparing version

to
2.1.0-beta.1

4

package.json
{
"name": "posthtml-component",
"version": "2.0.0",
"version": "2.1.0-beta.1",
"description": "Laravel Blade-inspired components for PostHTML with slots, attributes as props, custom tags and more.",

@@ -42,3 +42,3 @@ "license": "MIT",

"devDependencies": {
"@biomejs/biome": "1.8.3",
"@biomejs/biome": "1.9.4",
"@vitest/coverage-v8": "^2.0.4",

@@ -45,0 +45,0 @@ "conventional-changelog-cli": "^5.0.0",

@@ -61,3 +61,3 @@ [npm]: https://www.npmjs.com/package/posthtml-component

const posthtml = require('posthtml')
const components = require('posthtml-components')
const components = require('posthtml-component')
const { readFileSync, writeFileSync } = require('node:fs')

@@ -121,33 +121,33 @@

| Name | Type | Default | Description |
|--------------------------|-------------------|----------------------------------------------|-----------------------------------------------------------------------------------------|
| **root** | `String` | `'./'` | Root path for components lookup. |
| **folders** | `String[]` | `['']` | Array of paths relative to `options.root` or defined namespaces. |
| **tagPrefix** | `String` | `'x-'` | Tag prefix. |
| **tag** | `String\|Boolean` | `false` | Component tag. Use with `options.attribute`. Boolean only `false`. |
| **attribute** | `String` | `'src'` | Component attribute for setting path. |
| **namespaces** | `String[]` | `[]` | Array of namespace root paths, fallback paths, and custom override paths. |
| **namespaceSeparator** | `String` | `'::'` | Namespace separator for tag names. |
| **fileExtension** | `String` | `'html'` | File extension for component files. |
| **yield** | `String` | `'yield'` | Tag name for injecting main component content. |
| **slot** | `String` | `'slot'` | Tag name for slots. |
| **fill** | `String` | `'fill'` | Tag name for filling slots. |
| **slotSeparator** | `String` | `':'` | Name separator for `<slot>` and `<fill>` tags. |
| **push** | `String` | `'push'` | Tag name for `<push>`. |
| **stack** | `String` | `'stack'` | Tag name for `<stack>`. |
| **propsScriptAttribute** | `String` | `'props'` | Attribute in `<script props>` for retrieving component props. |
| **propsContext** | `String` | `'props'` | Name of the object inside the script for processing props. |
| **propsAttribute** | `String` | `'props'` | Attribute to define props as JSON. |
| **propsSlot** | `String` | `'props'` | Used to retrieve props passed to slot via `$slots.slotName.props`. |
| **parserOptions** | `Object` | `{recognizeSelfClosing: true}` | Pass options to `posthtml-parser`. |
| **expressions** | `Object` | `{}` | Pass options to `posthtml-expressions`. |
| **plugins** | `Array` | `[]` | PostHTML plugins to apply to every parsed component. |
| **matcher** | `Object` | `[{tag: options.tagPrefix}]` | Array of objects used to match tags. |
| **attrsParserRules** | `Object` | `{}` | Additional rules for attributes parser plugin. |
| **strict** | `Boolean` | `true` | Toggle exception throwing. |
| **mergeCustomizer** | `Function` | `function` | Callback for lodash `mergeWith` to merge `options.expressions.locals` and props. |
| **utilities** | `Object` | `{merge: _.mergeWith, template: _.template}` | Utility methods passed to `<script props>`. |
| **elementAttributes** | `Object` | `{}` | Object with tag names and function modifiers of `valid-attributes.js`. |
| **safelistAttributes** | `String[]` | `['data-*']` | Array of attribute names to add to default valid attributes. |
| **blocklistAttributes** | `String[]` | `[]` | Array of attribute names to remove from default valid attributes. |
| Name | Type | Default | Description |
|--------------------------|--------------------|----------------------------------------------|----------------------------------------------------------------------------------|
| **root** | `String` | `'./'` | Root path where to look for components. |
| **folders** | `String[]` | `['']` | Array of paths relative to `options.root` or defined namespaces. |
| **fileExtension** | `String\|String[]` | `'html'` | Component file extensions to look for. |
| **tagPrefix** | `String` | `'x-'` | Tag prefix. |
| **tag** | `String\|Boolean` | `false` | Component tag. Use with `options.attribute`. Boolean only `false`. |
| **attribute** | `String` | `'src'` | Attribute to use for defining path to component file. |
| **namespaces** | `String[]` | `[]` | Array of namespace root paths, fallback paths, and custom override paths. |
| **namespaceSeparator** | `String` | `'::'` | Namespace separator for tag names. |
| **yield** | `String` | `'yield'` | Tag name for injecting main component content. |
| **slot** | `String` | `'slot'` | Tag name for [slots](#slots) |
| **fill** | `String` | `'fill'` | Tag name for filling slots. |
| **slotSeparator** | `String` | `':'` | Name separator for `<slot>` and `<fill>` tags. |
| **stack** | `String` | `'stack'` | Tag name for [`<stack>`](#stacks). |
| **push** | `String` | `'push'` | Tag name for `<push>`. |
| **propsScriptAttribute** | `String` | `'props'` | Attribute in `<script props>` for retrieving [component props](#props). |
| **propsContext** | `String` | `'props'` | Name of the object inside the script for processing props. |
| **propsAttribute** | `String` | `'props'` | Attribute name to define props as JSON on a component tag. |
| **propsSlot** | `String` | `'props'` | Used to retrieve props passed to slot via `$slots.slotName.props`. |
| **parserOptions** | `Object` | `{recognizeSelfClosing: true}` | Pass options to `posthtml-parser`. |
| **expressions** | `Object` | `{}` | Pass options to `posthtml-expressions`. |
| **plugins** | `Array` | `[]` | PostHTML plugins to apply to every parsed component. |
| **matcher** | `Object` | `[{tag: options.tagPrefix}]` | Array of objects used to match tags. |
| **attrsParserRules** | `Object` | `{}` | Additional rules for attributes parser plugin. |
| **strict** | `Boolean` | `true` | Toggle exception throwing. |
| **mergeCustomizer** | `Function` | `function` | Callback for lodash `mergeWith` to merge `options.expressions.locals` and props. |
| **utilities** | `Object` | `{merge: _.mergeWith, template: _.template}` | Utility methods passed to `<script props>`. |
| **elementAttributes** | `Object` | `{}` | Object with tag names and function modifiers of `valid-attributes.js`. |
| **safelistAttributes** | `String[]` | `['data-*']` | Array of attribute names to add to default valid attributes. |
| **blocklistAttributes** | `String[]` | `[]` | Array of attribute names to remove from default valid attributes. |

@@ -160,5 +160,5 @@ ## Features

If you to use components as 'includes', you may define a tag and src attribute name.
If you want to use components as 'includes', you can define tag and `src` attribute names.
Using our previous button component example, we can define the tag and attribute names and then use it in this way:
Using our previous button component example, we can define the tag and attribute names and then use it like this:

@@ -180,3 +180,3 @@ ```hbs

require('posthtml')(
require('posthtml-components')({
require('posthtml-component')({
root: './src',

@@ -190,3 +190,3 @@ tag: 'component',

If you need more control over tag matching, you can pass an array of matcher or single object via `options.matcher` like this:
If you need more control over tag matching, you may pass an array of matcher or single object via `options.matcher`:

@@ -197,6 +197,10 @@ ```js

root: './src',
matcher: [{tag: 'a-tag'}, {tag: 'another-one'}, {tag: new RegExp(`^app-`, 'i')}]
matcher: [
{tag: 'a-tag'},
{tag: 'another-one'},
{tag: new RegExp(`^app-`, 'i')},
]
};
require('posthtml')(require('posthtml-components')(options))
require('posthtml')(require('posthtml-component')(options))
.process(/* ... */)

@@ -217,3 +221,3 @@ .then(/* ... */)

require('posthtml')(require('posthtml-components')(options))
require('posthtml')(require('posthtml-component')(options))
.process(/* ... */)

@@ -264,3 +268,3 @@ .then(/* ... */)

require('posthtml')(require('posthtml-components')(options))
require('posthtml')(require('posthtml-component')(options))
.process('some HTML', options.parserOptions)

@@ -270,3 +274,4 @@ .then(/* ... */)

Important: as you can see, whatever `parserOptions` you pass to the plugin, must also be passed in the `process` method in your code, otherwise your PostHTML build will use `posthtml-parser` defaults and will override anything you've passed to `posthtml-component`.
> [!IMPORTANT]
> The `parserOptions` that you pass to the plugin must also be passed in the `process` method in your code, otherwise your PostHTML build will use `posthtml-parser` defaults and will override anything you've passed to `posthtml-component`.

@@ -279,3 +284,3 @@ #### Self-closing tags

// index.js
require('posthtml')(require('posthtml-components')({root: './src'}))
require('posthtml')(require('posthtml-component')({root: './src'}))
.process('your HTML...', {recognizeSelfClosing: true})

@@ -300,3 +305,3 @@ .then(/* ... */)

require('posthtml')(require('posthtml-components')(options))
require('posthtml')(require('posthtml-component')(options))
.process(/* ... */)

@@ -840,3 +845,3 @@ .then(/* ... */)

const posthtml = require('posthtml')
const components = require('posthtml-components')
const components = require('posthtml-component')

@@ -843,0 +848,0 @@ const options = {

@@ -16,12 +16,16 @@ 'use strict';

module.exports = (tag, options) => {
const fileNameFromTag = tag
.replace(options.tagPrefix, '')
.split(folderSeparator)
.join(path.sep)
.concat(folderSeparator, options.fileExtension);
const extensions = Array.isArray(options.fileExtension) ? options.fileExtension : [options.fileExtension];
const fileNamesFromTag = extensions.map(ext =>
tag
.replace(options.tagPrefix, '')
.split(folderSeparator)
.join(path.sep)
.concat(folderSeparator, ext)
)
try {
return tag.includes(options.namespaceSeparator) ?
searchInNamespaces(tag, fileNameFromTag.split(options.namespaceSeparator), options) :
searchInFolders(tag, fileNameFromTag, options);
return tag.includes(options.namespaceSeparator)
? searchInNamespaces(tag, fileNamesFromTag, options)
: searchInFolders(tag, fileNamesFromTag, options);
} catch (error) {

@@ -40,11 +44,13 @@ if (options.strict) {

* @param {String} tag [tag name]
* @param {String} fileNameFromTag [filename converted from tag name]
* @param {Array} fileNamesFromTag [filename converted from tag name]
* @param {Object} options [posthtml options]
* @return {String|boolean} [custom tag root where the module is found]
*/
function searchInFolders(tag, fileNameFromTag, options) {
const componentPath = search(options.root, options.folders, fileNameFromTag, options.fileExtension);
function searchInFolders(tag, fileNamesFromTag, options) {
const componentPath = search(options.root, options.folders, fileNamesFromTag, options.fileExtension);
if (!componentPath) {
throw new Error(`[components] <${tag}> could not find ${fileNameFromTag} in the defined root paths (${options.folders.join(', ')})`);
throw new Error(
`[components] <${tag}> could not find ${fileNamesFromTag} in the defined root paths (${options.folders.join(', ')})`
);
}

@@ -59,64 +65,88 @@

* @param {String} tag [tag name with namespace]
* @param {String} namespace [tag's namespace]
* @param {String} fileNameFromTag [filename converted from tag name]
* @param {Array} namespaceAndFileNames Array of [namespace]::[filename]
* @param {Object} options [posthtml options]
* @return {String|boolean} [custom tag root where the module is found]
*/
function searchInNamespaces(tag, [namespace, fileNameFromTag], options) {
const namespaceOption = options.namespaces.find(n => n.name === namespace.replace(options.tagPrefix, ''));
function searchInNamespaces(tag, namespaceAndFileNames, options) {
let result = '';
if (!namespaceOption) {
throw new Error(`[components] Unknown component namespace: ${namespace}.`);
}
for (const namespaceAndFileName of namespaceAndFileNames) {
const [namespace, fileNameFromTag] = namespaceAndFileName.split('::');
let componentPath;
const namespaceOption = options.namespaces.find(n => n.name === namespace.replace(options.tagPrefix, ''));
// 1) Check in custom root
if (namespaceOption.custom) {
componentPath = search(namespaceOption.custom, options.folders, fileNameFromTag, options.fileExtension);
}
if (!namespaceOption) {
throw new Error(`[components] Unknown component namespace: ${namespace}.`);
}
// 2) Check in base root
if (!componentPath) {
componentPath = search(namespaceOption.root, options.folders, fileNameFromTag, options.fileExtension);
}
let componentPath;
// 3) Check in fallback root
if (!componentPath && namespaceOption.fallback) {
componentPath = search(namespaceOption.fallback, options.folders, fileNameFromTag, options.fileExtension);
}
// 1) Check in custom root
if (namespaceOption.custom) {
componentPath = search(namespaceOption.custom, options.folders, fileNameFromTag, options.fileExtension);
}
if (!componentPath && options.strict) {
throw new Error(`[components] <${tag}> could not find ${fileNameFromTag} in the defined namespace base path ${namespaceOption.root}`);
// 2) Check in base root
if (!componentPath) {
componentPath = search(namespaceOption.root, options.folders, fileNameFromTag, options.fileExtension);
}
// 3) Check in fallback root
if (!componentPath && namespaceOption.fallback) {
componentPath = search(namespaceOption.fallback, options.folders, fileNameFromTag, options.fileExtension);
}
if (!componentPath) {
throw new Error(`[components] <${tag}> could not find ${fileNameFromTag} in the defined namespace paths.`);
}
result = componentPath;
}
return componentPath;
return result;
}
/**
* Main search component file function
* Main component file search function
*
* @param {String} root Base root or namespace root from options
* @param {Array} folders Folders from options
* @param {String} fileName Filename converted from tag name
* @param {String} extension File extension from options
* @param {String} root Base root or namespace root from `options`
* @param {Array} folders Folders to search in from `options`
* @param {Array} fileNames Filenames converted from tag name
* @param {Array} extensions File extension(s) from `options`
* @return {String|boolean} [custom tag root where the module is found]
*/
function search(root, folders, fileName, extension) {
function search(root, folders, fileNames, extensions) {
let componentPath;
let componentFound = false;
let componentFound = folders.some(folder => {
componentPath = path.join(path.resolve(root, folder), fileName);
return existsSync(componentPath);
});
fileNames = Array.isArray(fileNames) ? fileNames : [fileNames];
if (!componentFound) {
fileName = fileName.replace(`.${extension}`, `${path.sep}index.${extension}`);
for (const fileName of fileNames) {
componentFound = folders.some(folder => {
componentPath = path.join(path.resolve(root, folder), fileName);
return existsSync(componentPath);
});
if (componentFound) break;
}
if (!componentFound) {
for (const extension of extensions) {
for (const fileName of fileNames) {
const newFileName = fileName.replace(`.${extension}`, `${path.sep}index.${extension}`);
componentFound = folders.some(folder => {
componentPath = path.join(path.resolve(root, folder), newFileName);
return existsSync(componentPath);
});
if (componentFound) break;
}
if (componentFound) break;
}
}
return componentFound ? componentPath : false;
}

@@ -77,6 +77,12 @@ 'use strict';

};
// Additional element attributes, in case already exist in valid-attributes.js it will replace all attributes
// It should be an object with key as tag name and as value a function modifier which receive
// the default attributes and return an array of attributes. Example:
// { TAG: (attributes) => { attributes[] = 'attribute-name'; return attributes; } }
/**
* Additional element attributes. If they already exist in valid-attributes.js,
* it will replace all attributes. It should be an object with tag name as
* the key and a function modifier as the value, which will receive the
* default attributes and return an array of attributes.
*
* Example:
* { TAG: (attributes) => { attributes[] = 'attribute-name'; return attributes; } }
*/
options.elementAttributes = isPlainObject(options.elementAttributes) ? options.elementAttributes : {};

@@ -86,5 +92,8 @@ options.safelistAttributes = Array.isArray(options.safelistAttributes) ? options.safelistAttributes : [];

// Merge customizer callback passed to lodash mergeWith
// for merge attribute `props` and all attributes starting with `merge:`
// @see https://lodash.com/docs/4.17.15#mergeWith
/**
* Merge customizer callback passed to `lodash.mergeWith` for merging
* attribute `props` and all attributes starting with `merge:`.
*
* @see {@link https://lodash.com/docs/4.17.15#mergeWith|Lodash}
*/
options.mergeCustomizer = options.mergeCustomizer || ((objectValue, sourceValue) => {

@@ -110,2 +119,3 @@ if (Array.isArray(objectValue)) {

options.matcher = [];
if (options.tagPrefix) {

@@ -122,4 +132,6 @@ options.matcher.push({tag: options.tagPrefix});

options.namespaces = Array.isArray(options.namespaces) ? options.namespaces : [options.namespaces];
options.namespaces.forEach((namespace, index) => {
options.namespaces[index].root = path.resolve(namespace.root);
if (namespace.fallback) {

@@ -155,3 +167,3 @@ options.namespaces[index].fallback = path.resolve(namespace.fallback);

// Used for reset aware props
// Used to reset aware props
let processCounter = 0;

@@ -163,3 +175,2 @@

*/
function processTree(options) {

@@ -236,11 +247,14 @@ const filledSlots = {};

// Remove attributes when value is 'null' or 'undefined'
// so we can conditionally add an attribute by setting value to 'undefined' or 'null'.
/**
* Remove attributes when value is 'null' or 'undefined' so we can
* conditionally add an attribute by setting the value to
* 'undefined' or 'null'.
*/
walk.call(currentNode, node => {
if (node && node.attrs) {
each(node.attrs, (value, key) => {
if (['undefined', 'null'].includes(value)) {
for (const key in node.attrs) {
if (node.attrs[key] === 'undefined' || node.attrs[key] === 'null') {
delete node.attrs[key];
}
});
}
}

@@ -247,0 +261,0 @@

@@ -11,3 +11,2 @@ 'use strict';

const difference = require('lodash/difference');
const each = require('lodash/each');
const has = require('lodash/has');

@@ -56,11 +55,12 @@ const extend = require('lodash/extend');

if (!isEmpty(options.elementAttributes)) {
each(options.elementAttributes, (modifier, tagName) => {
for (const tagName in options.elementAttributes) {
const modifier = options.elementAttributes[tagName];
if (typeof modifier === 'function' && isString(tagName)) {
tagName = tagName.toUpperCase();
const attributes = modifier(validAttributes.elementAttributes[tagName]);
const upperTagName = tagName.toUpperCase();
const attributes = modifier(validAttributes.elementAttributes[upperTagName]);
if (Array.isArray(attributes)) {
validAttributes.elementAttributes[tagName] = attributes;
validAttributes.elementAttributes[upperTagName] = attributes;
}
}
});
}
}

@@ -81,11 +81,14 @@

// Get additional specified attributes
each(attributes, (value, attr) => {
each(validAttributes.safelistAttributes, additionalAttr => {
if (additionalAttr === attr || (additionalAttr.endsWith('*') && attr.startsWith(additionalAttr.replace('*', '')))) {
mainNodeAttributes[attr] = value;
for (const attr in attributes) {
for (const additionalAttr of validAttributes.safelistAttributes) {
if (
additionalAttr === attr
|| (additionalAttr.endsWith('*') && attr.startsWith(additionalAttr.replace('*', '')))
) {
mainNodeAttributes[attr] = attributes[attr];
}
});
});
}
}
each(mainNodeAttributes, (value, key) => {
for (const key in mainNodeAttributes) {
if (['class', 'style'].includes(key)) {

@@ -106,3 +109,3 @@ if (!has(nodeAttrs, key)) {

delete attributes[key];
});
}

@@ -112,9 +115,9 @@ // The plugin posthtml-attrs-parser compose() method expects a string,

// So below we convert non string values to string.
each(nodeAttrs, (value, key) => {
for (const key in nodeAttrs) {
if (key !== 'compose' && !isObject(nodeAttrs[key]) && !isString(nodeAttrs[key])) {
nodeAttrs[key] = nodeAttrs[key].toString();
}
});
}
mainNode.attrs = nodeAttrs.compose();
};
'use strict';
const processScript = require('./process-script');
const pick = require('lodash/pick');
const each = require('lodash/each');
const assign = require('lodash/assign');
const mergeWith = require('lodash/mergeWith');
const processScript = require('./process-script');

@@ -26,9 +25,9 @@ const attributeTypes = ['aware', 'merge'];

const attributesByTypeName = {};
each(attributeTypes, type => {
for (const type of attributeTypes) {
attributesByTypeName[type] = [];
});
}
each(attributes, (value, key, attrs) => {
for (const key in attributes) {
let newKey = key;
each(attributeTypes, type => {
for (const type of attributeTypes) {
if (key.startsWith(`${type}:`)) {

@@ -38,16 +37,16 @@ newKey = newKey.replace(`${type}:`, '');

}
});
}
if (newKey !== key) {
attrs[newKey] = value;
delete attrs[key];
attributes[newKey] = attributes[key];
delete attributes[key];
}
});
}
// Parse JSON attributes
each(attributes, (value, key, attrs) => {
for (const key in attributes) {
try {
attrs[key] = JSON.parse(value);
attributes[key] = JSON.parse(attributes[key]);
} catch {}
});
}

@@ -70,3 +69,13 @@ // Merge or extend attribute props

// Process props from <script props>
const {props} = processScript(nextNode, {props: {...attributes}, $slots: filledSlots, propsScriptAttribute: options.propsScriptAttribute, propsContext: options.propsContext, utilities: options.utilities}, componentPath.replace(`.${options.fileExtension}`, '.js'));
const {props} = processScript(
nextNode,
{
props: {...attributes},
$slots: filledSlots,
propsScriptAttribute: options.propsScriptAttribute,
propsContext: options.propsContext,
utilities: options.utilities
},
componentPath.replace(`.${options.fileExtension}`, '.js')
);

@@ -73,0 +82,0 @@ if (props) {

@@ -5,3 +5,2 @@ 'use strict';

const {render} = require('posthtml-render');
const each = require('lodash/each');
const omit = require('lodash/omit');

@@ -29,7 +28,9 @@

if (props) {
each(props, (value, key, attrs) => {
try {
attrs[key] = JSON.parse(value);
} catch {}
});
for (const key in props) {
if (props.hasOwnProperty(key)) {
try {
props[key] = JSON.parse(props[key]);
} catch {}
}
}
}

@@ -36,0 +37,0 @@