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.
Files changed (57) hide show
  1. package/.c8rc +3 -0
  2. package/.clintonrc.json +20 -0
  3. package/.editorconfig +19 -0
  4. package/.huskyrc +7 -0
  5. package/.idea/posthtml-components.iml +8 -0
  6. package/.lintstagedrc +4 -0
  7. package/.nycrc +4 -0
  8. package/ava.config.js +5 -0
  9. package/changelog.md +48 -0
  10. package/license +21 -0
  11. package/package.json +52 -0
  12. package/readme.md +809 -0
  13. package/src/attributes.js +55 -0
  14. package/src/find-path.js +119 -0
  15. package/src/index.js +185 -0
  16. package/src/locals.js +105 -0
  17. package/src/slots.js +112 -0
  18. package/src/stacks.js +64 -0
  19. package/test/templates/components/child.html +41 -0
  20. package/test/templates/components/component-append-prepend.html +1 -0
  21. package/test/templates/components/component-locals-json-and-string.html +18 -0
  22. package/test/templates/components/component-locals.html +7 -0
  23. package/test/templates/components/component-mapped-attributes.html +7 -0
  24. package/test/templates/components/component-multiple-slot.html +1 -0
  25. package/test/templates/components/component.html +1 -0
  26. package/test/templates/components/form/index.html +1 -0
  27. package/test/templates/components/modal.html +1 -0
  28. package/test/templates/components/module-with-extend.html +1 -0
  29. package/test/templates/components/module.html +1 -0
  30. package/test/templates/components/nested-one-slot.html +5 -0
  31. package/test/templates/components/nested-one.html +1 -0
  32. package/test/templates/components/nested-three.html +1 -0
  33. package/test/templates/components/nested-two-slot.html +5 -0
  34. package/test/templates/components/nested-two.html +1 -0
  35. package/test/templates/components/parent.html +42 -0
  36. package/test/templates/components/script-locals.html +9 -0
  37. package/test/templates/custom/dark/components/button.html +1 -0
  38. package/test/templates/custom/dark/components/label/index.html +1 -0
  39. package/test/templates/dark/components/button.html +1 -0
  40. package/test/templates/dark/components/label/index.html +1 -0
  41. package/test/templates/dark/layouts/base.html +1 -0
  42. package/test/templates/layouts/base-locals.html +6 -0
  43. package/test/templates/layouts/base-render-slots-locals.html +8 -0
  44. package/test/templates/layouts/base.html +8 -0
  45. package/test/templates/layouts/extend-with-module.html +7 -0
  46. package/test/templates/layouts/extend.html +7 -0
  47. package/test/templates/layouts/playground.html +1 -0
  48. package/test/templates/layouts/slot-condition.html +8 -0
  49. package/test/templates/light/components/button.html +1 -0
  50. package/test/templates/light/layouts/base.html +1 -0
  51. package/test/test-errors.js +39 -0
  52. package/test/test-locals.js +74 -0
  53. package/test/test-nested.js +24 -0
  54. package/test/test-plugins.js +33 -0
  55. package/test/test-slots.js +76 -0
  56. package/test/test-x-tag.js +69 -0
  57. 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
+ };
@@ -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>