@katabatic/compiler 1.1.3 → 1.1.5

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/analyse/context.js +1 -1
  3. package/src/analyse/visitors/Program.js +2 -0
  4. package/src/builders.js +43 -13
  5. package/src/checkers.js +10 -6
  6. package/src/css-matcher.js +1 -0
  7. package/src/exp-matcher.js +14 -5
  8. package/src/parser/attributes.js +28 -8
  9. package/src/parser/element.js +1 -1
  10. package/src/parser/expression.js +18 -3
  11. package/src/parser/tokentype.js +39 -20
  12. package/src/router/index.js +2 -1
  13. package/src/transform/context.js +3 -3
  14. package/src/transform/index.js +7 -2
  15. package/src/transform/static/index.js +9 -2
  16. package/src/transform/static/visitors/Attribute.js +8 -5
  17. package/src/transform/static/visitors/CustomElement.js +33 -0
  18. package/src/transform/static/visitors/EachBlock.js +11 -11
  19. package/src/transform/static/visitors/Element.js +4 -4
  20. package/src/transform/static/visitors/ExpressionTag.js +8 -2
  21. package/src/transform/static/visitors/Identifier.js +7 -0
  22. package/src/transform/static/visitors/IfBlock.js +15 -18
  23. package/src/transform/static/visitors/Program.js +4 -1
  24. package/src/transform/static/visitors/Script.js +3 -3
  25. package/src/transform/static/visitors/SlotElement.js +2 -2
  26. package/src/transform/static/visitors/Style.js +2 -2
  27. package/src/transform/static/visitors/Template.js +2 -6
  28. package/src/transform/static/visitors/Text.js +2 -2
  29. package/src/transform/visitors/Attribute.js +5 -3
  30. package/src/transform/visitors/ExpressionTag.js +8 -2
  31. package/src/transform/visitors/MethodDefinition.js +13 -8
  32. package/src/transform/visitors/Program.js +52 -38
  33. package/src/utils/template.js +9 -1
  34. package/src/utils/misc.js +0 -9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@katabatic/compiler",
3
3
  "license": "MIT",
