@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@katabatic/compiler",
3
3
  "license": "MIT",
4
- "version": "1.1.5",
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.hasBinding = hasBinding
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: false
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
+ }
@@ -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
- return
58
+ break
60
59
  case 'PseudoClassSelector':
61
- classAttribute ??= node.attributes.find((a) => a.name === 'class')
62
- scopedElement = node
63
- scopedClassAttribute = classAttribute
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
- return
71
+ break
72
72
  case 'ClassSelector':
73
73
  classAttribute ??= node.attributes.find((a) => a.name === 'class')
74
- const classes = classAttribute?.value[0].data.split(/\s+/)
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
- return
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
@@ -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.some((b) => b.context?.name === node.name)) {
62
+ if (blocks?.some((b) => b.context?.name === node.name)) {
56
63
  node.metadata.isBlockVar = true
57
64
  return
58
65
  }
@@ -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 (name !== blockNameClose) throw new Error('wrong closing tag')
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
  }
@@ -30,13 +30,32 @@ export function parseElement(p) {
30
30
  p.skipWhitespaces()
31
31
  p.expectToken([TokenTypes.gte])
32
32
 
33
- if (name !== tagNameClose) throw new Error('wrong closing tag')
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([TokenTypes.lte, TokenTypes.braceLHash, TokenTypes.braceL])
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))
@@ -49,7 +49,11 @@ export function parseIfBlock(p, elseif = false) {
49
49
  p.skipWhitespaces()
50
50
  p.expectToken([TokenTypes.braceR])
51
51
 
52
- if (name !== blockNameClose) throw new Error('wrong closing tag')
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 }
@@ -61,7 +61,11 @@ export class Parser {
61
61
  if (this.readStringToken(type)) return
62
62
  }
63
63
  }
64
- throw new Error(`unexpected token ${this.input[this.pos]} at position ${this.pos}`)
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
- if (!type.test(this.input.charCodeAt(this.pos))) break
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
- if (!type.test(this.input.charCodeAt(pos))) break
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
@@ -24,7 +24,7 @@ export function parseRoot(p) {
24
24
  template = parseTemplate(p)
25
25
  break
26
26
  default:
27
- throw new Error('unexpected html tag at position ' + p.pos)
27
+ throw new Error(`unexpected html tag ${nameToken.value} at position ${p.pos}`)
28
28
  }
29
29
  p.skipWhitespaces()
30
30
  }
@@ -2,7 +2,7 @@
2
2
  * @typedef {Object} TokenType
3
3
  * @property {string} label
4
4
  * @property {Array<number>} [charCodes]
5
- * @property {(char: number) => boolean} [test]
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 {(char: number) => boolean} test
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 ({) and Ampersand (&)
93
- return code !== 0 && code !== 60 && code !== 123 && code !== 38
108
+ // Blocks NULL (0), Less-Than (<), Left-Brace ({)
109
+ return code !== 0 && code !== 60 && code !== 123
94
110
  }
@@ -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
- if (node.name === 'bind') {
34
- // binding handled in element visitor
35
- } else if (node.name === 'in' || node.name === 'out' || node.name === 'animate') {
36
- const direction = node.name === 'animate' ? 'both' : node.name
37
- const animateExpression = template.expressions[0]
38
- const stmt = b.$animate(direction, {
39
- ...animateExpression,
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
- ctx.state.effects.push(stmt1, stmt2)
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
- function getElementId() {
13
- if (!elementId) {
14
- elementId = nextElementId(ctx)
15
- const stmt = b.declaration(elementId, pathStmt(ctx))
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, { ...ctx.state, getElementId, getModuleId: () => moduleId })
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: [], binding: [] }
7
+ const init = { elem: [], text: [] }
8
+ const binds = []
8
9
  const effects = []
9
10
  const animates = []
10
- const handlers = []
11
+ const eventListeners = []
11
12
  const blocks = []
12
13
 
13
- ctx.visit(node.body, { ...ctx.state, template, init, effects, animates, handlers, blocks })
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
- ...init.binding,
32
+ ...binds,
23
33
  ...effects,
24
34
  ...animates,
25
- ...handlers,
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 { nextBindingId, nextElementId, pathStmt } from '../context.js'
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
- function getElementId() {
15
- if (!elementId) {
16
- elementId = nextElementId(ctx)
17
- const stmt = b.declaration(elementId, pathStmt(ctx))
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, getBindingId, changed })
20
+ ctx.visit(attribute, { ...ctx.state, getElementId: () => elementId })
39
21
  }
40
- appendText(ctx.state.template, '>')
41
- ctx.visit(node.fragment, { ...ctx.state, getElementId })
42
- appendText(ctx.state.template, `</${node.name}>`)
43
-
44
- if (changed.length > 0) {
45
- const stmt = b.addEventListener(bindingId, 'changed', changed)
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: [], binding: [] }
9
+ const init = { elem: [], text: [] }
10
+ const binds = []
10
11
  const effects = []
11
12
  const animates = []
12
- const handlers = []
13
+ const eventListeners = []
13
14
  const blocks = []
14
15
 
15
- ctx.visit(node, { ...ctx.state, template, init, effects, animates, handlers, blocks })
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
- ...init.binding,
35
+ ...binds,
26
36
  ...effects,
27
37
  ...animates,
28
- ...handlers,
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: [], binding: [] }
6
+ const init = { elem: [], text: [] }
7
+ const binds = []
7
8
  const effects = []
8
9
  const animates = []
9
- const handlers = []
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
- handlers,
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
- ...init.binding,
34
+ ...binds,
33
35
  ...effects,
34
36
  ...animates,
35
- ...handlers,
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
+ })