posthtml-component 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc +3 -0
- package/.clintonrc.json +20 -0
- package/.editorconfig +19 -0
- package/.huskyrc +7 -0
- package/.idea/posthtml-components.iml +8 -0
- package/.lintstagedrc +4 -0
- package/.nycrc +4 -0
- package/ava.config.js +5 -0
- package/changelog.md +48 -0
- package/license +21 -0
- package/package.json +52 -0
- package/readme.md +809 -0
- package/src/attributes.js +55 -0
- package/src/find-path.js +119 -0
- package/src/index.js +185 -0
- package/src/locals.js +105 -0
- package/src/slots.js +112 -0
- package/src/stacks.js +64 -0
- package/test/templates/components/child.html +41 -0
- package/test/templates/components/component-append-prepend.html +1 -0
- package/test/templates/components/component-locals-json-and-string.html +18 -0
- package/test/templates/components/component-locals.html +7 -0
- package/test/templates/components/component-mapped-attributes.html +7 -0
- package/test/templates/components/component-multiple-slot.html +1 -0
- package/test/templates/components/component.html +1 -0
- package/test/templates/components/form/index.html +1 -0
- package/test/templates/components/modal.html +1 -0
- package/test/templates/components/module-with-extend.html +1 -0
- package/test/templates/components/module.html +1 -0
- package/test/templates/components/nested-one-slot.html +5 -0
- package/test/templates/components/nested-one.html +1 -0
- package/test/templates/components/nested-three.html +1 -0
- package/test/templates/components/nested-two-slot.html +5 -0
- package/test/templates/components/nested-two.html +1 -0
- package/test/templates/components/parent.html +42 -0
- package/test/templates/components/script-locals.html +9 -0
- package/test/templates/custom/dark/components/button.html +1 -0
- package/test/templates/custom/dark/components/label/index.html +1 -0
- package/test/templates/dark/components/button.html +1 -0
- package/test/templates/dark/components/label/index.html +1 -0
- package/test/templates/dark/layouts/base.html +1 -0
- package/test/templates/layouts/base-locals.html +6 -0
- package/test/templates/layouts/base-render-slots-locals.html +8 -0
- package/test/templates/layouts/base.html +8 -0
- package/test/templates/layouts/extend-with-module.html +7 -0
- package/test/templates/layouts/extend.html +7 -0
- package/test/templates/layouts/playground.html +1 -0
- package/test/templates/layouts/slot-condition.html +8 -0
- package/test/templates/light/components/button.html +1 -0
- package/test/templates/light/layouts/base.html +1 -0
- package/test/test-errors.js +39 -0
- package/test/test-locals.js +74 -0
- package/test/test-nested.js +24 -0
- package/test/test-plugins.js +33 -0
- package/test/test-slots.js +76 -0
- package/test/test-x-tag.js +69 -0
- package/xo.config.js +15 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const parseAttrs = require('posthtml-attrs-parser');
|
|
4
|
+
const styleToObject = require('style-to-object');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map component attributes that it's not defined as locals to first element of node
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} currentNode
|
|
10
|
+
* @param {Object} attributes
|
|
11
|
+
* @param {Object} locals
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @return {void}
|
|
14
|
+
*/
|
|
15
|
+
module.exports = (currentNode, attributes, locals, options) => {
|
|
16
|
+
// Find by attribute 'attributes' ??
|
|
17
|
+
const index = currentNode.content.findIndex(content => typeof content === 'object');
|
|
18
|
+
|
|
19
|
+
if (index === -1) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nodeAttrs = parseAttrs(currentNode.content[index].attrs, options.attrsParserRules);
|
|
24
|
+
|
|
25
|
+
Object.keys(attributes).forEach(attr => {
|
|
26
|
+
if (typeof locals[attr] === 'undefined' && !attr.startsWith('$') && attr !== options.attribute && !Object.keys(options.aware).includes(attr) && !Object.keys(options.locals).includes(attr)) {
|
|
27
|
+
if (['class'].includes(attr)) {
|
|
28
|
+
if (typeof nodeAttrs.class === 'undefined') {
|
|
29
|
+
nodeAttrs.class = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
nodeAttrs.class.push(attributes.class);
|
|
33
|
+
delete attributes.class;
|
|
34
|
+
} else if (['override:class'].includes(attr)) {
|
|
35
|
+
nodeAttrs.class = attributes['override:class'];
|
|
36
|
+
delete attributes['override:class'];
|
|
37
|
+
} else if (['style'].includes(attr)) {
|
|
38
|
+
if (typeof nodeAttrs.style === 'undefined') {
|
|
39
|
+
nodeAttrs.style = {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
nodeAttrs.style = Object.assign(nodeAttrs.style, styleToObject(attributes.style));
|
|
43
|
+
delete attributes.style;
|
|
44
|
+
} else if (['override:style'].includes(attr)) {
|
|
45
|
+
nodeAttrs.style = attributes['override:style'];
|
|
46
|
+
delete attributes['override:style'];
|
|
47
|
+
} else {
|
|
48
|
+
nodeAttrs[attr] = attributes[attr];
|
|
49
|
+
delete attributes[attr];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
currentNode.content[index].attrs = nodeAttrs.compose();
|
|
55
|
+
};
|
package/src/find-path.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const {existsSync} = require('fs');
|
|
5
|
+
|
|
6
|
+
const folderSeparator = '.';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Find component path from tag name
|
|
10
|
+
*
|
|
11
|
+
* @param {String} tag Tag name
|
|
12
|
+
* @param {Object} options Plugin options
|
|
13
|
+
* @return {String|boolean} Full path or boolean false
|
|
14
|
+
*/
|
|
15
|
+
module.exports = (tag, options) => {
|
|
16
|
+
const fileNameFromTag = tag
|
|
17
|
+
.replace(options.tagPrefix, '')
|
|
18
|
+
.split(folderSeparator)
|
|
19
|
+
.join(path.sep)
|
|
20
|
+
.concat(folderSeparator, options.fileExtension);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return tag.includes(options.namespaceSeparator) ?
|
|
24
|
+
searchInNamespaces(tag, fileNameFromTag.split(options.namespaceSeparator), options) :
|
|
25
|
+
searchInFolders(tag, fileNameFromTag, options);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (options.strict) {
|
|
28
|
+
throw new Error(error.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Search component file in root
|
|
37
|
+
*
|
|
38
|
+
* @param {String} tag [tag name]
|
|
39
|
+
* @param {String} fileNameFromTag [filename converted from tag name]
|
|
40
|
+
* @param {Object} options [posthtml options]
|
|
41
|
+
* @return {String|boolean} [custom tag root where the module is found]
|
|
42
|
+
*/
|
|
43
|
+
function searchInFolders(tag, fileNameFromTag, options) {
|
|
44
|
+
const componentPath = search(options.root, options.folders, fileNameFromTag, options.fileExtension);
|
|
45
|
+
|
|
46
|
+
if (!componentPath) {
|
|
47
|
+
throw new Error(`[components] For the tag ${tag} was not found any template in defined root path ${options.folders.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return componentPath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Search component file within all defined namespaces
|
|
55
|
+
*
|
|
56
|
+
* @param {String} tag [tag name with namespace]
|
|
57
|
+
* @param {String} namespace [tag's namespace]
|
|
58
|
+
* @param {String} fileNameFromTag [filename converted from tag name]
|
|
59
|
+
* @param {Object} options [posthtml options]
|
|
60
|
+
* @return {String|boolean} [custom tag root where the module is found]
|
|
61
|
+
*/
|
|
62
|
+
function searchInNamespaces(tag, [namespace, fileNameFromTag], options) {
|
|
63
|
+
const namespaceOption = options.namespaces.find(n => n.name === namespace.replace(options.tagPrefix, ''));
|
|
64
|
+
|
|
65
|
+
if (!namespaceOption) {
|
|
66
|
+
throw new Error(`[components] Unknown component namespace ${namespace}.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let componentPath;
|
|
70
|
+
|
|
71
|
+
// 1) Check in custom root
|
|
72
|
+
if (namespaceOption.custom) {
|
|
73
|
+
componentPath = search(namespaceOption.custom, options.folders, fileNameFromTag, options.fileExtension);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2) Check in base root
|
|
77
|
+
if (!componentPath) {
|
|
78
|
+
componentPath = search(namespaceOption.root, options.folders, fileNameFromTag, options.fileExtension);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3) Check in fallback root
|
|
82
|
+
if (!componentPath && namespaceOption.fallback) {
|
|
83
|
+
componentPath = search(namespaceOption.fallback, options.folders, fileNameFromTag, options.fileExtension);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!componentPath && options.strict) {
|
|
87
|
+
throw new Error(`[components] For the tag ${tag} was not found any template in the defined namespace's base path ${namespaceOption.root}.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return componentPath;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Main search component file function
|
|
95
|
+
*
|
|
96
|
+
* @param {String} root Base root or namespace root from options
|
|
97
|
+
* @param {Array} folders Folders from options
|
|
98
|
+
* @param {String} fileName Filename converted from tag name
|
|
99
|
+
* @param {String} extension File extension from options
|
|
100
|
+
* @return {String|boolean} [custom tag root where the module is found]
|
|
101
|
+
*/
|
|
102
|
+
function search(root, folders, fileName, extension) {
|
|
103
|
+
let componentPath;
|
|
104
|
+
|
|
105
|
+
let componentFound = folders.some(folder => {
|
|
106
|
+
componentPath = path.join(path.resolve(root, folder), fileName);
|
|
107
|
+
return existsSync(componentPath);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!componentFound) {
|
|
111
|
+
fileName = fileName.replace(`.${extension}`, `${path.sep}index.${extension}`);
|
|
112
|
+
componentFound = folders.some(folder => {
|
|
113
|
+
componentPath = path.join(path.resolve(root, folder), fileName);
|
|
114
|
+
return existsSync(componentPath);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return componentFound ? componentPath : false;
|
|
119
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {readFileSync} = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
// const {inspect} = require('util');
|
|
6
|
+
const {parser} = require('posthtml-parser');
|
|
7
|
+
const {match} = require('posthtml/lib/api');
|
|
8
|
+
const expressions = require('posthtml-expressions');
|
|
9
|
+
const findPathFromTag = require('./find-path');
|
|
10
|
+
const processLocals = require('./locals');
|
|
11
|
+
const processAttributes = require('./attributes');
|
|
12
|
+
const {processPushes, processStacks} = require('./stacks');
|
|
13
|
+
const {setFilledSlots, processSlotContent, processFillContent} = require('./slots');
|
|
14
|
+
|
|
15
|
+
// const debug = true;
|
|
16
|
+
//
|
|
17
|
+
// const log = (object, what, method) => {
|
|
18
|
+
// if (debug === true || method === debug) {
|
|
19
|
+
// console.log(what, inspect(object, false, null, true));
|
|
20
|
+
// }
|
|
21
|
+
// };
|
|
22
|
+
|
|
23
|
+
/* eslint-disable complexity */
|
|
24
|
+
module.exports = (options = {}) => tree => {
|
|
25
|
+
options.root = path.resolve(options.root || './');
|
|
26
|
+
options.folders = options.folders || [''];
|
|
27
|
+
options.tagPrefix = options.tagPrefix || 'x-';
|
|
28
|
+
options.tag = options.tag || false;
|
|
29
|
+
options.attribute = options.attribute || 'src';
|
|
30
|
+
options.namespaces = options.namespaces || [];
|
|
31
|
+
options.namespaceSeparator = options.namespaceSeparator || '::';
|
|
32
|
+
options.fileExtension = options.fileExtension || 'html';
|
|
33
|
+
options.yield = options.yield || 'yield';
|
|
34
|
+
options.slot = options.slot || 'slot';
|
|
35
|
+
options.fill = options.fill || 'fill';
|
|
36
|
+
options.push = options.push || 'push';
|
|
37
|
+
options.stack = options.stack || 'stack';
|
|
38
|
+
options.localsAttr = options.localsAttr || 'props';
|
|
39
|
+
options.expressions = options.expressions || {};
|
|
40
|
+
options.plugins = options.plugins || [];
|
|
41
|
+
options.attrsParserRules = options.attrsParserRules || {};
|
|
42
|
+
options.strict = typeof options.strict === 'undefined' ? true : options.strict;
|
|
43
|
+
|
|
44
|
+
if (!(options.slot instanceof RegExp)) {
|
|
45
|
+
options.slot = new RegExp(`^${options.slot}:`, 'i');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!(options.fill instanceof RegExp)) {
|
|
49
|
+
options.fill = new RegExp(`^${options.fill}:`, 'i');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!(options.tagPrefix instanceof RegExp)) {
|
|
53
|
+
options.tagPrefix = new RegExp(`^${options.tagPrefix}`, 'i');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!Array.isArray(options.matcher)) {
|
|
57
|
+
options.matcher = [];
|
|
58
|
+
if (options.tagPrefix) {
|
|
59
|
+
options.matcher.push({tag: options.tagPrefix});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (options.tag) {
|
|
63
|
+
options.matcher.push({tag: options.tag});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
options.roots = Array.isArray(options.folders) ? options.folders : [options.folders];
|
|
68
|
+
// options.roots.forEach((root, index) => {
|
|
69
|
+
// options.roots[index] = path.resolve(options.root, root);
|
|
70
|
+
// });
|
|
71
|
+
options.namespaces = Array.isArray(options.namespaces) ? options.namespaces : [options.namespaces];
|
|
72
|
+
options.namespaces.forEach((namespace, index) => {
|
|
73
|
+
options.namespaces[index].root = path.resolve(namespace.root);
|
|
74
|
+
if (namespace.fallback) {
|
|
75
|
+
options.namespaces[index].fallback = path.resolve(namespace.fallback);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (namespace.custom) {
|
|
79
|
+
options.namespaces[index].custom = path.resolve(namespace.custom);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
options.locals = {...options.expressions.locals};
|
|
84
|
+
options.aware = {};
|
|
85
|
+
|
|
86
|
+
const pushedContent = {};
|
|
87
|
+
|
|
88
|
+
return processStacks(
|
|
89
|
+
processPushes(
|
|
90
|
+
processTree(options)(
|
|
91
|
+
expressions(options.expressions)(tree)
|
|
92
|
+
),
|
|
93
|
+
pushedContent,
|
|
94
|
+
options.push
|
|
95
|
+
),
|
|
96
|
+
pushedContent,
|
|
97
|
+
options.stack
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
/* eslint-enable complexity */
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {Object} options Plugin options
|
|
104
|
+
* @return {Object} PostHTML tree
|
|
105
|
+
*/
|
|
106
|
+
function processTree(options) {
|
|
107
|
+
const filledSlots = {};
|
|
108
|
+
|
|
109
|
+
let processCounter = 0;
|
|
110
|
+
|
|
111
|
+
return function (tree) {
|
|
112
|
+
match.call(tree, options.matcher, currentNode => {
|
|
113
|
+
if (!currentNode.attrs) {
|
|
114
|
+
currentNode.attrs = {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!currentNode.attrs[options.attribute]) {
|
|
118
|
+
console.log(currentNode.tag);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const componentFile = currentNode.attrs[options.attribute] || findPathFromTag(currentNode.tag, options);
|
|
122
|
+
|
|
123
|
+
if (!componentFile) {
|
|
124
|
+
return currentNode;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const componentPath = path.isAbsolute(componentFile) && componentFile !== currentNode.attrs[options.attribute] ?
|
|
128
|
+
componentFile :
|
|
129
|
+
path.join(options.root, componentFile);
|
|
130
|
+
|
|
131
|
+
if (!componentPath) {
|
|
132
|
+
return currentNode;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`${++processCounter}) Processing component ${componentPath}`);
|
|
136
|
+
|
|
137
|
+
let nextNode = parser(readFileSync(componentPath, 'utf8'));
|
|
138
|
+
|
|
139
|
+
// Set filled slots
|
|
140
|
+
setFilledSlots(currentNode, filledSlots, options);
|
|
141
|
+
|
|
142
|
+
// Reset previous locals with passed global and keep aware locals
|
|
143
|
+
options.expressions.locals = {...options.locals, ...options.aware};
|
|
144
|
+
|
|
145
|
+
const {attributes, locals} = processLocals(currentNode, nextNode, filledSlots, options);
|
|
146
|
+
|
|
147
|
+
options.expressions.locals = attributes;
|
|
148
|
+
options.expressions.locals.$slots = filledSlots;
|
|
149
|
+
// const plugins = [...options.plugins, expressions(options.expressions)];
|
|
150
|
+
nextNode = expressions(options.expressions)(nextNode);
|
|
151
|
+
|
|
152
|
+
// Process <yield> tag
|
|
153
|
+
const content = match.call(nextNode, {tag: options.yield}, nextNode => {
|
|
154
|
+
// Fill <yield> with current node content or default <yield> content or empty
|
|
155
|
+
return currentNode.content || nextNode.content || '';
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Process <fill> tags
|
|
159
|
+
processFillContent(nextNode, filledSlots, options);
|
|
160
|
+
|
|
161
|
+
// Process <slot> tags
|
|
162
|
+
processSlotContent(nextNode, filledSlots, options);
|
|
163
|
+
|
|
164
|
+
// Remove component tag and replace content with <yield>
|
|
165
|
+
currentNode.tag = false;
|
|
166
|
+
currentNode.content = content;
|
|
167
|
+
|
|
168
|
+
processAttributes(currentNode, attributes, locals, options);
|
|
169
|
+
|
|
170
|
+
// log(currentNode, 'currentNode', 'currentNode')
|
|
171
|
+
// currentNode.attrs.counter = processCounter;
|
|
172
|
+
// currentNode.attrs.data = JSON.stringify({ attributes, locals });
|
|
173
|
+
|
|
174
|
+
// messages.push({
|
|
175
|
+
// type: 'dependency',
|
|
176
|
+
// file: componentPath,
|
|
177
|
+
// from: options.root
|
|
178
|
+
// });
|
|
179
|
+
|
|
180
|
+
return currentNode;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return tree;
|
|
184
|
+
};
|
|
185
|
+
}
|
package/src/locals.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const merge = require('deepmerge');
|
|
4
|
+
const scriptDataLocals = require('posthtml-expressions/lib/locals');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse locals from attributes, globals and via script
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} currentNode - PostHTML tree
|
|
10
|
+
* @param {Array} nextNode - PostHTML tree
|
|
11
|
+
* @param {Object} slotContent - Slot locals
|
|
12
|
+
* @param {Object} options - Plugin options
|
|
13
|
+
* @return {Object} - Attribute locals and script locals
|
|
14
|
+
*/
|
|
15
|
+
module.exports = (currentNode, nextNode, slotContent, options) => {
|
|
16
|
+
let attributes = {...currentNode.attrs};
|
|
17
|
+
|
|
18
|
+
const merged = [];
|
|
19
|
+
const computed = [];
|
|
20
|
+
const aware = [];
|
|
21
|
+
|
|
22
|
+
Object.keys(attributes).forEach(attributeName => {
|
|
23
|
+
const newAttributeName = attributeName
|
|
24
|
+
.replace('merge:', '')
|
|
25
|
+
.replace('computed:', '')
|
|
26
|
+
.replace('aware:', '');
|
|
27
|
+
|
|
28
|
+
switch (true) {
|
|
29
|
+
case attributeName.startsWith('merge:'):
|
|
30
|
+
attributes[newAttributeName] = attributes[attributeName];
|
|
31
|
+
delete attributes[attributeName];
|
|
32
|
+
merged.push(newAttributeName);
|
|
33
|
+
break;
|
|
34
|
+
|
|
35
|
+
case attributeName.startsWith('computed:'):
|
|
36
|
+
attributes[newAttributeName] = attributes[attributeName];
|
|
37
|
+
delete attributes[attributeName];
|
|
38
|
+
computed.push(newAttributeName);
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
case attributeName.startsWith('aware:'):
|
|
42
|
+
attributes[newAttributeName] = attributes[attributeName];
|
|
43
|
+
delete attributes[attributeName];
|
|
44
|
+
aware.push(newAttributeName);
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
default: break;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Parse JSON attributes
|
|
52
|
+
Object.keys(attributes).forEach(attributeName => {
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(attributes[attributeName]);
|
|
55
|
+
if (attributeName === 'locals') {
|
|
56
|
+
if (merged.includes(attributeName)) {
|
|
57
|
+
attributes = merge(attributes, parsed);
|
|
58
|
+
merged.splice(merged.indexOf('locals'), 1);
|
|
59
|
+
} else {
|
|
60
|
+
// Override with merge see https://www.npmjs.com/package/deepmerge#arraymerge-example-overwrite-target-array
|
|
61
|
+
Object.assign(attributes, parsed);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
attributes[attributeName] = parsed;
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
delete attributes.locals;
|
|
70
|
+
|
|
71
|
+
// Merge with global
|
|
72
|
+
attributes = merge(options.expressions.locals, attributes);
|
|
73
|
+
|
|
74
|
+
// Retrieve default locals from <script props> and merge with attributes
|
|
75
|
+
const {locals} = scriptDataLocals(nextNode, {localsAttr: options.localsAttr, removeScriptLocals: true, locals: {...attributes, $slots: slotContent}});
|
|
76
|
+
|
|
77
|
+
// Merge default locals and attributes or overrides props with attributes
|
|
78
|
+
if (locals) {
|
|
79
|
+
if (merged.length > 0) {
|
|
80
|
+
/** @var {Object} mergedAttributes */
|
|
81
|
+
const mergedAttributes = Object.fromEntries(Object.entries(attributes).filter(([attribute]) => merged.includes(attribute)));
|
|
82
|
+
/** @var {Object} mergedAttributes */
|
|
83
|
+
const mergedLocals = Object.fromEntries(Object.entries(locals).filter(([local]) => merged.includes(local)));
|
|
84
|
+
|
|
85
|
+
if (Object.keys(mergedLocals).length > 0) {
|
|
86
|
+
merged.forEach(attributeName => {
|
|
87
|
+
attributes[attributeName] = merge(mergedLocals[attributeName], mergedAttributes[attributeName]);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Override attributes with props when is computed or attribute is not defined
|
|
93
|
+
Object.keys(locals).forEach(attributeName => {
|
|
94
|
+
if (computed.includes(attributeName) || typeof attributes[attributeName] === 'undefined') {
|
|
95
|
+
attributes[attributeName] = locals[attributeName];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (aware.length > 0) {
|
|
101
|
+
options.aware = Object.fromEntries(Object.entries(attributes).filter(([attributeName]) => aware.includes(attributeName)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {attributes, locals};
|
|
105
|
+
};
|
package/src/slots.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {match} = require('posthtml/lib/api');
|
|
4
|
+
const {render} = require('posthtml-render');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set filled slots
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} currentNode PostHTML tree
|
|
10
|
+
* @param {Object} filledSlots
|
|
11
|
+
* @param {Object} options Plugin options
|
|
12
|
+
* @return {void}
|
|
13
|
+
*/
|
|
14
|
+
function setFilledSlots(currentNode, filledSlots, {fill}) {
|
|
15
|
+
match.call(currentNode, {tag: fill}, fillNode => {
|
|
16
|
+
if (!fillNode.attrs) {
|
|
17
|
+
fillNode.attrs = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = fillNode.tag.split(':')[1];
|
|
21
|
+
|
|
22
|
+
/** @var {Object} locals - NOT YET TESTED */
|
|
23
|
+
const locals = Object.fromEntries(Object.entries(fillNode.attrs).filter(([attributeName]) => ![name, 'type'].includes(attributeName)));
|
|
24
|
+
|
|
25
|
+
if (locals) {
|
|
26
|
+
Object.keys(locals).forEach(local => {
|
|
27
|
+
try {
|
|
28
|
+
locals[local] = JSON.parse(locals[local]);
|
|
29
|
+
} catch {}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
filledSlots[name] = {
|
|
34
|
+
filled: true,
|
|
35
|
+
rendered: false,
|
|
36
|
+
tag: fillNode.tag,
|
|
37
|
+
attrs: fillNode.attrs,
|
|
38
|
+
content: fillNode.content,
|
|
39
|
+
source: render(fillNode.content),
|
|
40
|
+
locals
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return fillNode;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Process <fill> tag
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} tree PostHTML tree
|
|
51
|
+
* @param {Object} filledSlots Filled slots content
|
|
52
|
+
* @param {Object} options Plugin options
|
|
53
|
+
* @return {void}
|
|
54
|
+
*/
|
|
55
|
+
function processFillContent(tree, filledSlots, {fill}) {
|
|
56
|
+
match.call(tree, {tag: fill}, fillNode => {
|
|
57
|
+
const name = fillNode.tag.split(':')[1];
|
|
58
|
+
|
|
59
|
+
if (!filledSlots[name]) {
|
|
60
|
+
filledSlots[name] = {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
filledSlots[name].tag = fillNode.tag;
|
|
64
|
+
filledSlots[name].attrs = fillNode.attrs;
|
|
65
|
+
filledSlots[name].content = fillNode.content;
|
|
66
|
+
filledSlots[name].source = render(fillNode.content);
|
|
67
|
+
filledSlots[name].rendered = false;
|
|
68
|
+
|
|
69
|
+
fillNode.tag = false;
|
|
70
|
+
fillNode.content = null;
|
|
71
|
+
|
|
72
|
+
return fillNode;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Process <slot> tag
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} tree PostHTML tree
|
|
80
|
+
* @param {Object} filledSlots Filled slots content
|
|
81
|
+
* @param {Object} options Plugin options
|
|
82
|
+
* @return {void}
|
|
83
|
+
*/
|
|
84
|
+
function processSlotContent(tree, filledSlots, {slot}) {
|
|
85
|
+
match.call(tree, {tag: slot}, slotNode => {
|
|
86
|
+
const name = slotNode.tag.split(':')[1];
|
|
87
|
+
|
|
88
|
+
slotNode.tag = false;
|
|
89
|
+
|
|
90
|
+
if (filledSlots[name]?.rendered) {
|
|
91
|
+
slotNode.content = null;
|
|
92
|
+
} else if (slotNode.content && filledSlots[name]?.attrs && (typeof filledSlots[name]?.attrs.append !== 'undefined' || typeof filledSlots[name]?.attrs.prepend !== 'undefined')) {
|
|
93
|
+
slotNode.content = typeof filledSlots[name]?.attrs.append === 'undefined' ? filledSlots[name]?.content.concat(slotNode.content) : slotNode.content.concat(filledSlots[name]?.content);
|
|
94
|
+
} else {
|
|
95
|
+
slotNode.content = filledSlots[name]?.content;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Set rendered to true so a slot can be output only once,
|
|
99
|
+
// when not present "aware" attribute
|
|
100
|
+
if (filledSlots[name] && (!filledSlots[name]?.attrs || typeof filledSlots[name].attrs.aware === 'undefined')) {
|
|
101
|
+
filledSlots[name].rendered = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return slotNode;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
setFilledSlots,
|
|
110
|
+
processFillContent,
|
|
111
|
+
processSlotContent
|
|
112
|
+
};
|
package/src/stacks.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {match} = require('posthtml/lib/api');
|
|
4
|
+
const {render} = require('posthtml-render');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Process <push> tag
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} tree PostHTML tree
|
|
10
|
+
* @param {Object} content Content pushed by stack name
|
|
11
|
+
* @param {String} push Push tag name
|
|
12
|
+
* @return {Object} tree
|
|
13
|
+
*/
|
|
14
|
+
function processPushes(tree, content, push) {
|
|
15
|
+
match.call(tree, {tag: push}, pushNode => {
|
|
16
|
+
if (!pushNode.attrs || !pushNode.attrs.name) {
|
|
17
|
+
throw new Error(`[components] Push <${push}> tag must have an attribute "name".`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!content[pushNode.attrs.name]) {
|
|
21
|
+
content[pushNode.attrs.name] = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pushContent = render(pushNode.content);
|
|
25
|
+
|
|
26
|
+
if (typeof pushNode.attrs.once === 'undefined' || !content[pushNode.attrs.name].includes(pushContent)) {
|
|
27
|
+
if (typeof pushNode.attrs.prepend === 'undefined') {
|
|
28
|
+
content[pushNode.attrs.name].push(pushContent);
|
|
29
|
+
} else {
|
|
30
|
+
content[pushNode.attrs.name].unshift(pushContent);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pushNode.tag = false;
|
|
35
|
+
pushNode.content = null;
|
|
36
|
+
|
|
37
|
+
return pushNode;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return tree;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Process <stack> tag
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} tree PostHTML tree
|
|
47
|
+
* @param {Object} content Content pushed by stack name
|
|
48
|
+
* @param {String} stack Stack tag name
|
|
49
|
+
* @return {Object} tree
|
|
50
|
+
*/
|
|
51
|
+
function processStacks(tree, content, stack) {
|
|
52
|
+
match.call(tree, {tag: stack}, stackNode => {
|
|
53
|
+
stackNode.tag = false;
|
|
54
|
+
stackNode.content = content[stackNode.attrs.name];
|
|
55
|
+
return stackNode;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return tree;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
processPushes,
|
|
63
|
+
processStacks
|
|
64
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script props>
|
|
2
|
+
module.exports = {
|
|
3
|
+
aBoolean: true,
|
|
4
|
+
aString: 'My String Child',
|
|
5
|
+
aString2: locals.aString2 === 'yes' ? 'I am string 2' : 'I am not string 2',
|
|
6
|
+
anArray: ['one', 'two', 'three'],
|
|
7
|
+
anArray2: ['one2', 'two2', 'three2'],
|
|
8
|
+
anObject: { one: 'One', two: 'Two', three: 'Three'},
|
|
9
|
+
anObject2: { one: 'One2', two: 'Two2', three: 'Three2'}
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
CHILD:
|
|
13
|
+
<div>
|
|
14
|
+
aBoolean
|
|
15
|
+
value: {{ aBoolean }}
|
|
16
|
+
type: {{ typeof aBoolean }}
|
|
17
|
+
|
|
18
|
+
aString
|
|
19
|
+
value: {{ aString }}
|
|
20
|
+
type: {{ typeof aString }}
|
|
21
|
+
|
|
22
|
+
aString2
|
|
23
|
+
value: {{ aString2 }}
|
|
24
|
+
type: {{ typeof aString2 }}
|
|
25
|
+
|
|
26
|
+
anArray
|
|
27
|
+
value: {{ anArray }}
|
|
28
|
+
type: {{ typeof anArray }}
|
|
29
|
+
|
|
30
|
+
anObject
|
|
31
|
+
value: {{ anObject }}
|
|
32
|
+
type: {{ typeof anObject }}
|
|
33
|
+
|
|
34
|
+
anArray2
|
|
35
|
+
value: {{ anArray2 }}
|
|
36
|
+
type: {{ typeof anArray2 }}
|
|
37
|
+
|
|
38
|
+
anObject2
|
|
39
|
+
value: {{ anObject2 }}
|
|
40
|
+
type: {{ typeof anObject2 }}
|
|
41
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div><slot:title>Title</slot:title></div><div><slot:body>Body</slot:body></div>
|