posthtml-component 1.0.0-beta.8 → 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)) {
@@ -50,8 +90,6 @@ module.exports = (options = {}) => tree => {
50
90
  }
51
91
  });
52
92
 
53
- options.mergeCustomizer();
54
-
55
93
  if (!(options.slot instanceof RegExp)) {
56
94
  options.slot = new RegExp(`^${options.slot}${options.slotSeparator}`, 'i');
57
95
  }
@@ -88,11 +126,13 @@ module.exports = (options = {}) => tree => {
88
126
  }
89
127
  });
90
128
 
91
- options.locals = {...options.expressions.locals};
129
+ options.props = {...options.expressions.locals};
92
130
  options.aware = {};
93
131
 
94
132
  const pushedContent = {};
95
133
 
134
+ log('Start of processing..', 'init', 'success');
135
+
96
136
  return processStacks(
97
137
  processPushes(
98
138
  processTree(options)(
@@ -107,21 +147,27 @@ module.exports = (options = {}) => tree => {
107
147
  };
108
148
  /* eslint-enable complexity */
109
149
 
150
+ // Used for reset aware props
151
+ let processCounter = 0;
152
+
110
153
  /**
111
154
  * @param {Object} options Plugin options
112
155
  * @return {Object} PostHTML tree
113
156
  */
157
+
114
158
  function processTree(options) {
115
159
  const filledSlots = {};
116
160
 
117
- // let processCounter = 0;
118
-
119
161
  return function (tree) {
162
+ log(`Processing tree number ${processCounter}..`, 'processTree');
163
+
120
164
  if (options.plugins.length > 0) {
121
165
  tree = applyPluginsToTree(tree, options.plugins);
122
166
  }
123
167
 
124
168
  match.call(tree, options.matcher, currentNode => {
169
+ log(`Match found for tag "${currentNode.tag}"..`, 'processTree');
170
+
125
171
  if (!currentNode.attrs) {
126
172
  currentNode.attrs = {};
127
173
  }
@@ -132,20 +178,21 @@ function processTree(options) {
132
178
  return currentNode;
133
179
  }
134
180
 
135
- // console.log(`${++processCounter}) Processing component ${componentPath}`);
136
-
137
- // log(currentNode, 'currentNode');
181
+ log(`${++processCounter}) Processing "${currentNode.tag}" from "${componentPath}"`, 'processTree');
138
182
 
139
183
  let nextNode = parser(readFileSync(componentPath, 'utf8'));
140
184
 
141
185
  // Set filled slots
142
186
  setFilledSlots(currentNode, filledSlots, options);
143
- // setFilledSlots(nextNode, filledSlots, options);
144
187
 
145
- // Reset previous locals with passed global and keep aware locals
146
- options.expressions.locals = {...options.locals, ...options.aware};
188
+ const aware = transform(options.aware, (result, value) => {
189
+ assign(result, value);
190
+ }, {});
147
191
 
148
- 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);
149
196
 
150
197
  options.expressions.locals = attributes;
151
198
  options.expressions.locals.$slots = filledSlots;
@@ -174,21 +221,37 @@ function processTree(options) {
174
221
  currentNode.tag = false;
175
222
  currentNode.content = content;
176
223
 
177
- 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');
178
241
 
179
- // log(currentNode, 'currentNode', 'currentNode')
180
- // currentNode.attrs.counter = processCounter;
181
- // currentNode.attrs.data = JSON.stringify({ attributes, locals });
242
+ // Reset options.aware for current processCounter
243
+ delete options.aware[processCounter];
182
244
 
183
- // messages.push({
184
- // type: 'dependency',
185
- // file: componentPath,
186
- // from: options.root
187
- // });
245
+ // Decrement counter
246
+ processCounter--;
188
247
 
189
248
  return currentNode;
190
249
  });
191
250
 
251
+ if (processCounter === 0) {
252
+ log('End of processing', 'processTree', 'success');
253
+ }
254
+
192
255
  return tree;
193
256
  };
194
257
  }
@@ -207,6 +270,9 @@ function getComponentPath(currentNode, options) {
207
270
  }
208
271
  }
209
272
 
273
+ // Delete attribute used as path
274
+ delete currentNode.attrs[options.attribute];
275
+
210
276
  return componentPath;
211
277
  }
212
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]) {