4
- "version": "1.1.3",
4
+ "version": "1.1.5",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -10,7 +10,7 @@ export function getTemplate(ctx) {
10
10
 
11
11
  export function getProgram(ctx) {
12
12
  const root = ctx.path[0]
13
- return root.script.content
13
+ return root.script?.content
14
14
  }
15
15
 
16
16
  export function getScript(ctx) {
@@ -15,9 +15,11 @@ export function Program(node, ctx) {
15
15
 
16
16
  ctx.next({ ...ctx.state, modules, customElement })
17
17
 
18
+ const hasCustomElementClass = node.body.some(is.customElement)
18
19
  const hasDefineCustomElement = node.body.some(is.defineCustomElement)
19
20
 
20
21
  node.metadata ??= {}
22
+ node.metadata.hasCustomElementClass = hasCustomElementClass
21
23
  node.metadata.hasDefineCustomElement = hasDefineCustomElement
22
24
  node.metadata.modules = modules
23
25
  node.metadata.customElement = customElement
package/src/builders.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { hasExpression, hasOnlyExpression } from './utils/template.js'
2
+
1
3
  export function declaration(id, init, kind = 'const') {
2
4
  if (typeof id === 'string') {
3
5
  id = { type: 'Identifier', name: id }
@@ -29,10 +31,18 @@ export function binary(operator, left, right) {
29
31
  }
30
32
  }
31
33
 
32
- export function template({ text, expressions }) {
34
+ export function template(template) {
35
+ if (hasOnlyExpression(template)) {
36
+ return template.expressions[0]
37
+ }
38
+
39
+ if (!hasExpression(template)) {
40
+ return { type: 'Literal', value: template.text[0] }
41
+ }
42
+
43
+ const { text, expressions } = template
33
44
  if (text.length === expressions.length) {
34
- // template literals must start and end with a text
35
- text.push('')
45
+ text.push('') // template literals must start and end with a text
36
46
  }
37
47
 
38
48
  const quasis = text.map((raw) => ({
@@ -56,7 +66,7 @@ export function assignment(left, right, operator = '=') {
56
66
  }
57
67
  }
58
68
 
59
- export function member(object, property) {
69
+ export function member(object, property, { optional = false } = {}) {
60
70
  if (typeof object === 'string') {
61
71
  object = { type: 'Identifier', name: object }
62
72
  }
@@ -70,7 +80,7 @@ export function member(object, property) {
70
80
  object,
71
81
  property,
72
82
  computed: false,
73
- optional: false
83
+ optional
74
84
  }
75
85
  }
76
86
 
@@ -160,8 +170,8 @@ export function property(key, value) {
160
170
  return { type: 'Property', key, value }
161
171
  }
162
172
 
163
- export function object() {
164
- return { type: 'ObjectExpression', properties: [] }
173
+ export function object(properties = []) {
174
+ return { type: 'ObjectExpression', properties }
165
175
  }
166
176
 
167
177
  export function id(name, isPrivate = false) {
@@ -216,8 +226,8 @@ export function array(elements) {
216
226
  return { type: 'ArrayExpression', elements }
217
227
  }
218
228
 
219
- export function call(callee) {
220
- return { type: 'CallExpression', callee, arguments: [], optional: false }
229
+ export function call(callee, args = [], { optional = false } = {}) {
230
+ return { type: 'CallExpression', callee, arguments: args, optional }
221
231
  }
222
232
 
223
233
  export function ifStmt(test, consequent, alternate) {
@@ -460,6 +470,24 @@ export function includes(object, value) {
460
470
  }
461
471
  }
462
472
 
473
+ export function customElement(name) {
474
+ return {
475
+ type: 'ClassDeclaration',
476
+ id: {
477
+ type: 'Identifier',
478
+ name
479
+ },
480
+ superClass: {
481
+ type: 'Identifier',
482
+ name: 'HTMLElement'
483
+ },
484
+ body: {
485
+ type: 'ClassBody',
486
+ body: []
487
+ }
488
+ }
489
+ }
490
+
463
491
  export function defineCustomElement(elementName, className) {
464
492
  return {
465
493
  type: 'ExpressionStatement',
@@ -1276,12 +1304,14 @@ export function render(body = []) {
1276
1304
  async: false,
1277
1305
  params: [
1278
1306
  {
1279
- type: 'Identifier',
1280
- name: 'data'
1307
+ type: 'AssignmentPattern',
1308
+ left: { type: 'Identifier', name: 'data' },
1309
+ right: { type: 'ObjectExpression', properties: [] }
1281
1310
  },
1282
1311
  {
1283
- type: 'Identifier',
1284
- name: 'slot'
1312
+ type: 'AssignmentPattern',
1313
+ left: { type: 'Identifier', name: 'slot' },
1314
+ right: { type: 'ObjectExpression', properties: [] }
1285
1315
  }
1286
1316
  ],
1287
1317
  body: {
package/src/checkers.js CHANGED
@@ -62,6 +62,14 @@ export function querySelector(node) {
62
62
  )
63
63
  }
64
64
 
65
+ export function customElement(node) {
66
+ if (node.type === 'ExportNamedDeclaration') {
67
+ node = node.declaration
68
+ }
69
+
70
+ return node.type === 'ClassDeclaration'
71
+ }
72
+
65
73
  export function defineCustomElement(node) {
66
74
  if (node.type === 'ExpressionStatement') {
67
75
  node = node.expression
@@ -128,11 +136,7 @@ export function bindAttribute(node) {
128
136
  }
129
137
 
130
138
  export function staticAttribute(node) {
131
- return (
132
- node.type === 'Attribute' &&
133
- node.name === 'static' &&
134
- (node.value === true || (node.value[0]?.type === 'Text' && node.value[0]?.data === 'true'))
135
- )
139
+ return node.type === 'Attribute' && node.name === 'static'
136
140
  }
137
141
 
138
142
  export function shadowRootModeAttribute(node) {
@@ -141,4 +145,4 @@ export function shadowRootModeAttribute(node) {
141
145
 
142
146
  export function elseIfBlock(node) {
143
147
  return node.type === 'IfBlock' && node.elseif
144
- }
148
+ }
@@ -34,6 +34,7 @@ export function matchSelector(selector, template) {
34
34
 
35
35
  function matchSelectors(selectors, template) {
36
36
  if (selectors.length === 0) return false
37
+ if (!template) return false
37
38
 
38
39
  let result = false
39
40
 
@@ -1,7 +1,7 @@
1
1
  import { walk } from 'zimmerframe'
2
2
 
3
3
  export function matchExpression(expression, program, blocks) {
4
- let customElement = program.metadata?.customElement
4
+ let customElement = program?.metadata?.customElement
5
5
 
6
6
  walk(expression, undefined, {
7
7
  Identifier(node, ctx) {
@@ -20,6 +20,11 @@ export function matchExpression(expression, program, blocks) {
20
20
  setMetadata(node)
21
21
  }
22
22
  break
23
+ case 'Property':
24
+ if (parentNode.value === node) {
25
+ setMetadata(node)
26
+ }
27
+ break
23
28
  default:
24
29
  setMetadata(node)
25
30
  }
@@ -29,17 +34,19 @@ export function matchExpression(expression, program, blocks) {
29
34
  function setMethodMetadata(node) {
30
35
  node.metadata ??= {}
31
36
 
32
- if (customElement.methods.includes(node.name)) {
37
+ if (customElement?.methods.includes(node.name)) {
33
38
  node.metadata.isPrivate = false
34
39
  node.metadata.isMethod = true
35
40
  return
36
41
  }
37
42
 
38
- if (customElement.private.methods.includes(node.name)) {
43
+ if (customElement?.private.methods.includes(node.name)) {
39
44
  node.metadata.isPrivate = true
40
45
  node.metadata.isMethod = true
41
46
  return
42
47
  }
48
+
49
+ node.metadata.isData = true
43
50
  }
44
51
 
45
52
  function setMetadata(node) {
@@ -50,16 +57,18 @@ export function matchExpression(expression, program, blocks) {
50
57
  return
51
58
  }
52
59
 
53
- if (customElement.properties.includes(node.name)) {
60
+ if (customElement?.properties.includes(node.name)) {
54
61
  node.metadata.isPrivate = false
55
62
  node.metadata.isProperty = true
56
63
  return
57
64
  }
58
65
 
59
- if (customElement.private.properties.includes(node.name)) {
66
+ if (customElement?.private.properties.includes(node.name)) {
60
67
  node.metadata.isPrivate = true
61
68
  node.metadata.isProperty = true
62
69
  return
63
70
  }
71
+
72
+ node.metadata.isData = true
64
73
  }
65
74
  }
@@ -1,4 +1,4 @@
1
- import { parseAttributeExpressionTag } from './expression.js'
1
+ import { parseDoubleQuotedExpressionTag, parseQuotedExpressionTag } from './expression.js'
2
2
  import { Parser } from './parser.js'
3
3
  import { TokenTypes } from './tokentype.js'
4
4
 
@@ -35,12 +35,16 @@ function parseAttribute(p) {
35
35
  let value
36
36
  switch (punctToken?.type) {
37
37
  case TokenTypes.quoteBraceL:
38
+ value = [parseQuotedExpressionTag(p)]
39
+ break
38
40
  case TokenTypes.doubleQuoteBraceL:
39
- value = [parseAttributeExpressionTag(p)]
41
+ value = [parseDoubleQuotedExpressionTag(p)]
40
42
  break
41
43
  case TokenTypes.quote:
44
+ value = [parseQuotedText(p)]
45
+ break
42
46
  case TokenTypes.doubleQuote:
43
- value = [parseText(p)]
47
+ value = [parseDoubleQuotedText(p)]
44
48
  break
45
49
  default:
46
50
  p.raiseUnexpectedToken()
@@ -48,7 +52,7 @@ function parseAttribute(p) {
48
52
  }
49
53
  return { type: 'Attribute', name, value, start, end: p.pos }
50
54
  }
51
- return { type: 'Attribute', name, value: true, start, end: p.pos }
55
+ return { type: 'Attribute', name, value: [], start, end: p.pos }
52
56
  }
53
57
 
54
58
  /**
@@ -56,11 +60,27 @@ function parseAttribute(p) {
56
60
  * @param {Parser} p
57
61
  * @returns
58
62
  */
59
- function parseText(p) {
63
+ function parseQuotedText(p) {
60
64
  const start = p.pos
61
- p.expectToken([TokenTypes.quote, TokenTypes.doubleQuote])
62
- p.expectToken([TokenTypes.text])
65
+ p.expectToken([TokenTypes.quote])
66
+ p.expectToken([TokenTypes.quotedText])
63
67
  const data = p.value
64
- p.expectToken([TokenTypes.quote, TokenTypes.doubleQuote])
68
+ p.expectToken([TokenTypes.quote])
69
+
65
70
  return { type: 'Text', start, end: p.end, data }
66
71
  }
72
+
73
+ /**
74
+ *
75
+ * @param {Parser} p
76
+ * @returns
77
+ */
78
+ function parseDoubleQuotedText(p) {
79
+ const start = p.pos
80
+ p.expectToken([TokenTypes.doubleQuote])
81
+ p.expectToken([TokenTypes.doubleQuotedText])
82
+ const data = p.value
83
+ p.expectToken([TokenTypes.doubleQuote])
84
+
85
+ return { type: 'Text', start, end: p.end, data }
86
+ }
@@ -34,7 +34,7 @@ export function parseElement(p) {
34
34
 
35
35
  return { type, name, attributes, fragment, start, end: p.pos }
36
36
  }
37
- return { type, name, attributes, start, end: p.pos }
37
+ return { type, name, attributes, fragment: { type: 'Fragment', nodes: [] }, start, end: p.pos }
38
38
  }
39
39
 
40
40
  /**
@@ -22,12 +22,27 @@ export function parseExpressionTag(p) {
22
22
  * @param {Parser} p
23
23
  * @returns
24
24
  */
25
- export function parseAttributeExpressionTag(p) {
26
- p.expectToken([TokenTypes.quoteBraceL, TokenTypes.doubleQuoteBraceL])
25
+ export function parseQuotedExpressionTag(p) {
26
+ p.expectToken([TokenTypes.quoteBraceL])
27
27
  p.skipWhitespaces()
28
28
  const expression = parseExpression(p)
29
29
  p.skipWhitespaces()
30
- p.expectToken([TokenTypes.braceRQuote, TokenTypes.braceRDoubleQuote])
30
+ p.expectToken([TokenTypes.braceRQuote])
31
+
32
+ return { type: 'ExpressionTag', expression }
33
+ }
34
+
35
+ /**
36
+ *
37
+ * @param {Parser} p
38
+ * @returns
39
+ */
40
+ export function parseDoubleQuotedExpressionTag(p) {
41
+ p.expectToken([TokenTypes.doubleQuoteBraceL])
42
+ p.skipWhitespaces()
43
+ const expression = parseExpression(p)
44
+ p.skipWhitespaces()
45
+ p.expectToken([TokenTypes.braceRDoubleQuote])
31
46
 
32
47
  return { type: 'ExpressionTag', expression }
33
48
  }
@@ -27,6 +27,8 @@ function stringTT(label, test) {
27
27
 
28
28
  export const TokenTypes = {
29
29
  name: stringTT('name', name),
30
+ quotedText: stringTT('text', quotedText),
31
+ doubleQuotedText: stringTT('text', doubleQuotedText),
30
32
  text: stringTT('text', text),
31
33
 
32
34
  // html punctuation
@@ -35,13 +37,13 @@ export const TokenTypes = {
35
37
  slashGte: charTT('/>', [47, 62]),
36
38
  lteSlash: charTT('</', [60, 47]),
37
39
  eq: charTT('=', [61]),
38
- quote: charTT('\'', [39]),
40
+ quote: charTT("'", [39]),
39
41
  doubleQuote: charTT('"', [34]),
40
42
 
41
43
  // block punctuation
42
- quoteBraceL: charTT('\'{', [39, 123]),
44
+ quoteBraceL: charTT("'{", [39, 123]),
43
45
  doubleQuoteBraceL: charTT('"{', [34, 123]),
44
- braceRQuote: charTT('}\'', [125, 39]),
46
+ braceRQuote: charTT("}'", [125, 39]),
45
47
  braceRDoubleQuote: charTT('}"', [125, 34]),
46
48
  braceL: charTT('{', [123]),
47
49
  braceR: charTT('}', [125]),
@@ -49,27 +51,44 @@ export const TokenTypes = {
49
51
  braceLColumn: charTT('{:', [123, 58]),
50
52
  braceLSlash: charTT('{/', [123, 47]),
51
53
  parenthesesL: charTT('(', [40]),
52
- parenthesesR: charTT(')', [41]),
54
+ parenthesesR: charTT(')', [41])
53
55
  }
54
56
 
55
57
  function name(code) {
56
- if (code === 45) return true
57
- if (code < 48) return false
58
- if (code < 58) return true
59
- if (code < 65) return false
60
- if (code < 91) return true
61
- if (code < 123) return true
62
- return false
58
+ // Automatically allow all extended ASCII and UTF-16 surrogate bytes
59
+ if (code > 127) return true
60
+
61
+ // Fast check for control characters, whitespace, and quotes
62
+ if (code <= 47) {
63
+ return (
64
+ code !== 0 && // NULL
65
+ code !== 9 && // Tab (\t)
66
+ code !== 10 && // Line Feed (\n)
67
+ code !== 12 && // Form Feed (\f)
68
+ code !== 13 && // Carriage Return (\r)
69
+ code !== 32 && // Space ( )
70
+ code !== 34 && // Double Quote (")
71
+ code !== 39 && // Single Quote (')
72
+ code !== 47 // Forward Slash (/)
73
+ )
74
+ }
75
+
76
+ // Blocks Equals (=), Greater-Than (>), and Right-Brace (})
77
+ return code !== 61 && code !== 62 && code !== 125
78
+ }
79
+
80
+ function quotedText(code) {
81
+ return code > 0 && code !== 39 // Blocks NULL (0) and Single Quote (39)
82
+ }
83
+
84
+ function doubleQuotedText(code) {
85
+ return code > 0 && code !== 34 // Blocks NULL (0) and Double Quote (34)
63
86
  }
64
87
 
65
88
  function text(code) {
66
- if (code === 10) return true
67
- if (code === 32) return true
68
- if (code === 47) return true
69
- if (code < 48) return false
70
- if (code < 58) return true
71
- if (code < 65) return false
72
- if (code < 91) return true
73
- if (code < 123) return true
74
- return false
89
+ // Quickly allow all extended ASCII and UTF-16 surrogate bytes
90
+ if (code > 127) return true
91
+
92
+ // Blocks NULL (0), Less-Than (<), Left-Brace ({) and Ampersand (&)
93
+ return code !== 0 && code !== 60 && code !== 123 && code !== 38
75
94
  }
@@ -79,7 +79,8 @@ export async function route(path) {
79
79
  function load(modules, context) {
80
80
  const data = []
81
81
  for (let i = 0; i < modules.length; i++) {
82
- data.push(modules[i].load(context, data[i - 1]))
82
+ const parentData = { ...data[i - 1] }
83
+ data.push(modules[i].load?.(parentData, context) ?? parentData)
83
84
  }
84
85
  return Promise.all(data)
85
86
  }
@@ -58,10 +58,10 @@ function position(fragment, node) {
58
58
  (nodes[i - 1]?.type === 'ExpressionTag' || nodes[i - 1]?.type === 'Text')
59
59
  ) {
60
60
  // Text and ExpressionTag siblings are collapsed in one node in the html template
61
- continue
61
+ } else {
62
+ position++
62
63
  }
63
64
 
64
- position++
65
65
  if (nodes[i] === node) break
66
66
  }
67
67
  return position
@@ -73,4 +73,4 @@ export function getProgram(ctx) {
73
73
 
74
74
  export function getTemplate(ctx) {
75
75
  return ctx.path[0]
76
- }
76
+ }
@@ -1,4 +1,5 @@
1
1
  import { walk } from 'zimmerframe'
2
+ import * as b from '../builders.js'
2
3
  import { Program } from './visitors/Program.js'
3
4
  import { Identifier } from './visitors/Identifier.js'
4
5
  import { ExpressionTag } from './visitors/ExpressionTag.js'
@@ -51,6 +52,10 @@ const scriptVisitors = {
51
52
  }
52
53
 
53
54
  export function transform(ast, analysis, context) {
54
- const template = walk(ast.template, { analysis, context }, templateVisitors)
55
- return walk(ast.script.content, { analysis, template, context }, scriptVisitors)
55
+ let template
56
+
57
+ if (ast.template) {
58
+ template = walk(ast.template, { analysis, context }, templateVisitors)
59
+ }
60
+ return walk(ast.script?.content ?? b.program(), { analysis, template, context }, scriptVisitors)
56
61
  }
@@ -1,4 +1,5 @@
1
1
  import { walk } from 'zimmerframe'
2
+ import * as b from '../../builders.js'
2
3
  import { Element } from './visitors/Element.js'
3
4
  import { Text } from './visitors/Text.js'
4
5
  import { Program } from './visitors/Program.js'
@@ -12,14 +13,19 @@ import { CssTree, Selector } from '../visitors/Selector.js'
12
13
  import { CallExpression } from '../visitors/CallExpression.js'
13
14
  import { IfBlock } from './visitors/IfBlock.js'
14
15
  import { EachBlock } from './visitors/EachBlock.js'
16
+ import { Identifier } from './visitors/Identifier.js'
17
+ import { CustomElement } from './visitors/CustomElement.js'
18
+ import { ImportDeclaration } from '../visitors/ImportDeclaration.js'
15
19
 
16
20
  const templateVisitors = {
17
21
  Attribute,
18
22
  Element,
23
+ CustomElement,
19
24
  SlotElement,
20
25
  Text,
21
26
  ExpressionTag,
22
27
  Template,
28
+ Identifier,
23
29
  Style,
24
30
  Script,
25
31
  CallExpression,
@@ -31,10 +37,11 @@ const templateVisitors = {
31
37
  }
32
38
 
33
39
  const scriptVisitors = {
34
- Program
40
+ Program,
41
+ ImportDeclaration
35
42
  }
36
43
 
37
44
  export function transform(ast, analysis, context) {
38
45
  const template = walk(ast.template, { analysis, context }, templateVisitors)
39
- return walk(ast.script.content, { analysis, template, context }, scriptVisitors)
46
+ return walk(ast.script?.content ?? b.program(), { analysis, template, context }, scriptVisitors)
40
47
  }
@@ -1,5 +1,5 @@
1
1
  import * as b from '../../../builders.js'
2
- import { append } from '../../../utils/misc.js'
2
+ import { appendText } from '../../../utils/template.js'
3
3
  import { clx } from '../../../css.js'
4
4
 
5
5
  export function Attribute(node, ctx) {
@@ -19,9 +19,12 @@ export function Attribute(node, ctx) {
19
19
  }
20
20
  }
21
21
 
22
- append(ctx.state.text, ` ${node.name}="`)
23
- for (const val of value) {
24
- ctx.visit(val)
22
+ appendText(ctx.state.template, ` ${node.name}`)
23
+ if (value.length > 0) {
24
+ appendText(ctx.state.template, `="`)
25
+ for (const val of value) {
26
+ ctx.visit(val)
27
+ }
28
+ appendText(ctx.state.template, `"`)
25
29
  }
26
- append(ctx.state.text, `"`)
27
30
  }
@@ -0,0 +1,33 @@
1
+ import * as b from '../../../builders.js'
2
+ import { appendText, appendExpression } from '../../../utils/template.js'
3
+
4
+ export function CustomElement(node, ctx) {
5
+ if (node.metadata?.isModule) {
6
+ const dataStmt = b.object()
7
+ for (const attribute of node.attributes) {
8
+ const { name, value } = attribute
9
+
10
+ const template = { text: [''], expressions: [] }
11
+ for (const val of value) {
12
+ ctx.visit(val, { ...ctx.state, template, pretty: false })
13
+ }
14
+
15
+ const stmt = b.property(name, b.template(template))
16
+ dataStmt.properties.push(stmt)
17
+ }
18
+
19
+ const moduleId = b.id(`$Module_${node.metadata.index + 1}`)
20
+ const loadStmt = b.call(b.member(moduleId, 'load'), [dataStmt], { optional: true })
21
+ const renderStmt = b.call(b.member(moduleId, 'render'), [loadStmt])
22
+
23
+ appendExpression(ctx.state.template, renderStmt)
24
+ } else {
25
+ appendText(ctx.state.template, `<${node.name}`)
26
+ for (const attribute of node.attributes) {
27
+ ctx.visit(attribute)
28
+ }
29
+ appendText(ctx.state.template, '>')
30
+ ctx.visit(node.fragment)
31
+ appendText(ctx.state.template, `</${node.name}>`)
32
+ }
33
+ }
@@ -1,23 +1,23 @@
1
1
  import * as b from '../../../builders.js'
2
+ import { appendExpression } from '../../../utils/template.js'
2
3
  import { nextBlockId } from '../../context.js'
3
4
 
4
5
  export function EachBlock(node, ctx) {
5
- const text = ['']
6
- const expressions = []
6
+ const template = { text: [''], expressions: [] }
7
7
 
8
- ctx.visit(node.body, { ...ctx.state, text, expressions })
8
+ ctx.visit(node.body, { ...ctx.state, template })
9
9
 
10
10
  const blockId = nextBlockId(ctx)
11
- const resultId = b.id('result_')
12
- const block = b.func(blockId, [
11
+ const resultId = b.id('_result')
12
+ const expressionStmt = ctx.visit(node.expression)
13
+ const bodyStmt = [b.assignment(resultId, b.template(template), '+=')]
14
+
15
+ const blockStmt = b.func(blockId, [
13
16
  b.declaration(resultId, b.literal(''), 'let'),
14
- b.forStmt(node.context, node.expression, [
15
- b.assignment(resultId, b.template({ text, expressions }), '+=')
16
- ]),
17
+ b.forStmt(node.context, expressionStmt, bodyStmt),
17
18
  b.returnStmt(resultId)
18
19
  ])
19
20
 
20
- ctx.state.blocks.push(block)
21
- ctx.state.text.push('')
22
- ctx.state.expressions.push(b.call(blockId))
21
+ ctx.state.blocks.push(blockStmt)
22
+ appendExpression(ctx.state.template, b.call(blockId))
23
23
  }
@@ -1,5 +1,5 @@
1
1
  import * as b from '../../../builders.js'
2
- import { append } from '../../../utils/misc.js'
2
+ import { appendText } from '../../../utils/template.js'
3
3
 
4
4
  export function Element(node, ctx) {
5
5
  let attributes = node.attributes
@@ -7,11 +7,11 @@ export function Element(node, ctx) {
7
7
  attributes = [...attributes, b.attribute('class', '', { isScoped: true })]
8
8
  }
9
9
 
10
- append(ctx.state.text, `<${node.name}`)
10
+ appendText(ctx.state.template, `<${node.name}`)
11
11
  for (const attribute of attributes) {
12
12
  ctx.visit(attribute)
13
13
  }
14
- append(ctx.state.text, '>')
14
+ appendText(ctx.state.template, '>')
15
15
  ctx.visit(node.fragment)
16
- append(ctx.state.text, `</${node.name}>`)
16
+ appendText(ctx.state.template, `</${node.name}>`)
17
17
  }
@@ -1,6 +1,12 @@
1
+ import * as b from '../../../builders.js'
2
+ import { appendExpression } from '../../../utils/template.js'
3
+
1
4
  export function ExpressionTag(node, ctx) {
2
5
  node = ctx.next() ?? node
3
6
 
4
- ctx.state.text.push('')
5
- ctx.state.expressions.push(node.expression)
7
+ let expression = node.expression
8
+ if (ctx.state.pretty ?? true) {
9
+ expression = b.logical('??', expression, b.literal(''))
10
+ }
11
+ appendExpression(ctx.state.template, expression)
6
12
  }
@@ -0,0 +1,7 @@
1
+ import * as b from '../../../builders.js'
2
+
3
+ export function Identifier(node, ctx) {
4
+ if (node.metadata?.isData) {
5
+ return b.member(b.id('data'), node)
6
+ }
7
+ }
@@ -1,28 +1,25 @@
1
1
  import * as b from '../../../builders.js'
2
+ import { appendExpression } from '../../../utils/template.js'
2
3
  import { nextBlockId } from '../../context.js'
3
4
 
4
5
  export function IfBlock(node, ctx) {
5
- const text = ['']
6
- const expressions = []
6
+ function branchStmt(node) {
7
+ if (node) {
8
+ const template = { text: [''], expressions: [] }
7
9
 
8
- ctx.visit(node.consequent, { ...ctx.state, text, expressions })
9
- const stmt1 = b.returnStmt(b.template({ text, expressions }))
10
-
11
- let stmt2
12
- if (node.alternate) {
13
- const text = ['']
14
- const expressions = []
15
-
16
- ctx.visit(node.alternate, { ...ctx.state, text, expressions })
17
- stmt2 = b.returnStmt(b.template({ text, expressions }))
18
- } else {
19
- stmt2 = b.returnStmt(b.literal(''))
10
+ ctx.visit(node, { ...ctx.state, template })
11
+ return b.returnStmt(b.template(template))
12
+ }
13
+ return b.returnStmt(b.literal(''))
20
14
  }
21
15
 
16
+ const testStmt = ctx.visit(node.test)
17
+ const consequentStmt = branchStmt(node.consequent)
18
+ const alternateStmt = branchStmt(node.alternate)
19
+
22
20
  const blockId = nextBlockId(ctx)
23
- const block = b.func(blockId, [b.ifStmt(node.test, [stmt1]), stmt2])
21
+ const blockStmt = b.func(blockId, [b.ifStmt(testStmt, [consequentStmt]), alternateStmt])
24
22
 
25
- ctx.state.blocks.push(block)
26
- ctx.state.text.push('')
27
- ctx.state.expressions.push(b.call(blockId))
23
+ ctx.state.blocks.push(blockStmt)
24
+ appendExpression(ctx.state.template, b.call(blockId))
28
25
  }
@@ -4,7 +4,10 @@ export function Program(node, ctx) {
4
4
  node = ctx.next() ?? node
5
5
 
6
6
  const stmt = b.exp(
7
- b.render([...ctx.state.template.blocks, b.returnStmt(ctx.state.template.template)])
7
+ b.render([
8
+ ...ctx.state.template.blocks,
9
+ b.returnStmt(b.template(ctx.state.template.template))
10
+ ])
8
11
  )
9
12
  return { ...node, body: [...node.body, stmt] }
10
13
  }
@@ -1,9 +1,9 @@
1
1
  import { print } from 'esrap'
2
- import { append } from '../../../utils/misc.js'
2
+ import { appendText } from '../../../utils/template.js'
3
3
 
4
4
  export function Script(node, ctx) {
5
5
  node = ctx.next() ?? node
6
-
6
+
7
7
  const { code } = print(node.content)
8
- append(ctx.state.text, `<script type="module">${code}</script>`)
8
+ appendText(ctx.state.template, `<script type="module">${code}</script>`)
9
9
  }
@@ -1,8 +1,8 @@
1
1
  import * as b from '../../../builders.js'
2
+ import { appendExpression } from '../../../utils/template.js'
2
3
 
3
4
  export function SlotElement(node, ctx) {
4
5
  node = ctx.next() ?? node
5
6
 
6
- ctx.state.text.push('')
7
- ctx.state.expressions.push(b.member(b.id('slot'), b.id('default')))
7
+ appendExpression(ctx.state.template, b.member(b.id('slot'), b.id('default')))
8
8
  }
@@ -1,9 +1,9 @@
1
1
  import { generate } from 'css-tree'
2
- import { append } from '../../../utils/misc.js'
2
+ import { appendText } from '../../../utils/template.js'
3
3
 
4
4
  export function Style(node, ctx) {
5
5
  node = ctx.next() ?? node
6
6
 
7
7
  const css = generate(node.content)
8
- append(ctx.state.text, `<style>${css}</style>`)
8
+ appendText(ctx.state.template, `<style>${css}</style>`)
9
9
  }
@@ -1,12 +1,8 @@
1
- import * as b from '../../../builders.js'
2
-
3
1
  export function Template(node, ctx) {
4
- const text = ['']
5
- const expressions = []
2
+ const template = { text: [''], expressions: [] }
6
3
  const blocks = []
7
4
 
8
- ctx.visit(node.fragment, { ...ctx.state, text, expressions, blocks })
5
+ ctx.visit(node.fragment, { ...ctx.state, template, blocks })
9
6
 
10
- const template = b.template({ text, expressions })
11
7
  return { type: 'TemplateMod', template, blocks }
12
8
  }
@@ -1,5 +1,5 @@
1
- import { append } from '../../../utils/misc.js'
1
+ import { appendText } from '../../../utils/template.js'
2
2
 
3
3
  export function Text(node, ctx) {
4
- append(ctx.state.text, node.data)
4
+ appendText(ctx.state.template, node.data)
5
5
  }
@@ -1,6 +1,6 @@
1
1
  import * as b from '../../builders.js'
2
2
  import { clx } from '../../css.js'
3
- import { appendText, hasExpression } from '../../utils/template.js'
3
+ import { appendText, hasExpression, isEmpty } from '../../utils/template.js'
4
4
 
5
5
  export function Attribute(node, ctx) {
6
6
  let value = node.value
@@ -21,7 +21,7 @@ export function Attribute(node, ctx) {
21
21
 
22
22
  const template = { text: [''], expressions: [] }
23
23
  for (const val of value) {
24
- ctx.visit(val, { ...ctx.state, template })
24
+ ctx.visit(val, { ...ctx.state, template, pretty: false })
25
25
  }
26
26
 
27
27
  if (hasExpression(template)) {
@@ -66,7 +66,9 @@ export function Attribute(node, ctx) {
66
66
  ])
67
67
  ctx.state.effects.push(stmt)
68
68
  }
69
+ } else if (isEmpty(template)) {
70
+ appendText(ctx.state.template, ` ${node.name}`)
69
71
  } else {
70
- appendText(ctx.state.template, ` ${node.name}="${template.text[0] ?? 'true'}"`)
72
+ appendText(ctx.state.template, ` ${node.name}="${template.text[0]}"`)
71
73
  }
72
74
  }
@@ -1,6 +1,12 @@
1
- import { appendExpression } from "../../utils/template.js"
1
+ import * as b from '../../builders.js'
2
+ import { appendExpression } from '../../utils/template.js'
2
3
 
3
4
  export function ExpressionTag(node, ctx) {
4
5
  node = ctx.next() ?? node
5
- appendExpression(ctx.state.template, node.expression)
6
+
7
+ let expression = node.expression
8
+ if (ctx.state.pretty ?? true) {
9
+ expression = b.logical('??', expression, b.literal(''))
10
+ }
11
+ appendExpression(ctx.state.template, expression)
6
12
  }
@@ -6,7 +6,8 @@ export function MethodDefinition(node, ctx) {
6
6
 
7
7
  if (node.key.name === 'constructor') {
8
8
  const program = getProgram(ctx)
9
- const { properties, setters } = program.metadata?.customElement
9
+ const properties = program.metadata?.customElement.properties ?? []
10
+ const setters = program.metadata?.customElement.setters ?? []
10
11
 
11
12
  const stmt1 = b.assignment(b.$(), b.$$())
12
13
  const stmts2 = []
@@ -30,18 +31,22 @@ export function MethodDefinition(node, ctx) {
30
31
  }
31
32
 
32
33
  if (node.key.name === 'connectedCallback') {
33
- const shadowRootMode = ctx.state.template.metadata?.shadowRootMode
34
+ const stmts = []
34
35
 
35
- const stmts1 = []
36
- const stmts2 = []
37
- if (shadowRootMode) {
38
- stmts1.push(b.assignment(b.shadow(), b.attachShadow(shadowRootMode), '??='))
36
+ if (ctx.state.template?.metadata?.shadowRootMode) {
37
+ const { shadowRootMode } = ctx.state.template.metadata
38
+ stmts.push(b.assignment(b.shadow(), b.attachShadow(shadowRootMode), '??='))
39
+ }
40
+
41
+ if (ctx.state.template?.block) {
42
+ stmts.push(ctx.state.template.block)
39
43
  }
44
+
40
45
  if (node.value.body.body.length > 0) {
41
- stmts2.push(b.$boundary(node.value.body.body))
46
+ stmts.push(b.$boundary(node.value.body.body))
42
47
  }
43
48
 
44
- const stmt = b.$lifecycle('connected', [...stmts1, ctx.state.template.block, ...stmts2])
49
+ const stmt = b.$lifecycle('connected', stmts)
45
50
 
46
51
  return {
47
52
  ...node,
@@ -8,43 +8,47 @@ export function Program(node, ctx) {
8
8
  const stmts1 = []
9
9
  const stmts2 = []
10
10
 
11
+ if (!node.metadata?.hasCustomElementClass) {
12
+ const stmt = ctx.visit(b.customElement(ctx.state.context.customElementClassName))
13
+ stmts2.push(stmt)
14
+ }
15
+
16
+ if (!node.metadata?.hasDefineCustomElement) {
17
+ stmt = b.defineCustomElement(
18
+ ctx.state.context.customElementName,
19
+ node.metadata?.customElement.className ?? ctx.state.context.customElementClassName
20
+ )
21
+ stmts2.push(stmt)
22
+ }
23
+
11
24
  // import
12
25
  stmt = b.importSpecifier('$$', '@katabatic/runtime')
13
26
  stmts1.push(stmt)
14
27
 
15
28
  // html template
16
- const template = ctx.state.template.template
17
- stmt = b.declaration('TEMPLATE', b.template(template))
18
- stmts1.push(stmt)
29
+ if (ctx.state.template?.template) {
30
+ const { template } = ctx.state.template
31
+ stmt = b.declaration('TEMPLATE', b.template(template))
32
+ stmts1.push(stmt)
33
+ }
19
34
 
20
35
  // style
21
- const style = ctx.state.template.style
22
- stmt = b.declaration('STYLE', b.template(style))
23
- stmts1.push(stmt)
24
-
25
- // $name
26
- stmt = b.exp(
27
- b.declaration(
28
- '$name',
29
- b.literal(node.metadata?.customElement.name ?? ctx.state.context.customElementName)
30
- )
31
- )
32
- stmts1.push(stmt)
33
-
34
- // $class
35
- stmt = b.exp(b.declaration('$class', b.id(node.metadata?.customElement.className)))
36
- stmts2.push(stmt)
36
+ if (ctx.state.template?.style) {
37
+ const { style } = ctx.state.template
38
+ stmt = b.declaration('STYLE', b.template(style))
39
+ stmts1.push(stmt)
40
+ }
37
41
 
38
- // $shadowRootMode
39
- stmt = b.exp(
40
- b.declaration('$shadowRootMode', b.literal(ctx.state.template.metadata?.shadowRootMode))
41
- )
42
- stmts1.push(stmt)
42
+ //$hot
43
+ if (ctx.state.context.hot) {
44
+ const stmt = $hot({ ...node, body: [...node.body, ...stmts2] })
45
+ stmts2.push(stmt)
46
+ }
43
47
 
44
48
  // $set
45
49
  const properties = [
46
- ...node.metadata?.customElement.properties,
47
- ...node.metadata?.customElement.setters
50
+ ...(node.metadata?.customElement.properties ?? []),
51
+ ...(node.metadata?.customElement.setters ?? [])
48
52
  ]
49
53
  if (properties.length > 0) {
50
54
  stmt = b.$setDecl([
@@ -57,21 +61,31 @@ export function Program(node, ctx) {
57
61
  } else {
58
62
  stmt = b.$setDecl([b.setAttribute('node', 'attribute', 'value')])
59
63
  }
60
- stmts1.push(stmt)
64
+ stmts2.push(stmt)
61
65
 
62
- //$hot
63
- if (ctx.state.context.hot) {
64
- stmts1.push($hot(node))
65
- }
66
+ // $name
67
+ stmt = b.exp(
68
+ b.declaration(
69
+ '$name',
70
+ b.literal(node.metadata?.customElement.name ?? ctx.state.context.customElementName)
71
+ )
72
+ )
73
+ stmts2.push(stmt)
66
74
 
67
- // defineCustomElement
68
- if (!node.metadata?.hasDefineCustomElement) {
69
- stmt = b.defineCustomElement(
70
- ctx.state.context.customElementName,
71
- node.metadata?.customElement.className
75
+ // $class
76
+ stmt = b.exp(
77
+ b.declaration(
78
+ '$class',
79
+ b.id(node.metadata?.customElement.className ?? ctx.state.context.customElementClassName)
72
80
  )
73
- stmts2.push(stmt)
74
- }
81
+ )
82
+ stmts2.push(stmt)
83
+
84
+ // $shadowRootMode
85
+ stmt = b.exp(
86
+ b.declaration('$shadowRootMode', b.literal(ctx.state.template?.metadata?.shadowRootMode))
87
+ )
88
+ stmts2.push(stmt)
75
89
 
76
90
  return {
77
91
  ...node,
@@ -10,6 +10,14 @@ export function hasExpression(template) {
10
10
  return template.expressions.length > 0
11
11
  }
12
12
 
13
+ export function hasOnlyExpression(template) {
14
+ return (
15
+ template.expressions.length == 1 &&
16
+ template.text[0] === '' &&
17
+ (template.text[1] === undefined || template.text[1] === '')
18
+ )
19
+ }
20
+
13
21
  export function isEmpty(template) {
14
- return template.expressions.length == 0 && template.text.length == 1 && template.text[0] === ''
22
+ return template.expressions.length == 0 && template.text[0] === ''
15
23
  }
package/src/utils/misc.js DELETED
@@ -1,9 +0,0 @@
1
- export function append(array, value, options) {
2
- if (options?.spaceWord) {
3
- if (value && array.at(-1).at(-1) !== '"') {
4
- array[array.length - 1] += ' '
5
- }
6
- }
7
-
8
- array[array.length - 1] += value
9
- }