@fluffjs/cli 0.0.8 → 0.1.1

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 +7 -10
  4. package/Cli.js +139 -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 +193 -257
  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 +17 -0
  22. package/Parse5Helpers.js +95 -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 +9 -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 +8 -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 +10 -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/TemplateParser.js CHANGED
@@ -1,330 +1,524 @@
1
+ import { parseExpression } from '@babel/parser';
2
+ import * as t from '@babel/types';
3
+ import he from 'he';
1
4
  import * as parse5 from 'parse5';
2
- import { html as parse5Html } from 'parse5';
3
- import { ControlFlowParser } from './ControlFlowParser.js';
4
- import { expressionUsesVariable, parseInterpolations, transformInterpolation } from './ExpressionTransformer.js';
5
+ import { DomPreProcessor } from './DomPreProcessor.js';
6
+ import { ExpressionTransformer } from './ExpressionTransformer.js';
7
+ import { Typeguards } from './Typeguards.js';
5
8
  export class TemplateParser {
6
9
  bindingId = 0;
7
- bindings = [];
8
- controlFlows = [];
9
- controlFlowParser = new ControlFlowParser();
10
10
  templateRefs = new Set();
11
- parse(html) {
11
+ scopeStack = [];
12
+ getterDependencyMap = new Map();
13
+ testYieldBeforeGetterDepsLookup = null;
14
+ setGetterDependencyMap(map) {
15
+ this.getterDependencyMap = map;
16
+ }
17
+ __setTestYieldBeforeGetterDepsLookup(callback) {
18
+ this.testYieldBeforeGetterDepsLookup = callback;
19
+ }
20
+ async parse(html) {
12
21
  this.bindingId = 0;
13
- this.bindings = [];
14
- this.controlFlows = [];
15
22
  this.templateRefs = new Set();
16
- let result = html;
17
- result = this.processControlFlowBlocks(result);
18
- result = this.processWithParse5(result);
23
+ this.scopeStack = [{ variables: new Set() }];
24
+ const preProcessor = new DomPreProcessor();
25
+ const normalized = await preProcessor.process(html);
26
+ const fragment = parse5.parseFragment(normalized);
27
+ const root = await this.processNodes(fragment.childNodes);
19
28
  return {
20
- html: result,
21
- bindings: this.bindings,
22
- controlFlows: this.controlFlows,
29
+ root,
23
30
  templateRefs: Array.from(this.templateRefs)
24
31
  };
25
32
  }
26
- processControlFlowBlocks(html) {
27
- let result = this.processSwitchBlocksNew(html);
28
- result = this.processForBlocksNew(result);
29
- result = this.processIfBlocksNew(result);
30
- return result;
31
- }
32
- processSwitchBlocksNew(html) {
33
- const { result: parsed, blocks } = this.controlFlowParser.parseSwitchBlocks(html);
34
- let result = parsed;
35
- for (let i = 0; i < blocks.length; i++) {
36
- const block = blocks[i];
37
- if (!block)
38
- continue;
39
- const id = `l${this.bindingId++}`;
40
- const processedCases = block.cases.map(c => ({
41
- value: c.value,
42
- content: this.processControlFlowBlocks(this.processControlFlowContent(c.content))
43
- .trim(),
44
- fallthrough: c.fallthrough
45
- }));
46
- this.controlFlows.push({
47
- id, type: 'switch', expression: block.expression, cases: processedCases
48
- });
49
- const placeholder = `__SWITCH_BLOCK_${i}__`;
50
- const replacement = `<!--${id}--><!--/${id}-->`;
51
- result = result.replace(placeholder, replacement);
52
- }
53
- return result;
54
- }
55
- processForBlocksNew(html) {
56
- const { result: parsed, blocks } = this.controlFlowParser.parseForBlocks(html);
57
- let result = parsed;
58
- for (let i = 0; i < blocks.length; i++) {
59
- const block = blocks[i];
60
- if (!block)
33
+ async processNodes(nodes) {
34
+ const result = [];
35
+ let i = 0;
36
+ while (i < nodes.length) {
37
+ const node = nodes[i];
38
+ if (Typeguards.isElement(node) && node.tagName === 'x-fluff-if') {
39
+ const ifNode = await this.processConsolidatedIf(nodes, i);
40
+ result.push(ifNode.node);
41
+ i = ifNode.nextIndex;
61
42
  continue;
62
- const id = `l${this.bindingId++}`;
63
- let content = this.processControlFlowBlocks(block.content);
64
- content = this.processControlFlowContent(content, block.iterator);
65
- const trimmedContent = content.trim();
66
- const isOptionContent = trimmedContent.startsWith('<option');
67
- this.controlFlows.push({
68
- id,
69
- type: 'for',
70
- iterator: block.iterator,
71
- iterable: block.iterable,
72
- trackBy: block.trackBy,
73
- content: trimmedContent
74
- });
75
- const placeholder = `__FOR_BLOCK_${i}__`;
76
- const replacement = isOptionContent ? `<!--for-${id}--><!--/for-${id}-->` : `<!--${id}--><!--/${id}-->`;
77
- result = result.replace(placeholder, replacement);
78
- }
79
- return result;
80
- }
81
- processIfBlocksNew(html) {
82
- const { result: parsed, blocks } = this.controlFlowParser.parseIfBlocks(html);
83
- let result = parsed;
84
- for (let i = 0; i < blocks.length; i++) {
85
- const block = blocks[i];
86
- if (!block)
43
+ }
44
+ if (Typeguards.isElement(node) && node.tagName === 'x-fluff-for') {
45
+ const forNode = await this.processConsolidatedFor(nodes, i);
46
+ result.push(forNode.node);
47
+ i = forNode.nextIndex;
87
48
  continue;
88
- const id = `l${this.bindingId++}`;
89
- let ifContent = this.processControlFlowBlocks(block.ifContent);
90
- ifContent = this.processControlFlowContent(ifContent);
91
- let elseContent = '';
92
- if (block.elseContent) {
93
- elseContent = this.processControlFlowBlocks(block.elseContent);
94
- elseContent = this.processControlFlowContent(elseContent);
95
49
  }
96
- this.controlFlows.push({
97
- id, type: 'if', condition: block.condition, ifContent: ifContent.trim(), elseContent: elseContent.trim()
98
- });
99
- const placeholder = `__IF_BLOCK_${i}__`;
100
- const replacement = `<!--${id}--><!--/${id}-->`;
101
- result = result.replace(placeholder, replacement);
50
+ const processed = await this.processNode(node);
51
+ if (processed) {
52
+ result.push(processed);
53
+ }
54
+ i++;
102
55
  }
103
56
  return result;
104
57
  }
105
- processWithParse5(html) {
106
- const fragment = parse5.parseFragment(html, { sourceCodeLocationInfo: true });
107
- this.walkNodes(fragment.childNodes, html);
108
- return parse5.serialize(fragment);
109
- }
110
- walkNodes(nodes, source) {
111
- for (let i = 0; i < nodes.length; i++) {
58
+ async processConsolidatedIf(nodes, startIndex) {
59
+ const branches = [];
60
+ let i = startIndex;
61
+ while (i < nodes.length) {
112
62
  const node = nodes[i];
113
- if (this.isElement(node)) {
114
- this.processElement(node, source);
115
- if (node.childNodes) {
116
- this.walkNodes(node.childNodes, source);
63
+ if (!Typeguards.isElement(node)) {
64
+ if (Typeguards.isTextNode(node) && node.value.trim() === '') {
65
+ i++;
66
+ continue;
117
67
  }
68
+ break;
118
69
  }
119
- else if (this.isTextNode(node)) {
120
- const newNodes = this.processTextNode(node);
121
- if (newNodes.length > 0) {
122
- nodes.splice(i, 1, ...newNodes);
123
- i += newNodes.length - 1;
124
- }
70
+ if (node.tagName === 'x-fluff-if' && branches.length === 0) {
71
+ const conditionRaw = this.getAttr(node, 'x-fluff-condition') ?? '';
72
+ const children = await this.processNodes(node.childNodes ?? []);
73
+ const conditionDeps = await this.extractDeps(conditionRaw);
74
+ const condition = this.transformWithLocals(conditionRaw);
75
+ branches.push({
76
+ condition,
77
+ conditionDeps: conditionDeps.length > 0 ? conditionDeps : undefined,
78
+ children
79
+ });
80
+ i++;
81
+ }
82
+ else if (node.tagName === 'x-fluff-else-if' && branches.length > 0) {
83
+ const conditionRaw = this.getAttr(node, 'x-fluff-condition') ?? '';
84
+ const children = await this.processNodes(node.childNodes ?? []);
85
+ const conditionDeps = await this.extractDeps(conditionRaw);
86
+ const condition = this.transformWithLocals(conditionRaw);
87
+ branches.push({
88
+ condition,
89
+ conditionDeps: conditionDeps.length > 0 ? conditionDeps : undefined,
90
+ children
91
+ });
92
+ i++;
93
+ }
94
+ else if (node.tagName === 'x-fluff-else' && branches.length > 0) {
95
+ const children = await this.processNodes(node.childNodes ?? []);
96
+ branches.push({
97
+ children
98
+ });
99
+ i++;
100
+ break;
101
+ }
102
+ else {
103
+ break;
125
104
  }
126
105
  }
106
+ return {
107
+ node: {
108
+ type: 'if',
109
+ branches,
110
+ localVariables: this.getCurrentLocalVariables()
111
+ },
112
+ nextIndex: i
113
+ };
127
114
  }
128
- isTextNode(node) {
129
- return 'value' in node && !('tagName' in node);
130
- }
131
- processTextNode(textNode, iteratorVar, inControlFlow = false) {
132
- const text = textNode.value;
133
- const interpolations = parseInterpolations(text);
134
- if (interpolations.length === 0) {
135
- return [textNode];
115
+ async processConsolidatedFor(nodes, startIndex) {
116
+ const forElement = nodes[startIndex];
117
+ if (!Typeguards.isElement(forElement)) {
118
+ throw new Error('Expected element node');
136
119
  }
137
- const nodes = [];
138
- let lastIndex = 0;
139
- for (const { start, end, expr } of interpolations) {
140
- if (start > lastIndex) {
141
- nodes.push(this.createTextNode(text.slice(lastIndex, start)));
142
- }
143
- if (inControlFlow) {
144
- const usesIterator = iteratorVar && expressionUsesVariable(expr, iteratorVar);
145
- const transformed = transformInterpolation(expr, iteratorVar);
146
- if (usesIterator) {
147
- nodes.push(this.createTextNode(`\${${transformed}}`));
148
- }
149
- else {
150
- const id = `l${this.bindingId++}`;
151
- nodes.push(this.createSpanWithTextBind(transformed, id));
120
+ const forNode = await this.processForElement(forElement);
121
+ let i = startIndex + 1;
122
+ while (i < nodes.length) {
123
+ const node = nodes[i];
124
+ if (!Typeguards.isElement(node)) {
125
+ if (Typeguards.isTextNode(node) && node.value.trim() === '') {
126
+ i++;
127
+ continue;
152
128
  }
129
+ break;
130
+ }
131
+ if (node.tagName === 'x-fluff-empty') {
132
+ forNode.emptyContent = await this.processNodes(node.childNodes ?? []);
133
+ i++;
134
+ break;
153
135
  }
154
136
  else {
155
- const id = `l${this.bindingId++}`;
156
- this.bindings.push({
157
- id, type: 'text', expression: expr
158
- });
159
- nodes.push(this.createSpanElement(id));
137
+ break;
138
+ }
139
+ }
140
+ return {
141
+ node: forNode,
142
+ nextIndex: i
143
+ };
144
+ }
145
+ async processNode(node) {
146
+ if (Typeguards.isElement(node)) {
147
+ return this.processElement(node);
148
+ }
149
+ else if (Typeguards.isTextNode(node)) {
150
+ const text = node.value.trim();
151
+ if (text.length === 0) {
152
+ return null;
160
153
  }
161
- lastIndex = end;
154
+ return { type: 'text', content: node.value };
162
155
  }
163
- if (lastIndex < text.length) {
164
- nodes.push(this.createTextNode(text.slice(lastIndex)));
156
+ else if (Typeguards.isCommentNode(node)) {
157
+ return { type: 'comment', content: node.data };
165
158
  }
166
- return nodes;
159
+ return null;
167
160
  }
168
- createTextNode(value) {
161
+ async processElement(element) {
162
+ const { tagName } = element;
163
+ if (tagName === 'x-fluff-if') {
164
+ return null;
165
+ }
166
+ else if (tagName === 'x-fluff-else-if') {
167
+ return null;
168
+ }
169
+ else if (tagName === 'x-fluff-else') {
170
+ return null;
171
+ }
172
+ else if (tagName === 'x-fluff-for') {
173
+ return this.processForElement(element);
174
+ }
175
+ else if (tagName === 'x-fluff-switch') {
176
+ return this.processSwitchElement(element);
177
+ }
178
+ else if (tagName === 'x-fluff-case') {
179
+ return null;
180
+ }
181
+ else if (tagName === 'x-fluff-default') {
182
+ return null;
183
+ }
184
+ else if (tagName === 'x-fluff-empty') {
185
+ return null;
186
+ }
187
+ else if (tagName === 'x-fluff-fallthrough') {
188
+ return null;
189
+ }
190
+ else if (tagName === 'x-fluff-break') {
191
+ return { type: 'break' };
192
+ }
193
+ else if (tagName === 'x-fluff-text') {
194
+ return this.processTextElement(element);
195
+ }
196
+ return this.processRegularElement(element);
197
+ }
198
+ async processForElement(element) {
199
+ const iterator = this.getAttr(element, 'x-fluff-iterator') ?? 'item';
200
+ const iterableRaw = this.getAttr(element, 'x-fluff-iterable') ?? '';
201
+ const trackBy = this.getAttr(element, 'x-fluff-track');
202
+ const iterableDeps = await this.extractDeps(iterableRaw);
203
+ const iterable = this.transformWithLocals(iterableRaw);
204
+ this.pushScope([iterator, '$index']);
205
+ const childNodes = element.childNodes ?? [];
206
+ const children = [];
207
+ let emptyContent = undefined;
208
+ for (const child of childNodes) {
209
+ if (Typeguards.isElement(child) && child.tagName === 'x-fluff-empty') {
210
+ emptyContent = await this.processNodes(child.childNodes ?? []);
211
+ }
212
+ else {
213
+ const processed = await this.processNode(child);
214
+ if (processed) {
215
+ children.push(processed);
216
+ }
217
+ }
218
+ }
219
+ const localVariables = this.getCurrentLocalVariables();
220
+ this.popScope();
169
221
  return {
170
- nodeName: '#text',
171
- value,
172
- parentNode: null
222
+ type: 'for',
223
+ iterator,
224
+ iterable,
225
+ iterableDeps: iterableDeps.length > 0 ? iterableDeps : undefined,
226
+ trackBy: trackBy ?? undefined,
227
+ emptyContent,
228
+ children,
229
+ localVariables
173
230
  };
174
231
  }
175
- createSpanElement(id) {
232
+ async processSwitchElement(element) {
233
+ const expressionRaw = this.getAttr(element, 'x-fluff-expr') ?? '';
234
+ const expressionDeps = await this.extractDeps(expressionRaw);
235
+ const expression = this.transformWithLocals(expressionRaw);
236
+ const cases = [];
237
+ const childNodes = element.childNodes ?? [];
238
+ for (const child of childNodes) {
239
+ if (!Typeguards.isElement(child))
240
+ continue;
241
+ if (child.tagName === 'x-fluff-case') {
242
+ const valueRaw = this.getAttr(child, 'x-fluff-value') ?? '';
243
+ const valueExpression = this.transformWithLocals(valueRaw);
244
+ const caseChildren = await this.processNodes(child.childNodes ?? []);
245
+ const hasFallthrough = this.hasChildElement(child, 'x-fluff-fallthrough');
246
+ const filteredChildren = caseChildren.filter(c => !(c.type === 'break'));
247
+ cases.push({
248
+ valueExpression,
249
+ isDefault: false,
250
+ fallthrough: hasFallthrough,
251
+ children: filteredChildren
252
+ });
253
+ }
254
+ else if (child.tagName === 'x-fluff-default') {
255
+ const caseChildren = await this.processNodes(child.childNodes ?? []);
256
+ const hasFallthrough = this.hasChildElement(child, 'x-fluff-fallthrough');
257
+ const filteredChildren = caseChildren.filter(c => !(c.type === 'break'));
258
+ cases.push({
259
+ isDefault: true,
260
+ fallthrough: hasFallthrough,
261
+ children: filteredChildren
262
+ });
263
+ }
264
+ }
176
265
  return {
177
- nodeName: 'span',
178
- tagName: 'span',
179
- attrs: [{ name: 'data-lid', value: id }],
180
- childNodes: [],
181
- namespaceURI: parse5Html.NS.HTML,
182
- parentNode: null
266
+ type: 'switch',
267
+ expression,
268
+ expressionDeps: expressionDeps.length > 0 ? expressionDeps : undefined,
269
+ cases,
270
+ localVariables: this.getCurrentLocalVariables()
183
271
  };
184
272
  }
185
- isElement(node) {
186
- return 'tagName' in node;
273
+ hasChildElement(element, tagName) {
274
+ for (const child of element.childNodes ?? []) {
275
+ if (Typeguards.isElement(child) && child.tagName === tagName) {
276
+ return true;
277
+ }
278
+ }
279
+ return false;
187
280
  }
188
- getOriginalAttrName(element, attr, source) {
189
- const attrLocations = 'sourceCodeLocation' in element && element.sourceCodeLocation &&
190
- typeof element.sourceCodeLocation === 'object' && 'attrs' in element.sourceCodeLocation
191
- ? element.sourceCodeLocation.attrs
192
- : undefined;
193
- if (attrLocations?.[attr.name]) {
194
- const loc = attrLocations[attr.name];
195
- if (loc) {
196
- const [originalName] = source.slice(loc.startOffset, loc.endOffset)
197
- .split('=');
198
- return originalName;
281
+ async processTextElement(element) {
282
+ const expression = this.getAttr(element, 'x-fluff-expr') ?? '';
283
+ const pipesAttr = this.getAttr(element, 'x-fluff-pipes');
284
+ const id = `l${this.bindingId++}`;
285
+ const deps = await this.extractDeps(expression);
286
+ const transformedExpression = this.transformWithLocals(expression, { eventReplacementName: '__ev' });
287
+ const result = {
288
+ type: 'interpolation',
289
+ expression: transformedExpression,
290
+ deps: deps.length > 0 ? deps : undefined,
291
+ id
292
+ };
293
+ if (pipesAttr) {
294
+ try {
295
+ const parsed = JSON.parse(pipesAttr);
296
+ if (Array.isArray(parsed)) {
297
+ result.pipes = parsed.map(pipe => {
298
+ if (!Typeguards.isRecord(pipe)) {
299
+ return null;
300
+ }
301
+ if (typeof pipe.name !== 'string' || !Array.isArray(pipe.args)) {
302
+ return null;
303
+ }
304
+ const args = pipe.args.filter((arg) => typeof arg === 'string')
305
+ .map(arg => this.transformWithLocals(arg));
306
+ return {
307
+ name: pipe.name,
308
+ args
309
+ };
310
+ })
311
+ .filter((pipe) => pipe !== null);
312
+ }
313
+ }
314
+ catch {
199
315
  }
200
316
  }
201
- return attr.name;
317
+ return result;
202
318
  }
203
- processElement(element, source) {
319
+ async processRegularElement(element) {
204
320
  const bindings = [];
205
- const attrsToRemove = [];
321
+ const attributes = {};
322
+ let hasBindings = false;
206
323
  for (const attr of element.attrs) {
207
- if (attr.name.startsWith('[(') && attr.name.endsWith(')]')) {
208
- const prop = attr.name.slice(2, -2);
209
- const expr = attr.value;
210
- bindings.push({ type: 'property', target: prop, expression: expr });
211
- bindings.push({ type: 'event', eventName: 'input', expression: `${expr} = $event.target.${prop}` });
212
- attrsToRemove.push(attr.name);
213
- }
214
- else if (attr.name.startsWith('[') && attr.name.endsWith(']')) {
215
- const prop = attr.name.slice(1, -1);
216
- const expr = attr.value;
217
- if (prop.startsWith('class.')) {
218
- bindings.push({ type: 'class', className: prop.slice(6), expression: expr });
219
- }
220
- else if (prop.startsWith('style.')) {
221
- bindings.push({ type: 'style', styleProp: prop.slice(6), expression: expr });
222
- }
223
- else {
224
- const dangerousProps = ['innerHTML', 'outerHTML', 'href', 'src'];
225
- const isUnsafe = prop.endsWith('.unsafe');
226
- const baseProp = isUnsafe ? prop.slice(0, -7) : prop;
227
- if (dangerousProps.includes(baseProp) && !isUnsafe) {
228
- throw new Error(`XSS Protection: [${prop}] binding is blocked. ` + `Use [${prop}.unsafe] if you understand the security implications ` + 'and have sanitized user input.');
324
+ if (attr.name.startsWith('x-fluff-attrib-')) {
325
+ const bindingInfo = await this.parseBindingAttribute(attr.value);
326
+ if (bindingInfo) {
327
+ bindings.push(bindingInfo);
328
+ hasBindings = true;
329
+ if (bindingInfo.binding === 'ref') {
330
+ this.templateRefs.add(bindingInfo.name);
229
331
  }
230
- bindings.push({ type: 'property', target: baseProp, expression: expr });
231
332
  }
232
- attrsToRemove.push(attr.name);
233
- }
234
- else if (attr.name.startsWith('(') && attr.name.endsWith(')')) {
235
- const eventName = attr.name.slice(1, -1);
236
- bindings.push({ type: 'event', eventName, expression: attr.value });
237
- attrsToRemove.push(attr.name);
238
333
  }
239
- else if (attr.name.startsWith('#')) {
240
- const originalName = this.getOriginalAttrName(element, attr, source);
241
- const refName = originalName.slice(1);
242
- attrsToRemove.push(attr.name);
243
- element.attrs.push({ name: 'data-ref', value: refName });
244
- this.templateRefs.add(refName);
334
+ else {
335
+ attributes[attr.name] = attr.value;
245
336
  }
246
337
  }
247
- if (bindings.length > 0) {
248
- const id = `l${this.bindingId++}`;
249
- element.attrs = element.attrs.filter(a => !attrsToRemove.includes(a.name));
250
- element.attrs.push({ name: 'data-lid', value: id });
251
- for (const binding of bindings) {
252
- this.bindings.push({ ...binding, id });
338
+ const children = await this.processNodes(element.childNodes ?? []);
339
+ const node = {
340
+ type: 'element',
341
+ tagName: element.tagName,
342
+ attributes,
343
+ bindings,
344
+ children
345
+ };
346
+ if (hasBindings) {
347
+ node.id = `l${this.bindingId++}`;
348
+ }
349
+ return node;
350
+ }
351
+ async parseBindingAttribute(jsonValue) {
352
+ try {
353
+ const decoded = he.decode(jsonValue);
354
+ const parsed = JSON.parse(decoded);
355
+ if (!Typeguards.isRecord(parsed)) {
356
+ return null;
357
+ }
358
+ if (typeof parsed.name !== 'string' || typeof parsed.binding !== 'string' || typeof parsed.expression !== 'string') {
359
+ return null;
360
+ }
361
+ const validBindings = ['property', 'event', 'class', 'style', 'two-way', 'ref'];
362
+ const binding = validBindings.find(b => b === parsed.binding);
363
+ if (!binding) {
364
+ return null;
253
365
  }
366
+ const deps = await this.extractDeps(parsed.expression);
367
+ const transformedExpression = this.transformWithLocals(parsed.expression, {
368
+ eventReplacementName: '__ev',
369
+ templateRefs: Array.from(this.templateRefs)
370
+ });
371
+ const result = {
372
+ name: parsed.name,
373
+ binding,
374
+ expression: transformedExpression,
375
+ deps: deps.length > 0 ? deps : undefined
376
+ };
377
+ if (typeof parsed.subscribe === 'string') {
378
+ result.subscribe = parsed.subscribe;
379
+ }
380
+ return result;
381
+ }
382
+ catch {
383
+ return null;
254
384
  }
255
385
  }
256
- processControlFlowContent(html, iteratorVar) {
257
- const fragment = parse5.parseFragment(html, { sourceCodeLocationInfo: true });
258
- this.walkControlFlowNodes(fragment.childNodes, html, iteratorVar);
259
- return parse5.serialize(fragment);
386
+ getAttr(element, name) {
387
+ const attr = element.attrs.find(a => a.name === name);
388
+ return attr ? attr.value : null;
260
389
  }
261
- walkControlFlowNodes(nodes, source, iteratorVar) {
262
- for (let i = 0; i < nodes.length; i++) {
263
- const node = nodes[i];
264
- if (this.isElement(node)) {
265
- this.processControlFlowElement(node, source, iteratorVar);
266
- if (node.childNodes) {
267
- this.walkControlFlowNodes(node.childNodes, source, iteratorVar);
268
- }
269
- }
270
- else if (this.isTextNode(node)) {
271
- const newNodes = this.processTextNode(node, iteratorVar, true);
272
- if (newNodes.length > 0) {
273
- nodes.splice(i, 1, ...newNodes);
274
- i += newNodes.length - 1;
390
+ transformWithLocals(expression, options) {
391
+ const localVars = this.getCurrentLocalVariables();
392
+ return ExpressionTransformer.transformExpression(expression, {
393
+ addThisPrefix: true,
394
+ localVars,
395
+ localsObjectName: 'l',
396
+ eventReplacementName: options?.eventReplacementName,
397
+ templateRefs: options?.templateRefs
398
+ });
399
+ }
400
+ pushScope(variables) {
401
+ const parent = this.scopeStack[this.scopeStack.length - 1];
402
+ this.scopeStack.push({
403
+ variables: new Set(variables),
404
+ parent
405
+ });
406
+ }
407
+ popScope() {
408
+ if (this.scopeStack.length > 1) {
409
+ this.scopeStack.pop();
410
+ }
411
+ }
412
+ getCurrentLocalVariables() {
413
+ const allVars = [];
414
+ for (const scope of this.scopeStack) {
415
+ for (const v of scope.variables) {
416
+ if (!allVars.includes(v)) {
417
+ allVars.push(v);
275
418
  }
276
419
  }
277
420
  }
421
+ return allVars;
278
422
  }
279
- processControlFlowElement(element, source, iteratorVar) {
280
- for (const attr of element.attrs) {
281
- const originalName = this.getOriginalAttrName(element, attr, source);
282
- if (attr.name.startsWith('[') && attr.name.endsWith(']')) {
283
- const lowercaseProp = attr.name.slice(1, -1);
284
- const originalProp = originalName.slice(1, -1); // Remove [ ]
285
- if (originalProp !== lowercaseProp) {
286
- attr.value = JSON.stringify([originalProp, attr.value]);
423
+ async extractDeps(expression) {
424
+ const deps = [];
425
+ try {
426
+ const ast = parseExpression(expression);
427
+ const extractChain = (node) => {
428
+ if (t.isIdentifier(node)) {
429
+ return [node.name];
287
430
  }
288
- }
289
- else if (attr.name.startsWith('(') && attr.name.endsWith(')')) {
290
- const lowercaseEvent = attr.name.slice(1, -1);
291
- const originalEvent = originalName.slice(1, -1); // Remove ( )
292
- if (originalEvent !== lowercaseEvent) {
293
- attr.value = JSON.stringify([originalEvent, attr.value]);
431
+ else if (t.isMemberExpression(node)) {
432
+ const objectChain = extractChain(node.object);
433
+ if (!objectChain)
434
+ return null;
435
+ if (node.computed) {
436
+ if (t.isNumericLiteral(node.property)) {
437
+ return [...objectChain, `[${node.property.value}]`];
438
+ }
439
+ else if (t.isStringLiteral(node.property)) {
440
+ return [...objectChain, `[${JSON.stringify(node.property.value)}]`];
441
+ }
442
+ else if (t.isIdentifier(node.property)) {
443
+ return [...objectChain, `[${node.property.name}]`];
444
+ }
445
+ return null;
446
+ }
447
+ else if (t.isIdentifier(node.property)) {
448
+ return [...objectChain, node.property.name];
449
+ }
294
450
  }
295
- }
296
- else if (attr.name.startsWith('#')) {
297
- const refName = originalName.slice(1);
298
- attr.name = 'data-ref';
299
- attr.value = refName;
300
- this.templateRefs.add(refName);
301
- }
302
- const interpolations = parseInterpolations(attr.value);
303
- if (interpolations.length > 0) {
304
- let newValue = '';
305
- let lastEnd = 0;
306
- for (const { start, end, expr } of interpolations) {
307
- newValue += attr.value.slice(lastEnd, start);
308
- const transformed = transformInterpolation(expr, iteratorVar);
309
- newValue += `\${${transformed}}`;
310
- lastEnd = end;
451
+ return null;
452
+ };
453
+ const visited = new Set();
454
+ const visitNode = (node, skipMemberParts = false) => {
455
+ if (visited.has(node))
456
+ return;
457
+ visited.add(node);
458
+ if (t.isMemberExpression(node)) {
459
+ const chain = extractChain(node);
460
+ if (chain && chain.length > 0) {
461
+ const simplified = chain.length === 1 ? chain[0] : chain;
462
+ const exists = deps.some(d => JSON.stringify(d) === JSON.stringify(simplified));
463
+ if (!exists) {
464
+ deps.push(simplified);
465
+ }
466
+ }
467
+ return;
311
468
  }
312
- newValue += attr.value.slice(lastEnd);
313
- attr.value = newValue;
469
+ if (t.isIdentifier(node) && !skipMemberParts) {
470
+ const simplified = node.name;
471
+ const exists = deps.some(d => JSON.stringify(d) === JSON.stringify(simplified));
472
+ if (!exists) {
473
+ deps.push(simplified);
474
+ }
475
+ return;
476
+ }
477
+ if (!Typeguards.isRecord(node))
478
+ return;
479
+ for (const key of Object.keys(node)) {
480
+ if (key === 'type' || key === 'loc' || key === 'start' || key === 'end')
481
+ continue;
482
+ const child = node[key];
483
+ if (Array.isArray(child)) {
484
+ for (const item of child) {
485
+ if (Typeguards.isBabelNode(item)) {
486
+ visitNode(item);
487
+ }
488
+ }
489
+ }
490
+ else if (Typeguards.isBabelNode(child)) {
491
+ visitNode(child);
492
+ }
493
+ }
494
+ };
495
+ visitNode(ast);
496
+ }
497
+ catch {
498
+ // If parsing fails, return empty deps
499
+ }
500
+ if (this.getterDependencyMap.size > 0) {
501
+ const expanded = [];
502
+ for (const dep of deps) {
503
+ const firstProp = typeof dep === 'string' ? dep : (Array.isArray(dep) ? dep[0] : null);
504
+ if (typeof firstProp === 'string') {
505
+ if (this.testYieldBeforeGetterDepsLookup) {
506
+ await this.testYieldBeforeGetterDepsLookup();
507
+ }
508
+ const getterDeps = this.getterDependencyMap.get(firstProp);
509
+ if (getterDeps && getterDeps.length > 0) {
510
+ for (const gd of getterDeps) {
511
+ if (!expanded.includes(gd)) {
512
+ expanded.push(gd);
513
+ }
514
+ }
515
+ continue;
516
+ }
517
+ }
518
+ expanded.push(dep);
314
519
  }
520
+ return expanded;
315
521
  }
316
- }
317
- createSpanWithTextBind(expr, id) {
318
- return {
319
- nodeName: 'span',
320
- tagName: 'span',
321
- attrs: [
322
- { name: 'data-text-bind', value: expr },
323
- { name: 'data-lid', value: id }
324
- ],
325
- childNodes: [],
326
- namespaceURI: parse5Html.NS.HTML,
327
- parentNode: null
328
- };
522
+ return deps;
329
523
  }
330
524
  }