@fluffjs/cli 0.0.8 → 0.1.0

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 (142) hide show
  1. package/BabelHelpers.d.ts +26 -0
  2. package/BabelHelpers.js +65 -0
  3. package/Cli.d.ts +5 -10
  4. package/Cli.js +123 -52
  5. package/CodeGenerator.d.ts +53 -39
  6. package/CodeGenerator.js +330 -725
  7. package/ComponentCompiler.d.ts +14 -16
  8. package/ComponentCompiler.js +187 -256
  9. package/DomPreProcessor.d.ts +36 -0
  10. package/DomPreProcessor.js +645 -0
  11. package/ErrorHelpers.d.ts +5 -0
  12. package/ErrorHelpers.js +8 -0
  13. package/ExpressionTransformer.d.ts +38 -28
  14. package/ExpressionTransformer.js +558 -230
  15. package/Generator.d.ts +1 -5
  16. package/Generator.js +128 -67
  17. package/GetterDependencyExtractor.d.ts +4 -0
  18. package/GetterDependencyExtractor.js +73 -0
  19. package/IndexHtmlTransformer.d.ts +6 -7
  20. package/IndexHtmlTransformer.js +82 -88
  21. package/Parse5Helpers.d.ts +16 -0
  22. package/Parse5Helpers.js +81 -0
  23. package/TemplateParser.d.ts +39 -21
  24. package/TemplateParser.js +462 -268
  25. package/Typeguards.d.ts +24 -0
  26. package/Typeguards.js +30 -0
  27. package/babel-plugin-class-transform.d.ts +3 -18
  28. package/babel-plugin-class-transform.js +3 -11
  29. package/babel-plugin-component.d.ts +4 -13
  30. package/babel-plugin-component.js +7 -0
  31. package/babel-plugin-imports.d.ts +3 -11
  32. package/babel-plugin-imports.js +5 -31
  33. package/babel-plugin-reactive.d.ts +2 -19
  34. package/babel-plugin-reactive.js +21 -76
  35. package/bin.js +2 -2
  36. package/fluff-esbuild-plugin.d.ts +2 -5
  37. package/fluff-esbuild-plugin.js +4 -1
  38. package/index.d.ts +6 -2
  39. package/index.js +1 -1
  40. package/interfaces/BabelPluginClassTransformState.d.ts +5 -0
  41. package/interfaces/BabelPluginComponentState.d.ts +4 -0
  42. package/interfaces/BabelPluginComponentState.js +1 -0
  43. package/interfaces/BabelPluginImportsState.d.ts +5 -0
  44. package/interfaces/BabelPluginImportsState.js +1 -0
  45. package/interfaces/BabelPluginReactiveState.d.ts +13 -0
  46. package/interfaces/BabelPluginReactiveState.js +1 -0
  47. package/interfaces/BabelPluginReactiveWatchCallInfo.d.ts +7 -0
  48. package/interfaces/BabelPluginReactiveWatchCallInfo.js +1 -0
  49. package/interfaces/BabelPluginReactiveWatchInfo.d.ts +5 -0
  50. package/interfaces/BabelPluginReactiveWatchInfo.js +1 -0
  51. package/interfaces/BabelToken.d.ts +8 -0
  52. package/interfaces/BabelToken.js +1 -0
  53. package/interfaces/BindingInfo.d.ts +12 -0
  54. package/interfaces/BindingInfo.js +1 -0
  55. package/interfaces/BreakMarkerConfig.d.ts +4 -0
  56. package/interfaces/BreakMarkerConfig.js +1 -0
  57. package/interfaces/BreakNode.d.ts +4 -0
  58. package/interfaces/BreakNode.js +1 -0
  59. package/interfaces/BundleOptions.d.ts +8 -0
  60. package/interfaces/BundleOptions.js +1 -0
  61. package/interfaces/ClassTransformOptions.d.ts +10 -0
  62. package/interfaces/ClassTransformOptions.js +1 -0
  63. package/interfaces/CliOptions.d.ts +6 -0
  64. package/interfaces/CliOptions.js +1 -0
  65. package/interfaces/CommentNode.d.ts +5 -0
  66. package/interfaces/CommentNode.js +1 -0
  67. package/interfaces/CompileResult.d.ts +6 -0
  68. package/interfaces/CompileResult.js +1 -0
  69. package/interfaces/CompilerOptions.d.ts +6 -0
  70. package/interfaces/CompilerOptions.js +1 -0
  71. package/interfaces/ComponentInfo.d.ts +8 -0
  72. package/interfaces/ComponentInfo.js +1 -0
  73. package/interfaces/ComponentMetadata.d.ts +9 -0
  74. package/interfaces/ComponentMetadata.js +1 -0
  75. package/interfaces/ControlFlow.d.ts +19 -0
  76. package/interfaces/ControlFlow.js +1 -0
  77. package/interfaces/ControlFlowNode.d.ts +6 -0
  78. package/interfaces/ControlFlowNode.js +1 -0
  79. package/interfaces/ControlFlowParseResult.d.ts +10 -0
  80. package/interfaces/ControlFlowParseResult.js +1 -0
  81. package/interfaces/ElementNode.d.ts +11 -0
  82. package/interfaces/ElementNode.js +1 -0
  83. package/interfaces/FluffConfigInterface.d.ts +7 -0
  84. package/interfaces/FluffConfigInterface.js +1 -0
  85. package/interfaces/FluffPluginOptions.d.ts +9 -0
  86. package/interfaces/FluffPluginOptions.js +1 -0
  87. package/interfaces/FluffTarget.d.ts +15 -0
  88. package/interfaces/FluffTarget.js +1 -0
  89. package/interfaces/ForMarkerConfig.d.ts +9 -0
  90. package/interfaces/ForMarkerConfig.js +1 -0
  91. package/interfaces/ForNode.d.ts +13 -0
  92. package/interfaces/ForNode.js +1 -0
  93. package/interfaces/GeneratorOptions.d.ts +5 -0
  94. package/interfaces/GeneratorOptions.js +1 -0
  95. package/interfaces/HtmlTransformOptions.d.ts +9 -0
  96. package/interfaces/HtmlTransformOptions.js +1 -0
  97. package/interfaces/IfBranch.d.ts +8 -0
  98. package/interfaces/IfBranch.js +1 -0
  99. package/interfaces/IfMarkerConfig.d.ts +8 -0
  100. package/interfaces/IfMarkerConfig.js +1 -0
  101. package/interfaces/IfNode.d.ts +7 -0
  102. package/interfaces/IfNode.js +1 -0
  103. package/interfaces/ImportTransformOptions.d.ts +7 -0
  104. package/interfaces/ImportTransformOptions.js +1 -0
  105. package/interfaces/InterpolationNode.d.ts +12 -0
  106. package/interfaces/InterpolationNode.js +1 -0
  107. package/interfaces/ParsedTemplate.d.ts +6 -0
  108. package/interfaces/ParsedTemplate.js +1 -0
  109. package/interfaces/ParsedTemplateOld.d.ts +9 -0
  110. package/interfaces/ParsedTemplateOld.js +1 -0
  111. package/interfaces/PropertyChain.d.ts +2 -0
  112. package/interfaces/PropertyChain.js +1 -0
  113. package/interfaces/Scope.d.ts +5 -0
  114. package/interfaces/Scope.js +1 -0
  115. package/interfaces/ServeOptions.d.ts +5 -0
  116. package/interfaces/ServeOptions.js +1 -0
  117. package/interfaces/SwitchCase.d.ts +8 -0
  118. package/interfaces/SwitchCase.js +1 -0
  119. package/interfaces/SwitchMarkerConfig.d.ts +11 -0
  120. package/interfaces/SwitchMarkerConfig.js +1 -0
  121. package/interfaces/SwitchNode.d.ts +10 -0
  122. package/interfaces/SwitchNode.js +1 -0
  123. package/interfaces/TemplateBinding.d.ts +10 -0
  124. package/interfaces/TemplateBinding.js +1 -0
  125. package/interfaces/TemplateNode.d.ts +7 -0
  126. package/interfaces/TemplateNode.js +1 -0
  127. package/interfaces/TextMarkerConfig.d.ts +10 -0
  128. package/interfaces/TextMarkerConfig.js +1 -0
  129. package/interfaces/TextNode.d.ts +5 -0
  130. package/interfaces/TextNode.js +1 -0
  131. package/interfaces/TokenizeResult.d.ts +6 -0
  132. package/interfaces/TokenizeResult.js +1 -0
  133. package/interfaces/TransformOptions.d.ts +11 -0
  134. package/interfaces/TransformOptions.js +1 -0
  135. package/interfaces/index.d.ts +34 -0
  136. package/interfaces/index.js +1 -0
  137. package/package.json +9 -1
  138. package/types/FluffConfig.d.ts +5 -27
  139. package/ControlFlowParser.d.ts +0 -55
  140. package/ControlFlowParser.js +0 -279
  141. package/types.d.ts +0 -46
  142. /package/{types.js → interfaces/BabelPluginClassTransformState.js} +0 -0
