@katabatic/compiler 1.1.4 → 1.1.6

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.4",
4
+ "version": "1.1.6",
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",
@@ -71,9 +71,10 @@ function matchSelectors(selectors, template) {
71
71
  return
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
79
  break
79
80
  }
@@ -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
  }
@@ -47,13 +47,11 @@ export function Attribute(node, ctx) {
47
47
  )
48
48
  ctx.state.handlers.push(stmt)
49
49
  } else if (moduleId) {
50
- const stmt = b.$effect([
51
- b.$set(moduleId, elementId, node.name, template.expressions[0])
52
- ])
50
+ const stmt = b.$effect([b.$set(moduleId, elementId, node.name, b.template(template))])
53
51
  ctx.state.effects.push(stmt)
54
52
  } else if (bindingId) {
55
53
  const stmt1 = b.$effect([
56
- b.assignment(b.member(bindingId, node.name), template.expressions[0])
54
+ b.assignment(b.member(bindingId, node.name), b.template(template))
57
55
  ])
58
56
  const stmt2 = b.$effect([
59
57
  b.assignment(template.expressions[0], b.member(bindingId, node.name))
@@ -62,7 +60,7 @@ export function Attribute(node, ctx) {
62
60
  ctx.state.effects.push(stmt1, stmt2)
63
61
  } else {
64
62
  const stmt = b.$effect([
65
- b.setAttribute(elementId, b.literal(node.name), template.expressions[0])
63
+ b.setAttribute(elementId, b.literal(node.name), b.template(template))
66
64
  ])
67
65
  ctx.state.effects.push(stmt)
68
66
  }
@@ -82,11 +82,10 @@ export function Program(node, ctx) {
82
82
  stmts2.push(stmt)
83
83
 
84
84
  // $shadowRootMode
85
- if (ctx.state.template?.metadata?.shadowRootMode) {
86
- const { shadowRootMode } = ctx.state.template.metadata
87
- stmt = b.exp(b.declaration('$shadowRootMode', b.literal(shadowRootMode)))
88
- stmts2.push(stmt)
89
- }
85
+ stmt = b.exp(
86
+ b.declaration('$shadowRootMode', b.literal(ctx.state.template?.metadata?.shadowRootMode))
87
+ )
88
+ stmts2.push(stmt)
90
89
 
91
90
  return {
92
91
  ...node,
@@ -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
+ })