posthtml-component 1.0.0-beta.9 → 1.0.0

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/src/index.js CHANGED
@@ -3,21 +3,32 @@
3
3
  const {readFileSync, existsSync} = require('fs');
4
4
  const path = require('path');
5
5
  const {parser} = require('posthtml-parser');
6
- const {match} = require('posthtml/lib/api');
6
+ const {match, walk} = require('posthtml/lib/api');
7
7
  const expressions = require('posthtml-expressions');
8
8
  const findPathFromTag = require('./find-path');
9
- const processLocals = require('./locals');
10
- const processAttributes = require('./attributes');
11
- const {processPushes, processStacks} = require('./stacks');
12
- const {setFilledSlots, processSlotContent, processFillContent} = require('./slots');
13
-
14
- // const {inspect} = require('util');
15
- // const debug = true;
16
- // const log = (object, what, method) => {
17
- // if (debug === true || method === debug) {
18
- // console.log(what, inspect(object, false, null, true));
19
- // }
20
- // };
9
+ const processProps = require('./process-props');
10
+ const processAttributes = require('./process-attributes');
11
+ const {processPushes, processStacks} = require('./process-stacks');
12
+ const {setFilledSlots, processSlotContent, processFillContent} = require('./process-slots');
13
+ const log = require('./log');
14
+ const each = require('lodash/each');
15
+ const defaults = require('lodash/defaults');
16
+ const assignWith = require('lodash/assignWith');
17
+ const mergeWith = require('lodash/mergeWith');
18
+ const template = require('lodash/template');
19
+ const get = require('lodash/get');
20
+ const has = require('lodash/has');
21
+ const isObjectLike = require('lodash/isObjectLike');
22
+ const isArray = require('lodash/isArray');
23
+ const isEmpty = require('lodash/isEmpty');
24
+ const isBoolean = require('lodash/isBoolean');
25
+ const isUndefined = require('lodash/isUndefined'); // value === undefined
26
+ const isNull = require('lodash/isNull'); // value === null
27
+ const isNil = require('lodash/isNil'); // value == null
28
+ const uniqueId = require('lodash/uniqueId');
29
+ const transform = require('lodash/transform');
30
+ const assign = require('lodash/assign');
31
+ const isPlainObject = require('lodash/isPlainObject');
21
32
 
22
33
  /* eslint-disable complexity */