package/CodeGenerator.js CHANGED
@@ -1,767 +1,372 @@
1
+ import { parse } from '@babel/parser';
2
+ import * as t from '@babel/types';
1
3
  import * as parse5 from 'parse5';
2
- import { addThisPrefix, addThisPrefixSafe, expressionUsesVariable, extractRootIdentifier, parseInterpolations, parsePipedExpression, renameVariable, transformForExpression, transformForExpressionKeepIterator, transformPipedExpression } from './ExpressionTransformer.js';
4
+ import { generate, parseMethodBody } from './BabelHelpers.js';
5
+ import { ExpressionTransformer } from './ExpressionTransformer.js';
6
+ import { Parse5Helpers } from './Parse5Helpers.js';
7
+ const RESTRICTED_ELEMENT_PREFIX = 'x-fluff-el-';
3
8
  export class CodeGenerator {
4
- reactiveProperties = new Set();
5
- templateRefs = [];
6
- setReactiveProperties(props) {
7
- this.reactiveProperties = props;
8
- }
9
- setTemplateRefs(refs) {
10
- this.templateRefs = refs;
9
+ componentSelectors;
10
+ componentSelector;
11
+ static globalExprIdsByExpr = new Map();
12
+ static globalExprs = [];
13
+ static globalHandlerIdsByExpr = new Map();
14
+ static globalHandlers = [];
15
+ markerId = 0;
16
+ markerConfigs = new Map();
17
+ usedExprIds = [];
18
+ usedHandlerIds = [];
19
+ bindingsMap = new Map();
20
+ rootFragment = null;
21
+ collectedTemplates = [];
22
+ constructor(componentSelectors = new Set(), componentSelector = '') {
23
+ this.componentSelectors = componentSelectors;
24
+ this.componentSelector = componentSelector;
25
+ }
26
+ static resetGlobalState() {
27
+ CodeGenerator.globalExprIdsByExpr.clear();
28
+ CodeGenerator.globalExprs = [];
29
+ CodeGenerator.globalHandlerIdsByExpr.clear();
30
+ CodeGenerator.globalHandlers = [];
31
+ }
32
+ generateRenderMethod(template, styles) {
33
+ this.markerId = 0;
34
+ this.markerConfigs.clear();
35
+ const html = this.generateHtml(template);
36
+ const configJson = JSON.stringify(Array.from(this.markerConfigs.entries()));
37
+ return this.generateRenderMethodFromHtml(html, styles, configJson);
38
+ }
39
+ generateHtml(template) {
40
+ this.rootFragment = parse5.parseFragment('');
41
+ this.collectedTemplates.length = 0;
42
+ this.renderNodesToParent(template.root, this.rootFragment);
43
+ for (const tpl of this.collectedTemplates) {
44
+ tpl.parentNode = this.rootFragment;
45
+ this.rootFragment.childNodes.push(tpl);
46
+ }
47
+ return parse5.serialize(this.rootFragment);
11
48
  }
12
- generateRenderMethod(html, styles) {
13
- let content = '';
49
+ generateRenderMethodFromHtml(html, styles, markerConfigJson) {
50
+ let content = html;
14
51
  if (styles) {
15
- content += `<style>${styles}</style>`;
52
+ const fragment = parse5.parseFragment(html);
53
+ const styleElement = Parse5Helpers.createElement('style', []);
54
+ Parse5Helpers.appendText(styleElement, styles);
55
+ fragment.childNodes.unshift(styleElement);
56
+ styleElement.parentNode = fragment;
57
+ content = parse5.serialize(fragment);
16
58
  }
17
- content += html;
18
- content = this.escapeForTemplateLiteral(content);
19
- return `this.__getShadowRoot().innerHTML = \`${content}\`;`;
20
- }
21
- generateBindingsSetup(bindings, controlFlows) {
22
- const lines = [];
23
- for (const binding of bindings) {
24
- lines.push(this.generateBindingCode(binding));
59
+ const statements = [];
60
+ statements.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__getShadowRoot')), []), t.identifier('innerHTML')), t.stringLiteral(content))));
61
+ if (markerConfigJson) {
62
+ statements.push(t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__setMarkerConfigs')), [t.stringLiteral(markerConfigJson)])));
25
63
  }
26
- lines.push('const __renderById = {};');
27
- lines.push(`const __triggerNested = (container) => {
28
- const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
29
- while (walker.nextNode()) {
30
- const id = walker.currentNode.textContent;
31
- if (__renderById[id]) __renderById[id]();
32
- }
33
- };`);
34
- for (const cf of controlFlows) {
35
- lines.push(this.generateControlFlowCode(cf));
64
+ const program = t.program(statements);
65
+ return generate(program, { compact: false }).code;
66
+ }
67
+ getMarkerConfigJson() {
68
+ return JSON.stringify(Array.from(this.markerConfigs.entries()));
69
+ }
70
+ generateBindingsSetup() {
71
+ const statements = [
72
+ t.expressionStatement(t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('__initializeMarkers')), [t.identifier('MarkerManager')])),
73
+ t.expressionStatement(t.callExpression(t.memberExpression(t.super(), t.identifier('__setupBindings')), []))
74
+ ];
75
+ const program = t.program(statements);
76
+ return generate(program, { compact: false }).code;
77
+ }
78
+ getBindingsMap() {
79
+ return Object.fromEntries(this.bindingsMap.entries());
80
+ }
81
+ generateExpressionAssignments() {
82
+ const statements = [];
83
+ for (const id of this.usedExprIds) {
84
+ const expr = CodeGenerator.globalExprs[id];
85
+ const normalizedExpr = CodeGenerator.normalizeCompiledExpr(expr);
86
+ const arrowFunc = CodeGenerator.buildExpressionArrowFunction(['t', 'l'], normalizedExpr);
87
+ statements.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.memberExpression(t.identifier('FluffBase'), t.identifier('__e')), t.numericLiteral(id), true), arrowFunc)));
36
88
  }
37
- return lines.join('\n ');
38
- }
39
- getPropertyRef(propName) {
40
- if (this.reactiveProperties.has(propName)) {
41
- return `__${propName}`;
89
+ for (const id of this.usedHandlerIds) {
90
+ const handler = CodeGenerator.globalHandlers[id];
91
+ const normalizedHandler = CodeGenerator.normalizeCompiledExpr(handler);
92
+ const arrowFunc = CodeGenerator.buildHandlerArrowFunction(['t', 'l', '__ev'], normalizedHandler);
93
+ statements.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.memberExpression(t.identifier('FluffBase'), t.identifier('__h')), t.numericLiteral(id), true), arrowFunc)));
94
+ }
95
+ if (statements.length === 0) {
96
+ return '';
42
97
  }
