posthtml-component 1.0.0-RC.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/license +21 -0
- package/package.json +52 -0
- package/readme.md +1011 -0
- package/src/find-path.js +119 -0
- package/src/index.js +287 -0
- package/src/log.js +27 -0
- package/src/process-attributes.js +115 -0
- package/src/process-props.js +83 -0
- package/src/process-script.js +49 -0
- package/src/process-slots.js +116 -0
- package/src/process-stacks.js +65 -0
- package/src/valid-attributes.js +3560 -0
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,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {readFileSync, existsSync} = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {parser} = require('posthtml-parser');
|
|
6
|
+
const {match, walk} = require('posthtml/lib/api');
|
|
7
|
+
const expressions = require('posthtml-expressions');
|
|
8
|
+
const findPathFromTag = require('./find-path');
|
|
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');
|
|
32
|
+
|
|
33
|
+
/* eslint-disable complexity */
|
|
34
|
+
module.exports = (options = {}) => tree => {
|
|
35
|
+
options.root = path.resolve(options.root || './');
|
|
36
|
+
options.folders = options.folders || [''];
|
|
37
|
+
options.tagPrefix = options.tagPrefix || 'x-';
|
|
38
|
+
options.tag = options.tag || false;
|
|
39
|
+
options.attribute = options.attribute || 'src';
|
|
40
|
+
options.namespaces = options.namespaces || [];
|
|
41
|
+
options.namespaceSeparator = options.namespaceSeparator || '::';
|
|
42
|
+
options.fileExtension = options.fileExtension || 'html';
|
|
43
|
+
options.yield = options.yield || 'yield';
|
|
44
|
+
options.slot = options.slot || 'slot';
|
|
45
|
+
options.fill = options.fill || 'fill';
|
|
46
|
+
options.slotSeparator = options.slotSeparator || ':';
|
|
47
|
+
options.push = options.push || 'push';
|
|
48
|
+
options.stack = options.stack || 'stack';
|
|
49
|
+
options.propsScriptAttribute = options.propsScriptAttribute || 'props';
|
|
50
|
+
options.propsContext = options.propsContext || 'props';
|
|
51
|
+
options.propsAttribute = options.propsAttribute || 'props';
|
|
52
|
+
options.propsSlot = options.propsSlot || 'props';
|
|
53
|
+
options.expressions = options.expressions || {};
|
|
54
|
+
options.plugins = options.plugins || [];
|
|
55
|
+
options.attrsParserRules = options.attrsParserRules || {};
|
|
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: (attrsibutes) => { 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 : [];
|
|
83
|
+
|
|
84
|
+
// Merge customizer callback passed to lodash mergeWith
|
|
85
|
+
// for merge attribute `props` and all attributes starting with `merge:`
|
|
86
|
+
// @see https://lodash.com/docs/4.17.15#mergeWith
|
|
87
|
+
options.mergeCustomizer = options.mergeCustomizer || ((objectValue, sourceValue) => {
|
|
88
|
+
if (Array.isArray(objectValue)) {
|
|
89
|
+
return objectValue.concat(sourceValue);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!(options.slot instanceof RegExp)) {
|
|
94
|
+
options.slot = new RegExp(`^${options.slot}${options.slotSeparator}`, 'i');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!(options.fill instanceof RegExp)) {
|
|
98
|
+
options.fill = new RegExp(`^${options.fill}${options.slotSeparator}`, 'i');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!(options.tagPrefix instanceof RegExp)) {
|
|
102
|
+
options.tagPrefix = new RegExp(`^${options.tagPrefix}`, 'i');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!Array.isArray(options.matcher)) {
|
|
106
|
+
options.matcher = [];
|
|
107
|
+
if (options.tagPrefix) {
|
|
108
|
+
options.matcher.push({tag: options.tagPrefix});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (options.tag) {
|
|
112
|
+
options.matcher.push({tag: options.tag});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
options.folders = Array.isArray(options.folders) ? options.folders : [options.folders];
|
|
117
|
+
options.namespaces = Array.isArray(options.namespaces) ? options.namespaces : [options.namespaces];
|
|
118
|
+
options.namespaces.forEach((namespace, index) => {
|
|
119
|
+
options.namespaces[index].root = path.resolve(namespace.root);
|
|
120
|
+
if (namespace.fallback) {
|
|
121
|
+
options.namespaces[index].fallback = path.resolve(namespace.fallback);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (namespace.custom) {
|
|
125
|
+
options.namespaces[index].custom = path.resolve(namespace.custom);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
options.props = {...options.expressions.locals};
|
|
130
|
+
options.aware = {};
|
|
131
|
+
|
|
132
|
+
const pushedContent = {};
|
|
133
|
+
|
|
134
|
+
log('Start of processing..', 'init', 'success');
|
|
135
|
+
|
|
136
|
+
return processStacks(
|
|
137
|
+
processPushes(
|
|
138
|
+
processTree(options)(
|
|
139
|
+
expressions(options.expressions)(tree)
|
|
140
|
+
),
|
|
141
|
+
pushedContent,
|
|
142
|
+
options.push
|
|
143
|
+
),
|
|
144
|
+
pushedContent,
|
|
145
|
+
options.stack
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
/* eslint-enable complexity */
|
|
149
|
+
|
|
150
|
+
// Used for reset aware props
|
|
151
|
+
let processCounter = 0;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {Object} options Plugin options
|
|
155
|
+
* @return {Object} PostHTML tree
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
function processTree(options) {
|
|
159
|
+
const filledSlots = {};
|
|
160
|
+
|
|
161
|
+
return function (tree) {
|
|
162
|
+
log(`Processing tree number ${processCounter}..`, 'processTree');
|
|
163
|
+
|
|
164
|
+
if (options.plugins.length > 0) {
|
|
165
|
+
tree = applyPluginsToTree(tree, options.plugins);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
match.call(tree, options.matcher, currentNode => {
|
|
169
|
+
log(`Match found for tag "${currentNode.tag}"..`, 'processTree');
|
|
170
|
+
|
|
171
|
+
if (!currentNode.attrs) {
|
|
172
|
+
currentNode.attrs = {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const componentPath = getComponentPath(currentNode, options);
|
|
176
|
+
|
|
177
|
+
if (!componentPath) {
|
|
178
|
+
return currentNode;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
log(`${++processCounter}) Processing "${currentNode.tag}" from "${componentPath}"`, 'processTree');
|
|
182
|
+
|
|
183
|
+
let nextNode = parser(readFileSync(componentPath, 'utf8'));
|
|
184
|
+
|
|
185
|
+
// Set filled slots
|
|
186
|
+
setFilledSlots(currentNode, filledSlots, options);
|
|
187
|
+
|
|
188
|
+
const aware = transform(options.aware, (result, value) => {
|
|
189
|
+
assign(result, value);
|
|
190
|
+
}, {});
|
|
191
|
+
|
|
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);
|
|
196
|
+
|
|
197
|
+
options.expressions.locals = attributes;
|
|
198
|
+
options.expressions.locals.$slots = filledSlots;
|
|
199
|
+
// const plugins = [...options.plugins, expressions(options.expressions)];
|
|
200
|
+
nextNode = expressions(options.expressions)(nextNode);
|
|
201
|
+
|
|
202
|
+
if (options.plugins.length > 0) {
|
|
203
|
+
nextNode = applyPluginsToTree(nextNode, options.plugins);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Process <yield> tag
|
|
207
|
+
const content = match.call(nextNode, {tag: options.yield}, nextNode => {
|
|
208
|
+
// Fill <yield> with current node content or default <yield>
|
|
209
|
+
return currentNode.content || nextNode.content;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
nextNode = processTree(options)(nextNode);
|
|
213
|
+
|
|
214
|
+
// Process <fill> tags
|
|
215
|
+
processFillContent(nextNode, filledSlots, options);
|
|
216
|
+
|
|
217
|
+
// Process <slot> tags
|
|
218
|
+
processSlotContent(nextNode, filledSlots, options);
|
|
219
|
+
|
|
220
|
+
// Remove component tag and replace content with <yield>
|
|
221
|
+
currentNode.tag = false;
|
|
222
|
+
currentNode.content = content;
|
|
223
|
+
|
|
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');
|
|
241
|
+
|
|
242
|
+
// Reset options.aware for current processCounter
|
|
243
|
+
delete options.aware[processCounter];
|
|
244
|
+
|
|
245
|
+
// Decrement counter
|
|
246
|
+
processCounter--;
|
|
247
|
+
|
|
248
|
+
return currentNode;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (processCounter === 0) {
|
|
252
|
+
log('End of processing', 'processTree', 'success');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return tree;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getComponentPath(currentNode, options) {
|
|
260
|
+
const componentFile = currentNode.attrs[options.attribute];
|
|
261
|
+
|
|
262
|
+
if (componentFile) {
|
|
263
|
+
const componentPath = path.join(options.root, componentFile);
|
|
264
|
+
|
|
265
|
+
if (!existsSync(componentPath)) {
|
|
266
|
+
if (options.strict) {
|
|
267
|
+
throw new Error(`[components] The component was not found in ${componentPath}.`);
|
|
268
|
+
} else {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Delete attribute used as path
|
|
274
|
+
delete currentNode.attrs[options.attribute];
|
|
275
|
+
|
|
276
|
+
return componentPath;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return findPathFromTag(currentNode.tag, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function applyPluginsToTree(tree, plugins) {
|
|
283
|
+
return plugins.reduce((tree, plugin) => {
|
|
284
|
+
tree = plugin(tree);
|
|
285
|
+
return tree;
|
|
286
|
+
}, tree);
|
|
287
|
+
}
|
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, (tagName, modifier) => {
|
|
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
|
+
};
|