23
34
  module.exports = (options = {}) => tree => {
@@ -35,14 +46,43 @@ module.exports = (options = {}) => tree => {
35
46
  options.slotSeparator = options.slotSeparator || ':';
36
47
  options.push = options.push || 'push';
37
48
  options.stack = options.stack || 'stack';
38
- options.localsAttr = options.localsAttr || 'props';
49
+ options.propsScriptAttribute = options.propsScriptAttribute || 'props';
50
+ options.propsContext = options.propsContext || 'props';
51
+ options.propsAttribute = options.propsAttribute || 'props';
52
+ options.propsSlot = options.propsSlot || 'props';
39
53
  options.expressions = options.expressions || {};
40
54
  options.plugins = options.plugins || [];
41
55
  options.attrsParserRules = options.attrsParserRules || {};
42
56
  options.strict = typeof options.strict === 'undefined' ? true : options.strict;
57
+ options.utilities = options.utilities || {
58
+ each,
59
+ defaults,
60
+ assign: assignWith,
61
+ merge: mergeWith,
62
+ template,
63
+ get,
64
+ has,
65
+ isPlainObject,
66
+ isObject: isObjectLike,
67
+ isArray,
68
+ isEmpty,
69
+ isBoolean,
70
+ isUndefined,
71
+ isNull,
72
+ isNil,
73
+ uniqueId,
74
+ isEnabled: prop => prop === true || prop === ''
75
+ };
76
+ // Additional element attributes, in case already exist in valid-attributes.js it will replace all attributes
77
+ // It should be an object with key as tag name and as value a function modifier which receive
78
+ // the default attributes and return an array of attributes. Example:
79
+ // { TAG: (attributes) => { attributes[] = 'attribute-name'; return attributes; } }
80
+ options.elementAttributes = isPlainObject(options.elementAttributes) ? options.elementAttributes : {};
81
+ options.safelistAttributes = Array.isArray(options.safelistAttributes) ? options.safelistAttributes : [];
82
+ options.blacklistAttributes = Array.isArray(options.blacklistAttributes) ? options.blacklistAttributes : [];
43
83
 
44
84
  // Merge customizer callback passed to lodash mergeWith
45
- // for merge attribute `locals` and all attributes starting with `merge:`
85
+ // for merge attribute `props` and all attributes starting with `merge:`
46
86
  // @see https://lodash.com/docs/4.17.15#mergeWith
47
87
  options.mergeCustomizer = options.mergeCustomizer || ((objectValue, sourceValue) => {
48
88
  if (Array.isArray(objectValue)) {
@@ -86,11 +126,13 @@ module.exports = (options = {}) => tree => {
86
126
  }
87
127
  });
88
128
 
89
- options.locals = {...options.expressions.locals};
129
+ options.props = {...options.expressions.locals};
90
130
  options.aware = {};
91
131
 
92
132
  const pushedContent = {};
93
133
 
134
+ log('Start of processing..', 'init', 'success');
135
+
94
136
  return processStacks(
95
137
  processPushes(
96
138
  processTree(options)(
@@ -105,21 +147,27 @@ module.exports = (options = {}) => tree => {
105
147
  };
106
148
  /* eslint-enable complexity */
107
149
 
150
+ // Used for reset aware props
151
+ let processCounter = 0;
152
+
108
153
  /**
109
154
  * @param {Object} options Plugin options
110
155
  * @return {Object} PostHTML tree
111
156
  */
157
+
112
158
  function processTree(options) {
113
159
  const filledSlots = {};
114
160
 
115
- // let processCounter = 0;
116
-
117
161
  return function (tree) {
162
+ log(`Processing tree number ${processCounter}..`, 'processTree');
163
+
118
164
  if (options.plugins.length > 0) {
119
165
  tree = applyPluginsToTree(tree, options.plugins);
120
166
  }
121
167
 
122
168
  match.call(tree, options.matcher, currentNode => {
169
+ log(`Match found for tag "${currentNode.tag}"..`, 'processTree');
170
+
123
171
  if (!currentNode.attrs) {
124
172
  currentNode.attrs = {};
125
173
  }
@@ -130,20 +178,21 @@ function processTree(options) {
130
178
  return currentNode;
131
179
  }
132
180
 
133
- // console.log(`${++processCounter}) Processing component ${componentPath}`);
134
-
135
- // log(currentNode, 'currentNode');
181
+ log(`${++processCounter}) Processing "${currentNode.tag}" from "${componentPath}"`, 'processTree');
136
182
 
137
183
  let nextNode = parser(readFileSync(componentPath, 'utf8'));
138
184
 
139
185
  // Set filled slots
140
186
  setFilledSlots(currentNode, filledSlots, options);
141
- // setFilledSlots(nextNode, filledSlots, options);
142
187
 
143
- // Reset previous locals with passed global and keep aware locals
144
- options.expressions.locals = {...options.locals, ...options.aware};
188
+ const aware = transform(options.aware, (result, value) => {
189
+ assign(result, value);
190
+ }, {});
145
191
 
146
- const {attributes, locals} = processLocals(currentNode, nextNode, filledSlots, options);
192
+ // Reset options.expressions.locals and keep aware locals
193
+ options.expressions.locals = {...options.props, ...aware};
194
+
195
+ const {attributes, props} = processProps(currentNode, nextNode, filledSlots, options, componentPath, processCounter);
147
196
 
148
197
  options.expressions.locals = attributes;
149
198
  options.expressions.locals.$slots = filledSlots;
@@ -172,21 +221,37 @@ function processTree(options) {
172
221
  currentNode.tag = false;
173
222
  currentNode.content = content;
174
223
 
175
- processAttributes(currentNode, attributes, locals, options);
224
+ processAttributes(currentNode, attributes, props, options, aware);
225
+
226
+ // Remove attributes when value is 'null' or 'undefined'
227
+ // so we can conditionally add an attribute by setting value to 'undefined' or 'null'.
228
+ walk.call(currentNode, node => {
229
+ if (node && node.attrs) {
230
+ each(node.attrs, (value, key) => {
231
+ if (['undefined', 'null'].includes(value)) {
232
+ delete node.attrs[key];
233
+ }
234
+ });
235
+ }
236
+
237
+ return node;
238
+ });
239
+
240
+ log(`Done processing number ${processCounter}.`, 'processTree', 'success');
176
241
 
177
- // log(currentNode, 'currentNode', 'currentNode')
178
- // currentNode.attrs.counter = processCounter;
179
- // currentNode.attrs.data = JSON.stringify({ attributes, locals });
242
+ // Reset options.aware for current processCounter
243
+ delete options.aware[processCounter];
180
244
 
181
- // messages.push({
182
- // type: 'dependency',
183
- // file: componentPath,
184
- // from: options.root
185
- // });
245
+ // Decrement counter
246
+ processCounter--;
186
247
 
187
248
  return currentNode;
188
249
  });
189
250
 
251
+ if (processCounter === 0) {
252
+ log('End of processing', 'processTree', 'success');
253
+ }
254
+
190
255
  return tree;
191
256
  };
192
257
  }
@@ -205,6 +270,9 @@ function getComponentPath(currentNode, options) {
205
270
  }
206
271
  }
207
272
 
273
+ // Delete attribute used as path
274
+ delete currentNode.attrs[options.attribute];
275
+
208
276
  return componentPath;
209
277
  }
210
278
 
package/src/log.js ADDED
@@ -0,0 +1,27 @@
1
+ const {inspect} = require('util');
2
+
3
+ const debug = false;
4
+
5
+ // Colors
6
+ const colors = {
7
+ reset: '\u001B[0m',
8
+ error: '\u001B[31m',
9
+ success: '\u001B[32m',
10
+ warning: '\u001B[33m',
11
+
12
+ errorHighlight: '\u001B[41m',
13
+ successHighlight: '\u001B[42m',
14
+ warningHighlight: '\u001B[43m',
15
+
16
+ highlighted: '\u001B[45m'
17
+ };
18
+
19
+ module.exports = (message, method, level = 'reset', object = null) => {
20
+ if (debug === true || method === debug) {
21
+ if (object) {
22
+ console.log(`[${colors.highlighted}x-components ${method}()${colors.reset}]`, colors[level] || 'reset', message, inspect(object, false, null, true), colors.reset);
23
+ } else {
24
+ console.log(`[${colors.highlighted}x-components ${method}()${colors.reset}]`, colors[level] || 'reset', message, colors.reset);
25
+ }
26
+ }
27
+ };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const {match} = require('posthtml/lib/api');
4
+ const parseAttrs = require('posthtml-attrs-parser');
5
+ const styleToObject = require('style-to-object');
6
+ const validAttributes = require('./valid-attributes');
7
+ const keys = require('lodash/keys');
8
+ const union = require('lodash/union');
9
+ const pick = require('lodash/pick');
10
+ const difference = require('lodash/difference');
11
+ const each = require('lodash/each');
12
+ const has = require('lodash/has');
13
+ const extend = require('lodash/extend');
14
+ const isString = require('lodash/isString');
15
+ const isObject = require('lodash/isObject');
16
+ const isEmpty = require('lodash/isEmpty');
17
+
18
+ /**
19
+ * Map component attributes that it's not defined as props to first element of node
20
+ *
21
+ * @param {Object} currentNode
22
+ * @param {Object} attributes
23
+ * @param {Object} props
24
+ * @param {Object} options
25
+ * @param {Object} aware
26
+ * @return {void}
27
+ */
28
+ module.exports = (currentNode, attributes, props, options, aware) => {
29
+ let mainNode;
30
+ match.call(currentNode, {attrs: {attributes: ''}}, node => {
31
+ delete node.attrs.attributes;
32
+ mainNode = node;
33
+
34
+ return node;
35
+ });
36
+
37
+ if (!mainNode) {
38
+ const index = currentNode.content.findIndex(content => typeof content === 'object');
39
+
40
+ if (index === -1) {
41
+ return;
42
+ }
43
+
44
+ mainNode = currentNode.content[index];
45
+ }
46
+
47
+ const nodeAttrs = parseAttrs(mainNode.attrs, options.attrsParserRules);
48
+
49
+ // Merge elementAttributes and blacklistAttributes with options provided
50
+ validAttributes.blacklistAttributes = union(validAttributes.blacklistAttributes, options.blacklistAttributes);
51
+ validAttributes.safelistAttributes = union(validAttributes.safelistAttributes, options.safelistAttributes);
52
+
53
+ // Merge or override elementAttributes from options provided
54
+ if (!isEmpty(options.elementAttributes)) {
55
+ each(options.elementAttributes, (modifier, tagName) => {
56
+ if (typeof modifier === 'function' && isString(tagName)) {
57
+ tagName = tagName.toUpperCase();
58
+ const attributes = modifier(validAttributes.elementAttributes[tagName]);
59
+ if (Array.isArray(attributes)) {
60
+ validAttributes.elementAttributes[tagName] = attributes;
61
+ }
62
+ }
63
+ });
64
+ }
65
+
66
+ // Attributes to be excluded
67
+ const excludeAttributes = union(validAttributes.blacklistAttributes, keys(props), keys(aware), keys(options.props), ['$slots']);
68
+ // All valid HTML attributes for the main element
69
+ const allValidElementAttributes = isString(mainNode.tag) && has(validAttributes.elementAttributes, mainNode.tag.toUpperCase()) ? validAttributes.elementAttributes[mainNode.tag.toUpperCase()] : [];
70
+ // Valid HTML attributes without the excluded
71
+ const validElementAttributes = difference(allValidElementAttributes, excludeAttributes);
72
+ // Add override attributes
73
+ validElementAttributes.push('override:style');
74
+ validElementAttributes.push('override:class');
75
+ // Pick valid attributes from passed
76
+ const mainNodeAttributes = pick(attributes, validElementAttributes);
77
+
78
+ // Get additional specified attributes
79
+ each(attributes, (value, attr) => {
80
+ each(validAttributes.safelistAttributes, additionalAttr => {
81
+ if (additionalAttr === attr || (additionalAttr.endsWith('*') && attr.startsWith(additionalAttr.replace('*', '')))) {
82
+ mainNodeAttributes[attr] = value;
83
+ }
84
+ });
85
+ });
86
+
87
+ each(mainNodeAttributes, (value, key) => {
88
+ if (['class', 'style'].includes(key)) {
89
+ if (!has(nodeAttrs, key)) {
90
+ nodeAttrs[key] = key === 'class' ? [] : {};
91
+ }
92
+
93
+ if (key === 'class') {
94
+ nodeAttrs.class.push(attributes.class);
95
+ } else {
96
+ nodeAttrs.style = extend(nodeAttrs.style, styleToObject(attributes.style));
97
+ }
98
+ } else {
99
+ nodeAttrs[key.replace('override:', '')] = attributes[key];
100
+ }
101
+
102
+ delete attributes[key];
103
+ });
104
+
105
+ // The plugin posthtml-attrs-parser compose() method expects a string,
106
+ // but since we are JSON parsing, values like "-1" become number -1.
107
+ // So below we convert non string values to string.
108
+ each(nodeAttrs, (value, key) => {
109
+ if (key !== 'compose' && !isObject(nodeAttrs[key]) && !isString(nodeAttrs[key])) {
110
+ nodeAttrs[key] = nodeAttrs[key].toString();
111
+ }
112
+ });
113
+
114
+ mainNode.attrs = nodeAttrs.compose();
115
+ };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const processScript = require('./process-script');
4
+ const pick = require('lodash/pick');
5
+ const each = require('lodash/each');
6
+ const assign = require('lodash/assign');
7
+ const mergeWith = require('lodash/mergeWith');
8
+
9
+ const attributeTypes = ['aware', 'merge'];
10
+
11
+ /**
12
+ * Parse props from attributes, globals and via script
13
+ *
14
+ * @param {Object} currentNode - PostHTML tree
15
+ * @param {Array} nextNode - PostHTML tree
16
+ * @param {Object} filledSlots - Filled slots
17
+ * @param {Object} options - Plugin options
18
+ * @param {string} componentPath - Component path
19
+ * @param {number} processCounter
20
+ * @return {Object} - Attribute props and script props
21
+ */
22
+ module.exports = (currentNode, nextNode, filledSlots, options, componentPath, processCounter) => {
23
+ let attributes = {...currentNode.attrs};
24
+
25
+ const attributesByTypeName = {};
26
+ each(attributeTypes, type => {
27
+ attributesByTypeName[type] = [];
28
+ });
29
+
30
+ each(attributes, (value, key, attrs) => {
31
+ let newKey = key;
32
+ each(attributeTypes, type => {
33
+ if (key.startsWith(`${type}:`)) {
34
+ newKey = newKey.replace(`${type}:`, '');
35
+ attributesByTypeName[type].push(newKey);
36
+ }
37
+ });
38
+
39
+ if (newKey !== key) {
40
+ attrs[newKey] = value;
41
+ delete attrs[key];
42
+ }
43
+ });
44
+
45
+ // Parse JSON attributes
46
+ each(attributes, (value, key, attrs) => {
47
+ try {
48
+ attrs[key] = JSON.parse(value);
49
+ } catch {}
50
+ });
51
+
52
+ // Merge or extend attribute props
53
+ if (attributes[options.propsAttribute]) {
54
+ if (attributesByTypeName.merge.includes(options.propsAttribute)) {
55
+ attributesByTypeName.merge.splice(attributesByTypeName.merge.indexOf(options.propsAttribute), 1);
56
+ mergeWith(attributes, attributes[options.propsAttribute], options.mergeCustomizer);
57
+ } else {
58
+ assign(attributes, attributes[options.propsAttribute]);
59
+ }
60
+
61
+ delete attributes[options.propsAttribute];
62
+ }
63
+
64
+ // Merge with global
65
+ attributes = mergeWith({}, options.expressions.locals, attributes, options.mergeCustomizer);
66
+
67
+ // Process props from <script props>
68
+ const {props} = processScript(nextNode, {props: {...attributes}, $slots: filledSlots, propsScriptAttribute: options.propsScriptAttribute, propsContext: options.propsContext, utilities: options.utilities}, componentPath.replace(`.${options.fileExtension}`, '.js'));
69
+
70
+ if (props) {
71
+ assign(attributes, props);
72
+ // if (attributesByTypeName.merge.length > 0) {
73
+ // assign(attributes, mergeWith(pick(locals, attributesByTypeName.merge), pick(attributes, attributesByTypeName.merge), options.mergeCustomizer));
74
+ // }
75
+ }
76
+
77
+ // Set aware attributes
78
+ if (attributesByTypeName.aware.length > 0) {
79
+ options.aware[processCounter] = pick(attributes, attributesByTypeName.aware);
80
+ }
81
+
82
+ return {attributes, props};
83
+ };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const vm = require('vm');
4
+ const {existsSync, readFileSync} = require('fs');
5
+ const {render} = require('posthtml-render');
6
+ const {match} = require('posthtml/lib/api');
7
+
8
+ const ctx = vm.createContext({module, require});
9
+
10
+ /**
11
+ * Get the script tag with props from a node list and return process props.
12
+ * Custom posthtml-expressions/lib/locals.
13
+ *
14
+ * @param {Array} tree Nodes
15
+ * @param {Object} options Options
16
+ * @param {string} scriptPath - Component path
17
+ * @return {Object} {} Locals
18
+ */
19
+ module.exports = (tree, options, scriptPath) => {
20
+ const props = {};
21
+ const propsContext = options.props;
22
+ const utilities = {...options.utilities};
23
+ const context = {...utilities, ...ctx, [options.propsContext]: propsContext, $slots: options.$slots};
24
+
25
+ const runInContext = code => {
26
+ try {
27
+ const parsedContext = vm.createContext(context);
28
+ const parsedProps = vm.runInContext(code, parsedContext);
29
+
30
+ Object.assign(props, parsedProps);
31
+ } catch {}
32
+ };
33
+
34
+ if (existsSync(scriptPath)) {
35
+ runInContext(readFileSync(scriptPath, 'utf8'));
36
+ }
37
+
38
+ match.call(tree, {tag: 'script', attrs: {[options.propsScriptAttribute]: ''}}, node => {
39
+ if (node.content) {
40
+ runInContext(render(node.content));
41
+ }
42
+
43
+ return '';
44
+ });
45
+
46
+ return {
47
+ props
48
+ };
49
+ };
@@ -14,7 +14,7 @@ const omit = require('lodash/omit');
14
14
  * @param {String} slotSeparator Slot separator
15
15
  * @return {void}
16
16
  */
17
- function setFilledSlots(currentNode, filledSlots, {fill, slotSeparator}) {
17
+ function setFilledSlots(currentNode, filledSlots, {fill, slotSeparator, propsSlot}) {
18
18
  match.call(currentNode, {tag: fill}, fillNode => {
19
19
  if (!fillNode.attrs) {
20
20
  fillNode.attrs = {};
@@ -22,10 +22,10 @@ function setFilledSlots(currentNode, filledSlots, {fill, slotSeparator}) {
22
22
 
23
23
  const name = fillNode.tag.split(slotSeparator)[1];
24
24
 
25
- const locals = omit(fillNode.attrs, ['append', 'prepend', 'aware']);
25
+ const props = omit(fillNode.attrs, ['append', 'prepend', 'aware']);
26
26
 
27
- if (locals) {
28
- each(locals, (value, key, attrs) => {
27
+ if (props) {
28
+ each(props, (value, key, attrs) => {
29
29
  try {
30
30
  attrs[key] = JSON.parse(value);
31
31
  } catch {}
@@ -39,7 +39,7 @@ function setFilledSlots(currentNode, filledSlots, {fill, slotSeparator}) {
39
39
  attrs: fillNode.attrs,
40
40
  content: fillNode.content,
41
41
  source: render(fillNode.content),
42
- locals
42
+ [propsSlot]: props
43
43
  };
44
44
 
45
45
  return fillNode;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const {match} = require('posthtml/lib/api');
4
4
  const {render} = require('posthtml-render');
5
+ const get = require('lodash/get');
5
6
 
6
7
  /**
7
8
  * Process <push> tag
@@ -13,8 +14,12 @@ const {render} = require('posthtml-render');
13
14
  */
14
15
  function processPushes(tree, content, push) {
15
16
  match.call(tree, {tag: push}, pushNode => {
17
+ if (get(pushNode, 'attrs.name') === '') {
18
+ throw new Error(`[components] <${push}> tag requires a value for the "name" attribute.`);
19
+ }
20
+
16
21
  if (!pushNode.attrs || !pushNode.attrs.name) {
17
- throw new Error(`[components] Push <${push}> tag must have an attribute "name".`);
22
+ throw new Error(`[components] <${push}> tag requires a "name" attribute.`);
18
23
  }
19
24
 
20
25
  if (!content[pushNode.attrs.name]) {