43
- return propName;
98
+ const program = t.program(statements);
99
+ return generate(program, { compact: false }).code;
44
100
  }
45
- extractBaseProp(expr) {
46
- const result = extractRootIdentifier(expr);
47
- if (result === null) {
48
- throw new Error(`Failed to extract base property from expression: "${expr}"`);
101
+ static generateGlobalExprTable() {
102
+ const exprElements = CodeGenerator.globalExprs.map(e => {
103
+ const normalizedExpr = CodeGenerator.normalizeCompiledExpr(e);
104
+ return CodeGenerator.buildExpressionArrowFunction(['t', 'l'], normalizedExpr);
105
+ });
106
+ const handlerElements = CodeGenerator.globalHandlers.map(h => {
107
+ const normalizedHandler = CodeGenerator.normalizeCompiledExpr(h);
108
+ return CodeGenerator.buildHandlerArrowFunction(['t', 'l', '__ev'], normalizedHandler);
109
+ });
110
+ const statements = [
111
+ t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.identifier('FluffBase'), t.identifier('__e')), t.arrayExpression(exprElements))),
112
+ t.expressionStatement(t.assignmentExpression('=', t.memberExpression(t.identifier('FluffBase'), t.identifier('__h')), t.arrayExpression(handlerElements)))
113
+ ];
114
+ const program = t.program(statements);
115
+ return generate(program, { compact: false }).code;
116
+ }
117
+ static buildExpressionArrowFunction(params, bodyExpr) {
118
+ const paramNodes = params.map(p => t.identifier(p));
119
+ const exprAst = parse(`(${bodyExpr})`, { sourceType: 'module' });
120
+ const [exprStmt] = exprAst.program.body;
121
+ if (t.isExpressionStatement(exprStmt)) {
122
+ return t.arrowFunctionExpression(paramNodes, exprStmt.expression);
49
123
  }
50
- return result;
124
+ return t.arrowFunctionExpression(paramNodes, t.identifier('undefined'));
51
125
  }
