@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.
- package/BabelHelpers.d.ts +26 -0
- package/BabelHelpers.js +65 -0
- package/Cli.d.ts +7 -10
- package/Cli.js +139 -52
- package/CodeGenerator.d.ts +53 -39
- package/CodeGenerator.js +330 -725
- package/ComponentCompiler.d.ts +14 -16
- package/ComponentCompiler.js +193 -257
- package/DomPreProcessor.d.ts +36 -0
- package/DomPreProcessor.js +645 -0
- package/ErrorHelpers.d.ts +5 -0
- package/ErrorHelpers.js +8 -0
- package/ExpressionTransformer.d.ts +38 -28
- package/ExpressionTransformer.js +558 -230
- package/Generator.d.ts +1 -5
- package/Generator.js +128 -67
- package/GetterDependencyExtractor.d.ts +4 -0
- package/GetterDependencyExtractor.js +73 -0
- package/IndexHtmlTransformer.d.ts +6 -7
- package/IndexHtmlTransformer.js +82 -88
- package/Parse5Helpers.d.ts +17 -0
- package/Parse5Helpers.js +95 -0
- package/TemplateParser.d.ts +39 -21
- package/TemplateParser.js +462 -268
- package/Typeguards.d.ts +24 -0
- package/Typeguards.js +30 -0
- package/babel-plugin-class-transform.d.ts +3 -18
- package/babel-plugin-class-transform.js +3 -11
- package/babel-plugin-component.d.ts +4 -13
- package/babel-plugin-component.js +7 -0
- package/babel-plugin-imports.d.ts +3 -11
- package/babel-plugin-imports.js +5 -31
- package/babel-plugin-reactive.d.ts +2 -19
- package/babel-plugin-reactive.js +21 -76
- package/bin.js +2 -2
- package/fluff-esbuild-plugin.d.ts +2 -5
- package/fluff-esbuild-plugin.js +4 -1
- package/index.d.ts +6 -2
- package/index.js +1 -1
- package/interfaces/BabelPluginClassTransformState.d.ts +5 -0
- package/interfaces/BabelPluginComponentState.d.ts +4 -0
- package/interfaces/BabelPluginComponentState.js +1 -0
- package/interfaces/BabelPluginImportsState.d.ts +5 -0
- package/interfaces/BabelPluginImportsState.js +1 -0
- package/interfaces/BabelPluginReactiveState.d.ts +13 -0
- package/interfaces/BabelPluginReactiveState.js +1 -0
- package/interfaces/BabelPluginReactiveWatchCallInfo.d.ts +7 -0
- package/interfaces/BabelPluginReactiveWatchCallInfo.js +1 -0
- package/interfaces/BabelPluginReactiveWatchInfo.d.ts +5 -0
- package/interfaces/BabelPluginReactiveWatchInfo.js +1 -0
- package/interfaces/BabelToken.d.ts +8 -0
- package/interfaces/BabelToken.js +1 -0
- package/interfaces/BindingInfo.d.ts +12 -0
- package/interfaces/BindingInfo.js +1 -0
- package/interfaces/BreakMarkerConfig.d.ts +4 -0
- package/interfaces/BreakMarkerConfig.js +1 -0
- package/interfaces/BreakNode.d.ts +4 -0
- package/interfaces/BreakNode.js +1 -0
- package/interfaces/BundleOptions.d.ts +9 -0
- package/interfaces/BundleOptions.js +1 -0
- package/interfaces/ClassTransformOptions.d.ts +10 -0
- package/interfaces/ClassTransformOptions.js +1 -0
- package/interfaces/CliOptions.d.ts +8 -0
- package/interfaces/CliOptions.js +1 -0
- package/interfaces/CommentNode.d.ts +5 -0
- package/interfaces/CommentNode.js +1 -0
- package/interfaces/CompileResult.d.ts +6 -0
- package/interfaces/CompileResult.js +1 -0
- package/interfaces/CompilerOptions.d.ts +6 -0
- package/interfaces/CompilerOptions.js +1 -0
- package/interfaces/ComponentInfo.d.ts +8 -0
- package/interfaces/ComponentInfo.js +1 -0
- package/interfaces/ComponentMetadata.d.ts +9 -0
- package/interfaces/ComponentMetadata.js +1 -0
- package/interfaces/ControlFlow.d.ts +19 -0
- package/interfaces/ControlFlow.js +1 -0
- package/interfaces/ControlFlowNode.d.ts +6 -0
- package/interfaces/ControlFlowNode.js +1 -0
- package/interfaces/ControlFlowParseResult.d.ts +10 -0
- package/interfaces/ControlFlowParseResult.js +1 -0
- package/interfaces/ElementNode.d.ts +11 -0
- package/interfaces/ElementNode.js +1 -0
- package/interfaces/FluffConfigInterface.d.ts +7 -0
- package/interfaces/FluffConfigInterface.js +1 -0
- package/interfaces/FluffPluginOptions.d.ts +9 -0
- package/interfaces/FluffPluginOptions.js +1 -0
- package/interfaces/FluffTarget.d.ts +15 -0
- package/interfaces/FluffTarget.js +1 -0
- package/interfaces/ForMarkerConfig.d.ts +9 -0
- package/interfaces/ForMarkerConfig.js +1 -0
- package/interfaces/ForNode.d.ts +13 -0
- package/interfaces/ForNode.js +1 -0
- package/interfaces/GeneratorOptions.d.ts +5 -0
- package/interfaces/GeneratorOptions.js +1 -0
- package/interfaces/HtmlTransformOptions.d.ts +10 -0
- package/interfaces/HtmlTransformOptions.js +1 -0
- package/interfaces/IfBranch.d.ts +8 -0
- package/interfaces/IfBranch.js +1 -0
- package/interfaces/IfMarkerConfig.d.ts +8 -0
- package/interfaces/IfMarkerConfig.js +1 -0
- package/interfaces/IfNode.d.ts +7 -0
- package/interfaces/IfNode.js +1 -0
- package/interfaces/ImportTransformOptions.d.ts +7 -0
- package/interfaces/ImportTransformOptions.js +1 -0
- package/interfaces/InterpolationNode.d.ts +12 -0
- package/interfaces/InterpolationNode.js +1 -0
- package/interfaces/ParsedTemplate.d.ts +6 -0
- package/interfaces/ParsedTemplate.js +1 -0
- package/interfaces/ParsedTemplateOld.d.ts +9 -0
- package/interfaces/ParsedTemplateOld.js +1 -0
- package/interfaces/PropertyChain.d.ts +2 -0
- package/interfaces/PropertyChain.js +1 -0
- package/interfaces/Scope.d.ts +5 -0
- package/interfaces/Scope.js +1 -0
- package/interfaces/ServeOptions.d.ts +5 -0
- package/interfaces/ServeOptions.js +1 -0
- package/interfaces/SwitchCase.d.ts +8 -0
- package/interfaces/SwitchCase.js +1 -0
- package/interfaces/SwitchMarkerConfig.d.ts +11 -0
- package/interfaces/SwitchMarkerConfig.js +1 -0
- package/interfaces/SwitchNode.d.ts +10 -0
- package/interfaces/SwitchNode.js +1 -0
- package/interfaces/TemplateBinding.d.ts +10 -0
- package/interfaces/TemplateBinding.js +1 -0
- package/interfaces/TemplateNode.d.ts +7 -0
- package/interfaces/TemplateNode.js +1 -0
- package/interfaces/TextMarkerConfig.d.ts +10 -0
- package/interfaces/TextMarkerConfig.js +1 -0
- package/interfaces/TextNode.d.ts +5 -0
- package/interfaces/TextNode.js +1 -0
- package/interfaces/TokenizeResult.d.ts +6 -0
- package/interfaces/TokenizeResult.js +1 -0
- package/interfaces/TransformOptions.d.ts +11 -0
- package/interfaces/TransformOptions.js +1 -0
- package/interfaces/index.d.ts +34 -0
- package/interfaces/index.js +1 -0
- package/package.json +9 -1
- package/types/FluffConfig.d.ts +5 -27
- package/ControlFlowParser.d.ts +0 -55
- package/ControlFlowParser.js +0 -279
- package/types.d.ts +0 -46
- /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 {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
let content =
|
|
49
|
+
generateRenderMethodFromHtml(html, styles, markerConfigJson) {
|
|
50
|
+
let content = html;
|
|
14
51
|
if (styles) {
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
98
|
+
const program = t.program(statements);
|
|
99
|
+
return generate(program, { compact: false }).code;
|
|
44
100
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
124
|
+
return t.arrowFunctionExpression(paramNodes, t.identifier('undefined'));
|
|
51
125
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
let result =
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
return
|
|
141
|
+
nextMarkerId() {
|
|
142
|
+
return this.markerId++;
|
|
63
143
|
}
|
|
64
|
-
|
|
65
|
-
|
|
144
|
+
renderNodesToParent(nodes, parent) {
|
|
145
|
+
for (const node of nodes) {
|
|
146
|
+
this.renderNodeToParent(node, parent);
|
|
147
|
+
}
|
|
66
148
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
149
|
+
renderNodeToParent(node, parent) {
|
|
150
|
+
switch (node.type) {
|
|
151
|
+
case 'element':
|
|
152
|
+
this.renderElementToParent(node, parent);
|
|
153
|
+
break;
|
|
70
154
|
case 'text':
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
case '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
199
|
+
let { tagName } = node;
|
|
200
|
+
if (tagName.startsWith(RESTRICTED_ELEMENT_PREFIX)) {
|
|
201
|
+
tagName = tagName.slice(RESTRICTED_ELEMENT_PREFIX.length);
|
|
221
202
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
}
|