@katabatic/compiler 1.1.5 → 1.1.7
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/package.json +5 -1
- package/src/analyse/visitors/CustomElement.js +2 -0
- package/src/analyse/visitors/Element.js +4 -6
- package/src/builders.js +83 -10
- package/src/checkers.js +26 -6
- package/src/css-matcher.js +11 -11
- package/src/css.js +0 -54
- package/src/exp-matcher.js +12 -5
- package/src/parser/each-block.js +7 -3
- package/src/parser/element.js +29 -2
- package/src/parser/if-block.js +5 -1
- package/src/parser/parser.js +12 -18
- package/src/parser/root.js +1 -1
- package/src/parser/tokentype.js +21 -5
- package/src/transform/context.js +3 -5
- package/src/transform/visitors/Attribute.js +62 -38
- package/src/transform/visitors/CustomElement.js +10 -9
- package/src/transform/visitors/EachBlock.js +15 -5
- package/src/transform/visitors/Element.js +12 -31
- package/src/transform/visitors/IfBlock.js +15 -5
- package/src/transform/visitors/Template.js +7 -5
- package/test/parser/index.test.js +17 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@katabatic/compiler",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"import": "./src/router/index.js"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "vitest"
|
|
21
|
+
|
|
22
|
+
},
|
|
19
23
|
"devDependencies": {
|
|
20
24
|
"@types/css-tree": "^2.3.10",
|
|
21
25
|
"@types/node": "^22.15.21",
|
|
@@ -4,7 +4,9 @@ export function CustomElement(node, ctx) {
|
|
|
4
4
|
ctx.next()
|
|
5
5
|
|
|
6
6
|
const hasClass = node.attributes.some(is.classAttribute)
|
|
7
|
+
const hasExpressionAttribute = node.attributes.some(is.expressionAttribute)
|
|
7
8
|
|
|
8
9
|
node.metadata ??= {}
|
|
9
10
|
node.metadata.hasClass = hasClass
|
|
11
|
+
node.metadata.hasExpressionAttribute = hasExpressionAttribute
|
|
10
12
|
}
|
|
@@ -3,14 +3,12 @@ import * as is from '../../checkers.js'
|
|
|
3
3
|
export function Element(node, ctx) {
|
|
4
4
|
ctx.next()
|
|
5
5
|
|
|
6
|
+
const isVoid = is.voidElement(node)
|
|
6
7
|
const hasClass = node.attributes.some(is.classAttribute)
|
|
7
|
-
|
|
8
|
-
const bindAttribute = node.attributes.find(is.bindAttribute)
|
|
9
|
-
const hasBinding = !!bindAttribute
|
|
10
|
-
const bindExpression = bindAttribute?.value[0].expression
|
|
8
|
+
const hasExpressionAttribute = node.attributes.some(is.expressionAttribute)
|
|
11
9
|
|
|
12
10
|
node.metadata ??= {}
|
|
11
|
+
node.metadata.isVoid = isVoid
|
|
13
12
|
node.metadata.hasClass = hasClass
|
|
14
|
-
node.metadata.
|
|
15
|
-
node.metadata.bindExpression = bindExpression
|
|
13
|
+
node.metadata.hasExpressionAttribute = hasExpressionAttribute
|
|
16
14
|
}
|
package/src/builders.js
CHANGED
|
@@ -227,6 +227,10 @@ export function array(elements) {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
export function call(callee, args = [], { optional = false } = {}) {
|
|
230
|
+
if (typeof callee === 'string') {
|
|
231
|
+
callee = { type: 'Identifier', name: callee }
|
|
232
|
+
}
|
|
233
|
+
|
|
230
234
|
return { type: 'CallExpression', callee, arguments: args, optional }
|
|
231
235
|
}
|
|
232
236
|
|
|
@@ -577,14 +581,7 @@ export function $set(object, nodeId, attribute, value) {
|
|
|
577
581
|
computed: false,
|
|
578
582
|
optional: false
|
|
579
583
|
},
|
|
580
|
-
arguments: [
|
|
581
|
-
nodeId,
|
|
582
|
-
{
|
|
583
|
-
type: 'Literal',
|
|
584
|
-
value: attribute
|
|
585
|
-
},
|
|
586
|
-
value
|
|
587
|
-
],
|
|
584
|
+
arguments: [nodeId, attribute, value],
|
|
588
585
|
optional: false
|
|
589
586
|
}
|
|
590
587
|
}
|
|
@@ -1064,6 +1061,82 @@ export function $effect(body) {
|
|
|
1064
1061
|
}
|
|
1065
1062
|
}
|
|
1066
1063
|
|
|
1064
|
+
export function $bind(elementId, body) {
|
|
1065
|
+
return {
|
|
1066
|
+
type: 'ExpressionStatement',
|
|
1067
|
+
expression: {
|
|
1068
|
+
type: 'CallExpression',
|
|
1069
|
+
callee: {
|
|
1070
|
+
type: 'MemberExpression',
|
|
1071
|
+
object: {
|
|
1072
|
+
type: 'MemberExpression',
|
|
1073
|
+
object: {
|
|
1074
|
+
type: 'ThisExpression'
|
|
1075
|
+
},
|
|
1076
|
+
property: {
|
|
1077
|
+
type: 'Identifier',
|
|
1078
|
+
name: '$'
|
|
1079
|
+
},
|
|
1080
|
+
computed: false,
|
|
1081
|
+
optional: false
|
|
1082
|
+
},
|
|
1083
|
+
property: {
|
|
1084
|
+
type: 'Identifier',
|
|
1085
|
+
name: 'bind'
|
|
1086
|
+
},
|
|
1087
|
+
computed: false,
|
|
1088
|
+
optional: false
|
|
1089
|
+
},
|
|
1090
|
+
arguments: [
|
|
1091
|
+
elementId,
|
|
1092
|
+
{
|
|
1093
|
+
type: 'ArrowFunctionExpression',
|
|
1094
|
+
id: null,
|
|
1095
|
+
expression: false,
|
|
1096
|
+
generator: false,
|
|
1097
|
+
async: false,
|
|
1098
|
+
params: [{ type: 'Identifier', name: 'opts' }],
|
|
1099
|
+
body
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
type: 'Identifier',
|
|
1103
|
+
name: '$'
|
|
1104
|
+
}
|
|
1105
|
+
],
|
|
1106
|
+
optional: false
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export function $getBinding(elementId) {
|
|
1112
|
+
return {
|
|
1113
|
+
type: 'CallExpression',
|
|
1114
|
+
callee: {
|
|
1115
|
+
type: 'MemberExpression',
|
|
1116
|
+
object: {
|
|
1117
|
+
type: 'MemberExpression',
|
|
1118
|
+
object: {
|
|
1119
|
+
type: 'ThisExpression'
|
|
1120
|
+
},
|
|
1121
|
+
property: {
|
|
1122
|
+
type: 'Identifier',
|
|
1123
|
+
name: '$'
|
|
1124
|
+
},
|
|
1125
|
+
computed: false,
|
|
1126
|
+
optional: false
|
|
1127
|
+
},
|
|
1128
|
+
property: {
|
|
1129
|
+
type: 'Identifier',
|
|
1130
|
+
name: 'getBinding'
|
|
1131
|
+
},
|
|
1132
|
+
computed: false,
|
|
1133
|
+
optional: false
|
|
1134
|
+
},
|
|
1135
|
+
arguments: [elementId],
|
|
1136
|
+
optional: false
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1067
1140
|
export function $animate(value, body) {
|
|
1068
1141
|
return {
|
|
1069
1142
|
type: 'ExpressionStatement',
|
|
@@ -1099,7 +1172,7 @@ export function $animate(value, body) {
|
|
|
1099
1172
|
}
|
|
1100
1173
|
}
|
|
1101
1174
|
|
|
1102
|
-
export function addEventListener(object, value, body) {
|
|
1175
|
+
export function addEventListener(object, value, body, { optional = false } = {}) {
|
|
1103
1176
|
return {
|
|
1104
1177
|
type: 'ExpressionStatement',
|
|
1105
1178
|
expression: {
|
|
@@ -1134,7 +1207,7 @@ export function addEventListener(object, value, body) {
|
|
|
1134
1207
|
}
|
|
1135
1208
|
}
|
|
1136
1209
|
],
|
|
1137
|
-
optional
|
|
1210
|
+
optional
|
|
1138
1211
|
}
|
|
1139
1212
|
}
|
|
1140
1213
|
}
|
package/src/checkers.js
CHANGED
|
@@ -121,6 +121,10 @@ export function classAttribute(node, withExpressionTag) {
|
|
|
121
121
|
return result
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
export function expressionAttribute(node) {
|
|
125
|
+
return node.type === 'Attribute' && node.value[0]?.type === 'ExpressionTag'
|
|
126
|
+
}
|
|
127
|
+
|
|
124
128
|
export function idAttribute(node, withExpressionTag) {
|
|
125
129
|
let result = node.type === 'Attribute' && node.name === 'id'
|
|
126
130
|
if (withExpressionTag === true) {
|
|
@@ -129,12 +133,6 @@ export function idAttribute(node, withExpressionTag) {
|
|
|
129
133
|
return result
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
export function bindAttribute(node) {
|
|
133
|
-
return (
|
|
134
|
-
node.type === 'Attribute' && node.name === 'bind' && node.value[0]?.type === 'ExpressionTag'
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
136
|
export function staticAttribute(node) {
|
|
139
137
|
return node.type === 'Attribute' && node.name === 'static'
|
|
140
138
|
}
|
|
@@ -146,3 +144,25 @@ export function shadowRootModeAttribute(node) {
|
|
|
146
144
|
export function elseIfBlock(node) {
|
|
147
145
|
return node.type === 'IfBlock' && node.elseif
|
|
148
146
|
}
|
|
147
|
+
|
|
148
|
+
export function voidElement(node) {
|
|
149
|
+
return (
|
|
150
|
+
node.type === 'Element' &&
|
|
151
|
+
[
|
|
152
|
+
'area',
|
|
153
|
+
'base',
|
|
154
|
+
'br',
|
|
155
|
+
'col',
|
|
156
|
+
'embed',
|
|
157
|
+
'hr',
|
|
158
|
+
'img',
|
|
159
|
+
'input',
|
|
160
|
+
'link',
|
|
161
|
+
'meta',
|
|
162
|
+
'param',
|
|
163
|
+
'source',
|
|
164
|
+
'track',
|
|
165
|
+
'wbr'
|
|
166
|
+
].includes(node.name)
|
|
167
|
+
)
|
|
168
|
+
}
|
package/src/css-matcher.js
CHANGED
|
@@ -54,30 +54,30 @@ function matchSelectors(selectors, template) {
|
|
|
54
54
|
classAttribute ??= node.attributes.find((a) => a.name === 'class')
|
|
55
55
|
scopedElement = node
|
|
56
56
|
scopedClassAttribute = classAttribute
|
|
57
|
-
break
|
|
58
57
|
}
|
|
59
|
-
|
|
58
|
+
break
|
|
60
59
|
case 'PseudoClassSelector':
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
if (selector.name !== 'host') {
|
|
61
|
+
classAttribute ??= node.attributes.find((a) => a.name === 'class')
|
|
62
|
+
scopedElement = node
|
|
63
|
+
scopedClassAttribute = classAttribute
|
|
64
|
+
}
|
|
64
65
|
break
|
|
65
66
|
case 'IdSelector':
|
|
66
67
|
const idAttribute = node.attributes.find((a) => a.name === 'id')
|
|
67
68
|
if (idAttribute?.value[0].data === selector.name) {
|
|
68
69
|
scopedIdAttribute = idAttribute
|
|
69
|
-
break
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
break
|
|
72
72
|
case 'ClassSelector':
|
|
73
73
|
classAttribute ??= node.attributes.find((a) => a.name === 'class')
|
|
74
|
-
const
|
|
74
|
+
const expression = classAttribute?.value[0].expression
|
|
75
|
+
const classes = classAttribute?.value[0].data?.split(/\s+/)
|
|
75
76
|
|
|
76
|
-
if (classes?.includes(selector.name)) {
|
|
77
|
+
if (expression || classes?.includes(selector.name)) {
|
|
77
78
|
scopedClassAttribute = classAttribute
|
|
78
|
-
break
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
break
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
package/src/css.js
CHANGED
|
@@ -1,57 +1,3 @@
|
|
|
1
|
-
export function match(element, stylesheet) {
|
|
2
|
-
for (const rule of stylesheet.children) {
|
|
3
|
-
if (matchSelectorList(element, rule.prelude)) return true
|
|
4
|
-
}
|
|
5
|
-
return false
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function matchSelectorList(element, selectorList) {
|
|
9
|
-
if (selectorList) {
|
|
10
|
-
for (const selector of selectorList.children) {
|
|
11
|
-
if (matchSelector(element, selector)) return true
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return false
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function matchSelector(element, selector) {
|
|
18
|
-
let selectors = []
|
|
19
|
-
for (const child of selector.children) {
|
|
20
|
-
switch (child.type) {
|
|
21
|
-
case 'TypeSelector':
|
|
22
|
-
case 'ClassSelector':
|
|
23
|
-
selectors.push(child)
|
|
24
|
-
break
|
|
25
|
-
case 'PseudoClassSelector':
|
|
26
|
-
if (matchSelectorList(element, child.children?.first)) return true
|
|
27
|
-
selectors.push(child)
|
|
28
|
-
break
|
|
29
|
-
case 'Combinator':
|
|
30
|
-
if (matchSelectors(element, selectors)) return true
|
|
31
|
-
selectors = []
|
|
32
|
-
break
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return matchSelectors(element, selectors)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function matchSelectors(element, selectors) {
|
|
39
|
-
if (selectors.length === 0) return false
|
|
40
|
-
|
|
41
|
-
for (const selector of selectors) {
|
|
42
|
-
switch (selector.type) {
|
|
43
|
-
case 'TypeSelector':
|
|
44
|
-
if (selector.name !== element.name) return false
|
|
45
|
-
break
|
|
46
|
-
case 'ClassSelector':
|
|
47
|
-
const classAtt = element.attributes.find((a) => a.name === 'class')?.value.data
|
|
48
|
-
const classes = classAtt?.split(/\s+/)
|
|
49
|
-
if (!classes?.includes(selector.name)) return false
|
|
50
|
-
break
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return true
|
|
54
|
-
}
|
|
55
1
|
|
|
56
2
|
/**
|
|
57
3
|
* Dead simple clx assuming class2 is always set
|
package/src/exp-matcher.js
CHANGED
|
@@ -12,21 +12,28 @@ export function matchExpression(expression, program, blocks) {
|
|
|
12
12
|
if (parentNode.callee === node) {
|
|
13
13
|
setMethodMetadata(node)
|
|
14
14
|
} else {
|
|
15
|
-
setMetadata(node)
|
|
15
|
+
setMetadata(node, blocks)
|
|
16
16
|
}
|
|
17
17
|
break
|
|
18
18
|
case 'MemberExpression':
|
|
19
19
|
if (parentNode.object === node) {
|
|
20
|
+
setMetadata(node, blocks)
|
|
21
|
+
}
|
|
22
|
+
break
|
|
23
|
+
case 'AssignmentExpression':
|
|
24
|
+
if (parentNode.left === node) {
|
|
20
25
|
setMetadata(node)
|
|
26
|
+
} else {
|
|
27
|
+
setMetadata(node, blocks)
|
|
21
28
|
}
|
|
22
29
|
break
|
|
23
30
|
case 'Property':
|
|
24
31
|
if (parentNode.value === node) {
|
|
25
|
-
setMetadata(node)
|
|
32
|
+
setMetadata(node, blocks)
|
|
26
33
|
}
|
|
27
34
|
break
|
|
28
35
|
default:
|
|
29
|
-
setMetadata(node)
|
|
36
|
+
setMetadata(node, blocks)
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
})
|
|
@@ -49,10 +56,10 @@ export function matchExpression(expression, program, blocks) {
|
|
|
49
56
|
node.metadata.isData = true
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
function setMetadata(node) {
|
|
59
|
+
function setMetadata(node, blocks) {
|
|
53
60
|
node.metadata ??= {}
|
|
54
61
|
|
|
55
|
-
if (blocks
|
|
62
|
+
if (blocks?.some((b) => b.context?.name === node.name)) {
|
|
56
63
|
node.metadata.isBlockVar = true
|
|
57
64
|
return
|
|
58
65
|
}
|
package/src/parser/each-block.js
CHANGED
|
@@ -13,10 +13,10 @@ export function parseEachBlock(p) {
|
|
|
13
13
|
p.expectToken([TokenTypes.braceLHash, TokenTypes.braceLColumn])
|
|
14
14
|
p.expectToken([TokenTypes.name])
|
|
15
15
|
const name = p.value
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
p.skipWhitespaces()
|
|
18
18
|
const expression = parseExpression(p)
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
p.skipWhitespaces()
|
|
21
21
|
p.expectToken([TokenTypes.name])
|
|
22
22
|
const as = p.value
|
|
@@ -43,7 +43,11 @@ export function parseEachBlock(p) {
|
|
|
43
43
|
p.skipWhitespaces()
|
|
44
44
|
p.expectToken([TokenTypes.braceR])
|
|
45
45
|
|
|
46
|
-
if (
|
|
46
|
+
if (blockNameClose !== name) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`wrong closing block ${blockNameClose} at position ${p.pos}, expected ${name}`
|
|
49
|
+
)
|
|
50
|
+
}
|
|
47
51
|
|
|
48
52
|
return { type: 'EachBlock', expression, context, key, body, start, end: p.pos }
|
|
49
53
|
}
|
package/src/parser/element.js
CHANGED
|
@@ -30,13 +30,32 @@ export function parseElement(p) {
|
|
|
30
30
|
p.skipWhitespaces()
|
|
31
31
|
p.expectToken([TokenTypes.gte])
|
|
32
32
|
|
|
33
|
-
if (
|
|
33
|
+
if (tagNameClose !== name) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`wrong closing tag ${tagNameClose} at position ${p.pos}, expected ${name}`
|
|
36
|
+
)
|
|
37
|
+
}
|
|
34
38
|
|
|
35
39
|
return { type, name, attributes, fragment, start, end: p.pos }
|
|
36
40
|
}
|
|
37
41
|
return { type, name, attributes, fragment: { type: 'Fragment', nodes: [] }, start, end: p.pos }
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param {Parser} p
|
|
47
|
+
* @returns
|
|
48
|
+
*/
|
|
49
|
+
export function parseComment(p) {
|
|
50
|
+
const start = p.pos
|
|
51
|
+
p.expectToken([TokenTypes.lteExclHyph2])
|
|
52
|
+
p.expectToken([TokenTypes.commentText])
|
|
53
|
+
const data = p.value
|
|
54
|
+
p.expectToken([TokenTypes.hyph2Gte])
|
|
55
|
+
|
|
56
|
+
return { type: 'Comment', data, start, end: p.pos }
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
/**
|
|
41
60
|
*
|
|
42
61
|
* @param {Parser} p
|
|
@@ -47,7 +66,12 @@ export function parseFragment(p, allowScript = false, allowStyle = false) {
|
|
|
47
66
|
|
|
48
67
|
p.skipWhitespaces()
|
|
49
68
|
while (!p.peakToken([TokenTypes.lteSlash, TokenTypes.braceLSlash, TokenTypes.braceLColumn])) {
|
|
50
|
-
const punctToken = p.peakToken([
|
|
69
|
+
const punctToken = p.peakToken([
|
|
70
|
+
TokenTypes.lteExclHyph2,
|
|
71
|
+
TokenTypes.lte,
|
|
72
|
+
TokenTypes.braceLHash,
|
|
73
|
+
TokenTypes.braceL
|
|
74
|
+
])
|
|
51
75
|
const nameToken = p.peakToken([TokenTypes.name], punctToken)
|
|
52
76
|
|
|
53
77
|
switch (punctToken?.type) {
|
|
@@ -67,6 +91,9 @@ export function parseFragment(p, allowScript = false, allowStyle = false) {
|
|
|
67
91
|
|
|
68
92
|
nodes.push(parseElement(p))
|
|
69
93
|
break
|
|
94
|
+
case TokenTypes.lteExclHyph2:
|
|
95
|
+
nodes.push(parseComment(p))
|
|
96
|
+
break
|
|
70
97
|
case TokenTypes.braceLHash:
|
|
71
98
|
if (nameToken?.value === 'if') {
|
|
72
99
|
nodes.push(parseIfBlock(p))
|
package/src/parser/if-block.js
CHANGED
|
@@ -49,7 +49,11 @@ export function parseIfBlock(p, elseif = false) {
|
|
|
49
49
|
p.skipWhitespaces()
|
|
50
50
|
p.expectToken([TokenTypes.braceR])
|
|
51
51
|
|
|
52
|
-
if (
|
|
52
|
+
if (blockNameClose !== name) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`wrong closing block ${blockNameClose} at position ${p.pos}, expected ${name}`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
return { type: 'IfBlock', elseif, test, consequent, alternate, start, end: p.pos }
|
package/src/parser/parser.js
CHANGED
|
@@ -61,7 +61,11 @@ export class Parser {
|
|
|
61
61
|
if (this.readStringToken(type)) return
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
throw new Error(
|
|
64
|
+
throw new Error(
|
|
65
|
+
`wrong token ${this.input[this.pos]} at position ${this.pos}. expected ${types
|
|
66
|
+
.map((t) => t.label)
|
|
67
|
+
.join(', ')}`
|
|
68
|
+
)
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -88,8 +92,10 @@ export class Parser {
|
|
|
88
92
|
readStringToken(type) {
|
|
89
93
|
const start = this.pos
|
|
90
94
|
while (this.pos < this.input.length) {
|
|
91
|
-
|
|
95
|
+
const code = this.input.charCodeAt(this.pos)
|
|
96
|
+
if (!type.test(code, this.input, this.pos)) break
|
|
92
97
|
this.pos++
|
|
98
|
+
this.line = code
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
if (this.pos !== start) {
|
|
@@ -114,7 +120,8 @@ export class Parser {
|
|
|
114
120
|
const start = after?.end ?? this.pos
|
|
115
121
|
let pos = start
|
|
116
122
|
while (pos < this.input.length) {
|
|
117
|
-
|
|
123
|
+
const code = this.input.charCodeAt(pos)
|
|
124
|
+
if (!type.test(code, this.input, pos)) break
|
|
118
125
|
pos++
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -178,7 +185,8 @@ export class Parser {
|
|
|
178
185
|
return (
|
|
179
186
|
this.input.charCodeAt(pos) === type.charCodes[0] &&
|
|
180
187
|
(type.charCodes[1] ? this.input.charCodeAt(pos + 1) === type.charCodes[1] : true) &&
|
|
181
|
-
(type.charCodes[2] ? this.input.charCodeAt(pos + 2) === type.charCodes[2] : true)
|
|
188
|
+
(type.charCodes[2] ? this.input.charCodeAt(pos + 2) === type.charCodes[2] : true) &&
|
|
189
|
+
(type.charCodes[3] ? this.input.charCodeAt(pos + 3) === type.charCodes[3] : true)
|
|
182
190
|
)
|
|
183
191
|
}
|
|
184
192
|
|
|
@@ -236,20 +244,6 @@ export class Parser {
|
|
|
236
244
|
}
|
|
237
245
|
}
|
|
238
246
|
|
|
239
|
-
/**
|
|
240
|
-
*
|
|
241
|
-
* @param {number} code
|
|
242
|
-
* @returns {boolean}
|
|
243
|
-
*/
|
|
244
|
-
function isIdentifierChar(code) {
|
|
245
|
-
if (code < 48) return false
|
|
246
|
-
if (code < 58) return true
|
|
247
|
-
if (code < 65) return false
|
|
248
|
-
if (code < 91) return true
|
|
249
|
-
if (code < 123) return true
|
|
250
|
-
return false
|
|
251
|
-
}
|
|
252
|
-
|
|
253
247
|
/**
|
|
254
248
|
*
|
|
255
249
|
* @param {boolean} test
|
package/src/parser/root.js
CHANGED
|
@@ -24,7 +24,7 @@ export function parseRoot(p) {
|
|
|
24
24
|
template = parseTemplate(p)
|
|
25
25
|
break
|
|
26
26
|
default:
|
|
27
|
-
throw new Error(
|
|
27
|
+
throw new Error(`unexpected html tag ${nameToken.value} at position ${p.pos}`)
|
|
28
28
|
}
|
|
29
29
|
p.skipWhitespaces()
|
|
30
30
|
}
|
package/src/parser/tokentype.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @typedef {Object} TokenType
|
|
3
3
|
* @property {string} label
|
|
4
4
|
* @property {Array<number>} [charCodes]
|
|
5
|
-
* @property {(
|
|
5
|
+
* @property {(code: number, input: string, pos: number) => boolean} [test]
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -18,7 +18,7 @@ function charTT(label, charCodes) {
|
|
|
18
18
|
/**
|
|
19
19
|
*
|
|
20
20
|
* @param {string} label
|
|
21
|
-
* @param {(
|
|
21
|
+
* @param {(code: number, input: string, pos: number) => boolean} test
|
|
22
22
|
* @returns {TokenType}
|
|
23
23
|
*/
|
|
24
24
|
function stringTT(label, test) {
|
|
@@ -29,13 +29,16 @@ export const TokenTypes = {
|
|
|
29
29
|
name: stringTT('name', name),
|
|
30
30
|
quotedText: stringTT('text', quotedText),
|
|
31
31
|
doubleQuotedText: stringTT('text', doubleQuotedText),
|
|
32
|
+
commentText: stringTT('text', commentText),
|
|
32
33
|
text: stringTT('text', text),
|
|
33
34
|
|
|
34
35
|
// html punctuation
|
|
35
36
|
lte: charTT('<', [60]),
|
|
36
37
|
gte: charTT('>', [62]),
|
|
37
|
-
slashGte: charTT('/>', [47, 62]),
|
|
38
38
|
lteSlash: charTT('</', [60, 47]),
|
|
39
|
+
slashGte: charTT('/>', [47, 62]),
|
|
40
|
+
lteExclHyph2: charTT('<!--', [60, 33, 45, 45]),
|
|
41
|
+
hyph2Gte: charTT('-->', [45, 45, 62]),
|
|
39
42
|
eq: charTT('=', [61]),
|
|
40
43
|
quote: charTT("'", [39]),
|
|
41
44
|
doubleQuote: charTT('"', [34]),
|
|
@@ -85,10 +88,23 @@ function doubleQuotedText(code) {
|
|
|
85
88
|
return code > 0 && code !== 34 // Blocks NULL (0) and Double Quote (34)
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
function commentText(code, input, pos) {
|
|
92
|
+
if (
|
|
93
|
+
input.charCodeAt(pos) === 45 &&
|
|
94
|
+
input.charCodeAt(pos + 1) === 45 &&
|
|
95
|
+
input.charCodeAt(pos + 2) === 62
|
|
96
|
+
) {
|
|
97
|
+
// Blocks the sequence "-->"
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return code >= 32 || code === 9 || code === 10 || code === 13 // Allows printable characters and standard whitespace (Tab, LF, CR)
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
function text(code) {
|
|
89
105
|
// Quickly allow all extended ASCII and UTF-16 surrogate bytes
|
|
90
106
|
if (code > 127) return true
|
|
91
107
|
|
|
92
|
-
// Blocks NULL (0), Less-Than (<), Left-Brace ({)
|
|
93
|
-
return code !== 0 && code !== 60 && code !== 123
|
|
108
|
+
// Blocks NULL (0), Less-Than (<), Left-Brace ({)
|
|
109
|
+
return code !== 0 && code !== 60 && code !== 123
|
|
94
110
|
}
|
package/src/transform/context.js
CHANGED
|
@@ -12,10 +12,6 @@ export function nextBlockId(ctx) {
|
|
|
12
12
|
return b.id(`block_${ctx.state.blocks.length + 1}`)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function nextBindingId(ctx) {
|
|
16
|
-
return b.id(`binding_${ctx.state.init.binding.length + 1}`)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
export function pathStmt(ctx, nodes) {
|
|
20
16
|
const subPath = []
|
|
21
17
|
|
|
@@ -58,7 +54,9 @@ function position(fragment, node) {
|
|
|
58
54
|
(nodes[i - 1]?.type === 'ExpressionTag' || nodes[i - 1]?.type === 'Text')
|
|
59
55
|
) {
|
|
60
56
|
// Text and ExpressionTag siblings are collapsed in one node in the html template
|
|
61
|
-
} else {
|
|
57
|
+
} else if (nodes[i].type === 'Comment') {
|
|
58
|
+
// Comments are skiped in the html template
|
|
59
|
+
} else {
|
|
62
60
|
position++
|
|
63
61
|
}
|
|
64
62
|
|
|
@@ -24,49 +24,73 @@ export function Attribute(node, ctx) {
|
|
|
24
24
|
ctx.visit(val, { ...ctx.state, template, pretty: false })
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
if (node.name === ':in' || node.name === ':out' || node.name === ':animate') {
|
|
28
|
+
const elementId = ctx.state.getElementId()
|
|
29
|
+
const expression = template.expressions[0]
|
|
30
|
+
const direction = node.name === ':animate' ? 'both' : node.name.slice(1)
|
|
31
|
+
const stmt = b.$animate(direction, {
|
|
32
|
+
...expression,
|
|
33
|
+
arguments: [elementId, expression.arguments[0] ?? b.object(), b.id('o')]
|
|
34
|
+
})
|
|
35
|
+
ctx.state.animates.push(stmt)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (node.name === ':use') {
|
|
40
|
+
const elementId = ctx.state.getElementId()
|
|
41
|
+
const expression = template.expressions[0]
|
|
42
|
+
const bindExpression = {
|
|
43
|
+
...expression,
|
|
44
|
+
arguments: [elementId, b.call('opts', expression.arguments[0])]
|
|
45
|
+
}
|
|
46
|
+
const stmt = b.$bind(elementId, bindExpression)
|
|
47
|
+
ctx.state.binds.push(stmt)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (node.name.startsWith(':on')) {
|
|
52
|
+
const elementId = ctx.state.getElementId()
|
|
53
|
+
const expression = template.expressions[0]
|
|
54
|
+
const event = node.name.slice(3)
|
|
55
|
+
const stmt = b.addEventListener(b.$getBinding(elementId), event, [expression], {
|
|
56
|
+
optional: true
|
|
57
|
+
})
|
|
58
|
+
ctx.state.eventListeners.push(stmt)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (node.name.startsWith(':')) {
|
|
63
|
+
const elementId = ctx.state.getElementId()
|
|
64
|
+
const property = node.name.slice(1)
|
|
65
|
+
const stmt = b.$effect([
|
|
66
|
+
b.assignment(b.member(b.$getBinding(elementId), property), b.template(template))
|
|
67
|
+
])
|
|
68
|
+
ctx.state.effects.push(stmt)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (node.name.startsWith('on')) {
|
|
73
|
+
const elementId = ctx.state.getElementId()
|
|
74
|
+
const expression = template.expressions[0]
|
|
75
|
+
const event = node.name.slice(2)
|
|
76
|
+
const stmt = b.addEventListener(elementId, event, [expression])
|
|
77
|
+
ctx.state.eventListeners.push(stmt)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
27
81
|
if (hasExpression(template)) {
|
|
28
|
-
// expression attribute
|
|
29
82
|
const elementId = ctx.state.getElementId()
|
|
30
83
|
const moduleId = ctx.state.getModuleId?.()
|
|
31
|
-
const bindingId = ctx.state.getBindingId?.()
|
|
32
84
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
arguments: [elementId, animateExpression.arguments[0] ?? b.object(), b.id('o')]
|
|
41
|
-
})
|
|
42
|
-
ctx.state.animates.push(stmt)
|
|
43
|
-
} else if (node.name.startsWith('on')) {
|
|
44
|
-
const stmt = b.assignment(
|
|
45
|
-
b.member(elementId, node.name),
|
|
46
|
-
b.eventListener(template.expressions[0])
|
|
47
|
-
)
|
|
48
|
-
ctx.state.handlers.push(stmt)
|
|
49
|
-
} else if (moduleId) {
|
|
50
|
-
const stmt = b.$effect([
|
|
51
|
-
b.$set(moduleId, elementId, node.name, template.expressions[0])
|
|
52
|
-
])
|
|
53
|
-
ctx.state.effects.push(stmt)
|
|
54
|
-
} else if (bindingId) {
|
|
55
|
-
const stmt1 = b.$effect([
|
|
56
|
-
b.assignment(b.member(bindingId, node.name), template.expressions[0])
|
|
57
|
-
])
|
|
58
|
-
const stmt2 = b.$effect([
|
|
59
|
-
b.assignment(template.expressions[0], b.member(bindingId, node.name))
|
|
60
|
-
])
|
|
85
|
+
const setStmt = moduleId
|
|
86
|
+
? b.$set(moduleId, elementId, b.literal(node.name), b.template(template))
|
|
87
|
+
: b.setAttribute(elementId, b.literal(node.name), b.template(template))
|
|
88
|
+
const stmt = b.$effect([setStmt])
|
|
89
|
+
ctx.state.effects.push(stmt)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
61
92
|
|
|
62
|
-
|
|
63
|
-
} else {
|
|
64
|
-
const stmt = b.$effect([
|
|
65
|
-
b.setAttribute(elementId, b.literal(node.name), template.expressions[0])
|
|
66
|
-
])
|
|
67
|
-
ctx.state.effects.push(stmt)
|
|
68
|
-
}
|
|
69
|
-
} else if (isEmpty(template)) {
|
|
93
|
+
if (isEmpty(template)) {
|
|
70
94
|
appendText(ctx.state.template, ` ${node.name}`)
|
|
71
95
|
} else {
|
|
72
96
|
appendText(ctx.state.template, ` ${node.name}="${template.text[0]}"`)
|
|
@@ -9,13 +9,10 @@ export function CustomElement(node, ctx) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
let elementId
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
ctx.state.init.elem.push(stmt)
|
|
17
|
-
}
|
|
18
|
-
return elementId
|
|
12
|
+
if (node.metadata?.hasExpressionAttribute) {
|
|
13
|
+
elementId = nextElementId(ctx)
|
|
14
|
+
const stmt = b.declaration(elementId, pathStmt(ctx, node))
|
|
15
|
+
ctx.state.init.elem.push(stmt)
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
if (node.metadata?.isModule) {
|
|
@@ -24,7 +21,11 @@ export function CustomElement(node, ctx) {
|
|
|
24
21
|
appendText(ctx.state.template, '<')
|
|
25
22
|
appendExpression(ctx.state.template, b.$name(moduleId))
|
|
26
23
|
for (const attribute of attributes) {
|
|
27
|
-
ctx.visit(attribute, {
|
|
24
|
+
ctx.visit(attribute, {
|
|
25
|
+
...ctx.state,
|
|
26
|
+
getElementId: () => elementId,
|
|
27
|
+
getModuleId: () => moduleId
|
|
28
|
+
})
|
|
28
29
|
}
|
|
29
30
|
appendText(ctx.state.template, '>')
|
|
30
31
|
ctx.visit(node.fragment)
|
|
@@ -34,7 +35,7 @@ export function CustomElement(node, ctx) {
|
|
|
34
35
|
} else {
|
|
35
36
|
appendText(ctx.state.template, `<${node.name}`)
|
|
36
37
|
for (const attribute of attributes) {
|
|
37
|
-
ctx.visit(attribute, { ...ctx.state, getElementId })
|
|
38
|
+
ctx.visit(attribute, { ...ctx.state, getElementId: () => elementId })
|
|
38
39
|
}
|
|
39
40
|
appendText(ctx.state.template, '>')
|
|
40
41
|
ctx.visit(node.fragment)
|
|
@@ -4,13 +4,23 @@ import { nextElementId, pathStmt } from '../context.js'
|
|
|
4
4
|
|
|
5
5
|
export function EachBlock(node, ctx) {
|
|
6
6
|
const template = { text: [''], expressions: [] }
|
|
7
|
-
const init = { elem: [], text: []
|
|
7
|
+
const init = { elem: [], text: [] }
|
|
8
|
+
const binds = []
|
|
8
9
|
const effects = []
|
|
9
10
|
const animates = []
|
|
10
|
-
const
|
|
11
|
+
const eventListeners = []
|
|
11
12
|
const blocks = []
|
|
12
13
|
|
|
13
|
-
ctx.visit(node.body, {
|
|
14
|
+
ctx.visit(node.body, {
|
|
15
|
+
...ctx.state,
|
|
16
|
+
template,
|
|
17
|
+
init,
|
|
18
|
+
binds,
|
|
19
|
+
effects,
|
|
20
|
+
animates,
|
|
21
|
+
eventListeners,
|
|
22
|
+
blocks
|
|
23
|
+
})
|
|
14
24
|
|
|
15
25
|
const stmts1 = [
|
|
16
26
|
b.declaration('template', b.createElement('template')),
|
|
@@ -19,10 +29,10 @@ export function EachBlock(node, ctx) {
|
|
|
19
29
|
const stmts2 = [
|
|
20
30
|
...init.elem,
|
|
21
31
|
...init.text,
|
|
22
|
-
...
|
|
32
|
+
...binds,
|
|
23
33
|
...effects,
|
|
24
34
|
...animates,
|
|
25
|
-
...
|
|
35
|
+
...eventListeners,
|
|
26
36
|
...blocks
|
|
27
37
|
]
|
|
28
38
|
const stmt3 = b.insertBefore('anchor', b.member('template', 'content'))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as b from '../../builders.js'
|
|
2
2
|
import { appendText } from '../../utils/template.js'
|
|
3
|
-
import {
|
|
3
|
+
import { nextElementId, pathStmt } from '../context.js'
|
|
4
4
|
|
|
5
5
|
export function Element(node, ctx) {
|
|
6
6
|
let attributes = node.attributes
|
|
@@ -8,41 +8,22 @@ export function Element(node, ctx) {
|
|
|
8
8
|
attributes = [...attributes, b.attribute('class', '', { isScoped: true })]
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const changed = []
|
|
12
|
-
|
|
13
11
|
let elementId
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
ctx.state.init.elem.push(stmt)
|
|
19
|
-
}
|
|
20
|
-
return elementId
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let bindingId
|
|
24
|
-
function getBindingId() {
|
|
25
|
-
if (!bindingId && node.metadata?.hasBinding) {
|
|
26
|
-
bindingId = nextBindingId(ctx)
|
|
27
|
-
const stmt = b.declaration(bindingId, {
|
|
28
|
-
...node.metadata?.bindExpression,
|
|
29
|
-
arguments: [getElementId(), ...node.metadata?.bindExpression.arguments]
|
|
30
|
-
})
|
|
31
|
-
ctx.state.init.binding.push(stmt)
|
|
32
|
-
}
|
|
33
|
-
return bindingId
|
|
12
|
+
if (node.metadata?.hasExpressionAttribute) {
|
|
13
|
+
elementId = nextElementId(ctx)
|
|
14
|
+
const stmt = b.declaration(elementId, pathStmt(ctx, node))
|
|
15
|
+
ctx.state.init.elem.push(stmt)
|
|
34
16
|
}
|
|
35
17
|
|
|
36
18
|
appendText(ctx.state.template, `<${node.name}`)
|
|
37
19
|
for (const attribute of attributes) {
|
|
38
|
-
ctx.visit(attribute, { ...ctx.state, getElementId
|
|
20
|
+
ctx.visit(attribute, { ...ctx.state, getElementId: () => elementId })
|
|
39
21
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
ctx.state.handlers.push(stmt)
|
|
22
|
+
if (node.metadata?.isVoid) {
|
|
23
|
+
appendText(ctx.state.template, '/>')
|
|
24
|
+
} else {
|
|
25
|
+
appendText(ctx.state.template, '>')
|
|
26
|
+
ctx.visit(node.fragment, { ...ctx.state, getElementId: () => elementId })
|
|
27
|
+
appendText(ctx.state.template, `</${node.name}>`)
|
|
47
28
|
}
|
|
48
29
|
}
|
|
@@ -6,13 +6,23 @@ export function IfBlock(node, ctx) {
|
|
|
6
6
|
function branchStmt(node, hasElseif = false) {
|
|
7
7
|
if (node) {
|
|
8
8
|
const template = { text: [''], expressions: [] }
|
|
9
|
-
const init = { elem: [], text: []
|
|
9
|
+
const init = { elem: [], text: [] }
|
|
10
|
+
const binds = []
|
|
10
11
|
const effects = []
|
|
11
12
|
const animates = []
|
|
12
|
-
const
|
|
13
|
+
const eventListeners = []
|
|
13
14
|
const blocks = []
|
|
14
15
|
|
|
15
|
-
ctx.visit(node, {
|
|
16
|
+
ctx.visit(node, {
|
|
17
|
+
...ctx.state,
|
|
18
|
+
template,
|
|
19
|
+
init,
|
|
20
|
+
binds,
|
|
21
|
+
effects,
|
|
22
|
+
animates,
|
|
23
|
+
eventListeners,
|
|
24
|
+
blocks
|
|
25
|
+
})
|
|
16
26
|
|
|
17
27
|
if (!hasElseif) {
|
|
18
28
|
const stmts1 = [
|
|
@@ -22,10 +32,10 @@ export function IfBlock(node, ctx) {
|
|
|
22
32
|
const stmts2 = [
|
|
23
33
|
...init.elem,
|
|
24
34
|
...init.text,
|
|
25
|
-
...
|
|
35
|
+
...binds,
|
|
26
36
|
...effects,
|
|
27
37
|
...animates,
|
|
28
|
-
...
|
|
38
|
+
...eventListeners,
|
|
29
39
|
...blocks
|
|
30
40
|
]
|
|
31
41
|
const stmt3 = b.insertBefore('anchor', b.member('template', 'content'))
|
|
@@ -3,10 +3,11 @@ import * as b from '../../builders.js'
|
|
|
3
3
|
export function Template(node, ctx) {
|
|
4
4
|
const style = { text: [''], expressions: [] }
|
|
5
5
|
const template = { text: [''], expressions: [] }
|
|
6
|
-
const init = { elem: [], text: []
|
|
6
|
+
const init = { elem: [], text: [] }
|
|
7
|
+
const binds = []
|
|
7
8
|
const effects = []
|
|
8
9
|
const animates = []
|
|
9
|
-
const
|
|
10
|
+
const eventListeners = []
|
|
10
11
|
const blocks = []
|
|
11
12
|
|
|
12
13
|
ctx.visit(node.fragment, {
|
|
@@ -14,9 +15,10 @@ export function Template(node, ctx) {
|
|
|
14
15
|
style,
|
|
15
16
|
template,
|
|
16
17
|
init,
|
|
18
|
+
binds,
|
|
17
19
|
effects,
|
|
18
20
|
animates,
|
|
19
|
-
|
|
21
|
+
eventListeners,
|
|
20
22
|
blocks
|
|
21
23
|
})
|
|
22
24
|
|
|
@@ -29,10 +31,10 @@ export function Template(node, ctx) {
|
|
|
29
31
|
const stmts2 = [
|
|
30
32
|
...init.elem,
|
|
31
33
|
...init.text,
|
|
32
|
-
...
|
|
34
|
+
...binds,
|
|
33
35
|
...effects,
|
|
34
36
|
...animates,
|
|
35
|
-
...
|
|
37
|
+
...eventListeners,
|
|
36
38
|
...blocks
|
|
37
39
|
]
|
|
38
40
|
const stmt3 = b.replaceChildren(rootId, b.member('template', 'content'))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { parse } from '../../src/parser'
|
|
3
|
+
|
|
4
|
+
describe('parse()', () => {
|
|
5
|
+
describe('with template containing comments', () => {
|
|
6
|
+
it('should return an AST succesfully', () => {
|
|
7
|
+
const ast = parse(`<template><div><!-- comment --></div></template>`)
|
|
8
|
+
expect(ast).not.toBeNull()
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
describe('with template containing &', () => {
|
|
12
|
+
it('should return an AST succesfully', () => {
|
|
13
|
+
const ast = parse(`<template><div>&</div></template>`)
|
|
14
|
+
expect(ast).not.toBeNull()
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
})
|