/
utils.js
684 lines (621 loc) · 20.1 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
import {
BINDING_REDUNDANT_ATTRIBUTE_KEY,
BINDING_SELECTOR_KEY,
BINDING_SELECTOR_PREFIX,
BINDING_TEMPLATE_KEY,
BINDING_TYPES,
EACH_DIRECTIVE,
EXPRESSION_TYPES,
GET_COMPONENT_FN,
IF_DIRECTIVE,
IS_BOOLEAN_ATTRIBUTE,
IS_DIRECTIVE,
KEY_ATTRIBUTE,
SCOPE,
SLOT_ATTRIBUTE,
TEMPLATE_FN,
TEXT_NODE_EXPRESSION_PLACEHOLDER,
} from './constants.js'
import { builders, types } from '../../utils/build-types.js'
import { findIsAttribute, findStaticAttributes } from './find.js'
import {
hasExpressions,
isGlobal,
isTagNode,
isTextNode,
isVoidNode,
} from './checks.js'
import {
isIdentifier,
isLiteral,
isMemberExpression,
isObjectExpression,
} from '../../utils/ast-nodes-checks.js'
import { nullNode, simplePropertyNode } from '../../utils/custom-ast-nodes.js'
import addLinesOffset from '../../utils/add-lines-offset.js'
import compose from 'cumpa'
import { createExpression } from './expressions/index.js'
import encodeHTMLEntities from '../../utils/html-entities/encode.js'
import generateAST from '../../utils/generate-ast.js'
import unescapeChar from '../../utils/unescape-char.js'
const scope = builders.identifier(SCOPE)
export const getName = (node) => (node && node.name ? node.name : node)
/**
* Replace the path scope with a member Expression
* @param { types.NodePath } path - containing the current node visited
* @param { types.Node } property - node we want to prefix with the scope identifier
* @returns {undefined} this is a void function
*/
function replacePathScope(path, property) {
// make sure that for the scope injection the extra parenthesis get removed
removeExtraParenthesis(property)
path.replace(builders.memberExpression(scope, property, false))
}
/**
* Change the nodes scope adding the `scope` prefix
* @param { types.NodePath } path - containing the current node visited
* @returns { boolean } return false if we want to stop the tree traversal
* @context { types.visit }
*/
function updateNodeScope(path) {
if (!isGlobal(path)) {
replacePathScope(path, path.node)
return false
}
this.traverse(path)
}
/**
* Change the scope of the member expressions
* @param { types.NodePath } path - containing the current node visited
* @returns { boolean } return always false because we want to check only the first node object
*/
function visitMemberExpression(path) {
const traversePathObject = () => this.traverse(path.get('object'))
const currentObject = path.node.object
switch (true) {
case isGlobal(path):
if (currentObject.arguments && currentObject.arguments.length) {
traversePathObject()
}
break
case !path.value.computed && isIdentifier(currentObject):
replacePathScope(path, path.node)
break
default:
this.traverse(path)
}
return false
}
/**
* Objects properties should be handled a bit differently from the Identifier
* @param { types.NodePath } path - containing the current node visited
* @returns { boolean } return false if we want to stop the tree traversal
*/
function visitObjectProperty(path) {
const value = path.node.value
const isShorthand = path.node.shorthand
if (isIdentifier(value) || isMemberExpression(value) || isShorthand) {
// disable shorthand object properties
if (isShorthand) path.node.shorthand = false
updateNodeScope.call(this, path.get('value'))
} else {
this.traverse(path.get('value'))
}
return false
}
/**
* The this expressions should be replaced with the scope
* @param { types.NodePath } path - containing the current node visited
* @returns { boolean|undefined } return false if we want to stop the tree traversal
*/
function visitThisExpression(path) {
path.replace(scope)
this.traverse(path)
}
/**
* Replace the identifiers with the node scope
* @param { types.NodePath } path - containing the current node visited
* @returns { boolean|undefined } return false if we want to stop the tree traversal
*/
function visitIdentifier(path) {
const parentValue = path.parent.value
if (
(!isMemberExpression(parentValue) &&
// Esprima seem to behave differently from the default recast ast parser
// fix for https://github.com/riot/riot/issues/2983
parentValue.key !== path.node) ||
parentValue.computed
) {
updateNodeScope.call(this, path)
}
return false
}
/**
* Update the scope of the global nodes
* @param { Object } ast - ast program
* @returns { Object } the ast program with all the global nodes updated
*/
export function updateNodesScope(ast) {
const ignorePath = () => false
types.visit(ast, {
visitIdentifier,
visitMemberExpression,
visitObjectProperty,
visitThisExpression,
visitClassExpression: ignorePath,
})
return ast
}
/**
* Convert any expression to an AST tree
* @param { Object } expression - expression parsed by the riot parser
* @param { string } sourceFile - original tag file
* @param { string } sourceCode - original tag source code
* @returns { Object } the ast generated
*/
export function createASTFromExpression(expression, sourceFile, sourceCode) {
const code = sourceFile
? addLinesOffset(expression.text, sourceCode, expression)
: expression.text
return generateAST(`(${code})`, {
sourceFileName: sourceFile,
})
}
/**
* Create the bindings template property
* @param {Array} args - arguments to pass to the template function
* @returns {ASTNode} a binding template key
*/
export function createTemplateProperty(args) {
return simplePropertyNode(
BINDING_TEMPLATE_KEY,
args ? callTemplateFunction(...args) : nullNode(),
)
}
/**
* Try to get the expression of an attribute node
* @param { RiotParser.Node.Attribute } attribute - riot parser attribute node
* @returns { RiotParser.Node.Expression } attribute expression value
*/
export function getAttributeExpression(attribute) {
return attribute.expressions
? attribute.expressions[0]
: {
// if no expression was found try to typecast the attribute value
...attribute,
text: attribute.value,
}
}
/**
* Wrap the ast generated in a function call providing the scope argument
* @param {Object} ast - function body
* @returns {FunctionExpresion} function having the scope argument injected
*/
export function wrapASTInFunctionWithScope(ast) {
const fn = builders.arrowFunctionExpression([scope], ast)
// object expressions need to be wrapped in parentheses
// recast doesn't allow it
// see also https://github.com/benjamn/recast/issues/985
if (isObjectExpression(ast)) {
// doing a small hack here
// trying to figure out how the recast printer works internally
ast.extra = {
parenthesized: true,
}
}
return fn
}
/**
* Convert any parser option to a valid template one
* @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser
* @param { string } sourceFile - original tag file
* @param { string } sourceCode - original tag source code
* @returns { Object } a FunctionExpression object
*
* @example
* toScopedFunction('foo + bar') // scope.foo + scope.bar
*
* @example
* toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar
*/
export function toScopedFunction(expression, sourceFile, sourceCode) {
return compose(wrapASTInFunctionWithScope, transformExpression)(
expression,
sourceFile,
sourceCode,
)
}
/**
* Transform an expression node updating its global scope
* @param {RiotParser.Node.Expr} expression - riot parser expression node
* @param {string} sourceFile - source file
* @param {string} sourceCode - source code
* @returns {ASTExpression} ast expression generated from the riot parser expression node
*/
export function transformExpression(expression, sourceFile, sourceCode) {
return compose(
removeExtraParenthesis,
getExpressionAST,
updateNodesScope,
createASTFromExpression,
)(expression, sourceFile, sourceCode)
}
/**
* Remove the extra parents from the compiler generated expressions
* @param {AST.Expression} expr - ast expression
* @returns {AST.Expression} program expression output without parenthesis
*/
export function removeExtraParenthesis(expr) {
if (expr.extra) expr.extra.parenthesized = false
return expr
}
/**
* Get the parsed AST expression of riot expression node
* @param {AST.Program} sourceAST - raw node parsed
* @returns {AST.Expression} program expression output
*/
export function getExpressionAST(sourceAST) {
const astBody = sourceAST.program.body
return astBody[0] ? astBody[0].expression : astBody
}
/**
* Create the template call function
* @param {Array|string|Node.Literal} template - template string
* @param {Array<AST.Nodes>} bindings - template bindings provided as AST nodes
* @returns {Node.CallExpression} template call expression
*/
export function callTemplateFunction(template, bindings) {
return builders.callExpression(builders.identifier(TEMPLATE_FN), [
template ? builders.literal(template) : nullNode(),
bindings ? builders.arrayExpression(bindings) : nullNode(),
])
}
/**
* Create the template wrapper function injecting the dependencies needed to render the component html
* @param {Array<AST.Nodes>|AST.BlockStatement} body - function body
* @returns {AST.Node} arrow function expression
*/
export const createTemplateDependenciesInjectionWrapper = (body) =>
builders.arrowFunctionExpression(
[TEMPLATE_FN, EXPRESSION_TYPES, BINDING_TYPES, GET_COMPONENT_FN].map(
builders.identifier,
),
body,
)
/**
* Convert any DOM attribute into a valid DOM selector useful for the querySelector API
* @param { string } attributeName - name of the attribute to query
* @returns { string } the attribute transformed to a query selector
*/
export const attributeNameToDOMQuerySelector = (attributeName) =>
`[${attributeName}]`
/**
* Create the properties to query a DOM node
* @param { string } attributeName - attribute name needed to identify a DOM node
* @returns { Array<AST.Node> } array containing the selector properties needed for the binding
*/
export function createSelectorProperties(attributeName) {
return attributeName
? [
simplePropertyNode(
BINDING_REDUNDANT_ATTRIBUTE_KEY,
builders.literal(attributeName),
),
simplePropertyNode(
BINDING_SELECTOR_KEY,
compose(
builders.literal,
attributeNameToDOMQuerySelector,
)(attributeName),
),
]
: []
}
/**
* Clone the node filtering out the selector attribute from the attributes list
* @param {RiotParser.Node} node - riot parser node
* @param {string} selectorAttribute - name of the selector attribute to filter out
* @returns {RiotParser.Node} the node with the attribute cleaned up
*/
export function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) {
return {
...node,
attributes: getAttributesWithoutSelector(
getNodeAttributes(node),
selectorAttribute,
),
}
}
/**
* Get the node attributes without the selector one
* @param {Array<RiotParser.Attr>} attributes - attributes list
* @param {string} selectorAttribute - name of the selector attribute to filter out
* @returns {Array<RiotParser.Attr>} filtered attributes
*/
export function getAttributesWithoutSelector(attributes, selectorAttribute) {
if (selectorAttribute)
return attributes.filter(
(attribute) => attribute.name !== selectorAttribute,
)
return attributes
}
/**
* Clean binding or custom attributes
* @param {RiotParser.Node} node - riot parser node
* @returns {Array<RiotParser.Node.Attr>} only the attributes that are not bindings or directives
*/
export function cleanAttributes(node) {
return getNodeAttributes(node).filter(
(attribute) =>
![
IF_DIRECTIVE,
EACH_DIRECTIVE,
KEY_ATTRIBUTE,
SLOT_ATTRIBUTE,
IS_DIRECTIVE,
].includes(attribute.name),
)
}
/**
* Root node factory function needed for the top root nodes and the nested ones
* @param {RiotParser.Node} node - riot parser node
* @returns {RiotParser.Node} root node
*/
export function rootNodeFactory(node) {
return {
nodes: getChildrenNodes(node),
isRoot: true,
}
}
/**
* Create a root node proxing only its nodes and attributes
* @param {RiotParser.Node} node - riot parser node
* @returns {RiotParser.Node} root node
*/
export function createRootNode(node) {
return {
...rootNodeFactory(node),
attributes: compose(
// root nodes should always have attribute expressions
transformStaticAttributesIntoExpressions,
// root nodes shouldn't have directives
cleanAttributes,
)(node),
}
}
/**
* Create nested root node. Each and If directives create nested root nodes for example
* @param {RiotParser.Node} node - riot parser node
* @returns {RiotParser.Node} root node
*/
export function createNestedRootNode(node) {
return {
...rootNodeFactory(node),
attributes: cleanAttributes(node),
}
}
/**
* Transform the static node attributes into expressions, useful for the root nodes
* @param {Array<RiotParser.Node.Attr>} attributes - riot parser node
* @returns {Array<RiotParser.Node.Attr>} all the attributes received as attribute expressions
*/
export function transformStaticAttributesIntoExpressions(attributes) {
return attributes.map((attribute) => {
if (attribute.expressions) return attribute
return {
...attribute,
expressions: [
{
start: attribute.valueStart,
end: attribute.end,
text: `'${
attribute.value
? attribute.value
: // boolean attributes should be treated differently
attribute[IS_BOOLEAN_ATTRIBUTE]
? attribute.name
: ''
}'`,
},
],
}
})
}
/**
* Get all the child nodes of a RiotParser.Node
* @param {RiotParser.Node} node - riot parser node
* @returns {Array<RiotParser.Node>} all the child nodes found
*/
export function getChildrenNodes(node) {
return node && node.nodes ? node.nodes : []
}
/**
* Get all the attributes of a riot parser node
* @param {RiotParser.Node} node - riot parser node
* @returns {Array<RiotParser.Node.Attribute>} all the attributes find
*/
export function getNodeAttributes(node) {
return node.attributes ? node.attributes : []
}
/**
* Create custom tag name function
* @param {RiotParser.Node} node - riot parser node
* @param {string} sourceFile - original tag file
* @param {string} sourceCode - original tag source code
* @returns {RiotParser.Node.Attr} the node name as expression attribute
*/
export function createCustomNodeNameEvaluationFunction(
node,
sourceFile,
sourceCode,
) {
const isAttribute = findIsAttribute(node)
const toRawString = (val) => `'${val}'`
if (isAttribute) {
return isAttribute.expressions
? wrapASTInFunctionWithScope(
mergeAttributeExpressions(isAttribute, sourceFile, sourceCode),
)
: toScopedFunction(
{
...isAttribute,
text: toRawString(isAttribute.value),
},
sourceFile,
sourceCode,
)
}
return toScopedFunction(
{ ...node, text: toRawString(getName(node)) },
sourceFile,
sourceCode,
)
}
/**
* Convert all the node static attributes to strings
* @param {RiotParser.Node} node - riot parser node
* @returns {string} all the node static concatenated as string
*/
export function staticAttributesToString(node) {
return findStaticAttributes(node)
.map((attribute) =>
attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value
? attribute.name
: `${attribute.name}="${unescapeNode(attribute, 'value').value}"`,
)
.join(' ')
}
/**
* Make sure that node escaped chars will be unescaped
* @param {RiotParser.Node} node - riot parser node
* @param {string} key - key property to unescape
* @returns {RiotParser.Node} node with the text property unescaped
*/
export function unescapeNode(node, key) {
if (node.unescape) {
return {
...node,
[key]: unescapeChar(node[key], node.unescape),
}
}
return node
}
/**
* Convert a riot parser opening node into a string
* @param {RiotParser.Node} node - riot parser node
* @returns {string} the node as string
*/
export function nodeToString(node) {
const attributes = staticAttributesToString(node)
switch (true) {
case isTagNode(node):
return `<${node.name}${attributes ? ` ${attributes}` : ''}${
isVoidNode(node) ? '/' : ''
}>`
case isTextNode(node):
return hasExpressions(node)
? TEXT_NODE_EXPRESSION_PLACEHOLDER
: unescapeNode(node, 'text').text
default:
return node.text || ''
}
}
/**
* Close an html node
* @param {RiotParser.Node} node - riot parser node
* @returns {string} the closing tag of the html tag node passed to this function
*/
export function closeTag(node) {
return node.name ? `</${node.name}>` : ''
}
/**
* Create a strings array with the `join` call to transform it into a string
* @param {Array} stringsArray - array containing all the strings to concatenate
* @returns {AST.CallExpression} array with a `join` call
*/
export function createArrayString(stringsArray) {
return builders.callExpression(
builders.memberExpression(
builders.arrayExpression(stringsArray),
builders.identifier('join'),
false,
),
[builders.literal('')],
)
}
/**
* Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}""
* This helper aims to merge them in a template literal if it's necessary
* @param {RiotParser.Attr} node - riot parser node
* @param {string} sourceFile - original tag file
* @param {string} sourceCode - original tag source code
* @returns { Object } a template literal expression object
*/
export function mergeAttributeExpressions(node, sourceFile, sourceCode) {
if (!node.parts || node.parts.length === 1) {
return transformExpression(node.expressions[0], sourceFile, sourceCode)
}
const stringsArray = [
...node.parts.reduce((acc, str) => {
const expression = node.expressions.find((e) => e.text.trim() === str)
return [
...acc,
expression
? transformExpression(expression, sourceFile, sourceCode)
: builders.literal(encodeHTMLEntities(str)),
]
}, []),
].filter((expr) => !isLiteral(expr) || expr.value)
return createArrayString(stringsArray)
}
/**
* Create a selector that will be used to find the node via dom-bindings
* @param {number} id - temporary variable that will be increased anytime this function will be called
* @returns {string} selector attribute needed to bind a riot expression
*/
export const createBindingSelector = (function createSelector(id = 0) {
return () => `${BINDING_SELECTOR_PREFIX}${id++}`
})()
/**
* Create the AST array containing the attributes to bind to this node
* @param { RiotParser.Node.Tag } sourceNode - the custom tag
* @param { string } selectorAttribute - attribute needed to select the target node
* @param { string } sourceFile - source file path
* @param { string } sourceCode - original source
* @returns {AST.ArrayExpression} array containing the slot objects
*/
export function createBindingAttributes(
sourceNode,
selectorAttribute,
sourceFile,
sourceCode,
) {
return builders.arrayExpression([
...compose(
(attributes) =>
attributes.map((attribute) =>
createExpression(attribute, sourceFile, sourceCode, 0, sourceNode),
),
(attributes) => attributes.filter(hasExpressions),
(attributes) =>
getAttributesWithoutSelector(attributes, selectorAttribute),
cleanAttributes,
)(sourceNode),
])
}
/**
* Create an attribute evaluation function
* @param {RiotParser.Attr} sourceNode - riot parser node
* @param {string} sourceFile - original tag file
* @param {string} sourceCode - original tag source code
* @returns { AST.Node } an AST function expression to evaluate the attribute value
*/
export function createAttributeEvaluationFunction(
sourceNode,
sourceFile,
sourceCode,
) {
return wrapASTInFunctionWithScope(
mergeAttributeExpressions(sourceNode, sourceFile, sourceCode),
)
}