52
- escapeForTemplateLiteral(content) {
53
- return content.replace(/`/g, '\\`')
54
- .replace(/\$/g, '\\$');
126
+ static buildHandlerArrowFunction(params, bodyCode) {
127
+ const paramNodes = params.map(p => t.identifier(p));
128
+ const bodyStatements = parseMethodBody(bodyCode);
129
+ return t.arrowFunctionExpression(paramNodes, t.blockStatement(bodyStatements));
55
130
  }
56
- escapeForTemplateLiteralPreservingExpressions(content) {
57
- let result = content.replace(/`/g, '\\`');
58
- result = result.replace(/\$(?!\{)/g, '\\$');
131
+ static normalizeCompiledExpr(expr) {
132
+ let result = expr;
133
+ if (result.includes('this')) {
134
+ result = ExpressionTransformer.replaceThisExpression(result, 't');
135
+ }
136
+ if (result.includes('$event')) {
137
+ result = ExpressionTransformer.renameVariable(result, '$event', '__ev');
138
+ }
59
139
  return result;
60
140
  }
61
- dotsToDashes(str) {
62
- return str.replace(/\./g, '-');
141
+ nextMarkerId() {
142
+ return this.markerId++;
63
143
  }
64
- removeIndexSuffix(str) {
65
- return str.replace(/-\d+$/, '');
144
+ renderNodesToParent(nodes, parent) {
145
+ for (const node of nodes) {
146
+ this.renderNodeToParent(node, parent);
147
+ }
66
148
  }
67
- generateBindingCode(binding) {
68
- const { id, type, expression, target, eventName, className, styleProp } = binding;
69
- switch (type) {
149
+ renderNodeToParent(node, parent) {
150
+ switch (node.type) {
151
+ case 'element':
152
+ this.renderElementToParent(node, parent);
153
+ break;
70
154
  case 'text':
71
- return this.generateTextBinding(id, expression);
72
- case 'property':
73
- return target ? this.generatePropertyBinding(id, target, expression) : '';
74
- case 'event':
75
- return eventName ? this.generateEventBinding(id, eventName, expression) : '';
76
- case 'class':
77
- return className ? this.generateClassBinding(id, className, expression) : '';
78
- case 'style':
79
- return styleProp ? this.generateStyleBinding(id, styleProp, expression) : '';
80
- default:
81
- return '';
155
+ this.renderTextToParent(node, parent);
156
+ break;
157
+ case 'interpolation':
158
+ this.renderInterpolationToParent(node, parent);
159
+ break;
160
+ case 'comment':
161
+ this.renderCommentToParent(node, parent);
162
+ break;
163
+ case 'if':
164
+ this.renderIfToParent(node, parent);
165
+ break;
166
+ case 'for':
167
+ this.renderForToParent(node, parent);
168
+ break;
169
+ case 'switch':
170
+ this.renderSwitchToParent(node, parent);
171
+ break;
172
+ case 'break':
173
+ this.renderBreakToParent(parent);
174
+ break;
82
175
  }
83
176
  }
84
- generateTextBinding(id, expression) {
85
- const { expression: baseExpr, pipes } = parsePipedExpression(expression);
86
- const hasPipes = pipes.length > 0;
87
- const baseProp = this.extractBaseProp(baseExpr);
88
- const propRef = this.getPropertyRef(baseProp);
89
- const restPath = baseExpr.split('.')
90
- .slice(1);
91
- const safeAccess = restPath.length > 0 ? `Array.isArray(val) ? val.${restPath.join('.')} : (val && typeof val === 'object' ? val.${restPath.join('.')} : val)` : 'val';
92
- const reactivePropsArray = Array.from(this.reactiveProperties);
93
- const subscribeToAll = reactivePropsArray.map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __update);`)
94
- .join('\n ');
95
- if (hasPipes) {
96
- const transformedExpr = transformPipedExpression(expression);
97
- return `{
98
- const __update = () => {
99
- const result = ${transformedExpr};
100
- this.__setText("${id}", String(result ?? ''));
101
- };
102
- ${subscribeToAll}
103
- __update();
104
- }`;
177
+ renderElementToParent(node, parent) {
178
+ const attrs = [];
179
+ if (this.isComponentTag(node.tagName)) {
180
+ attrs.push({ name: 'x-fluff-component', value: '' });
105
181
  }
106
- return `{
107
- const prop = this.${propRef};
108
- if (prop && prop.onChange) {
109
- const __getText = (val) => {
110
- if (val == null) return '';
111
- ${restPath.length > 0 ? `return String(${safeAccess} ?? '');` : 'return String(val ?? \'\');'}
112
- };
113
- this.__bindPropertyChange(prop, (val) => {
114
- this.__setText("${id}", __getText(val));
115
- });
116
- } else {
117
- const __update = () => {
118
- const raw = this.${propRef};
119
- const val = (raw && typeof raw === 'object' && raw.getValue) ? raw.getValue() : raw;
120
- ${restPath.length > 0 ? `const result = val != null ? val.${restPath.join('.')} : undefined;` : 'const result = val;'}
121
- this.__setText("${id}", String(result ?? ''));
122
- };
123
- ${subscribeToAll}
124
- __update();
125
- }
126
- }`;
127
- }
128
- generatePropertyBinding(id, target, expression) {
129
- const baseProp = this.extractBaseProp(expression);
130
- const propRef = this.getPropertyRef(baseProp);
131
- const targetCamel = this.kebabToCamel(target);
132
- const restPath = expression.split('.')
133
- .slice(1);
134
- const valueExpr = restPath.length > 0 ? `val?.${restPath.join('.')}` : 'val';
135
- return `{
136
- const prop = this.${propRef};
137
- if (prop && prop.onChange) {
138
- this.__bindPropertyChange(prop, (val) => {
139
- this.__bindToChild("${id}", "${targetCamel}", ${valueExpr});
140
- });
141
- } else {
142
- const val = (this.${propRef} && typeof this.${propRef} === 'object' && this.${propRef}.getValue) ? this.${propRef}.getValue() : this.${propRef};
143
- this.__bindToChild("${id}", "${targetCamel}", ${restPath.length > 0 ? `val?.${restPath.join('.')}` : 'val'});
144
- }
145
- }`;
146
- }
147
- generateEventBinding(id, eventName, expression) {
148
- const [baseEvent, ...modifiers] = eventName.split('.');
149
- const handlerCode = renameVariable(expression, '$event', '__ev');
150
- const eventCamel = this.kebabToCamel(baseEvent);
151
- const refLookups = this.generateRefLookups(expression);
152
- if (modifiers.length > 0) {
153
- const conditions = modifiers.map(m => {
154
- if (m === 'enter')
155
- return '__ev.key === "Enter"';
156
- if (m === 'escape')
157
- return '__ev.key === "Escape"';
158
- if (m === 'space')
159
- return '__ev.key === " "';
160
- return 'true';
161
- })
162
- .join(' && ');
163
- return `this.__bindEvent("${id}", "${baseEvent}", (__ev) => { ${refLookups}if (${conditions}) { this.${handlerCode}; } });`;
182
+ for (const [name, value] of Object.entries(node.attributes)) {
183
+ attrs.push({ name, value });
164
184
  }
165
- return `this.__bindOutput("${id}", "${eventCamel}", (__ev: any) => { ${refLookups}this.${handlerCode}; });`;
166
- }
167
- generateRefLookups(expression) {
168
- const usedRefs = this.templateRefs.filter(ref => new RegExp(`\\b${ref}\\b`).test(expression));
169
- if (usedRefs.length === 0)
170
- return '';
171
- return usedRefs.map(ref => `const ${ref} = this.__getShadowRoot().querySelector('[data-ref="${ref}"]');`)
172
- .join(' ') + ' ';
173
- }
174
- generateClassBinding(id, className, expression) {
175
- const baseProp = this.extractBaseProp(expression);
176
- const propRef = this.getPropertyRef(baseProp);
177
- const restPath = expression.split('.')
178
- .slice(1);
179
- const safeExpr = restPath.length > 0 ? `this.${baseProp}?.${restPath.join('.')}` : `this.${baseProp}`;
180
- return `{
181
- const prop = this.${propRef};
182
- const el = this.__getElement("${id}");
183
- if (prop && prop.onChange) {
184
- this.__bindPropertyChange(prop, (val) => {
185
- const finalVal = ${restPath.length > 0 ? `val?.${restPath.join('.')}` : 'val'};
186
- if (finalVal) { this.__addClass("${id}", "${className}"); }
187
- else { this.__removeClass("${id}", "${className}"); }
188
- });
189
- } else if (el) {
190
- if (${safeExpr}) { el.classList.add("${className}"); }
191
- else { el.classList.remove("${className}"); }
185
+ if (node.id) {
186
+ attrs.push({ name: 'data-lid', value: node.id });
187
+ }
188
+ if (node.bindings.length > 0) {
189
+ if (!node.id) {
190
+ throw new Error(`Bindings on <${node.tagName}> require a data-lid`);
192
191
  }
193
- }`;
194
- }
195
- generateStyleBinding(id, styleProp, expression) {
196
- const reactivePropsArray = Array.from(this.reactiveProperties);
197
- const subscribeToAll = reactivePropsArray.map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __update);`)
198
- .join('\n ');
199
- const transformedExpr = addThisPrefix(expression);
200
- const stylePropCamel = this.kebabToCamel(styleProp);
201
- return `{
202
- const el = this.__getElement("${id}");
203
- const __update = () => {
204
- if (el) {
205
- (el as HTMLElement).style.${stylePropCamel} = ${transformedExpr};
206
- }
207
- };
208
- ${subscribeToAll}
209
- __update();
210
- }`;
211
- }
212
- generateControlFlowCode(cf) {
213
- if (cf.type === 'if') {
214
- return this.generateIfCode(cf);
192
+ const bindingsPayload = node.bindings.map(b => this.serializeBinding(b));
193
+ this.bindingsMap.set(node.id, bindingsPayload);
215
194
  }
216
- else if (cf.type === 'for') {
217
- return this.generateForCode(cf);
195
+ const refBinding = node.bindings.find(binding => binding.binding === 'ref');
196
+ if (refBinding) {
197
+ attrs.push({ name: 'data-ref', value: refBinding.name });
218
198
  }
219
- else if (cf.type === 'switch') {
220
- return this.generateSwitchCode(cf);
199
+ let { tagName } = node;
200
+ if (tagName.startsWith(RESTRICTED_ELEMENT_PREFIX)) {
201
+ tagName = tagName.slice(RESTRICTED_ELEMENT_PREFIX.length);
221
202
  }
222
- return '';
223
- }
224
- processContentBindings(content, prefix, skipEscape = false) {
225
- const events = new Set();
226
- let bindIdx = 0;
227
- const fragment = parse5.parseFragment(content);
228
- this.walkAndTransformBindings(fragment.childNodes, prefix, events, () => bindIdx++);
229
- let processed = parse5.serialize(fragment);
230
- if (!skipEscape) {
231
- processed = this.escapeForTemplateLiteral(processed);
203
+ const el = Parse5Helpers.createElement(tagName, attrs);
204
+ Parse5Helpers.appendChild(parent, el);
205
+ this.renderNodesToParent(node.children, el);
206
+ }
207
+ renderTextToParent(node, parent) {
208
+ Parse5Helpers.appendText(parent, node.content);
209
+ }
210
+ isComponentTag(tagName) {
211
+ const resolvedTagName = tagName.startsWith(RESTRICTED_ELEMENT_PREFIX)
212
+ ? tagName.slice(RESTRICTED_ELEMENT_PREFIX.length)
213
+ : tagName;
214
+ return this.componentSelectors.has(resolvedTagName);
215
+ }
216
+ serializeBinding(binding) {
217
+ const result = {
218
+ n: binding.name,
219
+ b: binding.binding
220
+ };
221
+ if (binding.binding === 'ref') {
222
+ return result;
232
223
  }
233
- return { processed, events: [...events] };
234
- }
235
- walkAndTransformBindings(nodes, prefix, events, getBindIdx) {
236
- for (const node of nodes) {
237
- if (this.isParse5Element(node)) {
238
- const attrsToRemove = [];
239
- const attrsToAdd = [];
240
- const subscribeMap = new Map();
241
- for (const attr of node.attrs) {
242
- if (attr.name === '[subscribe]') {
243
- const parts = attr.value.split(':');
244
- if (parts.length === 2) {
245
- subscribeMap.set(parts[0].trim(), parts[1].trim());
246
- }
247
- }
248
- }
249
- for (const attr of node.attrs) {
250
- if (attr.name === '[subscribe]') {
251
- attrsToRemove.push(attr.name);
252
- continue;
253
- }
254
- if (attr.name.startsWith('#')) {
255
- const refName = attr.name.slice(1);
256
- attrsToRemove.push(attr.name);
257
- attrsToAdd.push({ name: 'data-ref', value: refName });
258
- continue;
259
- }
260
- if (attr.name.startsWith('(') && attr.name.endsWith(')')) {
261
- let event = '';
262
- let handler = '';
263
- if (attr.value.startsWith('[')) {
264
- try {
265
- const parsed = JSON.parse(attr.value);
266
- if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
267
- [event, handler] = parsed;
268
- }
269
- }
270
- catch {
271
- event = attr.name.slice(1, -1);
272
- handler = attr.value;
273
- }
274
- }
275
- else {
276
- event = attr.name.slice(1, -1);
277
- handler = attr.value;
278
- }
279
- const safeEvent = this.dotsToDashes(event);
280
- events.add(safeEvent);
281
- attrsToRemove.push(attr.name);
282
- attrsToAdd.push({
283
- name: `data-${prefix}-event-${safeEvent}`,
284
- value: JSON.stringify([event, handler])
285
- });
286
- }
287
- else if (attr.name.startsWith('[') && attr.name.endsWith(']')) {
288
- let prop = '';
289
- let expr = '';
290
- if (attr.value.startsWith('[')) {
291
- try {
292
- const parsed = JSON.parse(attr.value);
293
- if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
294
- [prop, expr] = parsed;
295
- }
296
- }
297
- catch {
298
- prop = attr.name.slice(1, -1);
299
- expr = attr.value;
300
- }
301
- }
302
- else {
303
- prop = attr.name.slice(1, -1);
304
- expr = attr.value;
305
- }
306
- const dangerousProps = ['innerHTML', 'outerHTML', 'href', 'src'];
307
- const isUnsafe = prop.endsWith('.unsafe');
308
- const baseProp = isUnsafe ? prop.slice(0, -7) : prop;
309
- if (dangerousProps.includes(baseProp) && !isUnsafe) {
310
- throw new Error(`XSS Protection: [${prop}] binding is blocked. ` + `Use [${prop}.unsafe] if you understand the security implications ` + 'and have sanitized user input.');
311
- }
312
- const safeProp = this.dotsToDashes(baseProp);
313
- attrsToRemove.push(attr.name);
314
- const subscribeTo = subscribeMap.get(prop) ?? subscribeMap.get(baseProp);
315
- const bindingData = subscribeTo ? [baseProp, expr, subscribeTo] : [baseProp, expr];
316
- attrsToAdd.push({
317
- name: `data-${prefix}-bind-${safeProp}-${getBindIdx()}`,
318
- value: JSON.stringify(bindingData)
319
- });
320
- }
321
- }
322
- node.attrs = node.attrs.filter(a => !attrsToRemove.includes(a.name));
323
- for (const newAttr of attrsToAdd) {
324
- node.attrs.push(newAttr);
325
- }
326
- if (node.childNodes) {
327
- this.walkAndTransformBindings(node.childNodes, prefix, events, getBindIdx);
328
- }
329
- }
224
+ if (binding.deps) {
225
+ result.d = binding.deps;
330
226
  }
331
- }
332
- isParse5Element(node) {
333
- return 'tagName' in node;
334
- }
335
- processAndGenerateBindings(content, prefix) {
336
- const result = this.processContentBindings(content, prefix, true);
337
- const withInterpolations = this.transformInterpolationsInContent(result.processed);
338
- const escaped = this.escapeForTemplateLiteralPreservingExpressions(withInterpolations);
339
- const bindingSetup = this.generateBindingSetupCode(prefix, result.events, withInterpolations);
340
- return { processed: escaped, bindingSetup };
341
- }
342
- transformInterpolationsInContent(content, iteratorVar) {
343
- const interpolations = parseInterpolations(content);
344
- if (interpolations.length === 0)
345
- return content;
346
- let newContent = '';
347
- let lastEnd = 0;
348
- for (const { start, end, expr } of interpolations) {
349
- newContent += content.slice(lastEnd, start);
350
- const { expression: baseExpr, pipes } = parsePipedExpression(expr);
351
- if (pipes.length > 0) {
352
- let result = iteratorVar && expressionUsesVariable(baseExpr, iteratorVar) ? baseExpr : `this.${baseExpr}`;
353
- for (const pipe of pipes) {
354
- const argsStr = pipe.args.length > 0 ? ', ' + pipe.args.join(', ') : '';
355
- result = `this.__pipe('${pipe.name}', ${result}${argsStr})`;
356
- }
357
- newContent += `\${${result} ?? ''}`;
358
- }
359
- else {
360
- const usesIterator = iteratorVar && expressionUsesVariable(expr, iteratorVar);
361
- if (usesIterator) {
362
- newContent += `\${${expr}}`;
363
- }
364
- else {
365
- newContent += `\${this.${expr} ?? ''}`;
366
- }
367
- }
368
- lastEnd = end;
369
- }
370
- newContent += content.slice(lastEnd);
371
- return newContent;
372
- }
373
- generateBindingSetupCode(prefix, events, content) {
374
- const lines = [];
375
- for (const event of events) {
376
- lines.push(`this.__wireEvents(container, 'data-${prefix}-event-${event}');`);
377
- }
378
- const textBindings = this.extractTextBindings(content);
379
- for (const { id, expr } of textBindings) {
380
- const subscriptions = Array.from(this.reactiveProperties)
381
- .map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __update);`)
382
- .join('\n ');
383
- lines.push(`{
384
- const el = container.querySelector('[data-lid="${id}"]');
385
- if (el) {
386
- const __update = () => {
387
- const __val = ${expr};
388
- el.textContent = __val != null ? String(__val) : '';
389
- };
390
- ${subscriptions}
391
- __update();
392
- }
393
- }`);
227
+ if (binding.subscribe) {
228
+ result.s = binding.subscribe;
394
229
  }
395
- const propBindings = this.extractPropertyBindings(content, prefix);
396
- for (const { prop, expr, fullAttr } of propBindings) {
397
- const transformedExpr = addThisPrefix(expr);
398
- const baseProp = this.extractBaseProp(expr);
399
- const propRef = this.reactiveProperties.has(baseProp) ? `__${baseProp}` : baseProp;
400
- if (prop.startsWith('class.')) {
401
- const className = prop.slice(6);
402
- lines.push(`{
403
- const __updateClass = () => {
404
- container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach(el => {
405
- if (${transformedExpr}) el.classList.add('${className}');
406
- else el.classList.remove('${className}');
407
- });
408
- };
409
- if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateClass);
410
- __updateClass();
411
- }`);
412
- }
413
- else {
414
- const propCamel = this.kebabToCamel(prop);
415
- lines.push(`{
416
- const __updateProp = () => {
417
- container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach(el => {
418
- this.__setChildPropertyDeferred(el, '${propCamel}', ${transformedExpr});
419
- });
420
- };
421
- if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateProp);
422
- __updateProp();
423
- }`);
230
+ if (binding.binding === 'event') {
231
+ if (!binding.expression) {
232
+ throw new Error(`Event binding for ${binding.name} is missing expression`);
424
233
  }
234
+ result.h = this.internHandler(binding.expression);
235
+ return result;
425
236
  }
426
- return lines.join('\n ');
427
- }
428
- extractPropertyBindings(content, prefix) {
429
- const bindings = [];
430
- const fragment = parse5.parseFragment(content);
431
- const attrPrefix = `data-${prefix}-bind-`;
432
- this.walkAndExtractPropertyBindings(fragment.childNodes, attrPrefix, bindings);
433
- return bindings;
434
- }
435
- generateUnifiedRender(id, baseProp, contentEvalCode, bindingSetupCode) {
436
- const propRef = this.getPropertyRef(baseProp);
437
- return `{
438
- const __findMarkers = () => {
439
- const walker = document.createTreeWalker(this.__getShadowRoot(), NodeFilter.SHOW_COMMENT);
440
- let start = null, end = null;
441
- while (walker.nextNode()) {
442
- const text = walker.currentNode.textContent;
443
- if (text === '${id}') start = walker.currentNode;
444
- else if (text === '/${id}') end = walker.currentNode;
445
- }
446
- return { start, end };
447
- };
448
- const prop = this.${propRef};
449
-
450
- const __render = () => {
451
- const { start: marker, end: endMarker } = __findMarkers();
452
- if (!marker) return;
453
-
454
- if (endMarker) {
455
- while (marker.nextSibling && marker.nextSibling !== endMarker) {
456
- marker.nextSibling.remove();
457
- }
458
- }
459
-
460
- ${contentEvalCode}
461
-
462
- const __tmp = document.createElement('div');
463
- __tmp.innerHTML = __content;
464
- const __frag = document.createDocumentFragment();
465
- const __newNodes = [];
466
- while (__tmp.firstChild) {
467
- __newNodes.push(__tmp.firstChild);
468
- __frag.appendChild(__tmp.firstChild);
469
- }
470
- marker.parentNode.insertBefore(__frag, endMarker || marker.nextSibling);
471
- const container = marker.parentNode;
472
-
473
- ${bindingSetupCode}
474
-
475
- __newNodes.forEach(n => {
476
- if (n.nodeType === 1) __triggerNested(n);
477
- else if (n.nodeType === 8 && __renderById[n.textContent]) __renderById[n.textContent]();
478
- });
479
- };
480
-
481
- __renderById['${id}'] = __render;
482
- if (prop && prop.onChange) {
483
- this.__bindPropertyChange(prop, __render);
237
+ if (binding.binding === 'two-way') {
238
+ if (!binding.expression) {
239
+ throw new Error(`Two-way binding for ${binding.name} is missing expression`);
484
240
  }
485
- __render();
486
- }`;
487
- }
488
- generateIfCode(cf) {
489
- const { id, condition, ifContent, elseContent } = cf;
490
- if (!condition)
491
- return '';
492
- const baseProp = this.extractBaseProp(condition);
493
- const transformedCondition = addThisPrefixSafe(condition);
494
- const ifBranch = this.processAndGenerateBindings(ifContent ?? '', `${id}-if`);
495
- const elseBranch = this.processAndGenerateBindings(elseContent ?? '', `${id}-else`);
496
- const contentEvalCode = `const rawCond = ${transformedCondition};
497
- const cond = (rawCond && typeof rawCond === 'object' && rawCond.getValue) ? rawCond.getValue() : rawCond;
498
- const __content = cond ? \`${ifBranch.processed}\` : \`${elseBranch.processed}\`;
499
- const __bindingFn = cond
500
- ? () => { ${ifBranch.bindingSetup} }
501
- : () => { ${elseBranch.bindingSetup} };`;
502
- const bindingSetupCode = '__bindingFn();';
503
- return this.generateUnifiedRender(id, baseProp, contentEvalCode, bindingSetupCode);
504
- }
505
- generateForCode(cf) {
506
- const { id, iterator, iterable, content } = cf;
507
- if (!iterator || !iterable)
508
- return '';
509
- const baseProp = this.extractBaseProp(iterable);
510
- const isOptionContent = (content ?? '').trim()
511
- .startsWith('<option');
512
- const markerId = isOptionContent ? `for-${id}` : id;
513
- const prefix = `${id}-for`;
514
- const processed = this.processContentBindings(content ?? '', prefix, true);
515
- let forContent = processed.processed;
516
- const interpolations = parseInterpolations(forContent);
517
- if (interpolations.length > 0) {
518
- let newContent = '';
519
- let lastEnd = 0;
520
- for (const { start, end, expr } of interpolations) {
521
- newContent += forContent.slice(lastEnd, start);
522
- if (expr.includes(iterator)) {
523
- newContent += `\${${expr}}`;
524
- }
525
- else {
526
- newContent += `\${this.${expr}}`;
527
- }
528
- lastEnd = end;
241
+ if (!binding.expression.startsWith('this.')) {
242
+ throw new Error(`Two-way binding for ${binding.name} must target a component property`);
529
243
  }
530
- newContent += forContent.slice(lastEnd);
531
- forContent = newContent;
532
- }
533
- const bindingSetup = this.generateForBindingSetup(prefix, processed.events, forContent, iterator);
534
- const escapedForContent = this.escapeForTemplateLiteralPreservingExpressions(forContent);
535
- const contentEvalCode = `const __items = this.${iterable};
536
- if (!Array.isArray(__items)) return;
537
- const __content = __items.map((${iterator}, __idx) => \`${escapedForContent}\`).join('');`;
538
- return this.generateUnifiedRender(markerId, baseProp, contentEvalCode, bindingSetup);
539
- }
540
- generateForBindingSetup(prefix, _events, content, iterator) {
541
- const lines = [];
542
- const eventBindings = this.extractEventBindings(content, prefix);
543
- for (const { event, handler, safeEvent } of eventBindings) {
544
- lines.push(`container.querySelectorAll('[data-${prefix}-event-${safeEvent}]').forEach((el, __idx) => {
545
- const __item = __items[__idx];
546
- this.__bindOutputOnElement(el, '${event}', ($event) => {
547
- const fn = new Function('${iterator}', '$event', 'return this.${handler}').bind(this);
548
- fn(__item, $event);
549
- });
550
- });`);
244
+ result.t = binding.expression.slice('this.'.length);
551
245
  }
552
- const propBindings = this.extractPropertyBindings(content, prefix);
553
- for (const { prop, expr, fullAttr, subscribeTo } of propBindings) {
554
- const propCamel = this.kebabToCamel(prop);
555
- const evalExpr = expr === iterator ? '__items[__idx]' : transformForExpression(expr, iterator, '__items[__idx]');
556
- const baseProp = this.extractBaseProp(expr);
557
- const exprUsesIterator = expressionUsesVariable(expr, iterator);
558
- if (subscribeTo) {
559
- const exprWithThis = transformForExpressionKeepIterator(expr, iterator);
560
- const subscribeProps = subscribeTo.split(',')
561
- .map(p => p.trim());
562
- const subscriptions = subscribeProps
563
- .map(p => `if (this.__${p}?.onChange) this.__bindPropertyChange(this.__${p}, __updateProp);`)
564
- .join('\n ');
565
- lines.push(`{
566
- const __updateProp = () => {
567
- const __currentItems = prop?.getValue ? prop.getValue() : __items;
568
- if (!Array.isArray(__currentItems)) return;
569
- container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el, __idx) => {
570
- if (__idx < __currentItems.length) {
571
- const ${iterator} = __currentItems[__idx];
572
- this.__setChildPropertyDeferred(el, '${propCamel}', ${exprWithThis});
573
- }
574
- });
575
- };
576
- ${subscriptions}
577
- __updateProp();
578
- }`);
579
- }
580
- else if (baseProp !== iterator && this.reactiveProperties.has(baseProp)) {
581
- const propRef = `__${baseProp}`;
582
- if (exprUsesIterator) {
583
- const exprWithThis = transformForExpressionKeepIterator(expr, iterator);
584
- lines.push(`{
585
- const __updateProp = () => {
586
- const __currentItems = prop?.getValue ? prop.getValue() : __items;
587
- if (!Array.isArray(__currentItems)) return;
588
- container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el, __idx) => {
589
- if (__idx < __currentItems.length) {
590
- const ${iterator} = __currentItems[__idx];
591
- this.__setChildPropertyDeferred(el, '${propCamel}', ${exprWithThis});
592
- }
593
- });
594
- };
595
- if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateProp);
596
- __updateProp();
597
- }`);
598
- }
599
- else {
600
- lines.push(`{
601
- const __updateProp = () => {
602
- container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el) => {
603
- this.__setChildPropertyDeferred(el, '${propCamel}', ${evalExpr});
604
- });
605
- };
606
- if (this.${propRef}?.onChange) this.__bindPropertyChange(this.${propRef}, __updateProp);
607
- __updateProp();
608
- }`);
609
- }
610
- }
611
- else {
612
- lines.push(`container.querySelectorAll('[data-${prefix}-bind-${fullAttr}]').forEach((el, __idx) => {
613
- this.__setChildPropertyDeferred(el, '${propCamel}', ${evalExpr});
614
- });`);
615
- }
246
+ if (!binding.expression) {
247
+ throw new Error(`Binding for ${binding.name} is missing expression`);
616
248
  }
617
- return lines.join('\n ');
618
- }
619
- extractEventBindings(content, prefix) {
620
- const bindings = [];
621
- const fragment = parse5.parseFragment(content);
622
- const attrPrefix = `data-${prefix}-event-`;
623
- this.walkAndExtractEventBindings(fragment.childNodes, attrPrefix, bindings);
624
- return bindings;
249
+ result.e = this.internExpression(binding.expression);
250
+ return result;
625
251
  }
626
- walkAndExtractEventBindings(nodes, attrPrefix, bindings) {
627
- for (const node of nodes) {
628
- if (this.isParse5Element(node)) {
629
- for (const attr of node.attrs) {
630
- if (attr.name.startsWith(attrPrefix)) {
631
- const safeEvent = attr.name.slice(attrPrefix.length);
632
- let event = '';
633
- let handler = '';
634
- if (attr.value.startsWith('[')) {
635
- try {
636
- const unescaped = attr.value.replace(/\\\$/g, '$');
637
- const parsed = JSON.parse(unescaped);
638
- if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
639
- [event, handler] = parsed;
640
- }
641
- }
642
- catch (e) {
643
- console.error('Failed to parse event binding JSON:', attr.value, e);
644
- event = this.kebabToCamel(safeEvent);
645
- handler = attr.value;
646
- }
647
- }
648
- else {
649
- event = this.kebabToCamel(safeEvent);
650
- handler = attr.value;
651
- }
652
- bindings.push({ event, handler, safeEvent });
653
- }
654
- }
655
- if (node.childNodes) {
656
- this.walkAndExtractEventBindings(node.childNodes, attrPrefix, bindings);
657
- }
252
+ internExpression(expr) {
253
+ const existing = CodeGenerator.globalExprIdsByExpr.get(expr);
254
+ if (existing !== undefined) {
255
+ if (!this.usedExprIds.includes(existing)) {
256
+ this.usedExprIds.push(existing);
658
257
  }
258
+ return existing;
659
259
  }
660
- }
661
- generateSwitchCode(cf) {
662
- const { id, expression, cases } = cf;
663
- if (!expression)
664
- return '';
665
- const baseProp = this.extractBaseProp(expression);
666
- const transformedExpr = addThisPrefixSafe(expression);
667
- const processedCases = (cases ?? []).map((c, idx) => {
668
- const { processed, bindingSetup } = this.processAndGenerateBindings(c.content, `${id}-case-${idx}`);
669
- return {
670
- value: c.value, fallthrough: c.fallthrough, content: processed, bindingSetup
671
- };
672
- });
673
- const caseContents = processedCases.map(c => `\`${c.content}\``)
674
- .join(', ');
675
- const caseConditions = processedCases.map((c, idx) => {
676
- if (c.value === null)
677
- return `{ idx: ${idx}, match: true }`;
678
- return `{ idx: ${idx}, match: __switchVal === (${addThisPrefixSafe(c.value)}) }`;
679
- })
680
- .join(', ');
681
- const caseBindings = processedCases.map(c => `(container) => { ${c.bindingSetup} }`)
682
- .join(', ');
683
- const fallthrough = processedCases.map(c => c.fallthrough.toString())
684
- .join(', ');
685
- const contentEvalCode = `const __switchVal = ${transformedExpr};
686
- const __cases = [${caseContents}];
687
- const __conditions = [${caseConditions}];
688
- const __bindings = [${caseBindings}];
689
- const __fallthrough = [${fallthrough}];
690
-
691
- let __content = '';
692
- let __bindingFns = [];
693
- let __matched = false;
694
- for (let i = 0; i < __conditions.length; i++) {
695
- if (__matched || __conditions[i].match) {
696
- __matched = true;
697
- __content += __cases[i];
698
- __bindingFns.push(__bindings[i]);
699
- if (!__fallthrough[i]) break;
700
- }
701
- }`;
702
- const bindingSetupCode = '__bindingFns.forEach(fn => fn(container));';
703
- return this.generateUnifiedRender(id, baseProp, contentEvalCode, bindingSetupCode);
704
- }
705
- kebabToCamel(str) {
706
- return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
707
- }
708
- extractTextBindings(content) {
709
- const bindings = [];
710
- const fragment = parse5.parseFragment(content);
711
- this.walkAndExtractTextBindings(fragment.childNodes, bindings);
712
- return bindings;
713
- }
714
- walkAndExtractTextBindings(nodes, bindings) {
715
- for (const node of nodes) {
716
- if (this.isParse5Element(node)) {
717
- if (node.tagName === 'span') {
718
- const textBindAttr = node.attrs.find(a => a.name === 'data-text-bind');
719
- const lidAttr = node.attrs.find(a => a.name === 'data-lid');
720
- if (textBindAttr && lidAttr) {
721
- bindings.push({ expr: textBindAttr.value, id: lidAttr.value });
722
- }
723
- }
724
- if (node.childNodes) {
725
- this.walkAndExtractTextBindings(node.childNodes, bindings);
726
- }
260
+ const id = CodeGenerator.globalExprs.length;
261
+ CodeGenerator.globalExprs.push(expr);
262
+ CodeGenerator.globalExprIdsByExpr.set(expr, id);
263
+ this.usedExprIds.push(id);
264
+ return id;
265
+ }
266
+ internHandler(expr) {
267
+ const existing = CodeGenerator.globalHandlerIdsByExpr.get(expr);
268
+ if (existing !== undefined) {
269
+ if (!this.usedHandlerIds.includes(existing)) {
270
+ this.usedHandlerIds.push(existing);
727
271
  }
272
+ return existing;
728
273
  }
729
- }
730
- walkAndExtractPropertyBindings(nodes, attrPrefix, bindings) {
731
- for (const node of nodes) {
732
- if (this.isParse5Element(node)) {
733
- for (const attr of node.attrs) {
734
- if (attr.name.startsWith(attrPrefix)) {
735
- const fullAttr = attr.name.slice(attrPrefix.length);
736
- let prop = '';
737
- let expr = '';
738
- let subscribeTo = undefined;
739
- if (attr.value.startsWith('[')) {
740
- try {
741
- const parsed = JSON.parse(attr.value);
742
- if (Array.isArray(parsed) && typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
743
- [prop, expr] = parsed;
744
- if (typeof parsed[2] === 'string') {
745
- [, , subscribeTo] = parsed;
746
- }
747
- }
748
- }
749
- catch {
750
- prop = this.removeIndexSuffix(fullAttr);
751
- expr = attr.value;
752
- }
753
- }
754
- else {
755
- prop = this.removeIndexSuffix(fullAttr);
756
- expr = attr.value;
757
- }
758
- bindings.push({ prop, expr, fullAttr, subscribeTo });
759
- }
760
- }
761
- if (node.childNodes) {
762
- this.walkAndExtractPropertyBindings(node.childNodes, attrPrefix, bindings);
763
- }
764
- }
274
+ const id = CodeGenerator.globalHandlers.length;
275
+ CodeGenerator.globalHandlers.push(expr);
276
+ CodeGenerator.globalHandlerIdsByExpr.set(expr, id);
277
+ this.usedHandlerIds.push(id);
278
+ return id;
279
+ }
280
+ renderInterpolationToParent(node, parent) {
281
+ const id = this.nextMarkerId();
282
+ const config = {
283
+ type: 'text',
284
+ exprId: this.internExpression(node.expression),
285
+ deps: node.deps,
286
+ pipes: node.pipes?.map(pipe => ({
287
+ name: pipe.name,
288
+ argExprIds: pipe.args.map(arg => this.internExpression(arg))
289
+ }))
290
+ };
291
+ this.markerConfigs.set(id, config);
292
+ Parse5Helpers.appendComment(parent, `fluff:text:${id}`);
293
+ Parse5Helpers.appendComment(parent, `/fluff:text:${id}`);
294
+ }
295
+ renderCommentToParent(node, parent) {
296
+ Parse5Helpers.appendComment(parent, node.content);
297
+ }
298
+ renderIfToParent(node, parent) {
299
+ const id = this.nextMarkerId();
300
+ const config = {
301
+ type: 'if',
302
+ branches: node.branches.map(b => ({
303
+ exprId: b.condition ? this.internExpression(b.condition) : undefined,
304
+ deps: b.conditionDeps
305
+ }))
306
+ };
307
+ this.markerConfigs.set(id, config);
308
+ Parse5Helpers.appendComment(parent, `fluff:if:${id}`);
309
+ for (let i = 0; i < node.branches.length; i++) {
310
+ const branch = node.branches[i];
311
+ const templateId = `${this.componentSelector}-${id}-${i}`;
312
+ const tpl = Parse5Helpers.createElement('template', [{ name: 'data-fluff-branch', value: templateId }]);
313
+ this.renderNodesToParent(branch.children, Parse5Helpers.getTemplateContent(tpl));
314
+ this.collectedTemplates.push(tpl);
315
+ }
316
+ Parse5Helpers.appendComment(parent, `/fluff:if:${id}`);
317
+ }
318
+ renderForToParent(node, parent) {
319
+ const id = this.nextMarkerId();
320
+ const config = {
321
+ type: 'for',
322
+ iterator: node.iterator,
323
+ iterableExprId: this.internExpression(node.iterable),
324
+ deps: node.iterableDeps,
325
+ trackBy: node.trackBy,
326
+ hasEmpty: !!node.emptyContent
327
+ };
328
+ this.markerConfigs.set(id, config);
329
+ Parse5Helpers.appendComment(parent, `fluff:for:${id}`);
330
+ const templateId = `${this.componentSelector}-${id}`;
331
+ const tpl = Parse5Helpers.createElement('template', [{ name: 'data-fluff-tpl', value: templateId }]);
332
+ this.renderNodesToParent(node.children, Parse5Helpers.getTemplateContent(tpl));
333
+ this.collectedTemplates.push(tpl);
334
+ if (node.emptyContent) {
335
+ const emptyTpl = Parse5Helpers.createElement('template', [{ name: 'data-fluff-empty', value: templateId }]);
336
+ this.renderNodesToParent(node.emptyContent, Parse5Helpers.getTemplateContent(emptyTpl));
337
+ this.collectedTemplates.push(emptyTpl);
338
+ }
339
+ Parse5Helpers.appendComment(parent, `/fluff:for:${id}`);
340
+ }
341
+ renderSwitchToParent(node, parent) {
342
+ const id = this.nextMarkerId();
343
+ const config = {
344
+ type: 'switch',
345
+ expressionExprId: this.internExpression(node.expression),
346
+ deps: node.expressionDeps,
347
+ cases: node.cases.map(c => ({
348
+ valueExprId: c.valueExpression ? this.internExpression(c.valueExpression) : undefined,
349
+ isDefault: c.isDefault,
350
+ fallthrough: c.fallthrough
351
+ }))
352
+ };
353
+ this.markerConfigs.set(id, config);
354
+ Parse5Helpers.appendComment(parent, `fluff:switch:${id}`);
355
+ for (let i = 0; i < node.cases.length; i++) {
356
+ const caseNode = node.cases[i];
357
+ const templateId = `${this.componentSelector}-${id}-${i}`;
358
+ const tpl = Parse5Helpers.createElement('template', [{ name: 'data-fluff-case', value: templateId }]);
359
+ this.renderNodesToParent(caseNode.children, Parse5Helpers.getTemplateContent(tpl));
360
+ this.collectedTemplates.push(tpl);
765
361
  }
362
+ Parse5Helpers.appendComment(parent, `/fluff:switch:${id}`);
363
+ }
364
+ renderBreakToParent(parent) {
365
+ const id = this.nextMarkerId();
366
+ const config = {
367
+ type: 'break'
368
+ };
369
+ this.markerConfigs.set(id, config);
370
+ Parse5Helpers.appendComment(parent, `fluff:break:${id}`);
766
371
  }
767
372
  }