@collie-lang/compiler 1.0.0 → 1.2.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.
- package/dist/index.cjs +3067 -764
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +109 -3
- package/dist/index.d.ts +109 -3
- package/dist/index.js +3052 -764
- package/dist/index.js.map +1 -1
- package/package.json +11 -5
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
// src/codegen.ts
|
|
2
|
-
function
|
|
3
|
-
const {
|
|
4
|
-
const
|
|
5
|
-
const aliasEnv = buildClassAliasEnvironment(root.classAliases);
|
|
6
|
-
const jsx = renderRootChildren(root.children, aliasEnv);
|
|
7
|
-
const propsDestructure = emitPropsDestructure(root.props);
|
|
8
|
-
const parts = [];
|
|
9
|
-
if (root.clientComponent) {
|
|
10
|
-
parts.push(`"use client";`);
|
|
11
|
-
}
|
|
12
|
-
if (jsxRuntime === "classic" && templateUsesJsx(root)) {
|
|
13
|
-
parts.push(`import React from "react";`);
|
|
14
|
-
}
|
|
15
|
-
parts.push(emitPropsType(root.props, flavor));
|
|
2
|
+
function generateRenderModule(root, options) {
|
|
3
|
+
const { prelude, propsType, propsDestructure, jsx, isTsx } = buildModuleParts(root, options);
|
|
4
|
+
const parts = [...prelude, propsType];
|
|
16
5
|
if (!isTsx) {
|
|
17
|
-
parts.push(`/** @param {
|
|
6
|
+
parts.push(`/** @param {any} props */`);
|
|
18
7
|
}
|
|
19
8
|
const functionLines = [
|
|
20
|
-
isTsx ?
|
|
9
|
+
isTsx ? "export function render(props: any) {" : "export function render(props) {"
|
|
21
10
|
];
|
|
22
11
|
if (propsDestructure) {
|
|
23
12
|
functionLines.push(` ${propsDestructure}`);
|
|
@@ -26,6 +15,22 @@ function generateModule(root, options) {
|
|
|
26
15
|
parts.push(functionLines.join("\n"));
|
|
27
16
|
return parts.join("\n\n");
|
|
28
17
|
}
|
|
18
|
+
function buildModuleParts(root, options) {
|
|
19
|
+
const { jsxRuntime, flavor } = options;
|
|
20
|
+
const isTsx = flavor === "tsx";
|
|
21
|
+
const aliasEnv = buildClassAliasEnvironment(root.classAliases);
|
|
22
|
+
const jsx = renderRootChildren(root.children, aliasEnv);
|
|
23
|
+
const propsDestructure = emitPropsDestructure(root.props);
|
|
24
|
+
const prelude = [];
|
|
25
|
+
if (root.clientComponent) {
|
|
26
|
+
prelude.push(`"use client";`);
|
|
27
|
+
}
|
|
28
|
+
if (jsxRuntime === "classic" && templateUsesJsx(root)) {
|
|
29
|
+
prelude.push(`import React from "react";`);
|
|
30
|
+
}
|
|
31
|
+
const propsType = emitPropsType(root.props, flavor);
|
|
32
|
+
return { prelude, propsType, propsDestructure, jsx, isTsx };
|
|
33
|
+
}
|
|
29
34
|
function buildClassAliasEnvironment(decl) {
|
|
30
35
|
const env = /* @__PURE__ */ new Map();
|
|
31
36
|
if (!decl) {
|
|
@@ -37,7 +42,7 @@ function buildClassAliasEnvironment(decl) {
|
|
|
37
42
|
return env;
|
|
38
43
|
}
|
|
39
44
|
function renderRootChildren(children, aliasEnv) {
|
|
40
|
-
return emitNodesExpression(children, aliasEnv);
|
|
45
|
+
return emitNodesExpression(children, aliasEnv, /* @__PURE__ */ new Set());
|
|
41
46
|
}
|
|
42
47
|
function templateUsesJsx(root) {
|
|
43
48
|
if (root.children.length === 0) {
|
|
@@ -69,58 +74,58 @@ function branchUsesJsx(branch) {
|
|
|
69
74
|
}
|
|
70
75
|
return branch.body.some((child) => nodeUsesJsx(child));
|
|
71
76
|
}
|
|
72
|
-
function emitNodeInJsx(node, aliasEnv) {
|
|
77
|
+
function emitNodeInJsx(node, aliasEnv, locals) {
|
|
73
78
|
if (node.type === "Text") {
|
|
74
|
-
return emitText(node);
|
|
79
|
+
return emitText(node, locals);
|
|
75
80
|
}
|
|
76
81
|
if (node.type === "Expression") {
|
|
77
|
-
return `{${node.value}}`;
|
|
82
|
+
return `{${emitExpressionValue(node.value, locals)}}`;
|
|
78
83
|
}
|
|
79
84
|
if (node.type === "JSXPassthrough") {
|
|
80
|
-
return `{${node.expression}}`;
|
|
85
|
+
return `{${emitJsxExpression(node.expression, locals)}}`;
|
|
81
86
|
}
|
|
82
87
|
if (node.type === "Conditional") {
|
|
83
|
-
return `{${emitConditionalExpression(node, aliasEnv)}}`;
|
|
88
|
+
return `{${emitConditionalExpression(node, aliasEnv, locals)}}`;
|
|
84
89
|
}
|
|
85
90
|
if (node.type === "For") {
|
|
86
|
-
return `{${emitForExpression(node, aliasEnv)}}`;
|
|
91
|
+
return `{${emitForExpression(node, aliasEnv, locals)}}`;
|
|
87
92
|
}
|
|
88
93
|
if (node.type === "Component") {
|
|
89
|
-
return wrapWithGuard(emitComponent(node, aliasEnv), node.guard, "jsx");
|
|
94
|
+
return wrapWithGuard(emitComponent(node, aliasEnv, locals), node.guard, "jsx", locals);
|
|
90
95
|
}
|
|
91
|
-
return wrapWithGuard(emitElement(node, aliasEnv), node.guard, "jsx");
|
|
96
|
+
return wrapWithGuard(emitElement(node, aliasEnv, locals), node.guard, "jsx", locals);
|
|
92
97
|
}
|
|
93
|
-
function emitElement(node, aliasEnv) {
|
|
98
|
+
function emitElement(node, aliasEnv, locals) {
|
|
94
99
|
const expanded = expandClasses(node.classes, aliasEnv);
|
|
95
100
|
const classAttr = expanded.length ? ` className="${expanded.join(" ")}"` : "";
|
|
96
|
-
const attrs = emitAttributes(node.attributes, aliasEnv);
|
|
101
|
+
const attrs = emitAttributes(node.attributes, aliasEnv, locals);
|
|
97
102
|
const allAttrs = classAttr + attrs;
|
|
98
|
-
const children = emitChildrenWithSpacing(node.children, aliasEnv);
|
|
103
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv, locals);
|
|
99
104
|
if (children.length > 0) {
|
|
100
105
|
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
101
106
|
} else {
|
|
102
107
|
return `<${node.name}${allAttrs} />`;
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
|
-
function emitComponent(node, aliasEnv) {
|
|
106
|
-
const attrs = emitAttributes(node.attributes, aliasEnv);
|
|
107
|
-
const slotProps = emitSlotProps(node, aliasEnv);
|
|
110
|
+
function emitComponent(node, aliasEnv, locals) {
|
|
111
|
+
const attrs = emitAttributes(node.attributes, aliasEnv, locals);
|
|
112
|
+
const slotProps = emitSlotProps(node, aliasEnv, locals);
|
|
108
113
|
const allAttrs = `${attrs}${slotProps}`;
|
|
109
|
-
const children = emitChildrenWithSpacing(node.children, aliasEnv);
|
|
114
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv, locals);
|
|
110
115
|
if (children.length > 0) {
|
|
111
116
|
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
112
117
|
} else {
|
|
113
118
|
return `<${node.name}${allAttrs} />`;
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
|
-
function emitChildrenWithSpacing(children, aliasEnv) {
|
|
121
|
+
function emitChildrenWithSpacing(children, aliasEnv, locals) {
|
|
117
122
|
if (children.length === 0) {
|
|
118
123
|
return "";
|
|
119
124
|
}
|
|
120
125
|
const parts = [];
|
|
121
126
|
for (let i = 0; i < children.length; i++) {
|
|
122
127
|
const child = children[i];
|
|
123
|
-
const emitted = emitNodeInJsx(child, aliasEnv);
|
|
128
|
+
const emitted = emitNodeInJsx(child, aliasEnv, locals);
|
|
124
129
|
parts.push(emitted);
|
|
125
130
|
if (i < children.length - 1) {
|
|
126
131
|
const nextChild = children[i + 1];
|
|
@@ -132,7 +137,7 @@ function emitChildrenWithSpacing(children, aliasEnv) {
|
|
|
132
137
|
}
|
|
133
138
|
return parts.join("");
|
|
134
139
|
}
|
|
135
|
-
function emitAttributes(attributes, aliasEnv) {
|
|
140
|
+
function emitAttributes(attributes, aliasEnv, locals) {
|
|
136
141
|
if (attributes.length === 0) {
|
|
137
142
|
return "";
|
|
138
143
|
}
|
|
@@ -140,28 +145,32 @@ function emitAttributes(attributes, aliasEnv) {
|
|
|
140
145
|
if (attr.value === null) {
|
|
141
146
|
return ` ${attr.name}`;
|
|
142
147
|
}
|
|
143
|
-
return ` ${attr.name}=${attr.value}`;
|
|
148
|
+
return ` ${attr.name}=${emitAttributeValue(attr.value, locals)}`;
|
|
144
149
|
}).join("");
|
|
145
150
|
}
|
|
146
|
-
function emitSlotProps(node, aliasEnv) {
|
|
151
|
+
function emitSlotProps(node, aliasEnv, locals) {
|
|
147
152
|
if (!node.slots || node.slots.length === 0) {
|
|
148
153
|
return "";
|
|
149
154
|
}
|
|
150
155
|
return node.slots.map((slot) => {
|
|
151
|
-
const expr = emitNodesExpression(slot.children, aliasEnv);
|
|
156
|
+
const expr = emitNodesExpression(slot.children, aliasEnv, locals);
|
|
152
157
|
return ` ${slot.name}={${expr}}`;
|
|
153
158
|
}).join("");
|
|
154
159
|
}
|
|
155
|
-
function wrapWithGuard(rendered, guard, context) {
|
|
160
|
+
function wrapWithGuard(rendered, guard, context, locals) {
|
|
156
161
|
if (!guard) {
|
|
157
162
|
return rendered;
|
|
158
163
|
}
|
|
159
|
-
const
|
|
164
|
+
const condition = emitExpressionValue(guard, locals);
|
|
165
|
+
const expression = `(${condition}) && ${rendered}`;
|
|
160
166
|
return context === "jsx" ? `{${expression}}` : expression;
|
|
161
167
|
}
|
|
162
|
-
function emitForExpression(node, aliasEnv) {
|
|
163
|
-
const
|
|
164
|
-
|
|
168
|
+
function emitForExpression(node, aliasEnv, locals) {
|
|
169
|
+
const arrayExpr = emitExpressionValue(node.arrayExpr, locals);
|
|
170
|
+
const nextLocals = new Set(locals);
|
|
171
|
+
nextLocals.add(node.itemName);
|
|
172
|
+
const body = emitNodesExpression(node.body, aliasEnv, nextLocals);
|
|
173
|
+
return `(${arrayExpr} ?? []).map((${node.itemName}) => ${body})`;
|
|
165
174
|
}
|
|
166
175
|
function expandClasses(classes, aliasEnv) {
|
|
167
176
|
const result = [];
|
|
@@ -179,7 +188,29 @@ function expandClasses(classes, aliasEnv) {
|
|
|
179
188
|
}
|
|
180
189
|
return result;
|
|
181
190
|
}
|
|
182
|
-
function
|
|
191
|
+
function emitExpressionValue(expression, locals) {
|
|
192
|
+
return rewriteExpression(expression, locals);
|
|
193
|
+
}
|
|
194
|
+
function emitJsxExpression(expression, locals) {
|
|
195
|
+
const trimmed = expression.trimStart();
|
|
196
|
+
if (trimmed.startsWith("<")) {
|
|
197
|
+
return rewriteJsxExpression(expression, locals);
|
|
198
|
+
}
|
|
199
|
+
return rewriteExpression(expression, locals);
|
|
200
|
+
}
|
|
201
|
+
function emitAttributeValue(value, locals) {
|
|
202
|
+
const trimmed = value.trim();
|
|
203
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
207
|
+
const inner = trimmed.slice(1, -1);
|
|
208
|
+
const rewritten = rewriteExpression(inner, locals);
|
|
209
|
+
return `{${rewritten}}`;
|
|
210
|
+
}
|
|
211
|
+
return rewriteExpression(trimmed, locals);
|
|
212
|
+
}
|
|
213
|
+
function emitText(node, locals) {
|
|
183
214
|
if (!node.parts.length) {
|
|
184
215
|
return "";
|
|
185
216
|
}
|
|
@@ -187,65 +218,66 @@ function emitText(node) {
|
|
|
187
218
|
if (part.type === "text") {
|
|
188
219
|
return escapeText(part.value);
|
|
189
220
|
}
|
|
190
|
-
return `{${part.value}}`;
|
|
221
|
+
return `{${emitExpressionValue(part.value, locals)}}`;
|
|
191
222
|
}).join("");
|
|
192
223
|
}
|
|
193
|
-
function emitConditionalExpression(node, aliasEnv) {
|
|
224
|
+
function emitConditionalExpression(node, aliasEnv, locals) {
|
|
194
225
|
if (!node.branches.length) {
|
|
195
226
|
return "null";
|
|
196
227
|
}
|
|
197
228
|
const first = node.branches[0];
|
|
198
229
|
if (node.branches.length === 1 && first.test) {
|
|
199
|
-
|
|
230
|
+
const test = emitExpressionValue(first.test, locals);
|
|
231
|
+
return `(${test}) && ${emitBranchExpression(first, aliasEnv, locals)}`;
|
|
200
232
|
}
|
|
201
233
|
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
202
|
-
let fallback = hasElse ? emitBranchExpression(node.branches[node.branches.length - 1], aliasEnv) : "null";
|
|
234
|
+
let fallback = hasElse ? emitBranchExpression(node.branches[node.branches.length - 1], aliasEnv, locals) : "null";
|
|
203
235
|
const startIndex = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
204
236
|
if (startIndex < 0) {
|
|
205
237
|
return fallback;
|
|
206
238
|
}
|
|
207
239
|
for (let i = startIndex; i >= 0; i--) {
|
|
208
240
|
const branch = node.branches[i];
|
|
209
|
-
const test = branch.test
|
|
210
|
-
fallback = `(${test}) ? ${emitBranchExpression(branch, aliasEnv)} : ${fallback}`;
|
|
241
|
+
const test = branch.test ? emitExpressionValue(branch.test, locals) : "false";
|
|
242
|
+
fallback = `(${test}) ? ${emitBranchExpression(branch, aliasEnv, locals)} : ${fallback}`;
|
|
211
243
|
}
|
|
212
244
|
return fallback;
|
|
213
245
|
}
|
|
214
|
-
function emitBranchExpression(branch, aliasEnv) {
|
|
215
|
-
return emitNodesExpression(branch.body, aliasEnv);
|
|
246
|
+
function emitBranchExpression(branch, aliasEnv, locals) {
|
|
247
|
+
return emitNodesExpression(branch.body, aliasEnv, locals);
|
|
216
248
|
}
|
|
217
|
-
function emitNodesExpression(children, aliasEnv) {
|
|
249
|
+
function emitNodesExpression(children, aliasEnv, locals) {
|
|
218
250
|
if (children.length === 0) {
|
|
219
251
|
return "null";
|
|
220
252
|
}
|
|
221
253
|
if (children.length === 1) {
|
|
222
|
-
return emitSingleNodeExpression(children[0], aliasEnv);
|
|
254
|
+
return emitSingleNodeExpression(children[0], aliasEnv, locals);
|
|
223
255
|
}
|
|
224
|
-
return `<>${children.map((child) => emitNodeInJsx(child, aliasEnv)).join("")}</>`;
|
|
256
|
+
return `<>${children.map((child) => emitNodeInJsx(child, aliasEnv, locals)).join("")}</>`;
|
|
225
257
|
}
|
|
226
|
-
function emitSingleNodeExpression(node, aliasEnv) {
|
|
258
|
+
function emitSingleNodeExpression(node, aliasEnv, locals) {
|
|
227
259
|
if (node.type === "Expression") {
|
|
228
|
-
return node.value;
|
|
260
|
+
return emitExpressionValue(node.value, locals);
|
|
229
261
|
}
|
|
230
262
|
if (node.type === "JSXPassthrough") {
|
|
231
|
-
return node.expression;
|
|
263
|
+
return emitJsxExpression(node.expression, locals);
|
|
232
264
|
}
|
|
233
265
|
if (node.type === "Conditional") {
|
|
234
|
-
return emitConditionalExpression(node, aliasEnv);
|
|
266
|
+
return emitConditionalExpression(node, aliasEnv, locals);
|
|
235
267
|
}
|
|
236
268
|
if (node.type === "For") {
|
|
237
|
-
return emitForExpression(node, aliasEnv);
|
|
269
|
+
return emitForExpression(node, aliasEnv, locals);
|
|
238
270
|
}
|
|
239
271
|
if (node.type === "Element") {
|
|
240
|
-
return wrapWithGuard(emitElement(node, aliasEnv), node.guard, "expression");
|
|
272
|
+
return wrapWithGuard(emitElement(node, aliasEnv, locals), node.guard, "expression", locals);
|
|
241
273
|
}
|
|
242
274
|
if (node.type === "Component") {
|
|
243
|
-
return wrapWithGuard(emitComponent(node, aliasEnv), node.guard, "expression");
|
|
275
|
+
return wrapWithGuard(emitComponent(node, aliasEnv, locals), node.guard, "expression", locals);
|
|
244
276
|
}
|
|
245
277
|
if (node.type === "Text") {
|
|
246
|
-
return `<>${emitNodeInJsx(node, aliasEnv)}</>`;
|
|
278
|
+
return `<>${emitNodeInJsx(node, aliasEnv, locals)}</>`;
|
|
247
279
|
}
|
|
248
|
-
return emitNodeInJsx(node, aliasEnv);
|
|
280
|
+
return emitNodeInJsx(node, aliasEnv, locals);
|
|
249
281
|
}
|
|
250
282
|
function emitPropsType(props, flavor) {
|
|
251
283
|
if (flavor === "tsx") {
|
|
@@ -281,7 +313,7 @@ function emitPropsDestructure(props) {
|
|
|
281
313
|
return null;
|
|
282
314
|
}
|
|
283
315
|
const names = props.fields.map((field) => field.name);
|
|
284
|
-
return `const { ${names.join(", ")} } = props;`;
|
|
316
|
+
return `const { ${names.join(", ")} } = props ?? {};`;
|
|
285
317
|
}
|
|
286
318
|
function escapeText(value) {
|
|
287
319
|
return value.replace(/[&<>{}]/g, (char) => {
|
|
@@ -301,177 +333,463 @@ function escapeText(value) {
|
|
|
301
333
|
}
|
|
302
334
|
});
|
|
303
335
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
336
|
+
var IGNORED_IDENTIFIERS = /* @__PURE__ */ new Set([
|
|
337
|
+
"null",
|
|
338
|
+
"undefined",
|
|
339
|
+
"true",
|
|
340
|
+
"false",
|
|
341
|
+
"NaN",
|
|
342
|
+
"Infinity",
|
|
343
|
+
"this",
|
|
344
|
+
"props"
|
|
345
|
+
]);
|
|
346
|
+
var RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
347
|
+
"await",
|
|
348
|
+
"break",
|
|
349
|
+
"case",
|
|
350
|
+
"catch",
|
|
351
|
+
"class",
|
|
352
|
+
"const",
|
|
353
|
+
"continue",
|
|
354
|
+
"debugger",
|
|
355
|
+
"default",
|
|
356
|
+
"delete",
|
|
357
|
+
"do",
|
|
358
|
+
"else",
|
|
359
|
+
"enum",
|
|
360
|
+
"export",
|
|
361
|
+
"extends",
|
|
362
|
+
"false",
|
|
363
|
+
"finally",
|
|
364
|
+
"for",
|
|
365
|
+
"function",
|
|
366
|
+
"if",
|
|
367
|
+
"import",
|
|
368
|
+
"in",
|
|
369
|
+
"instanceof",
|
|
370
|
+
"let",
|
|
371
|
+
"new",
|
|
372
|
+
"null",
|
|
373
|
+
"return",
|
|
374
|
+
"super",
|
|
375
|
+
"switch",
|
|
376
|
+
"this",
|
|
377
|
+
"throw",
|
|
378
|
+
"true",
|
|
379
|
+
"try",
|
|
380
|
+
"typeof",
|
|
381
|
+
"var",
|
|
382
|
+
"void",
|
|
383
|
+
"while",
|
|
384
|
+
"with",
|
|
385
|
+
"yield"
|
|
386
|
+
]);
|
|
387
|
+
function emitIdentifier(name) {
|
|
388
|
+
return `props?.${name}`;
|
|
327
389
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
390
|
+
function rewriteExpression(expression, locals) {
|
|
391
|
+
let i = 0;
|
|
392
|
+
let state = "code";
|
|
393
|
+
let output = "";
|
|
394
|
+
while (i < expression.length) {
|
|
395
|
+
const ch = expression[i];
|
|
396
|
+
if (state === "code") {
|
|
397
|
+
if (ch === "'" || ch === '"') {
|
|
398
|
+
state = ch === "'" ? "single" : "double";
|
|
399
|
+
output += ch;
|
|
400
|
+
i++;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (ch === "`") {
|
|
404
|
+
state = "template";
|
|
405
|
+
output += ch;
|
|
406
|
+
i++;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (ch === "/" && expression[i + 1] === "/") {
|
|
410
|
+
state = "line";
|
|
411
|
+
output += ch;
|
|
412
|
+
i++;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (ch === "/" && expression[i + 1] === "*") {
|
|
416
|
+
state = "block";
|
|
417
|
+
output += ch;
|
|
418
|
+
i++;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (isIdentifierStart(ch)) {
|
|
422
|
+
const start = i;
|
|
423
|
+
i++;
|
|
424
|
+
while (i < expression.length && isIdentifierPart(expression[i])) {
|
|
425
|
+
i++;
|
|
426
|
+
}
|
|
427
|
+
const name = expression.slice(start, i);
|
|
428
|
+
const prevNonSpace = findPreviousNonSpace(expression, start - 1);
|
|
429
|
+
const nextNonSpace = findNextNonSpace(expression, i);
|
|
430
|
+
const isMemberAccess = prevNonSpace === ".";
|
|
431
|
+
const isObjectKey = nextNonSpace === ":";
|
|
432
|
+
if (isMemberAccess || isObjectKey || locals.has(name) || shouldIgnoreIdentifier(name)) {
|
|
433
|
+
output += name;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
output += emitIdentifier(name);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
output += ch;
|
|
440
|
+
i++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (state === "line") {
|
|
444
|
+
output += ch;
|
|
445
|
+
if (ch === "\n") {
|
|
446
|
+
state = "code";
|
|
447
|
+
}
|
|
448
|
+
i++;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (state === "block") {
|
|
452
|
+
output += ch;
|
|
453
|
+
if (ch === "*" && expression[i + 1] === "/") {
|
|
454
|
+
output += "/";
|
|
455
|
+
i += 2;
|
|
456
|
+
state = "code";
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
i++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (state === "single") {
|
|
463
|
+
output += ch;
|
|
464
|
+
if (ch === "\\") {
|
|
465
|
+
if (i + 1 < expression.length) {
|
|
466
|
+
output += expression[i + 1];
|
|
467
|
+
i += 2;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (ch === "'") {
|
|
472
|
+
state = "code";
|
|
473
|
+
}
|
|
474
|
+
i++;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (state === "double") {
|
|
478
|
+
output += ch;
|
|
479
|
+
if (ch === "\\") {
|
|
480
|
+
if (i + 1 < expression.length) {
|
|
481
|
+
output += expression[i + 1];
|
|
482
|
+
i += 2;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (ch === '"') {
|
|
487
|
+
state = "code";
|
|
488
|
+
}
|
|
489
|
+
i++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (state === "template") {
|
|
493
|
+
output += ch;
|
|
494
|
+
if (ch === "\\") {
|
|
495
|
+
if (i + 1 < expression.length) {
|
|
496
|
+
output += expression[i + 1];
|
|
497
|
+
i += 2;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (ch === "`") {
|
|
502
|
+
state = "code";
|
|
503
|
+
}
|
|
504
|
+
i++;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
331
507
|
}
|
|
332
|
-
|
|
333
|
-
return concatSegments(segments);
|
|
508
|
+
return output;
|
|
334
509
|
}
|
|
335
|
-
function
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
510
|
+
function rewriteJsxExpression(expression, locals) {
|
|
511
|
+
let output = "";
|
|
512
|
+
let i = 0;
|
|
513
|
+
while (i < expression.length) {
|
|
514
|
+
const ch = expression[i];
|
|
515
|
+
if (ch === "{") {
|
|
516
|
+
const braceResult = readBalancedBraces(expression, i + 1);
|
|
517
|
+
if (!braceResult) {
|
|
518
|
+
output += expression.slice(i);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
const rewritten = rewriteExpression(braceResult.content, locals);
|
|
522
|
+
output += `{${rewritten}}`;
|
|
523
|
+
i = braceResult.endIndex + 1;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
output += ch;
|
|
527
|
+
i++;
|
|
353
528
|
}
|
|
529
|
+
return output;
|
|
354
530
|
}
|
|
355
|
-
function
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
531
|
+
function readBalancedBraces(source, startIndex) {
|
|
532
|
+
let i = startIndex;
|
|
533
|
+
let depth = 1;
|
|
534
|
+
let state = "code";
|
|
535
|
+
while (i < source.length) {
|
|
536
|
+
const ch = source[i];
|
|
537
|
+
if (state === "code") {
|
|
538
|
+
if (ch === "'" || ch === '"') {
|
|
539
|
+
state = ch === "'" ? "single" : "double";
|
|
540
|
+
i++;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (ch === "`") {
|
|
544
|
+
state = "template";
|
|
545
|
+
i++;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
549
|
+
state = "line";
|
|
550
|
+
i += 2;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
554
|
+
state = "block";
|
|
555
|
+
i += 2;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (ch === "{") {
|
|
559
|
+
depth += 1;
|
|
560
|
+
} else if (ch === "}") {
|
|
561
|
+
depth -= 1;
|
|
562
|
+
if (depth === 0) {
|
|
563
|
+
return { content: source.slice(startIndex, i), endIndex: i };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
i++;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (state === "line") {
|
|
570
|
+
if (ch === "\n") {
|
|
571
|
+
state = "code";
|
|
572
|
+
}
|
|
573
|
+
i++;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (state === "block") {
|
|
577
|
+
if (ch === "*" && source[i + 1] === "/") {
|
|
578
|
+
i += 2;
|
|
579
|
+
state = "code";
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
i++;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (state === "single") {
|
|
586
|
+
if (ch === "\\") {
|
|
587
|
+
i += 2;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (ch === "'") {
|
|
591
|
+
state = "code";
|
|
592
|
+
}
|
|
593
|
+
i++;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (state === "double") {
|
|
597
|
+
if (ch === "\\") {
|
|
598
|
+
i += 2;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (ch === '"') {
|
|
602
|
+
state = "code";
|
|
603
|
+
}
|
|
604
|
+
i++;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (state === "template") {
|
|
608
|
+
if (ch === "\\") {
|
|
609
|
+
i += 2;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (ch === "`") {
|
|
613
|
+
state = "code";
|
|
614
|
+
}
|
|
615
|
+
i++;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
361
618
|
}
|
|
362
|
-
|
|
363
|
-
const end = literal(`</${node.name}>`);
|
|
364
|
-
return concatSegments([start, children, end]);
|
|
619
|
+
return null;
|
|
365
620
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return start;
|
|
373
|
-
}
|
|
374
|
-
const childSegments = [];
|
|
375
|
-
if (node.children.length) {
|
|
376
|
-
childSegments.push(emitNodesString(node.children, aliasEnv));
|
|
621
|
+
function findPreviousNonSpace(text, index) {
|
|
622
|
+
for (let i = index; i >= 0; i--) {
|
|
623
|
+
const ch = text[i];
|
|
624
|
+
if (!/\s/.test(ch)) {
|
|
625
|
+
return ch;
|
|
626
|
+
}
|
|
377
627
|
}
|
|
378
|
-
|
|
379
|
-
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
function findNextNonSpace(text, index) {
|
|
631
|
+
for (let i = index; i < text.length; i++) {
|
|
632
|
+
const ch = text[i];
|
|
633
|
+
if (!/\s/.test(ch)) {
|
|
634
|
+
return ch;
|
|
635
|
+
}
|
|
380
636
|
}
|
|
381
|
-
|
|
382
|
-
const end = literal(`</${node.name}>`);
|
|
383
|
-
return concatSegments([start, children, end]);
|
|
637
|
+
return null;
|
|
384
638
|
}
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
const body = emitNodesString(slot.children, aliasEnv);
|
|
388
|
-
const end = literal("</template>");
|
|
389
|
-
return concatSegments([start, body, end]);
|
|
639
|
+
function isIdentifierStart(ch) {
|
|
640
|
+
return /[A-Za-z_$]/.test(ch);
|
|
390
641
|
}
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
return '""';
|
|
394
|
-
}
|
|
395
|
-
const first = node.branches[0];
|
|
396
|
-
if (node.branches.length === 1 && first.test) {
|
|
397
|
-
return `(${first.test}) ? ${emitBranch(first, aliasEnv)} : ""`;
|
|
398
|
-
}
|
|
399
|
-
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
400
|
-
let fallback = hasElse ? emitBranch(node.branches[node.branches.length - 1], aliasEnv) : '""';
|
|
401
|
-
const limit = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
402
|
-
for (let i = limit; i >= 0; i--) {
|
|
403
|
-
const branch = node.branches[i];
|
|
404
|
-
const test = branch.test ?? "false";
|
|
405
|
-
fallback = `(${test}) ? ${emitBranch(branch, aliasEnv)} : ${fallback}`;
|
|
406
|
-
}
|
|
407
|
-
return fallback;
|
|
642
|
+
function isIdentifierPart(ch) {
|
|
643
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
408
644
|
}
|
|
409
|
-
function
|
|
410
|
-
return
|
|
645
|
+
function shouldIgnoreIdentifier(name) {
|
|
646
|
+
return IGNORED_IDENTIFIERS.has(name) || RESERVED_KEYWORDS.has(name);
|
|
411
647
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
648
|
+
|
|
649
|
+
// src/html-codegen.ts
|
|
650
|
+
function generateHtml(root, options = {}) {
|
|
651
|
+
const indent = options.indent ?? " ";
|
|
652
|
+
const aliasEnv = buildClassAliasEnvironment2(root.classAliases);
|
|
653
|
+
const rendered = emitNodes(root.children, aliasEnv, indent, 0);
|
|
654
|
+
return rendered.trimEnd();
|
|
415
655
|
}
|
|
416
|
-
function
|
|
417
|
-
|
|
418
|
-
|
|
656
|
+
function emitNodes(children, aliasEnv, indent, depth) {
|
|
657
|
+
let html = "";
|
|
658
|
+
for (const child of children) {
|
|
659
|
+
const chunk = emitNode(child, aliasEnv, indent, depth);
|
|
660
|
+
if (chunk) {
|
|
661
|
+
html += chunk;
|
|
662
|
+
}
|
|
419
663
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
664
|
+
return html;
|
|
665
|
+
}
|
|
666
|
+
function emitNode(node, aliasEnv, indent, depth) {
|
|
667
|
+
switch (node.type) {
|
|
668
|
+
case "Element":
|
|
669
|
+
return emitElement2(node, aliasEnv, indent, depth);
|
|
670
|
+
case "Text":
|
|
671
|
+
return emitTextBlock(node, indent, depth);
|
|
672
|
+
default:
|
|
673
|
+
return "";
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function emitElement2(node, aliasEnv, indent, depth) {
|
|
677
|
+
const indentText = indent.repeat(depth);
|
|
678
|
+
const classNames = expandClasses2(node.classes, aliasEnv);
|
|
679
|
+
const attrs = renderAttributes(node.attributes, classNames);
|
|
680
|
+
const openTag = `<${node.name}${attrs}>`;
|
|
681
|
+
if (node.children.length === 0) {
|
|
682
|
+
return `${indentText}${openTag}</${node.name}>
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
if (node.children.length === 1 && node.children[0].type === "Text") {
|
|
686
|
+
const inline = emitInlineText(node.children[0]);
|
|
687
|
+
if (inline !== null) {
|
|
688
|
+
return `${indentText}${openTag}${inline}</${node.name}>
|
|
689
|
+
`;
|
|
423
690
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
691
|
+
}
|
|
692
|
+
const children = emitNodes(node.children, aliasEnv, indent, depth + 1);
|
|
693
|
+
if (!children) {
|
|
694
|
+
return `${indentText}${openTag}</${node.name}>
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
return `${indentText}${openTag}
|
|
698
|
+
${children}${indentText}</${node.name}>
|
|
699
|
+
`;
|
|
427
700
|
}
|
|
428
|
-
function
|
|
701
|
+
function renderAttributes(attributes, classNames) {
|
|
429
702
|
const segments = [];
|
|
430
703
|
if (classNames.length) {
|
|
431
|
-
segments.push(
|
|
704
|
+
segments.push(`class="${escapeAttributeValue(classNames.join(" "))}"`);
|
|
432
705
|
}
|
|
433
706
|
for (const attr of attributes) {
|
|
434
707
|
if (attr.value === null) {
|
|
435
|
-
segments.push(
|
|
708
|
+
segments.push(attr.name);
|
|
436
709
|
continue;
|
|
437
710
|
}
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
"})()"
|
|
445
|
-
].join(" ")
|
|
446
|
-
);
|
|
711
|
+
const literal = extractStaticAttributeValue(attr.value);
|
|
712
|
+
if (literal === null) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const name = attr.name === "className" ? "class" : attr.name;
|
|
716
|
+
segments.push(`${name}="${escapeAttributeValue(literal)}"`);
|
|
447
717
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
function attributeExpression(raw) {
|
|
451
|
-
const trimmed = raw.trim();
|
|
452
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
453
|
-
return trimmed.slice(1, -1).trim();
|
|
718
|
+
if (!segments.length) {
|
|
719
|
+
return "";
|
|
454
720
|
}
|
|
455
|
-
return
|
|
721
|
+
return " " + segments.join(" ");
|
|
456
722
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
723
|
+
function emitTextBlock(node, indent, depth) {
|
|
724
|
+
const inline = emitInlineText(node);
|
|
725
|
+
if (inline === null || inline.trim().length === 0) {
|
|
726
|
+
return "";
|
|
460
727
|
}
|
|
461
|
-
return
|
|
728
|
+
return `${indent.repeat(depth)}${inline}
|
|
729
|
+
`;
|
|
462
730
|
}
|
|
463
|
-
function
|
|
464
|
-
|
|
731
|
+
function emitInlineText(node) {
|
|
732
|
+
if (!node.parts.length) {
|
|
733
|
+
return "";
|
|
734
|
+
}
|
|
735
|
+
let text = "";
|
|
736
|
+
for (const part of node.parts) {
|
|
737
|
+
if (part.type !== "text") {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
text += escapeStaticText(part.value);
|
|
741
|
+
}
|
|
742
|
+
return text;
|
|
465
743
|
}
|
|
466
|
-
function
|
|
467
|
-
const
|
|
468
|
-
if (
|
|
469
|
-
return
|
|
744
|
+
function extractStaticAttributeValue(raw) {
|
|
745
|
+
const trimmed = raw.trim();
|
|
746
|
+
if (trimmed.length < 2) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
const quote = trimmed[0];
|
|
750
|
+
if (quote !== '"' && quote !== "'" || trimmed[trimmed.length - 1] !== quote) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
const body = trimmed.slice(1, -1);
|
|
754
|
+
let result = "";
|
|
755
|
+
let escaping = false;
|
|
756
|
+
for (const char of body) {
|
|
757
|
+
if (escaping) {
|
|
758
|
+
result += unescapeChar(char, quote);
|
|
759
|
+
escaping = false;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (char === "\\") {
|
|
763
|
+
escaping = true;
|
|
764
|
+
} else {
|
|
765
|
+
result += char;
|
|
766
|
+
}
|
|
470
767
|
}
|
|
471
|
-
if (
|
|
472
|
-
|
|
768
|
+
if (escaping) {
|
|
769
|
+
result += "\\";
|
|
770
|
+
}
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
function unescapeChar(char, quote) {
|
|
774
|
+
switch (char) {
|
|
775
|
+
case "n":
|
|
776
|
+
return "\n";
|
|
777
|
+
case "r":
|
|
778
|
+
return "\r";
|
|
779
|
+
case "t":
|
|
780
|
+
return " ";
|
|
781
|
+
case "\\":
|
|
782
|
+
return "\\";
|
|
783
|
+
case '"':
|
|
784
|
+
return '"';
|
|
785
|
+
case "'":
|
|
786
|
+
return "'";
|
|
787
|
+
default:
|
|
788
|
+
if (char === quote) {
|
|
789
|
+
return quote;
|
|
790
|
+
}
|
|
791
|
+
return char;
|
|
473
792
|
}
|
|
474
|
-
return filtered.join(" + ");
|
|
475
793
|
}
|
|
476
794
|
function buildClassAliasEnvironment2(decl) {
|
|
477
795
|
const env = /* @__PURE__ */ new Map();
|
|
@@ -499,26 +817,6 @@ function expandClasses2(classes, aliasEnv) {
|
|
|
499
817
|
}
|
|
500
818
|
return result;
|
|
501
819
|
}
|
|
502
|
-
function emitJsDocPropsType2(props) {
|
|
503
|
-
if (!props) {
|
|
504
|
-
return "/** @typedef {any} Props */";
|
|
505
|
-
}
|
|
506
|
-
if (!props.fields.length) {
|
|
507
|
-
return "/** @typedef {{}} Props */";
|
|
508
|
-
}
|
|
509
|
-
const fields = props.fields.map((field) => {
|
|
510
|
-
const optional = field.optional ? "?" : "";
|
|
511
|
-
return `${field.name}${optional}: ${field.typeText}`;
|
|
512
|
-
}).join("; ");
|
|
513
|
-
return `/** @typedef {{ ${fields} }} Props */`;
|
|
514
|
-
}
|
|
515
|
-
function emitPropsDestructure2(props) {
|
|
516
|
-
if (!props || props.fields.length === 0) {
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
const names = props.fields.map((field) => field.name);
|
|
520
|
-
return `const { ${names.join(", ")} } = props;`;
|
|
521
|
-
}
|
|
522
820
|
function escapeStaticText(value) {
|
|
523
821
|
return value.replace(/[&<>{}]/g, (char) => {
|
|
524
822
|
switch (char) {
|
|
@@ -537,47 +835,21 @@ function escapeStaticText(value) {
|
|
|
537
835
|
}
|
|
538
836
|
});
|
|
539
837
|
}
|
|
540
|
-
function
|
|
541
|
-
return [
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
' return ">";',
|
|
556
|
-
" default:",
|
|
557
|
-
" return char;",
|
|
558
|
-
" }",
|
|
559
|
-
"}",
|
|
560
|
-
"function __collie_escapeAttr(value) {",
|
|
561
|
-
" if (value === null || value === undefined) {",
|
|
562
|
-
' return "";',
|
|
563
|
-
" }",
|
|
564
|
-
' return String(value).replace(/["&<>]/g, __collie_escapeAttrChar);',
|
|
565
|
-
"}",
|
|
566
|
-
"function __collie_escapeAttrChar(char) {",
|
|
567
|
-
" switch (char) {",
|
|
568
|
-
' case "&":',
|
|
569
|
-
' return "&";',
|
|
570
|
-
' case "<":',
|
|
571
|
-
' return "<";',
|
|
572
|
-
' case ">":',
|
|
573
|
-
' return ">";',
|
|
574
|
-
` case '"':`,
|
|
575
|
-
' return """;',
|
|
576
|
-
" default:",
|
|
577
|
-
" return char;",
|
|
578
|
-
" }",
|
|
579
|
-
"}"
|
|
580
|
-
];
|
|
838
|
+
function escapeAttributeValue(value) {
|
|
839
|
+
return value.replace(/["&<>]/g, (char) => {
|
|
840
|
+
switch (char) {
|
|
841
|
+
case "&":
|
|
842
|
+
return "&";
|
|
843
|
+
case "<":
|
|
844
|
+
return "<";
|
|
845
|
+
case ">":
|
|
846
|
+
return ">";
|
|
847
|
+
case '"':
|
|
848
|
+
return """;
|
|
849
|
+
default:
|
|
850
|
+
return char;
|
|
851
|
+
}
|
|
852
|
+
});
|
|
581
853
|
}
|
|
582
854
|
|
|
583
855
|
// src/diagnostics.ts
|
|
@@ -589,142 +861,902 @@ function createSpan(line, col, length, lineOffset) {
|
|
|
589
861
|
};
|
|
590
862
|
}
|
|
591
863
|
|
|
592
|
-
// src/
|
|
593
|
-
function
|
|
594
|
-
const match = line.match(/^\s*/);
|
|
595
|
-
return match ? match[0].length / 2 : 0;
|
|
596
|
-
}
|
|
597
|
-
function parse(source) {
|
|
864
|
+
// src/dialect.ts
|
|
865
|
+
function enforceDialect(root, config) {
|
|
598
866
|
const diagnostics = [];
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
867
|
+
if (root.idToken) {
|
|
868
|
+
diagnostics.push(
|
|
869
|
+
...evaluateToken(
|
|
870
|
+
{ kind: "id", token: root.idToken, span: root.idTokenSpan },
|
|
871
|
+
config.tokens.id
|
|
872
|
+
)
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
walkNodes(root.children, (occurrence) => {
|
|
876
|
+
const rule = config.tokens[occurrence.kind];
|
|
877
|
+
diagnostics.push(...evaluateToken(occurrence, rule));
|
|
878
|
+
});
|
|
879
|
+
return diagnostics;
|
|
880
|
+
}
|
|
881
|
+
function walkNodes(nodes, onToken) {
|
|
882
|
+
for (const node of nodes) {
|
|
883
|
+
if (node.type === "For") {
|
|
884
|
+
onFor(node, onToken);
|
|
885
|
+
walkNodes(node.body, onToken);
|
|
617
886
|
continue;
|
|
618
887
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
pushDiag(
|
|
622
|
-
diagnostics,
|
|
623
|
-
"COLLIE001",
|
|
624
|
-
"Tabs are not allowed; use spaces for indentation.",
|
|
625
|
-
lineNumber,
|
|
626
|
-
tabIndex + 1,
|
|
627
|
-
lineOffset
|
|
628
|
-
);
|
|
888
|
+
if (node.type === "Conditional") {
|
|
889
|
+
onConditional(node, onToken);
|
|
629
890
|
continue;
|
|
630
891
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
892
|
+
if (node.type === "Element" || node.type === "Component") {
|
|
893
|
+
walkNodes(node.children, onToken);
|
|
894
|
+
if (node.type === "Component" && node.slots) {
|
|
895
|
+
for (const slot of node.slots) {
|
|
896
|
+
walkNodes(slot.children, onToken);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function onFor(node, onToken) {
|
|
904
|
+
if (!node.token) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
onToken({ kind: "for", token: node.token, span: node.tokenSpan });
|
|
908
|
+
}
|
|
909
|
+
function onConditional(node, onToken) {
|
|
910
|
+
for (const branch of node.branches) {
|
|
911
|
+
onBranch(branch, onToken);
|
|
912
|
+
walkNodes(branch.body, onToken);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function onBranch(branch, onToken) {
|
|
916
|
+
if (!branch.token || !branch.kind) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
onToken({ kind: branch.kind, token: branch.token, span: branch.tokenSpan });
|
|
920
|
+
}
|
|
921
|
+
function evaluateToken(occurrence, rule) {
|
|
922
|
+
const diagnostics = [];
|
|
923
|
+
const used = occurrence.token;
|
|
924
|
+
const preferred = rule.preferred;
|
|
925
|
+
const isAllowed = rule.allow.includes(used);
|
|
926
|
+
if (!isAllowed) {
|
|
927
|
+
const severity = levelToSeverity(rule.onDisallowed);
|
|
928
|
+
if (severity) {
|
|
929
|
+
diagnostics.push(
|
|
930
|
+
createDialectDiagnostic(
|
|
931
|
+
"dialect.token.disallowed",
|
|
932
|
+
severity,
|
|
933
|
+
used,
|
|
934
|
+
preferred,
|
|
935
|
+
occurrence.span,
|
|
936
|
+
`Token "${used}" is not allowed for ${occurrence.kind}. Preferred: "${preferred}".`
|
|
937
|
+
)
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
return diagnostics;
|
|
941
|
+
}
|
|
942
|
+
if (used !== preferred) {
|
|
943
|
+
const severity = levelToSeverity(rule.onDisallowed);
|
|
944
|
+
if (severity) {
|
|
945
|
+
diagnostics.push(
|
|
946
|
+
createDialectDiagnostic(
|
|
947
|
+
"dialect.token.nonPreferred",
|
|
948
|
+
severity,
|
|
949
|
+
used,
|
|
950
|
+
preferred,
|
|
951
|
+
occurrence.span,
|
|
952
|
+
`Token "${used}" is allowed but not preferred for ${occurrence.kind}. Preferred: "${preferred}".`
|
|
953
|
+
)
|
|
643
954
|
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return diagnostics;
|
|
958
|
+
}
|
|
959
|
+
function createDialectDiagnostic(code, severity, used, preferred, span, message) {
|
|
960
|
+
const fix = span ? {
|
|
961
|
+
range: span,
|
|
962
|
+
replacementText: preferred
|
|
963
|
+
} : void 0;
|
|
964
|
+
return {
|
|
965
|
+
severity,
|
|
966
|
+
code,
|
|
967
|
+
message: message.replace(/\\s+/g, " "),
|
|
968
|
+
span,
|
|
969
|
+
fix
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function levelToSeverity(level) {
|
|
973
|
+
if (level === "off") {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
if (level === "error") {
|
|
977
|
+
return "error";
|
|
978
|
+
}
|
|
979
|
+
return "warning";
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/props.ts
|
|
983
|
+
var IGNORED_IDENTIFIERS2 = /* @__PURE__ */ new Set([
|
|
984
|
+
"null",
|
|
985
|
+
"undefined",
|
|
986
|
+
"true",
|
|
987
|
+
"false",
|
|
988
|
+
"NaN",
|
|
989
|
+
"Infinity",
|
|
990
|
+
"this",
|
|
991
|
+
"props"
|
|
992
|
+
]);
|
|
993
|
+
var RESERVED_KEYWORDS2 = /* @__PURE__ */ new Set([
|
|
994
|
+
"await",
|
|
995
|
+
"break",
|
|
996
|
+
"case",
|
|
997
|
+
"catch",
|
|
998
|
+
"class",
|
|
999
|
+
"const",
|
|
1000
|
+
"continue",
|
|
1001
|
+
"debugger",
|
|
1002
|
+
"default",
|
|
1003
|
+
"delete",
|
|
1004
|
+
"do",
|
|
1005
|
+
"else",
|
|
1006
|
+
"enum",
|
|
1007
|
+
"export",
|
|
1008
|
+
"extends",
|
|
1009
|
+
"false",
|
|
1010
|
+
"finally",
|
|
1011
|
+
"for",
|
|
1012
|
+
"function",
|
|
1013
|
+
"if",
|
|
1014
|
+
"import",
|
|
1015
|
+
"in",
|
|
1016
|
+
"instanceof",
|
|
1017
|
+
"let",
|
|
1018
|
+
"new",
|
|
1019
|
+
"null",
|
|
1020
|
+
"return",
|
|
1021
|
+
"super",
|
|
1022
|
+
"switch",
|
|
1023
|
+
"this",
|
|
1024
|
+
"throw",
|
|
1025
|
+
"true",
|
|
1026
|
+
"try",
|
|
1027
|
+
"typeof",
|
|
1028
|
+
"var",
|
|
1029
|
+
"void",
|
|
1030
|
+
"while",
|
|
1031
|
+
"with",
|
|
1032
|
+
"yield"
|
|
1033
|
+
]);
|
|
1034
|
+
function enforceProps(root, propsConfig) {
|
|
1035
|
+
const diagnostics = [];
|
|
1036
|
+
const declaredProps = /* @__PURE__ */ new Map();
|
|
1037
|
+
const usedLocal = /* @__PURE__ */ new Map();
|
|
1038
|
+
const usedNamespace = /* @__PURE__ */ new Map();
|
|
1039
|
+
const usedAny = /* @__PURE__ */ new Set();
|
|
1040
|
+
const missingReported = /* @__PURE__ */ new Set();
|
|
1041
|
+
const localStyleReported = /* @__PURE__ */ new Set();
|
|
1042
|
+
const namespaceStyleReported = /* @__PURE__ */ new Set();
|
|
1043
|
+
if (root.props?.fields) {
|
|
1044
|
+
for (const field of root.props.fields) {
|
|
1045
|
+
declaredProps.set(field.name, field);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const preferStyle = propsConfig.preferAccessStyle;
|
|
1049
|
+
const flagLocalStyle = !propsConfig.allowDeclaredLocals || preferStyle === "namespace";
|
|
1050
|
+
const flagNamespaceStyle = !propsConfig.allowPropsNamespace || preferStyle === "locals";
|
|
1051
|
+
const walkNodes2 = (nodes, locals) => {
|
|
1052
|
+
for (const node of nodes) {
|
|
1053
|
+
if (node.type === "Conditional") {
|
|
1054
|
+
handleConditional(node, locals);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
if (node.type === "For") {
|
|
1058
|
+
handleFor(node, locals);
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (node.type === "Expression") {
|
|
1062
|
+
handleExpression(node.value, node.span, locals);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
if (node.type === "JSXPassthrough") {
|
|
1066
|
+
handleExpression(node.expression, node.span, locals);
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (node.type === "Text") {
|
|
1070
|
+
handleText(node.parts, locals);
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
if (node.type === "Element") {
|
|
1074
|
+
handleElement(node, locals);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (node.type === "Component") {
|
|
1078
|
+
handleComponent(node, locals);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
const handleConditional = (node, locals) => {
|
|
1084
|
+
for (const branch of node.branches) {
|
|
1085
|
+
if (branch.test) {
|
|
1086
|
+
handleExpression(branch.test, branch.testSpan, locals);
|
|
1087
|
+
}
|
|
1088
|
+
walkNodes2(branch.body, locals);
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
const handleFor = (node, locals) => {
|
|
1092
|
+
handleExpression(node.arrayExpr, node.arrayExprSpan, locals);
|
|
1093
|
+
const nextLocals = new Set(locals);
|
|
1094
|
+
nextLocals.add(node.itemName);
|
|
1095
|
+
walkNodes2(node.body, nextLocals);
|
|
1096
|
+
};
|
|
1097
|
+
const handleElement = (node, locals) => {
|
|
1098
|
+
if (node.guard) {
|
|
1099
|
+
handleExpression(node.guard, node.guardSpan, locals);
|
|
1100
|
+
}
|
|
1101
|
+
handleAttributes(node.attributes, locals);
|
|
1102
|
+
walkNodes2(node.children, locals);
|
|
1103
|
+
};
|
|
1104
|
+
const handleComponent = (node, locals) => {
|
|
1105
|
+
if (node.guard) {
|
|
1106
|
+
handleExpression(node.guard, node.guardSpan, locals);
|
|
1107
|
+
}
|
|
1108
|
+
handleAttributes(node.attributes, locals);
|
|
1109
|
+
if (node.slots) {
|
|
1110
|
+
for (const slot of node.slots) {
|
|
1111
|
+
walkNodes2(slot.children, locals);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
walkNodes2(node.children, locals);
|
|
1115
|
+
};
|
|
1116
|
+
const handleText = (parts, locals) => {
|
|
1117
|
+
for (const part of parts) {
|
|
1118
|
+
if (part.type === "expr") {
|
|
1119
|
+
handleExpression(part.value, part.span, locals);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
const handleAttributes = (attributes, locals) => {
|
|
1124
|
+
for (const attr of attributes) {
|
|
1125
|
+
if (!attr.value) continue;
|
|
1126
|
+
const trimmed = attr.value.trim();
|
|
1127
|
+
if (!trimmed || trimmed.startsWith("'") || trimmed.startsWith('"')) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
handleExpression(trimmed, void 0, locals);
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
const handleExpression = (expression, span, locals) => {
|
|
1134
|
+
const occurrences = scanExpression(expression);
|
|
1135
|
+
for (const occurrence of occurrences) {
|
|
1136
|
+
const name = occurrence.name;
|
|
1137
|
+
if (occurrence.kind === "local" && locals.has(name)) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
if (shouldIgnoreIdentifier2(name)) {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const usageSpan = span ? offsetSpan(span, occurrence.index, occurrence.length) : void 0;
|
|
1144
|
+
if (occurrence.kind === "namespace") {
|
|
1145
|
+
recordUsage(usedNamespace, name, usageSpan);
|
|
1146
|
+
usedAny.add(name);
|
|
1147
|
+
if (propsConfig.requireDeclarationForLocals && !declaredProps.has(name) && !missingReported.has(name)) {
|
|
1148
|
+
const severity = levelToSeverity2(propsConfig.diagnostics.missingDeclaration);
|
|
1149
|
+
if (severity) {
|
|
1150
|
+
diagnostics.push(createMissingDeclarationDiagnostic(name, severity, usageSpan));
|
|
1151
|
+
missingReported.add(name);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (flagNamespaceStyle && !namespaceStyleReported.has(name)) {
|
|
1155
|
+
const severity = levelToSeverity2(propsConfig.diagnostics.style);
|
|
1156
|
+
if (severity) {
|
|
1157
|
+
diagnostics.push(
|
|
1158
|
+
createStyleDiagnostic(
|
|
1159
|
+
name,
|
|
1160
|
+
"namespace",
|
|
1161
|
+
severity,
|
|
1162
|
+
usageSpan,
|
|
1163
|
+
propsConfig.allowPropsNamespace
|
|
1164
|
+
)
|
|
1165
|
+
);
|
|
1166
|
+
namespaceStyleReported.add(name);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
recordUsage(usedLocal, name, usageSpan);
|
|
1172
|
+
usedAny.add(name);
|
|
1173
|
+
if (propsConfig.requireDeclarationForLocals && !declaredProps.has(name) && !missingReported.has(name)) {
|
|
1174
|
+
const severity = levelToSeverity2(propsConfig.diagnostics.missingDeclaration);
|
|
1175
|
+
if (severity) {
|
|
1176
|
+
diagnostics.push(createMissingDeclarationDiagnostic(name, severity, usageSpan));
|
|
1177
|
+
missingReported.add(name);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (flagLocalStyle && !localStyleReported.has(name)) {
|
|
1181
|
+
const severity = levelToSeverity2(propsConfig.diagnostics.style);
|
|
1182
|
+
if (severity) {
|
|
1183
|
+
diagnostics.push(
|
|
1184
|
+
createStyleDiagnostic(name, "local", severity, usageSpan, propsConfig.allowDeclaredLocals)
|
|
1185
|
+
);
|
|
1186
|
+
localStyleReported.add(name);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
walkNodes2(root.children, /* @__PURE__ */ new Set());
|
|
1192
|
+
if (root.props?.fields) {
|
|
1193
|
+
for (const field of root.props.fields) {
|
|
1194
|
+
if (!usedAny.has(field.name)) {
|
|
1195
|
+
const severity = levelToSeverity2(propsConfig.diagnostics.unusedDeclaration);
|
|
1196
|
+
if (severity) {
|
|
1197
|
+
diagnostics.push({
|
|
1198
|
+
severity,
|
|
1199
|
+
code: "props.unusedDeclaration",
|
|
1200
|
+
message: `Prop "${field.name}" is declared but never used.`,
|
|
1201
|
+
span: field.span
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (propsConfig.requirePropsBlockWhen.enabled && !root.props && usedAny.size >= propsConfig.requirePropsBlockWhen.minUniquePropsUsed) {
|
|
1208
|
+
const severity = levelToSeverity2(propsConfig.requirePropsBlockWhen.severity);
|
|
1209
|
+
if (severity) {
|
|
1210
|
+
diagnostics.push({
|
|
1211
|
+
severity,
|
|
1212
|
+
code: "props.block.recommendedOrRequired",
|
|
1213
|
+
message: `Props block recommended: ${usedAny.size} unique prop${usedAny.size === 1 ? "" : "s"} used.`
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return diagnostics;
|
|
1218
|
+
}
|
|
1219
|
+
function createMissingDeclarationDiagnostic(name, severity, span) {
|
|
1220
|
+
return {
|
|
1221
|
+
severity,
|
|
1222
|
+
code: "props.missingDeclaration",
|
|
1223
|
+
message: `Prop \`${name}\` is used but not declared in \`#props\`.`,
|
|
1224
|
+
span,
|
|
1225
|
+
data: {
|
|
1226
|
+
kind: "addPropDeclaration",
|
|
1227
|
+
propName: name
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
function createStyleDiagnostic(name, kind, severity, span, allowed) {
|
|
1232
|
+
if (kind === "namespace") {
|
|
1233
|
+
const message2 = allowed ? `props.${name} is allowed but not preferred; use "${name}" instead.` : `props.${name} is disabled; use "${name}" instead.`;
|
|
1234
|
+
return {
|
|
1235
|
+
severity,
|
|
1236
|
+
code: "props.style.nonPreferred",
|
|
1237
|
+
message: message2,
|
|
1238
|
+
span
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const message = allowed ? `"${name}" is allowed but not preferred; use props.${name} instead.` : `"${name}" is disabled; use props.${name} instead.`;
|
|
1242
|
+
return {
|
|
1243
|
+
severity,
|
|
1244
|
+
code: "props.style.nonPreferred",
|
|
1245
|
+
message,
|
|
1246
|
+
span
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function recordUsage(map, name, span) {
|
|
1250
|
+
const existing = map.get(name);
|
|
1251
|
+
if (existing) {
|
|
1252
|
+
existing.count += 1;
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
map.set(name, { count: 1, span });
|
|
1256
|
+
}
|
|
1257
|
+
function scanExpression(expression) {
|
|
1258
|
+
const occurrences = [];
|
|
1259
|
+
let i = 0;
|
|
1260
|
+
let state = "code";
|
|
1261
|
+
while (i < expression.length) {
|
|
1262
|
+
const ch = expression[i];
|
|
1263
|
+
if (state === "code") {
|
|
1264
|
+
if (ch === "'" || ch === '"') {
|
|
1265
|
+
state = ch === "'" ? "single" : "double";
|
|
1266
|
+
i++;
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (ch === "`") {
|
|
1270
|
+
state = "template";
|
|
1271
|
+
i++;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
if (ch === "/" && expression[i + 1] === "/") {
|
|
1275
|
+
state = "line";
|
|
1276
|
+
i += 2;
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (ch === "/" && expression[i + 1] === "*") {
|
|
1280
|
+
state = "block";
|
|
1281
|
+
i += 2;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (isIdentifierStart2(ch)) {
|
|
1285
|
+
const start = i;
|
|
1286
|
+
i++;
|
|
1287
|
+
while (i < expression.length && isIdentifierPart2(expression[i])) {
|
|
1288
|
+
i++;
|
|
1289
|
+
}
|
|
1290
|
+
const name = expression.slice(start, i);
|
|
1291
|
+
const prevNonSpace = findPreviousNonSpace2(expression, start - 1);
|
|
1292
|
+
if (name === "props" && prevNonSpace !== ".") {
|
|
1293
|
+
const namespace = readNamespaceAccess(expression, i);
|
|
1294
|
+
if (namespace) {
|
|
1295
|
+
occurrences.push({
|
|
1296
|
+
name: namespace.name,
|
|
1297
|
+
kind: "namespace",
|
|
1298
|
+
index: namespace.index,
|
|
1299
|
+
length: namespace.name.length
|
|
1300
|
+
});
|
|
1301
|
+
i = namespace.endIndex;
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (prevNonSpace !== ".") {
|
|
1306
|
+
occurrences.push({ name, kind: "local", index: start, length: name.length });
|
|
1307
|
+
}
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
i++;
|
|
644
1311
|
continue;
|
|
645
1312
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1313
|
+
if (state === "line") {
|
|
1314
|
+
if (ch === "\n") {
|
|
1315
|
+
state = "code";
|
|
1316
|
+
}
|
|
1317
|
+
i++;
|
|
1318
|
+
continue;
|
|
649
1319
|
}
|
|
650
|
-
if (
|
|
651
|
-
|
|
1320
|
+
if (state === "block") {
|
|
1321
|
+
if (ch === "*" && expression[i + 1] === "/") {
|
|
1322
|
+
state = "code";
|
|
1323
|
+
i += 2;
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
i++;
|
|
1327
|
+
continue;
|
|
652
1328
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
"
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
lineOffset
|
|
664
|
-
);
|
|
665
|
-
level = top.level + 1;
|
|
1329
|
+
if (state === "single") {
|
|
1330
|
+
if (ch === "\\") {
|
|
1331
|
+
i += 2;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (ch === "'") {
|
|
1335
|
+
state = "code";
|
|
1336
|
+
}
|
|
1337
|
+
i++;
|
|
1338
|
+
continue;
|
|
666
1339
|
}
|
|
667
|
-
|
|
668
|
-
|
|
1340
|
+
if (state === "double") {
|
|
1341
|
+
if (ch === "\\") {
|
|
1342
|
+
i += 2;
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
if (ch === '"') {
|
|
1346
|
+
state = "code";
|
|
1347
|
+
}
|
|
1348
|
+
i++;
|
|
1349
|
+
continue;
|
|
669
1350
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1351
|
+
if (state === "template") {
|
|
1352
|
+
if (ch === "\\") {
|
|
1353
|
+
i += 2;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (ch === "`") {
|
|
1357
|
+
state = "code";
|
|
1358
|
+
i++;
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
i++;
|
|
1362
|
+
continue;
|
|
675
1363
|
}
|
|
676
|
-
|
|
677
|
-
|
|
1364
|
+
}
|
|
1365
|
+
return occurrences;
|
|
1366
|
+
}
|
|
1367
|
+
function readNamespaceAccess(expression, startIndex) {
|
|
1368
|
+
let i = startIndex;
|
|
1369
|
+
while (i < expression.length && /\s/.test(expression[i])) {
|
|
1370
|
+
i++;
|
|
1371
|
+
}
|
|
1372
|
+
if (expression[i] === "?") {
|
|
1373
|
+
if (expression[i + 1] !== ".") {
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
i += 2;
|
|
1377
|
+
} else if (expression[i] === ".") {
|
|
1378
|
+
i++;
|
|
1379
|
+
} else {
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
while (i < expression.length && /\s/.test(expression[i])) {
|
|
1383
|
+
i++;
|
|
1384
|
+
}
|
|
1385
|
+
if (!isIdentifierStart2(expression[i])) {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
const propStart = i;
|
|
1389
|
+
i++;
|
|
1390
|
+
while (i < expression.length && isIdentifierPart2(expression[i])) {
|
|
1391
|
+
i++;
|
|
1392
|
+
}
|
|
1393
|
+
return {
|
|
1394
|
+
name: expression.slice(propStart, i),
|
|
1395
|
+
index: propStart,
|
|
1396
|
+
endIndex: i
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function findPreviousNonSpace2(text, index) {
|
|
1400
|
+
for (let i = index; i >= 0; i--) {
|
|
1401
|
+
const ch = text[i];
|
|
1402
|
+
if (!/\s/.test(ch)) {
|
|
1403
|
+
return ch;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
function isIdentifierStart2(ch) {
|
|
1409
|
+
return /[A-Za-z_$]/.test(ch);
|
|
1410
|
+
}
|
|
1411
|
+
function isIdentifierPart2(ch) {
|
|
1412
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
1413
|
+
}
|
|
1414
|
+
function shouldIgnoreIdentifier2(name) {
|
|
1415
|
+
return IGNORED_IDENTIFIERS2.has(name) || RESERVED_KEYWORDS2.has(name);
|
|
1416
|
+
}
|
|
1417
|
+
function levelToSeverity2(level) {
|
|
1418
|
+
if (level === "off") {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
if (level === "error") {
|
|
1422
|
+
return "error";
|
|
1423
|
+
}
|
|
1424
|
+
return "warning";
|
|
1425
|
+
}
|
|
1426
|
+
function offsetSpan(base, index, length) {
|
|
1427
|
+
const startOffset = base.start.offset + index;
|
|
1428
|
+
const startCol = base.start.col + index;
|
|
1429
|
+
return {
|
|
1430
|
+
start: {
|
|
1431
|
+
line: base.start.line,
|
|
1432
|
+
col: startCol,
|
|
1433
|
+
offset: startOffset
|
|
1434
|
+
},
|
|
1435
|
+
end: {
|
|
1436
|
+
line: base.start.line,
|
|
1437
|
+
col: startCol + length,
|
|
1438
|
+
offset: startOffset + length
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// src/parser.ts
|
|
1444
|
+
var TEMPLATE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
1445
|
+
function getIndentLevel(line) {
|
|
1446
|
+
const match = line.match(/^\s*/);
|
|
1447
|
+
return match ? match[0].length / 2 : 0;
|
|
1448
|
+
}
|
|
1449
|
+
function getIdValueSpan(lineContent, indent, lineNumber, lineOffset, tokenLength, valueLength) {
|
|
1450
|
+
if (valueLength <= 0) {
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
let cursor = tokenLength;
|
|
1454
|
+
while (cursor < lineContent.length && /\s/.test(lineContent[cursor])) {
|
|
1455
|
+
cursor++;
|
|
1456
|
+
}
|
|
1457
|
+
if (lineContent[cursor] === ":" || lineContent[cursor] === "=") {
|
|
1458
|
+
cursor++;
|
|
1459
|
+
while (cursor < lineContent.length && /\s/.test(lineContent[cursor])) {
|
|
1460
|
+
cursor++;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const column = indent + cursor + 1;
|
|
1464
|
+
return createSpan(lineNumber, column, valueLength, lineOffset);
|
|
1465
|
+
}
|
|
1466
|
+
function parse(source, options = {}) {
|
|
1467
|
+
const diagnostics = [];
|
|
1468
|
+
const templates = [];
|
|
1469
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
1470
|
+
const lines = normalized.split("\n");
|
|
1471
|
+
const lineOffsets = buildLineOffsets(lines);
|
|
1472
|
+
let currentHeader = null;
|
|
1473
|
+
let sawIdBlock = false;
|
|
1474
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
1475
|
+
const finalizeTemplate = (endIndex) => {
|
|
1476
|
+
if (!currentHeader) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const result = parseTemplateBlock(lines, lineOffsets, currentHeader.bodyStartIndex, endIndex, options);
|
|
1480
|
+
const prefixedDiagnostics = prefixDiagnostics(result.diagnostics, currentHeader.id);
|
|
1481
|
+
const unit = {
|
|
1482
|
+
id: currentHeader.id,
|
|
1483
|
+
rawId: currentHeader.rawId,
|
|
1484
|
+
span: currentHeader.span,
|
|
1485
|
+
ast: result.root,
|
|
1486
|
+
diagnostics: prefixedDiagnostics
|
|
1487
|
+
};
|
|
1488
|
+
templates.push(unit);
|
|
1489
|
+
diagnostics.push(...prefixedDiagnostics);
|
|
1490
|
+
currentHeader = null;
|
|
1491
|
+
};
|
|
1492
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1493
|
+
const rawLine = lines[i];
|
|
1494
|
+
const lineNumber = i + 1;
|
|
1495
|
+
const lineOffset = lineOffsets[i];
|
|
1496
|
+
if (/^\s*$/.test(rawLine)) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const indentMatch = rawLine.match(/^\s*/) ?? [""];
|
|
1500
|
+
const indent = indentMatch[0].length;
|
|
1501
|
+
const lineContent = rawLine.slice(indent);
|
|
1502
|
+
const trimmed = lineContent.trimEnd();
|
|
1503
|
+
const idMatch = trimmed.match(/^#id\b(.*)$/);
|
|
1504
|
+
if (idMatch) {
|
|
1505
|
+
if (indent !== 0) {
|
|
678
1506
|
pushDiag(
|
|
679
1507
|
diagnostics,
|
|
680
|
-
"
|
|
681
|
-
"
|
|
1508
|
+
"COLLIE701",
|
|
1509
|
+
"#id directives must appear at the top level.",
|
|
682
1510
|
lineNumber,
|
|
683
1511
|
indent + 1,
|
|
684
1512
|
lineOffset,
|
|
685
1513
|
trimmed.length
|
|
686
1514
|
);
|
|
687
|
-
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
finalizeTemplate(i);
|
|
1518
|
+
sawIdBlock = true;
|
|
1519
|
+
const remainderRaw = idMatch[1] ?? "";
|
|
1520
|
+
if (remainderRaw && !/^[\s:=]/.test(remainderRaw)) {
|
|
688
1521
|
pushDiag(
|
|
689
1522
|
diagnostics,
|
|
690
|
-
"
|
|
691
|
-
|
|
1523
|
+
"COLLIE702",
|
|
1524
|
+
'Invalid #id directive syntax. Use "#id <id>".',
|
|
692
1525
|
lineNumber,
|
|
693
1526
|
indent + 1,
|
|
694
1527
|
lineOffset,
|
|
695
1528
|
trimmed.length
|
|
696
1529
|
);
|
|
697
|
-
} else {
|
|
698
|
-
if (!root.classAliases) {
|
|
699
|
-
root.classAliases = { aliases: [] };
|
|
700
|
-
}
|
|
701
|
-
classesBlockLevel = level;
|
|
702
1530
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1531
|
+
let valuePart = remainderRaw.trim();
|
|
1532
|
+
if (valuePart.startsWith("=") || valuePart.startsWith(":")) {
|
|
1533
|
+
valuePart = valuePart.slice(1).trim();
|
|
1534
|
+
}
|
|
1535
|
+
const valueSpan = getIdValueSpan(
|
|
1536
|
+
lineContent,
|
|
1537
|
+
indent,
|
|
1538
|
+
lineNumber,
|
|
1539
|
+
lineOffset,
|
|
1540
|
+
"#id".length,
|
|
1541
|
+
valuePart.length
|
|
1542
|
+
);
|
|
1543
|
+
const valueColumn = valueSpan?.start.col ?? indent + 1;
|
|
1544
|
+
const valueLength = valueSpan ? valuePart.length : trimmed.length;
|
|
1545
|
+
if (!valuePart) {
|
|
707
1546
|
pushDiag(
|
|
708
1547
|
diagnostics,
|
|
709
|
-
"
|
|
710
|
-
"
|
|
1548
|
+
"COLLIE702",
|
|
1549
|
+
"#id directives must specify an identifier value.",
|
|
711
1550
|
lineNumber,
|
|
712
|
-
|
|
1551
|
+
valueColumn,
|
|
713
1552
|
lineOffset,
|
|
714
|
-
|
|
1553
|
+
valueLength
|
|
715
1554
|
);
|
|
716
|
-
} else if (
|
|
1555
|
+
} else if (!TEMPLATE_ID_PATTERN.test(valuePart)) {
|
|
717
1556
|
pushDiag(
|
|
718
1557
|
diagnostics,
|
|
719
|
-
"
|
|
720
|
-
|
|
1558
|
+
"COLLIE702",
|
|
1559
|
+
'Invalid #id value. IDs must match "^[A-Za-z][A-Za-z0-9._-]*$".',
|
|
721
1560
|
lineNumber,
|
|
722
|
-
|
|
1561
|
+
valueColumn,
|
|
1562
|
+
lineOffset,
|
|
1563
|
+
valueLength
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
if (valuePart && TEMPLATE_ID_PATTERN.test(valuePart)) {
|
|
1567
|
+
const previous = seenIds.get(valuePart);
|
|
1568
|
+
if (previous) {
|
|
1569
|
+
const previousLine = previous.start.line;
|
|
1570
|
+
pushDiag(
|
|
1571
|
+
diagnostics,
|
|
1572
|
+
"COLLIE703",
|
|
1573
|
+
`Duplicate #id "${valuePart}" (first declared on line ${previousLine}).`,
|
|
1574
|
+
lineNumber,
|
|
1575
|
+
valueColumn,
|
|
1576
|
+
lineOffset,
|
|
1577
|
+
valueLength
|
|
1578
|
+
);
|
|
1579
|
+
} else {
|
|
1580
|
+
seenIds.set(valuePart, valueSpan);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
currentHeader = {
|
|
1584
|
+
id: valuePart,
|
|
1585
|
+
rawId: valuePart,
|
|
1586
|
+
span: valueSpan,
|
|
1587
|
+
bodyStartIndex: i + 1
|
|
1588
|
+
};
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
if (!sawIdBlock && !currentHeader) {
|
|
1592
|
+
pushDiag(
|
|
1593
|
+
diagnostics,
|
|
1594
|
+
"COLLIE701",
|
|
1595
|
+
"Content before the first #id block is not allowed.",
|
|
1596
|
+
lineNumber,
|
|
1597
|
+
indent + 1,
|
|
1598
|
+
lineOffset,
|
|
1599
|
+
trimmed.length
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
finalizeTemplate(lines.length);
|
|
1604
|
+
if (!sawIdBlock) {
|
|
1605
|
+
pushDiag(
|
|
1606
|
+
diagnostics,
|
|
1607
|
+
"COLLIE701",
|
|
1608
|
+
"A .collie file must contain at least one #id block.",
|
|
1609
|
+
1,
|
|
1610
|
+
1,
|
|
1611
|
+
0
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
return { templates, diagnostics };
|
|
1615
|
+
}
|
|
1616
|
+
function parseTemplateBlock(lines, lineOffsets, startIndex, endIndex, options) {
|
|
1617
|
+
const diagnostics = [];
|
|
1618
|
+
const root = { type: "Root", children: [] };
|
|
1619
|
+
const stack = [{ node: root, level: -1 }];
|
|
1620
|
+
let propsBlockLevel = null;
|
|
1621
|
+
let classesBlockLevel = null;
|
|
1622
|
+
let sawTopLevelTemplateNode = false;
|
|
1623
|
+
const conditionalChains = /* @__PURE__ */ new Map();
|
|
1624
|
+
const branchLocations = [];
|
|
1625
|
+
let i = startIndex;
|
|
1626
|
+
while (i < endIndex) {
|
|
1627
|
+
const rawLine = lines[i];
|
|
1628
|
+
const lineNumber = i + 1;
|
|
1629
|
+
const lineOffset = lineOffsets[i];
|
|
1630
|
+
i++;
|
|
1631
|
+
if (/^\s*$/.test(rawLine)) {
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
const tabIndex = rawLine.indexOf(" ");
|
|
1635
|
+
if (tabIndex !== -1) {
|
|
1636
|
+
pushDiag(
|
|
1637
|
+
diagnostics,
|
|
1638
|
+
"COLLIE001",
|
|
1639
|
+
"Tabs are not allowed; use spaces for indentation.",
|
|
1640
|
+
lineNumber,
|
|
1641
|
+
tabIndex + 1,
|
|
1642
|
+
lineOffset
|
|
1643
|
+
);
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
const indentMatch = rawLine.match(/^\s*/) ?? [""];
|
|
1647
|
+
const indent = indentMatch[0].length;
|
|
1648
|
+
const lineContent = rawLine.slice(indent);
|
|
1649
|
+
const trimmed = lineContent.trimEnd();
|
|
1650
|
+
if (indent % 2 !== 0) {
|
|
1651
|
+
pushDiag(
|
|
1652
|
+
diagnostics,
|
|
1653
|
+
"COLLIE002",
|
|
1654
|
+
"Indentation must be multiples of two spaces.",
|
|
1655
|
+
lineNumber,
|
|
1656
|
+
indent + 1,
|
|
1657
|
+
lineOffset
|
|
1658
|
+
);
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
let level = indent / 2;
|
|
1662
|
+
if (propsBlockLevel !== null && level <= propsBlockLevel) {
|
|
1663
|
+
propsBlockLevel = null;
|
|
1664
|
+
}
|
|
1665
|
+
if (classesBlockLevel !== null && level <= classesBlockLevel) {
|
|
1666
|
+
classesBlockLevel = null;
|
|
1667
|
+
}
|
|
1668
|
+
const isInPropsBlock = propsBlockLevel !== null && level > propsBlockLevel;
|
|
1669
|
+
const isInClassesBlock = classesBlockLevel !== null && level > classesBlockLevel;
|
|
1670
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
|
1671
|
+
stack.pop();
|
|
1672
|
+
}
|
|
1673
|
+
const parentLevel = stack[stack.length - 1].level;
|
|
1674
|
+
if (level > parentLevel + 1 && !isInPropsBlock && !isInClassesBlock) {
|
|
1675
|
+
pushDiag(
|
|
1676
|
+
diagnostics,
|
|
1677
|
+
"COLLIE003",
|
|
1678
|
+
"Indentation jumped more than one level.",
|
|
1679
|
+
lineNumber,
|
|
1680
|
+
indent + 1,
|
|
1681
|
+
lineOffset
|
|
1682
|
+
);
|
|
1683
|
+
level = parentLevel + 1;
|
|
1684
|
+
}
|
|
1685
|
+
cleanupConditionalChains(conditionalChains, level);
|
|
1686
|
+
const isElseIfLine = /^@elseIf\b/.test(trimmed);
|
|
1687
|
+
const isElseLine = /^@else\b/.test(trimmed) && !isElseIfLine;
|
|
1688
|
+
if (!isElseIfLine && !isElseLine) {
|
|
1689
|
+
conditionalChains.delete(level);
|
|
1690
|
+
}
|
|
1691
|
+
if (trimmed === "classes") {
|
|
1692
|
+
if (level !== 0) {
|
|
1693
|
+
pushDiag(
|
|
1694
|
+
diagnostics,
|
|
1695
|
+
"COLLIE301",
|
|
1696
|
+
"Classes block must be at the top level.",
|
|
1697
|
+
lineNumber,
|
|
1698
|
+
indent + 1,
|
|
1699
|
+
lineOffset,
|
|
1700
|
+
trimmed.length
|
|
1701
|
+
);
|
|
1702
|
+
} else if (sawTopLevelTemplateNode) {
|
|
1703
|
+
pushDiag(
|
|
1704
|
+
diagnostics,
|
|
1705
|
+
"COLLIE302",
|
|
1706
|
+
"Classes block must appear before any template nodes.",
|
|
1707
|
+
lineNumber,
|
|
1708
|
+
indent + 1,
|
|
1709
|
+
lineOffset,
|
|
1710
|
+
trimmed.length
|
|
1711
|
+
);
|
|
1712
|
+
} else {
|
|
1713
|
+
if (!root.classAliases) {
|
|
1714
|
+
root.classAliases = { aliases: [] };
|
|
1715
|
+
}
|
|
1716
|
+
classesBlockLevel = level;
|
|
1717
|
+
}
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
if (trimmed === "props") {
|
|
1721
|
+
pushDiag(
|
|
1722
|
+
diagnostics,
|
|
1723
|
+
"COLLIE103",
|
|
1724
|
+
"`props` must be declared using `#props`.",
|
|
1725
|
+
lineNumber,
|
|
1726
|
+
indent + 1,
|
|
1727
|
+
lineOffset,
|
|
1728
|
+
trimmed.length
|
|
1729
|
+
);
|
|
1730
|
+
if (level === 0) {
|
|
1731
|
+
propsBlockLevel = level;
|
|
1732
|
+
}
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
if (trimmed === "#props") {
|
|
1736
|
+
if (level !== 0) {
|
|
1737
|
+
pushDiag(
|
|
1738
|
+
diagnostics,
|
|
1739
|
+
"COLLIE102",
|
|
1740
|
+
"#props block must be at the top level.",
|
|
1741
|
+
lineNumber,
|
|
1742
|
+
indent + 1,
|
|
1743
|
+
lineOffset,
|
|
1744
|
+
trimmed.length
|
|
1745
|
+
);
|
|
1746
|
+
} else if (root.props) {
|
|
1747
|
+
pushDiag(
|
|
1748
|
+
diagnostics,
|
|
1749
|
+
"COLLIE101",
|
|
1750
|
+
"Only one #props block is allowed per #id.",
|
|
1751
|
+
lineNumber,
|
|
1752
|
+
indent + 1,
|
|
723
1753
|
lineOffset,
|
|
724
1754
|
trimmed.length
|
|
725
1755
|
);
|
|
726
1756
|
} else {
|
|
727
1757
|
root.props = { fields: [] };
|
|
1758
|
+
}
|
|
1759
|
+
if (level === 0) {
|
|
728
1760
|
propsBlockLevel = level;
|
|
729
1761
|
}
|
|
730
1762
|
continue;
|
|
@@ -770,17 +1802,13 @@ function parse(source) {
|
|
|
770
1802
|
pushDiag(
|
|
771
1803
|
diagnostics,
|
|
772
1804
|
"COLLIE102",
|
|
773
|
-
"
|
|
1805
|
+
"#props lines must be indented two spaces under the #props header.",
|
|
774
1806
|
lineNumber,
|
|
775
1807
|
indent + 1,
|
|
776
1808
|
lineOffset
|
|
777
1809
|
);
|
|
778
1810
|
continue;
|
|
779
1811
|
}
|
|
780
|
-
const field = parsePropsField(trimmed, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
781
|
-
if (field && root.props) {
|
|
782
|
-
root.props.fields.push(field);
|
|
783
|
-
}
|
|
784
1812
|
continue;
|
|
785
1813
|
}
|
|
786
1814
|
if (classesBlockLevel !== null && level > classesBlockLevel) {
|
|
@@ -817,7 +1845,10 @@ function parse(source) {
|
|
|
817
1845
|
type: "For",
|
|
818
1846
|
itemName: forHeader.itemName,
|
|
819
1847
|
arrayExpr: forHeader.arrayExpr,
|
|
820
|
-
body: []
|
|
1848
|
+
body: [],
|
|
1849
|
+
token: forHeader.token,
|
|
1850
|
+
tokenSpan: forHeader.tokenSpan,
|
|
1851
|
+
arrayExprSpan: forHeader.arrayExprSpan
|
|
821
1852
|
};
|
|
822
1853
|
addChildToParent(parent, forNode);
|
|
823
1854
|
if (parent === root) {
|
|
@@ -839,7 +1870,14 @@ function parse(source) {
|
|
|
839
1870
|
continue;
|
|
840
1871
|
}
|
|
841
1872
|
const chain = { type: "Conditional", branches: [] };
|
|
842
|
-
const branch = {
|
|
1873
|
+
const branch = {
|
|
1874
|
+
kind: "if",
|
|
1875
|
+
test: header.test,
|
|
1876
|
+
body: [],
|
|
1877
|
+
token: header.token,
|
|
1878
|
+
tokenSpan: header.tokenSpan,
|
|
1879
|
+
testSpan: header.testSpan
|
|
1880
|
+
};
|
|
843
1881
|
chain.branches.push(branch);
|
|
844
1882
|
addChildToParent(parent, chain);
|
|
845
1883
|
if (parent === root) {
|
|
@@ -906,7 +1944,14 @@ function parse(source) {
|
|
|
906
1944
|
if (!header) {
|
|
907
1945
|
continue;
|
|
908
1946
|
}
|
|
909
|
-
const branch = {
|
|
1947
|
+
const branch = {
|
|
1948
|
+
kind: "elseIf",
|
|
1949
|
+
test: header.test,
|
|
1950
|
+
body: [],
|
|
1951
|
+
token: header.token,
|
|
1952
|
+
tokenSpan: header.tokenSpan,
|
|
1953
|
+
testSpan: header.testSpan
|
|
1954
|
+
};
|
|
910
1955
|
chain.node.branches.push(branch);
|
|
911
1956
|
branchLocations.push({
|
|
912
1957
|
branch,
|
|
@@ -961,7 +2006,13 @@ function parse(source) {
|
|
|
961
2006
|
if (!header) {
|
|
962
2007
|
continue;
|
|
963
2008
|
}
|
|
964
|
-
const branch = {
|
|
2009
|
+
const branch = {
|
|
2010
|
+
kind: "else",
|
|
2011
|
+
test: void 0,
|
|
2012
|
+
body: [],
|
|
2013
|
+
token: header.token,
|
|
2014
|
+
tokenSpan: header.tokenSpan
|
|
2015
|
+
};
|
|
965
2016
|
chain.node.branches.push(branch);
|
|
966
2017
|
chain.hasElse = true;
|
|
967
2018
|
branchLocations.push({
|
|
@@ -1044,10 +2095,9 @@ function parse(source) {
|
|
|
1044
2095
|
}
|
|
1045
2096
|
if (lineContent.startsWith("=")) {
|
|
1046
2097
|
const payload = lineContent.slice(1).trim();
|
|
1047
|
-
if (payload.endsWith("(") || payload.endsWith("<") || i <
|
|
2098
|
+
if (payload.endsWith("(") || payload.endsWith("<") || i < endIndex && level < getIndentLevel(lines[i])) {
|
|
1048
2099
|
let jsxContent = payload;
|
|
1049
|
-
|
|
1050
|
-
while (i < lines.length) {
|
|
2100
|
+
while (i < endIndex) {
|
|
1051
2101
|
const nextRaw = lines[i];
|
|
1052
2102
|
const nextIndent = getIndentLevel(nextRaw);
|
|
1053
2103
|
const nextTrimmed = nextRaw.trim();
|
|
@@ -1105,7 +2155,7 @@ function parse(source) {
|
|
|
1105
2155
|
let multilineEnd = i;
|
|
1106
2156
|
if (trimmed.includes("(") && !trimmed.includes(")")) {
|
|
1107
2157
|
let parenDepth = (trimmed.match(/\(/g) || []).length - (trimmed.match(/\)/g) || []).length;
|
|
1108
|
-
while (multilineEnd <
|
|
2158
|
+
while (multilineEnd < endIndex && parenDepth > 0) {
|
|
1109
2159
|
const nextRaw = lines[multilineEnd];
|
|
1110
2160
|
multilineEnd++;
|
|
1111
2161
|
fullLine += "\n" + nextRaw;
|
|
@@ -1113,8 +2163,8 @@ function parse(source) {
|
|
|
1113
2163
|
}
|
|
1114
2164
|
i = multilineEnd;
|
|
1115
2165
|
}
|
|
1116
|
-
const
|
|
1117
|
-
if (!
|
|
2166
|
+
const elementResult = parseElementWithInfo(fullLine, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
2167
|
+
if (!elementResult) {
|
|
1118
2168
|
const textNode = parseTextPayload(trimmed, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
1119
2169
|
if (textNode && textNode.parts.length > 0) {
|
|
1120
2170
|
addChildToParent(parent, textNode);
|
|
@@ -1124,11 +2174,31 @@ function parse(source) {
|
|
|
1124
2174
|
}
|
|
1125
2175
|
continue;
|
|
1126
2176
|
}
|
|
2177
|
+
const element = elementResult.node;
|
|
2178
|
+
let hasIndentedAttributeLines = false;
|
|
2179
|
+
if ((element.type === "Element" || element.type === "Component") && element.children.length === 0) {
|
|
2180
|
+
const indentedAttributes = collectIndentedAttributeLines(
|
|
2181
|
+
lines,
|
|
2182
|
+
lineOffsets,
|
|
2183
|
+
i,
|
|
2184
|
+
endIndex,
|
|
2185
|
+
level,
|
|
2186
|
+
diagnostics
|
|
2187
|
+
);
|
|
2188
|
+
if (indentedAttributes.attributes.length > 0) {
|
|
2189
|
+
element.attributes.push(...indentedAttributes.attributes);
|
|
2190
|
+
hasIndentedAttributeLines = true;
|
|
2191
|
+
}
|
|
2192
|
+
i = indentedAttributes.nextIndex;
|
|
2193
|
+
}
|
|
1127
2194
|
addChildToParent(parent, element);
|
|
1128
2195
|
if (parent === root) {
|
|
1129
2196
|
sawTopLevelTemplateNode = true;
|
|
1130
2197
|
}
|
|
1131
2198
|
stack.push({ node: element, level });
|
|
2199
|
+
if (hasIndentedAttributeLines) {
|
|
2200
|
+
stack.push({ node: element, level: level + 1 });
|
|
2201
|
+
}
|
|
1132
2202
|
}
|
|
1133
2203
|
if (root.classAliases) {
|
|
1134
2204
|
validateClassAliasDefinitions(root.classAliases, diagnostics);
|
|
@@ -1147,8 +2217,33 @@ function parse(source) {
|
|
|
1147
2217
|
);
|
|
1148
2218
|
}
|
|
1149
2219
|
}
|
|
2220
|
+
if (options.dialect) {
|
|
2221
|
+
diagnostics.push(...enforceDialect(root, options.dialect));
|
|
2222
|
+
diagnostics.push(...enforceProps(root, options.dialect.props));
|
|
2223
|
+
}
|
|
1150
2224
|
return { root, diagnostics };
|
|
1151
2225
|
}
|
|
2226
|
+
function buildLineOffsets(lines) {
|
|
2227
|
+
const offsets = [];
|
|
2228
|
+
let offset = 0;
|
|
2229
|
+
for (const line of lines) {
|
|
2230
|
+
offsets.push(offset);
|
|
2231
|
+
offset += line.length + 1;
|
|
2232
|
+
}
|
|
2233
|
+
return offsets;
|
|
2234
|
+
}
|
|
2235
|
+
function prefixDiagnostics(diagnostics, templateId) {
|
|
2236
|
+
if (!templateId) {
|
|
2237
|
+
return diagnostics;
|
|
2238
|
+
}
|
|
2239
|
+
const prefix = `In template "${templateId}": `;
|
|
2240
|
+
return diagnostics.map((diag) => {
|
|
2241
|
+
if (diag.message.startsWith(prefix)) {
|
|
2242
|
+
return diag;
|
|
2243
|
+
}
|
|
2244
|
+
return { ...diag, message: `${prefix}${diag.message}` };
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
1152
2247
|
function cleanupConditionalChains(state, level) {
|
|
1153
2248
|
for (const key of Array.from(state.keys())) {
|
|
1154
2249
|
if (key > level) {
|
|
@@ -1171,21 +2266,59 @@ function isComponentNode(parent) {
|
|
|
1171
2266
|
}
|
|
1172
2267
|
function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
1173
2268
|
const trimmed = lineContent.trimEnd();
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
if (!match) {
|
|
2269
|
+
const token = kind === "if" ? "@if" : "@elseIf";
|
|
2270
|
+
if (!trimmed.startsWith(token)) {
|
|
1177
2271
|
pushDiag(
|
|
1178
2272
|
diagnostics,
|
|
1179
2273
|
"COLLIE201",
|
|
1180
|
-
kind === "if" ? "Invalid @if syntax. Use @if
|
|
2274
|
+
kind === "if" ? "Invalid @if syntax. Use @if <condition>." : "Invalid @elseIf syntax. Use @elseIf <condition>.",
|
|
1181
2275
|
lineNumber,
|
|
1182
2276
|
column,
|
|
1183
2277
|
lineOffset,
|
|
1184
|
-
trimmed.length ||
|
|
2278
|
+
trimmed.length || token.length
|
|
2279
|
+
);
|
|
2280
|
+
return null;
|
|
2281
|
+
}
|
|
2282
|
+
const tokenSpan = createSpan(lineNumber, column, token.length, lineOffset);
|
|
2283
|
+
const remainder = trimmed.slice(token.length);
|
|
2284
|
+
if (!remainder.trim()) {
|
|
2285
|
+
pushDiag(
|
|
2286
|
+
diagnostics,
|
|
2287
|
+
"COLLIE201",
|
|
2288
|
+
kind === "if" ? "@if condition cannot be empty." : "@elseIf condition cannot be empty.",
|
|
2289
|
+
lineNumber,
|
|
2290
|
+
column,
|
|
2291
|
+
lineOffset,
|
|
2292
|
+
trimmed.length || token.length
|
|
1185
2293
|
);
|
|
1186
2294
|
return null;
|
|
1187
2295
|
}
|
|
1188
|
-
const
|
|
2296
|
+
const remainderTrimmed = remainder.trimStart();
|
|
2297
|
+
const usesParens = remainderTrimmed.startsWith("(");
|
|
2298
|
+
let testRaw = "";
|
|
2299
|
+
let remainderRaw = "";
|
|
2300
|
+
if (usesParens) {
|
|
2301
|
+
const openIndex = trimmed.indexOf("(", token.length);
|
|
2302
|
+
const closeIndex = trimmed.lastIndexOf(")");
|
|
2303
|
+
if (openIndex === -1 || closeIndex <= openIndex) {
|
|
2304
|
+
pushDiag(
|
|
2305
|
+
diagnostics,
|
|
2306
|
+
"COLLIE201",
|
|
2307
|
+
kind === "if" ? "Invalid @if syntax. Use @if <condition>." : "Invalid @elseIf syntax. Use @elseIf <condition>.",
|
|
2308
|
+
lineNumber,
|
|
2309
|
+
column,
|
|
2310
|
+
lineOffset,
|
|
2311
|
+
trimmed.length || token.length
|
|
2312
|
+
);
|
|
2313
|
+
return null;
|
|
2314
|
+
}
|
|
2315
|
+
testRaw = trimmed.slice(openIndex + 1, closeIndex);
|
|
2316
|
+
remainderRaw = trimmed.slice(closeIndex + 1);
|
|
2317
|
+
} else {
|
|
2318
|
+
testRaw = remainderTrimmed;
|
|
2319
|
+
remainderRaw = "";
|
|
2320
|
+
}
|
|
2321
|
+
const test = testRaw.trim();
|
|
1189
2322
|
if (!test) {
|
|
1190
2323
|
pushDiag(
|
|
1191
2324
|
diagnostics,
|
|
@@ -1198,7 +2331,9 @@ function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffse
|
|
|
1198
2331
|
);
|
|
1199
2332
|
return null;
|
|
1200
2333
|
}
|
|
1201
|
-
const
|
|
2334
|
+
const testLeadingWhitespace = testRaw.length - testRaw.trimStart().length;
|
|
2335
|
+
const testColumn = usesParens ? column + trimmed.indexOf("(", token.length) + 1 + testLeadingWhitespace : column + token.length + (remainder.length - remainder.trimStart().length) + testLeadingWhitespace;
|
|
2336
|
+
const testSpan = createSpan(lineNumber, testColumn, test.length, lineOffset);
|
|
1202
2337
|
const inlineBody = remainderRaw.trim();
|
|
1203
2338
|
const remainderOffset = trimmed.length - remainderRaw.length;
|
|
1204
2339
|
const leadingWhitespace = remainderRaw.length - inlineBody.length;
|
|
@@ -1207,7 +2342,10 @@ function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffse
|
|
|
1207
2342
|
test,
|
|
1208
2343
|
inlineBody: inlineBody.length ? inlineBody : void 0,
|
|
1209
2344
|
inlineColumn,
|
|
1210
|
-
directiveLength: trimmed.length || 3
|
|
2345
|
+
directiveLength: trimmed.length || 3,
|
|
2346
|
+
token,
|
|
2347
|
+
tokenSpan,
|
|
2348
|
+
testSpan
|
|
1211
2349
|
};
|
|
1212
2350
|
}
|
|
1213
2351
|
function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
@@ -1225,6 +2363,8 @@ function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostic
|
|
|
1225
2363
|
);
|
|
1226
2364
|
return null;
|
|
1227
2365
|
}
|
|
2366
|
+
const token = "@else";
|
|
2367
|
+
const tokenSpan = createSpan(lineNumber, column, token.length, lineOffset);
|
|
1228
2368
|
const remainderRaw = match[1] ?? "";
|
|
1229
2369
|
const inlineBody = remainderRaw.trim();
|
|
1230
2370
|
const remainderOffset = trimmed.length - remainderRaw.length;
|
|
@@ -1233,7 +2373,9 @@ function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostic
|
|
|
1233
2373
|
return {
|
|
1234
2374
|
inlineBody: inlineBody.length ? inlineBody : void 0,
|
|
1235
2375
|
inlineColumn,
|
|
1236
|
-
directiveLength: trimmed.length || 4
|
|
2376
|
+
directiveLength: trimmed.length || 4,
|
|
2377
|
+
token,
|
|
2378
|
+
tokenSpan
|
|
1237
2379
|
};
|
|
1238
2380
|
}
|
|
1239
2381
|
function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
@@ -1251,6 +2393,8 @@ function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics
|
|
|
1251
2393
|
);
|
|
1252
2394
|
return null;
|
|
1253
2395
|
}
|
|
2396
|
+
const token = "@for";
|
|
2397
|
+
const tokenSpan = createSpan(lineNumber, column, token.length, lineOffset);
|
|
1254
2398
|
const itemName = match[1];
|
|
1255
2399
|
const arrayExprRaw = match[2];
|
|
1256
2400
|
if (!itemName || !arrayExprRaw) {
|
|
@@ -1278,7 +2422,11 @@ function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics
|
|
|
1278
2422
|
);
|
|
1279
2423
|
return null;
|
|
1280
2424
|
}
|
|
1281
|
-
|
|
2425
|
+
const arrayExprLeadingWhitespace = arrayExprRaw.length - arrayExprRaw.trimStart().length;
|
|
2426
|
+
const arrayExprStart = trimmed.length - arrayExprRaw.length;
|
|
2427
|
+
const arrayExprColumn = column + arrayExprStart + arrayExprLeadingWhitespace;
|
|
2428
|
+
const arrayExprSpan = createSpan(lineNumber, arrayExprColumn, arrayExpr.length, lineOffset);
|
|
2429
|
+
return { itemName, arrayExpr, token, tokenSpan, arrayExprSpan };
|
|
1282
2430
|
}
|
|
1283
2431
|
function parseInlineNode(source, lineNumber, column, lineOffset, diagnostics) {
|
|
1284
2432
|
const trimmed = source.trim();
|
|
@@ -1386,7 +2534,14 @@ function parseTextPayload(payload, lineNumber, payloadColumn, lineOffset, diagno
|
|
|
1386
2534
|
exprEnd2 - exprStart2
|
|
1387
2535
|
);
|
|
1388
2536
|
} else {
|
|
1389
|
-
|
|
2537
|
+
const innerRaw = payload.slice(exprStart2 + 2, exprEnd2);
|
|
2538
|
+
const leadingWhitespace = innerRaw.length - innerRaw.trimStart().length;
|
|
2539
|
+
const exprColumn = payloadColumn + exprStart2 + 2 + leadingWhitespace;
|
|
2540
|
+
parts.push({
|
|
2541
|
+
type: "expr",
|
|
2542
|
+
value: inner2,
|
|
2543
|
+
span: createSpan(lineNumber, exprColumn, inner2.length, lineOffset)
|
|
2544
|
+
});
|
|
1390
2545
|
}
|
|
1391
2546
|
cursor = exprEnd2 + 2;
|
|
1392
2547
|
continue;
|
|
@@ -1417,7 +2572,14 @@ function parseTextPayload(payload, lineNumber, payloadColumn, lineOffset, diagno
|
|
|
1417
2572
|
exprEnd - exprStart
|
|
1418
2573
|
);
|
|
1419
2574
|
} else {
|
|
1420
|
-
|
|
2575
|
+
const innerRaw = payload.slice(exprStart + 1, exprEnd);
|
|
2576
|
+
const leadingWhitespace = innerRaw.length - innerRaw.trimStart().length;
|
|
2577
|
+
const exprColumn = payloadColumn + exprStart + 1 + leadingWhitespace;
|
|
2578
|
+
parts.push({
|
|
2579
|
+
type: "expr",
|
|
2580
|
+
value: inner,
|
|
2581
|
+
span: createSpan(lineNumber, exprColumn, inner.length, lineOffset)
|
|
2582
|
+
});
|
|
1421
2583
|
}
|
|
1422
2584
|
cursor = exprEnd + 1;
|
|
1423
2585
|
continue;
|
|
@@ -1492,474 +2654,1534 @@ function parseExpressionLine(line, lineNumber, column, lineOffset, diagnostics)
|
|
|
1492
2654
|
);
|
|
1493
2655
|
return null;
|
|
1494
2656
|
}
|
|
1495
|
-
|
|
2657
|
+
const innerRaw = trimmed.slice(2, closeIndex);
|
|
2658
|
+
const leadingWhitespace = innerRaw.length - innerRaw.trimStart().length;
|
|
2659
|
+
const exprColumn = column + 2 + leadingWhitespace;
|
|
2660
|
+
return {
|
|
2661
|
+
type: "Expression",
|
|
2662
|
+
value: inner,
|
|
2663
|
+
span: createSpan(lineNumber, exprColumn, inner.length, lineOffset)
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
function parseJSXPassthrough(line, lineNumber, column, lineOffset, diagnostics) {
|
|
2667
|
+
if (!line.startsWith("=")) {
|
|
2668
|
+
return null;
|
|
2669
|
+
}
|
|
2670
|
+
const payload = line.slice(1).trim();
|
|
2671
|
+
if (!payload) {
|
|
2672
|
+
pushDiag(
|
|
2673
|
+
diagnostics,
|
|
2674
|
+
"COLLIE005",
|
|
2675
|
+
"JSX passthrough expression cannot be empty.",
|
|
2676
|
+
lineNumber,
|
|
2677
|
+
column,
|
|
2678
|
+
lineOffset
|
|
2679
|
+
);
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2682
|
+
const rawPayload = line.slice(1);
|
|
2683
|
+
const leadingWhitespace = rawPayload.length - rawPayload.trimStart().length;
|
|
2684
|
+
const exprColumn = column + 1 + leadingWhitespace;
|
|
2685
|
+
return {
|
|
2686
|
+
type: "JSXPassthrough",
|
|
2687
|
+
expression: payload,
|
|
2688
|
+
span: createSpan(lineNumber, exprColumn, payload.length, lineOffset)
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
function parseClassAliasLine(line, lineNumber, column, lineOffset, diagnostics) {
|
|
2692
|
+
const match = line.match(/^([^=]+?)\s*=\s*(.+)$/);
|
|
2693
|
+
if (!match) {
|
|
2694
|
+
pushDiag(
|
|
2695
|
+
diagnostics,
|
|
2696
|
+
"COLLIE304",
|
|
2697
|
+
"Classes lines must be in the form `name = class.tokens`.",
|
|
2698
|
+
lineNumber,
|
|
2699
|
+
column,
|
|
2700
|
+
lineOffset,
|
|
2701
|
+
Math.max(line.length, 1)
|
|
2702
|
+
);
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
const rawName = match[1].trim();
|
|
2706
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(rawName)) {
|
|
2707
|
+
pushDiag(
|
|
2708
|
+
diagnostics,
|
|
2709
|
+
"COLLIE305",
|
|
2710
|
+
`Class alias name '${rawName}' must be a valid identifier.`,
|
|
2711
|
+
lineNumber,
|
|
2712
|
+
column,
|
|
2713
|
+
lineOffset,
|
|
2714
|
+
Math.max(rawName.length, 1)
|
|
2715
|
+
);
|
|
2716
|
+
return null;
|
|
2717
|
+
}
|
|
2718
|
+
const rhs = match[2];
|
|
2719
|
+
const rhsIndex = line.indexOf(rhs);
|
|
2720
|
+
const rhsColumn = rhsIndex >= 0 ? column + rhsIndex : column;
|
|
2721
|
+
const classes = parseAliasClasses(rhs, lineNumber, rhsColumn, lineOffset, diagnostics);
|
|
2722
|
+
if (!classes.length) {
|
|
2723
|
+
return null;
|
|
2724
|
+
}
|
|
2725
|
+
const nameIndex = line.indexOf(rawName);
|
|
2726
|
+
const nameColumn = nameIndex >= 0 ? column + nameIndex : column;
|
|
2727
|
+
const span = createSpan(lineNumber, nameColumn, rawName.length, lineOffset);
|
|
2728
|
+
return { name: rawName, classes, span };
|
|
2729
|
+
}
|
|
2730
|
+
function parseAliasClasses(rhs, lineNumber, column, lineOffset, diagnostics) {
|
|
2731
|
+
const trimmed = rhs.trim();
|
|
2732
|
+
if (!trimmed) {
|
|
2733
|
+
pushDiag(
|
|
2734
|
+
diagnostics,
|
|
2735
|
+
"COLLIE304",
|
|
2736
|
+
"Classes lines must provide one or more class tokens after '='.",
|
|
2737
|
+
lineNumber,
|
|
2738
|
+
column,
|
|
2739
|
+
lineOffset,
|
|
2740
|
+
Math.max(rhs.length, 1)
|
|
2741
|
+
);
|
|
2742
|
+
return [];
|
|
2743
|
+
}
|
|
2744
|
+
const withoutDotPrefix = trimmed.startsWith(".") ? trimmed.slice(1) : trimmed;
|
|
2745
|
+
const parts = withoutDotPrefix.split(".");
|
|
2746
|
+
const classes = [];
|
|
2747
|
+
for (const part of parts) {
|
|
2748
|
+
const token = part.trim();
|
|
2749
|
+
if (!token) {
|
|
2750
|
+
pushDiag(
|
|
2751
|
+
diagnostics,
|
|
2752
|
+
"COLLIE304",
|
|
2753
|
+
"Classes lines must provide one or more class tokens after '='.",
|
|
2754
|
+
lineNumber,
|
|
2755
|
+
column,
|
|
2756
|
+
lineOffset,
|
|
2757
|
+
Math.max(rhs.length, 1)
|
|
2758
|
+
);
|
|
2759
|
+
return [];
|
|
2760
|
+
}
|
|
2761
|
+
classes.push(token);
|
|
2762
|
+
}
|
|
2763
|
+
return classes;
|
|
2764
|
+
}
|
|
2765
|
+
function validateClassAliasDefinitions(classAliases, diagnostics) {
|
|
2766
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2767
|
+
for (const alias of classAliases.aliases) {
|
|
2768
|
+
const previous = seen.get(alias.name);
|
|
2769
|
+
if (previous) {
|
|
2770
|
+
if (alias.span) {
|
|
2771
|
+
diagnostics.push({
|
|
2772
|
+
severity: "error",
|
|
2773
|
+
code: "COLLIE306",
|
|
2774
|
+
message: `Duplicate class alias '${alias.name}'.`,
|
|
2775
|
+
span: alias.span
|
|
2776
|
+
});
|
|
2777
|
+
} else {
|
|
2778
|
+
pushDiag(diagnostics, "COLLIE306", `Duplicate class alias '${alias.name}'.`, 1, 1, 0);
|
|
2779
|
+
}
|
|
2780
|
+
continue;
|
|
2781
|
+
}
|
|
2782
|
+
seen.set(alias.name, alias);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
function validateClassAliasUsages(root, diagnostics) {
|
|
2786
|
+
const defined = new Set(root.classAliases?.aliases.map((alias) => alias.name) ?? []);
|
|
2787
|
+
for (const child of root.children) {
|
|
2788
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
function validateNodeClassAliases(node, defined, diagnostics) {
|
|
2792
|
+
if (node.type === "Element" || node.type === "Component") {
|
|
2793
|
+
const spans = node.type === "Element" ? node.classSpans ?? [] : [];
|
|
2794
|
+
const classes = node.type === "Element" ? node.classes : [];
|
|
2795
|
+
classes.forEach((cls, index) => {
|
|
2796
|
+
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
2797
|
+
if (!match) {
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
const aliasName = match[1];
|
|
2801
|
+
if (defined.has(aliasName)) {
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const span = spans[index];
|
|
2805
|
+
if (span) {
|
|
2806
|
+
diagnostics.push({
|
|
2807
|
+
severity: "error",
|
|
2808
|
+
code: "COLLIE307",
|
|
2809
|
+
message: `Undefined class alias '${aliasName}'.`,
|
|
2810
|
+
span
|
|
2811
|
+
});
|
|
2812
|
+
} else {
|
|
2813
|
+
pushDiag(diagnostics, "COLLIE307", `Undefined class alias '${aliasName}'.`, 1, 1, 0);
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
for (const child of node.children) {
|
|
2817
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
2818
|
+
}
|
|
2819
|
+
if (node.type === "Component" && node.slots) {
|
|
2820
|
+
for (const slot of node.slots) {
|
|
2821
|
+
for (const child of slot.children) {
|
|
2822
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
if (node.type === "Conditional") {
|
|
2829
|
+
for (const branch of node.branches) {
|
|
2830
|
+
for (const child of branch.body) {
|
|
2831
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
if (node.type === "For") {
|
|
2836
|
+
for (const child of node.body) {
|
|
2837
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
function parseAttributeTokensFromStart(source, lineNumber, column, lineOffset, diagnostics) {
|
|
2842
|
+
let remaining = source;
|
|
2843
|
+
let consumed = 0;
|
|
2844
|
+
let parsedAny = false;
|
|
2845
|
+
const attributes = [];
|
|
2846
|
+
while (remaining.length > 0) {
|
|
2847
|
+
if (!/^([A-Za-z][A-Za-z0-9_-]*)\s*=/.test(remaining)) {
|
|
2848
|
+
break;
|
|
2849
|
+
}
|
|
2850
|
+
parsedAny = true;
|
|
2851
|
+
const before = remaining;
|
|
2852
|
+
const next = parseAndAddAttribute(
|
|
2853
|
+
remaining,
|
|
2854
|
+
attributes,
|
|
2855
|
+
diagnostics,
|
|
2856
|
+
lineNumber,
|
|
2857
|
+
column + consumed,
|
|
2858
|
+
lineOffset
|
|
2859
|
+
);
|
|
2860
|
+
if (next.length === before.length) {
|
|
2861
|
+
break;
|
|
2862
|
+
}
|
|
2863
|
+
consumed += before.length - next.length;
|
|
2864
|
+
remaining = next;
|
|
2865
|
+
}
|
|
2866
|
+
if (!parsedAny) {
|
|
2867
|
+
return null;
|
|
2868
|
+
}
|
|
2869
|
+
return {
|
|
2870
|
+
attributes,
|
|
2871
|
+
rest: remaining,
|
|
2872
|
+
restColumn: column + consumed
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
function parseAttributeLine(source, lineNumber, column, lineOffset, diagnostics) {
|
|
2876
|
+
const result = parseAttributeTokensFromStart(
|
|
2877
|
+
source,
|
|
2878
|
+
lineNumber,
|
|
2879
|
+
column,
|
|
2880
|
+
lineOffset,
|
|
2881
|
+
diagnostics
|
|
2882
|
+
);
|
|
2883
|
+
if (!result || result.rest.length > 0) {
|
|
2884
|
+
return null;
|
|
2885
|
+
}
|
|
2886
|
+
return result.attributes;
|
|
2887
|
+
}
|
|
2888
|
+
function parseElementWithInfo(line, lineNumber, column, lineOffset, diagnostics) {
|
|
2889
|
+
let name;
|
|
2890
|
+
let cursor = 0;
|
|
2891
|
+
let hasAttributeGroup = false;
|
|
2892
|
+
if (line[cursor] === ".") {
|
|
2893
|
+
name = "div";
|
|
2894
|
+
} else {
|
|
2895
|
+
const nameMatch = line.match(/^([A-Za-z][A-Za-z0-9_]*)/);
|
|
2896
|
+
if (!nameMatch) {
|
|
2897
|
+
return null;
|
|
2898
|
+
}
|
|
2899
|
+
name = nameMatch[1];
|
|
2900
|
+
cursor = name.length;
|
|
2901
|
+
}
|
|
2902
|
+
const nextPart = line.slice(cursor);
|
|
2903
|
+
const isComponent = /^[A-Z]/.test(name);
|
|
2904
|
+
if (isComponent && nextPart.length > 0) {
|
|
2905
|
+
const trimmedNext = nextPart.trimStart();
|
|
2906
|
+
if (trimmedNext.length > 0 && !trimmedNext.startsWith("(")) {
|
|
2907
|
+
return null;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
if (cursor < line.length) {
|
|
2911
|
+
const nextChar = line[cursor];
|
|
2912
|
+
if (nextChar !== "." && nextChar !== "(" && !/\s/.test(nextChar)) {
|
|
2913
|
+
return null;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
const classes = [];
|
|
2917
|
+
const classSpans = [];
|
|
2918
|
+
if (!isComponent) {
|
|
2919
|
+
while (cursor < line.length && line[cursor] === ".") {
|
|
2920
|
+
cursor++;
|
|
2921
|
+
const classMatch = line.slice(cursor).match(/^([A-Za-z0-9_$-]+)/);
|
|
2922
|
+
if (!classMatch) {
|
|
2923
|
+
pushDiag(
|
|
2924
|
+
diagnostics,
|
|
2925
|
+
"COLLIE004",
|
|
2926
|
+
"Class names must contain only letters, numbers, underscores, hyphens, or `$` (for aliases).",
|
|
2927
|
+
lineNumber,
|
|
2928
|
+
column + cursor,
|
|
2929
|
+
lineOffset
|
|
2930
|
+
);
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
const className = classMatch[1];
|
|
2934
|
+
classes.push(className);
|
|
2935
|
+
classSpans.push(createSpan(lineNumber, column + cursor, className.length, lineOffset));
|
|
2936
|
+
cursor += className.length;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
const attributes = [];
|
|
2940
|
+
if (cursor < line.length && line[cursor] === "(") {
|
|
2941
|
+
const attrResult = parseAttributes(line, cursor, lineNumber, column, lineOffset, diagnostics);
|
|
2942
|
+
if (!attrResult) {
|
|
2943
|
+
return null;
|
|
2944
|
+
}
|
|
2945
|
+
attributes.push(...attrResult.attributes);
|
|
2946
|
+
hasAttributeGroup = true;
|
|
2947
|
+
cursor = attrResult.endIndex;
|
|
2948
|
+
}
|
|
2949
|
+
let guard;
|
|
2950
|
+
let guardSpan;
|
|
2951
|
+
const guardProbeStart = cursor;
|
|
2952
|
+
while (cursor < line.length && /\s/.test(line[cursor])) {
|
|
2953
|
+
cursor++;
|
|
2954
|
+
}
|
|
2955
|
+
if (cursor < line.length && line[cursor] === "?") {
|
|
2956
|
+
const guardColumn = column + cursor;
|
|
2957
|
+
cursor++;
|
|
2958
|
+
const guardRaw = line.slice(cursor);
|
|
2959
|
+
const guardExpr = guardRaw.trim();
|
|
2960
|
+
if (!guardExpr) {
|
|
2961
|
+
pushDiag(
|
|
2962
|
+
diagnostics,
|
|
2963
|
+
"COLLIE601",
|
|
2964
|
+
"Guard expressions require a condition after '?'.",
|
|
2965
|
+
lineNumber,
|
|
2966
|
+
guardColumn,
|
|
2967
|
+
lineOffset
|
|
2968
|
+
);
|
|
2969
|
+
} else {
|
|
2970
|
+
guard = guardExpr;
|
|
2971
|
+
const leadingWhitespace = guardRaw.length - guardRaw.trimStart().length;
|
|
2972
|
+
const guardExprColumn = column + cursor + leadingWhitespace;
|
|
2973
|
+
guardSpan = createSpan(lineNumber, guardExprColumn, guardExpr.length, lineOffset);
|
|
2974
|
+
}
|
|
2975
|
+
cursor = line.length;
|
|
2976
|
+
} else {
|
|
2977
|
+
cursor = guardProbeStart;
|
|
2978
|
+
}
|
|
2979
|
+
const restRaw = line.slice(cursor);
|
|
2980
|
+
let rest = restRaw.trimStart();
|
|
2981
|
+
let restColumn = column + cursor + (restRaw.length - rest.length);
|
|
2982
|
+
const children = [];
|
|
2983
|
+
if (rest.length > 0) {
|
|
2984
|
+
const inlineAttrs = parseAttributeTokensFromStart(
|
|
2985
|
+
rest,
|
|
2986
|
+
lineNumber,
|
|
2987
|
+
restColumn,
|
|
2988
|
+
lineOffset,
|
|
2989
|
+
diagnostics
|
|
2990
|
+
);
|
|
2991
|
+
if (inlineAttrs) {
|
|
2992
|
+
attributes.push(...inlineAttrs.attributes);
|
|
2993
|
+
rest = inlineAttrs.rest;
|
|
2994
|
+
restColumn = inlineAttrs.restColumn;
|
|
2995
|
+
}
|
|
2996
|
+
if (rest.length > 0) {
|
|
2997
|
+
if (!rest.startsWith("|")) {
|
|
2998
|
+
pushDiag(
|
|
2999
|
+
diagnostics,
|
|
3000
|
+
"COLLIE004",
|
|
3001
|
+
"Inline text must start with '|'.",
|
|
3002
|
+
lineNumber,
|
|
3003
|
+
restColumn,
|
|
3004
|
+
lineOffset,
|
|
3005
|
+
Math.max(rest.length, 1)
|
|
3006
|
+
);
|
|
3007
|
+
} else {
|
|
3008
|
+
let payload = rest.slice(1);
|
|
3009
|
+
let payloadColumn = restColumn + 1;
|
|
3010
|
+
if (payload.startsWith(" ")) {
|
|
3011
|
+
payload = payload.slice(1);
|
|
3012
|
+
payloadColumn += 1;
|
|
3013
|
+
}
|
|
3014
|
+
const textNode = parseTextPayload(
|
|
3015
|
+
payload,
|
|
3016
|
+
lineNumber,
|
|
3017
|
+
payloadColumn,
|
|
3018
|
+
lineOffset,
|
|
3019
|
+
diagnostics
|
|
3020
|
+
);
|
|
3021
|
+
if (textNode) {
|
|
3022
|
+
children.push(textNode);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
if (isComponent) {
|
|
3028
|
+
const component = {
|
|
3029
|
+
type: "Component",
|
|
3030
|
+
name,
|
|
3031
|
+
attributes,
|
|
3032
|
+
children
|
|
3033
|
+
};
|
|
3034
|
+
if (guard) {
|
|
3035
|
+
component.guard = guard;
|
|
3036
|
+
component.guardSpan = guardSpan;
|
|
3037
|
+
}
|
|
3038
|
+
return { node: component, hasAttributeGroup };
|
|
3039
|
+
} else {
|
|
3040
|
+
const element = {
|
|
3041
|
+
type: "Element",
|
|
3042
|
+
name,
|
|
3043
|
+
classes,
|
|
3044
|
+
attributes,
|
|
3045
|
+
children
|
|
3046
|
+
};
|
|
3047
|
+
if (classSpans.length) {
|
|
3048
|
+
element.classSpans = classSpans;
|
|
3049
|
+
}
|
|
3050
|
+
if (guard) {
|
|
3051
|
+
element.guard = guard;
|
|
3052
|
+
element.guardSpan = guardSpan;
|
|
3053
|
+
}
|
|
3054
|
+
return { node: element, hasAttributeGroup };
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
function parseElement(line, lineNumber, column, lineOffset, diagnostics) {
|
|
3058
|
+
const result = parseElementWithInfo(line, lineNumber, column, lineOffset, diagnostics);
|
|
3059
|
+
return result ? result.node : null;
|
|
3060
|
+
}
|
|
3061
|
+
function collectIndentedAttributeLines(lines, lineOffsets, startIndex, endIndex, parentLevel, diagnostics) {
|
|
3062
|
+
const attributes = [];
|
|
3063
|
+
let index = startIndex;
|
|
3064
|
+
while (index < endIndex) {
|
|
3065
|
+
const rawLine = lines[index];
|
|
3066
|
+
if (/^\s*$/.test(rawLine)) {
|
|
3067
|
+
break;
|
|
3068
|
+
}
|
|
3069
|
+
if (rawLine.includes(" ")) {
|
|
3070
|
+
break;
|
|
3071
|
+
}
|
|
3072
|
+
const indentMatch = rawLine.match(/^\s*/) ?? [""];
|
|
3073
|
+
const indent = indentMatch[0].length;
|
|
3074
|
+
if (indent % 2 !== 0) {
|
|
3075
|
+
break;
|
|
3076
|
+
}
|
|
3077
|
+
const level = indent / 2;
|
|
3078
|
+
if (level !== parentLevel + 1) {
|
|
3079
|
+
break;
|
|
3080
|
+
}
|
|
3081
|
+
const lineContent = rawLine.slice(indent);
|
|
3082
|
+
const trimmed = lineContent.trimEnd();
|
|
3083
|
+
const leadingWhitespace = trimmed.length - trimmed.trimStart().length;
|
|
3084
|
+
const attrLine = trimmed.trimStart();
|
|
3085
|
+
if (!attrLine) {
|
|
3086
|
+
break;
|
|
3087
|
+
}
|
|
3088
|
+
const lineNumber = index + 1;
|
|
3089
|
+
const lineOffset = lineOffsets[index];
|
|
3090
|
+
const attrColumn = indent + 1 + leadingWhitespace;
|
|
3091
|
+
const lineAttributes = parseAttributeLine(
|
|
3092
|
+
attrLine,
|
|
3093
|
+
lineNumber,
|
|
3094
|
+
attrColumn,
|
|
3095
|
+
lineOffset,
|
|
3096
|
+
diagnostics
|
|
3097
|
+
);
|
|
3098
|
+
if (!lineAttributes) {
|
|
3099
|
+
break;
|
|
3100
|
+
}
|
|
3101
|
+
attributes.push(...lineAttributes);
|
|
3102
|
+
index++;
|
|
3103
|
+
}
|
|
3104
|
+
return { attributes, nextIndex: index };
|
|
3105
|
+
}
|
|
3106
|
+
function parseAttributes(line, startIndex, lineNumber, column, lineOffset, diagnostics) {
|
|
3107
|
+
if (line[startIndex] !== "(") {
|
|
3108
|
+
return null;
|
|
3109
|
+
}
|
|
3110
|
+
const attributes = [];
|
|
3111
|
+
let cursor = startIndex + 1;
|
|
3112
|
+
let depth = 1;
|
|
3113
|
+
let attrBuffer = "";
|
|
3114
|
+
while (cursor < line.length && depth > 0) {
|
|
3115
|
+
const ch = line[cursor];
|
|
3116
|
+
if (ch === "(") {
|
|
3117
|
+
depth++;
|
|
3118
|
+
attrBuffer += ch;
|
|
3119
|
+
} else if (ch === ")") {
|
|
3120
|
+
depth--;
|
|
3121
|
+
if (depth > 0) {
|
|
3122
|
+
attrBuffer += ch;
|
|
3123
|
+
}
|
|
3124
|
+
} else {
|
|
3125
|
+
attrBuffer += ch;
|
|
3126
|
+
}
|
|
3127
|
+
cursor++;
|
|
3128
|
+
}
|
|
3129
|
+
if (depth !== 0) {
|
|
3130
|
+
pushDiag(
|
|
3131
|
+
diagnostics,
|
|
3132
|
+
"COLLIE004",
|
|
3133
|
+
"Unclosed attribute parentheses.",
|
|
3134
|
+
lineNumber,
|
|
3135
|
+
column + startIndex,
|
|
3136
|
+
lineOffset
|
|
3137
|
+
);
|
|
3138
|
+
return null;
|
|
3139
|
+
}
|
|
3140
|
+
const trimmedAttrs = attrBuffer.trim();
|
|
3141
|
+
if (trimmedAttrs.length === 0) {
|
|
3142
|
+
return { attributes: [], endIndex: cursor };
|
|
3143
|
+
}
|
|
3144
|
+
const attrLines = trimmedAttrs.split("\n");
|
|
3145
|
+
let currentAttr = "";
|
|
3146
|
+
for (const attrLine of attrLines) {
|
|
3147
|
+
const trimmedLine = attrLine.trim();
|
|
3148
|
+
if (trimmedLine.length === 0) continue;
|
|
3149
|
+
const eqIndex = trimmedLine.indexOf("=");
|
|
3150
|
+
if (eqIndex > 0 && /^[A-Za-z][A-Za-z0-9_-]*\s*=/.test(trimmedLine)) {
|
|
3151
|
+
if (currentAttr) {
|
|
3152
|
+
let remaining = parseAndAddAttribute(currentAttr, attributes, diagnostics, lineNumber, column, lineOffset);
|
|
3153
|
+
while (remaining) {
|
|
3154
|
+
remaining = parseAndAddAttribute(remaining, attributes, diagnostics, lineNumber, column, lineOffset);
|
|
3155
|
+
}
|
|
3156
|
+
currentAttr = "";
|
|
3157
|
+
}
|
|
3158
|
+
currentAttr = trimmedLine;
|
|
3159
|
+
} else {
|
|
3160
|
+
if (currentAttr) {
|
|
3161
|
+
currentAttr += " " + trimmedLine;
|
|
3162
|
+
} else {
|
|
3163
|
+
currentAttr = trimmedLine;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
if (currentAttr) {
|
|
3168
|
+
let remaining = parseAndAddAttribute(currentAttr, attributes, diagnostics, lineNumber, column, lineOffset);
|
|
3169
|
+
while (remaining) {
|
|
3170
|
+
remaining = parseAndAddAttribute(remaining, attributes, diagnostics, lineNumber, column, lineOffset);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return { attributes, endIndex: cursor };
|
|
3174
|
+
}
|
|
3175
|
+
function scanBraceAttributeValue(source, diagnostics, lineNumber, column, lineOffset) {
|
|
3176
|
+
if (!source.startsWith("{")) {
|
|
3177
|
+
return null;
|
|
3178
|
+
}
|
|
3179
|
+
let braceDepth = 0;
|
|
3180
|
+
let parenDepth = 0;
|
|
3181
|
+
let bracketDepth = 0;
|
|
3182
|
+
let quote = null;
|
|
3183
|
+
let escaped = false;
|
|
3184
|
+
for (let i = 0; i < source.length; i++) {
|
|
3185
|
+
const char = source[i];
|
|
3186
|
+
if (quote) {
|
|
3187
|
+
if (escaped) {
|
|
3188
|
+
escaped = false;
|
|
3189
|
+
continue;
|
|
3190
|
+
}
|
|
3191
|
+
if (char === "\\") {
|
|
3192
|
+
escaped = true;
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
if (char === quote) {
|
|
3196
|
+
quote = null;
|
|
3197
|
+
}
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
3201
|
+
quote = char;
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
if (char === "{") {
|
|
3205
|
+
braceDepth++;
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
if (char === "}") {
|
|
3209
|
+
braceDepth--;
|
|
3210
|
+
if (braceDepth === 0 && parenDepth === 0 && bracketDepth === 0) {
|
|
3211
|
+
return { value: source.slice(0, i + 1), rest: source.slice(i + 1).trim() };
|
|
3212
|
+
}
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
if (char === "(") {
|
|
3216
|
+
parenDepth++;
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
if (char === ")") {
|
|
3220
|
+
if (parenDepth > 0) {
|
|
3221
|
+
parenDepth--;
|
|
3222
|
+
}
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
if (char === "[") {
|
|
3226
|
+
bracketDepth++;
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3229
|
+
if (char === "]") {
|
|
3230
|
+
if (bracketDepth > 0) {
|
|
3231
|
+
bracketDepth--;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
pushDiag(
|
|
3236
|
+
diagnostics,
|
|
3237
|
+
"COLLIE004",
|
|
3238
|
+
"Unclosed brace in attribute value.",
|
|
3239
|
+
lineNumber,
|
|
3240
|
+
column,
|
|
3241
|
+
lineOffset
|
|
3242
|
+
);
|
|
3243
|
+
return null;
|
|
3244
|
+
}
|
|
3245
|
+
function parseAndAddAttribute(attrStr, attributes, diagnostics, lineNumber, column, lineOffset) {
|
|
3246
|
+
const trimmed = attrStr.trim();
|
|
3247
|
+
const nameMatch = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*=\s*/);
|
|
3248
|
+
if (nameMatch) {
|
|
3249
|
+
const attrName = nameMatch[1];
|
|
3250
|
+
const afterEquals = trimmed.slice(nameMatch[0].length);
|
|
3251
|
+
if (afterEquals.length === 0) {
|
|
3252
|
+
pushDiag(
|
|
3253
|
+
diagnostics,
|
|
3254
|
+
"COLLIE004",
|
|
3255
|
+
`Attribute ${attrName} missing value`,
|
|
3256
|
+
lineNumber,
|
|
3257
|
+
column,
|
|
3258
|
+
lineOffset
|
|
3259
|
+
);
|
|
3260
|
+
return "";
|
|
3261
|
+
}
|
|
3262
|
+
const braceValue = scanBraceAttributeValue(afterEquals, diagnostics, lineNumber, column, lineOffset);
|
|
3263
|
+
if (braceValue) {
|
|
3264
|
+
attributes.push({ name: attrName, value: braceValue.value });
|
|
3265
|
+
return braceValue.rest;
|
|
3266
|
+
}
|
|
3267
|
+
const quoteChar = afterEquals[0];
|
|
3268
|
+
if (quoteChar === '"' || quoteChar === "'") {
|
|
3269
|
+
let i = 1;
|
|
3270
|
+
let value = "";
|
|
3271
|
+
let escaped = false;
|
|
3272
|
+
while (i < afterEquals.length) {
|
|
3273
|
+
const char = afterEquals[i];
|
|
3274
|
+
if (escaped) {
|
|
3275
|
+
value += char;
|
|
3276
|
+
escaped = false;
|
|
3277
|
+
} else if (char === "\\") {
|
|
3278
|
+
escaped = true;
|
|
3279
|
+
} else if (char === quoteChar) {
|
|
3280
|
+
attributes.push({ name: attrName, value: quoteChar + value + quoteChar });
|
|
3281
|
+
return afterEquals.slice(i + 1).trim();
|
|
3282
|
+
} else {
|
|
3283
|
+
value += char;
|
|
3284
|
+
}
|
|
3285
|
+
i++;
|
|
3286
|
+
}
|
|
3287
|
+
pushDiag(
|
|
3288
|
+
diagnostics,
|
|
3289
|
+
"COLLIE004",
|
|
3290
|
+
`Unclosed quote in attribute ${attrName}`,
|
|
3291
|
+
lineNumber,
|
|
3292
|
+
column,
|
|
3293
|
+
lineOffset
|
|
3294
|
+
);
|
|
3295
|
+
return "";
|
|
3296
|
+
} else {
|
|
3297
|
+
const unquotedMatch = afterEquals.match(/^(\S+)/);
|
|
3298
|
+
if (unquotedMatch) {
|
|
3299
|
+
attributes.push({ name: attrName, value: unquotedMatch[1] });
|
|
3300
|
+
return afterEquals.slice(unquotedMatch[1].length).trim();
|
|
3301
|
+
}
|
|
3302
|
+
return "";
|
|
3303
|
+
}
|
|
3304
|
+
} else {
|
|
3305
|
+
const boolMatch = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*)(\s+.*)?$/);
|
|
3306
|
+
if (boolMatch) {
|
|
3307
|
+
attributes.push({ name: boolMatch[1], value: null });
|
|
3308
|
+
return boolMatch[2] ? boolMatch[2].trim() : "";
|
|
3309
|
+
} else {
|
|
3310
|
+
pushDiag(
|
|
3311
|
+
diagnostics,
|
|
3312
|
+
"COLLIE004",
|
|
3313
|
+
`Invalid attribute syntax: ${trimmed.slice(0, 30)}`,
|
|
3314
|
+
lineNumber,
|
|
3315
|
+
column,
|
|
3316
|
+
lineOffset
|
|
3317
|
+
);
|
|
3318
|
+
return "";
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
function pushDiag(diagnostics, code, message, line, column, lineOffset, length = 1) {
|
|
3323
|
+
diagnostics.push({
|
|
3324
|
+
severity: "error",
|
|
3325
|
+
code,
|
|
3326
|
+
message,
|
|
3327
|
+
span: createSpan(line, column, Math.max(length, 1), lineOffset)
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// src/index.ts
|
|
3332
|
+
import {
|
|
3333
|
+
defineConfig,
|
|
3334
|
+
loadConfig,
|
|
3335
|
+
loadAndNormalizeConfig,
|
|
3336
|
+
normalizeConfig
|
|
3337
|
+
} from "@collie-lang/config";
|
|
3338
|
+
|
|
3339
|
+
// src/fixes.ts
|
|
3340
|
+
function applyFixes(sourceText, fixes) {
|
|
3341
|
+
const normalized = [];
|
|
3342
|
+
const skipped = [];
|
|
3343
|
+
for (const fix of fixes) {
|
|
3344
|
+
const offsets = getSpanOffsets(fix.range);
|
|
3345
|
+
if (!offsets) {
|
|
3346
|
+
skipped.push(fix);
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
if (offsets.start < 0 || offsets.end < offsets.start || offsets.end > sourceText.length) {
|
|
3350
|
+
skipped.push(fix);
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
normalized.push({ fix, start: offsets.start, end: offsets.end });
|
|
3354
|
+
}
|
|
3355
|
+
normalized.sort((a, b) => a.start === b.start ? a.end - b.end : a.start - b.start);
|
|
3356
|
+
const accepted = [];
|
|
3357
|
+
let currentEnd = -1;
|
|
3358
|
+
for (const item of normalized) {
|
|
3359
|
+
if (item.start < currentEnd) {
|
|
3360
|
+
skipped.push(item.fix);
|
|
3361
|
+
continue;
|
|
3362
|
+
}
|
|
3363
|
+
accepted.push(item);
|
|
3364
|
+
currentEnd = item.end;
|
|
3365
|
+
}
|
|
3366
|
+
let text = sourceText;
|
|
3367
|
+
for (let i = accepted.length - 1; i >= 0; i--) {
|
|
3368
|
+
const { start, end, fix } = accepted[i];
|
|
3369
|
+
text = `${text.slice(0, start)}${fix.replacementText}${text.slice(end)}`;
|
|
3370
|
+
}
|
|
3371
|
+
return { text, applied: accepted.map((item) => item.fix), skipped };
|
|
1496
3372
|
}
|
|
1497
|
-
function
|
|
1498
|
-
|
|
3373
|
+
function fixAllFromDiagnostics(sourceText, diagnostics) {
|
|
3374
|
+
const fixes = diagnostics.flatMap((diag) => diag.fix ? [diag.fix] : []);
|
|
3375
|
+
return applyFixes(sourceText, fixes);
|
|
3376
|
+
}
|
|
3377
|
+
function getSpanOffsets(span) {
|
|
3378
|
+
if (!span) {
|
|
1499
3379
|
return null;
|
|
1500
3380
|
}
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
diagnostics,
|
|
1505
|
-
"COLLIE005",
|
|
1506
|
-
"JSX passthrough expression cannot be empty.",
|
|
1507
|
-
lineNumber,
|
|
1508
|
-
column,
|
|
1509
|
-
lineOffset
|
|
1510
|
-
);
|
|
3381
|
+
const start = span.start?.offset;
|
|
3382
|
+
const end = span.end?.offset;
|
|
3383
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
1511
3384
|
return null;
|
|
1512
3385
|
}
|
|
1513
|
-
return {
|
|
3386
|
+
return { start, end };
|
|
1514
3387
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
);
|
|
1527
|
-
return null;
|
|
1528
|
-
}
|
|
1529
|
-
const [, name, optionalFlag, typePart] = match;
|
|
1530
|
-
const typeText = typePart.trim();
|
|
1531
|
-
if (!typeText) {
|
|
1532
|
-
pushDiag(
|
|
3388
|
+
|
|
3389
|
+
// src/format.ts
|
|
3390
|
+
function formatCollie(source, options = {}) {
|
|
3391
|
+
const indentSize = validateIndentOption(options.indent);
|
|
3392
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
3393
|
+
const parseResult = parse(normalized);
|
|
3394
|
+
const diagnostics = normalizeDiagnostics(parseResult.diagnostics);
|
|
3395
|
+
const hasErrors2 = diagnostics.some((diag) => diag.severity === "error");
|
|
3396
|
+
if (hasErrors2) {
|
|
3397
|
+
return {
|
|
3398
|
+
formatted: source,
|
|
1533
3399
|
diagnostics,
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
lineNumber,
|
|
1537
|
-
column,
|
|
1538
|
-
lineOffset,
|
|
1539
|
-
Math.max(line.length, 1)
|
|
1540
|
-
);
|
|
1541
|
-
return null;
|
|
3400
|
+
success: false
|
|
3401
|
+
};
|
|
1542
3402
|
}
|
|
3403
|
+
const serialized = serializeTemplates(parseResult.templates, indentSize);
|
|
3404
|
+
const formatted = ensureTrailingNewline(serialized);
|
|
1543
3405
|
return {
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
3406
|
+
formatted,
|
|
3407
|
+
diagnostics,
|
|
3408
|
+
success: true
|
|
1547
3409
|
};
|
|
1548
3410
|
}
|
|
1549
|
-
function
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
3411
|
+
function normalizeDiagnostics(diagnostics) {
|
|
3412
|
+
return diagnostics.map((diag) => {
|
|
3413
|
+
if (diag.range || !diag.span) {
|
|
3414
|
+
return diag;
|
|
3415
|
+
}
|
|
3416
|
+
return {
|
|
3417
|
+
...diag,
|
|
3418
|
+
range: diag.span
|
|
3419
|
+
};
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
function validateIndentOption(indent) {
|
|
3423
|
+
if (indent === void 0) {
|
|
3424
|
+
return 2;
|
|
1562
3425
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
pushDiag(
|
|
1566
|
-
diagnostics,
|
|
1567
|
-
"COLLIE305",
|
|
1568
|
-
`Class alias name '${rawName}' must be a valid identifier.`,
|
|
1569
|
-
lineNumber,
|
|
1570
|
-
column,
|
|
1571
|
-
lineOffset,
|
|
1572
|
-
Math.max(rawName.length, 1)
|
|
1573
|
-
);
|
|
1574
|
-
return null;
|
|
3426
|
+
if (!Number.isFinite(indent) || indent < 1) {
|
|
3427
|
+
throw new Error("Indent width must be a positive integer.");
|
|
1575
3428
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
3429
|
+
return Math.floor(indent);
|
|
3430
|
+
}
|
|
3431
|
+
function serializeTemplates(templates, indentSize) {
|
|
3432
|
+
const lines = [];
|
|
3433
|
+
for (const template of templates) {
|
|
3434
|
+
if (lines.length && lines[lines.length - 1] !== "") {
|
|
3435
|
+
lines.push("");
|
|
3436
|
+
}
|
|
3437
|
+
const idValue = template.rawId || template.id;
|
|
3438
|
+
lines.push(cleanLine(`#id ${idValue}`));
|
|
3439
|
+
const body = serializeRoot(template.ast, indentSize);
|
|
3440
|
+
if (body.trim().length > 0) {
|
|
3441
|
+
lines.push(...body.split("\n"));
|
|
3442
|
+
}
|
|
1582
3443
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
return
|
|
3444
|
+
while (lines.length && lines[lines.length - 1] === "") {
|
|
3445
|
+
lines.pop();
|
|
3446
|
+
}
|
|
3447
|
+
return lines.join("\n");
|
|
1587
3448
|
}
|
|
1588
|
-
function
|
|
1589
|
-
const
|
|
1590
|
-
if (
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
3449
|
+
function serializeRoot(root, indentSize) {
|
|
3450
|
+
const sections = [];
|
|
3451
|
+
if (root.classAliases && root.classAliases.aliases.length > 0) {
|
|
3452
|
+
sections.push(formatClassAliases(root.classAliases, indentSize));
|
|
3453
|
+
}
|
|
3454
|
+
if (root.props && root.props.fields.length > 0) {
|
|
3455
|
+
sections.push(formatProps(root.props, indentSize));
|
|
3456
|
+
}
|
|
3457
|
+
if (root.children.length > 0) {
|
|
3458
|
+
sections.push(formatNodes(root.children, 0, indentSize));
|
|
3459
|
+
}
|
|
3460
|
+
const lines = [];
|
|
3461
|
+
for (const section of sections) {
|
|
3462
|
+
if (!section.length) continue;
|
|
3463
|
+
if (lines.length && lines[lines.length - 1] !== "") {
|
|
3464
|
+
lines.push("");
|
|
3465
|
+
}
|
|
3466
|
+
for (const line of section) {
|
|
3467
|
+
lines.push(line);
|
|
3468
|
+
}
|
|
1601
3469
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
3470
|
+
while (lines.length && lines[lines.length - 1] === "") {
|
|
3471
|
+
lines.pop();
|
|
3472
|
+
}
|
|
3473
|
+
return lines.join("\n");
|
|
3474
|
+
}
|
|
3475
|
+
function formatClassAliases(decl, indentSize) {
|
|
3476
|
+
const indent = indentString(1, indentSize);
|
|
3477
|
+
const lines = ["classes"];
|
|
3478
|
+
for (const alias of decl.aliases) {
|
|
3479
|
+
const rhs = alias.classes.join(".");
|
|
3480
|
+
lines.push(cleanLine(`${indent}${alias.name} = ${rhs}`));
|
|
3481
|
+
}
|
|
3482
|
+
return lines;
|
|
3483
|
+
}
|
|
3484
|
+
function formatProps(props, indentSize) {
|
|
3485
|
+
const indent = indentString(1, indentSize);
|
|
3486
|
+
const lines = ["props"];
|
|
3487
|
+
for (const field of props.fields) {
|
|
3488
|
+
const optionalFlag = field.optional ? "?" : "";
|
|
3489
|
+
lines.push(cleanLine(`${indent}${field.name}${optionalFlag}: ${field.typeText.trim()}`));
|
|
3490
|
+
}
|
|
3491
|
+
return lines;
|
|
3492
|
+
}
|
|
3493
|
+
function formatNodes(nodes, level, indentSize) {
|
|
3494
|
+
const lines = [];
|
|
3495
|
+
for (const node of nodes) {
|
|
3496
|
+
lines.push(...formatNode(node, level, indentSize));
|
|
3497
|
+
}
|
|
3498
|
+
return lines;
|
|
3499
|
+
}
|
|
3500
|
+
function formatNode(node, level, indentSize) {
|
|
3501
|
+
switch (node.type) {
|
|
3502
|
+
case "Element":
|
|
3503
|
+
return formatElement(node, level, indentSize);
|
|
3504
|
+
case "Component":
|
|
3505
|
+
return formatComponent(node, level, indentSize);
|
|
3506
|
+
case "Text":
|
|
3507
|
+
return [formatTextNode(node, level, indentSize)];
|
|
3508
|
+
case "Expression":
|
|
3509
|
+
return [cleanLine(`${indentString(level, indentSize)}{{ ${node.value} }}`)];
|
|
3510
|
+
case "JSXPassthrough":
|
|
3511
|
+
return formatJsxPassthrough(node.expression, level, indentSize);
|
|
3512
|
+
case "For":
|
|
3513
|
+
return formatFor(node, level, indentSize);
|
|
3514
|
+
case "Conditional":
|
|
3515
|
+
return formatConditional(node, level, indentSize);
|
|
3516
|
+
default:
|
|
1617
3517
|
return [];
|
|
1618
|
-
}
|
|
1619
|
-
classes.push(token);
|
|
1620
3518
|
}
|
|
1621
|
-
return classes;
|
|
1622
3519
|
}
|
|
1623
|
-
function
|
|
1624
|
-
const
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
3520
|
+
function formatElement(node, level, indentSize) {
|
|
3521
|
+
const indent = indentString(level, indentSize);
|
|
3522
|
+
let line = `${indent}${node.name}${formatClassList(node.classes)}`;
|
|
3523
|
+
const attrs = formatAttributes(node.attributes);
|
|
3524
|
+
if (attrs) {
|
|
3525
|
+
line += `(${attrs})`;
|
|
3526
|
+
}
|
|
3527
|
+
const children = formatNodes(node.children, level + 1, indentSize);
|
|
3528
|
+
if (children.length === 0) {
|
|
3529
|
+
return [cleanLine(line)];
|
|
3530
|
+
}
|
|
3531
|
+
return [cleanLine(line), ...children];
|
|
3532
|
+
}
|
|
3533
|
+
function formatComponent(node, level, indentSize) {
|
|
3534
|
+
const indent = indentString(level, indentSize);
|
|
3535
|
+
let line = `${indent}${node.name}`;
|
|
3536
|
+
const attrs = formatAttributes(node.attributes);
|
|
3537
|
+
if (attrs) {
|
|
3538
|
+
line += `(${attrs})`;
|
|
3539
|
+
}
|
|
3540
|
+
const children = formatNodes(node.children, level + 1, indentSize);
|
|
3541
|
+
if (children.length === 0) {
|
|
3542
|
+
return [cleanLine(line)];
|
|
3543
|
+
}
|
|
3544
|
+
return [cleanLine(line), ...children];
|
|
3545
|
+
}
|
|
3546
|
+
function formatTextNode(node, level, indentSize) {
|
|
3547
|
+
const indent = indentString(level, indentSize);
|
|
3548
|
+
const text = renderTextParts(node.parts);
|
|
3549
|
+
return cleanLine(`${indent}| ${text}`);
|
|
3550
|
+
}
|
|
3551
|
+
function renderTextParts(parts) {
|
|
3552
|
+
return parts.map((part) => {
|
|
3553
|
+
if (part.type === "text") {
|
|
3554
|
+
return part.value;
|
|
3555
|
+
}
|
|
3556
|
+
return `{${part.value}}`;
|
|
3557
|
+
}).join("");
|
|
3558
|
+
}
|
|
3559
|
+
function formatJsxPassthrough(expression, level, indentSize) {
|
|
3560
|
+
const indent = indentString(level, indentSize);
|
|
3561
|
+
const childIndent = indentString(level + 1, indentSize);
|
|
3562
|
+
const normalized = expression.replace(/\r\n?/g, "\n").trimEnd();
|
|
3563
|
+
if (!normalized.trim()) {
|
|
3564
|
+
return [cleanLine(`${indent}= ${normalized.trim()}`)];
|
|
3565
|
+
}
|
|
3566
|
+
const lines = normalized.split("\n");
|
|
3567
|
+
const [first, ...rest] = lines;
|
|
3568
|
+
const result = [cleanLine(`${indent}= ${first.trim()}`)];
|
|
3569
|
+
if (rest.length === 0) {
|
|
3570
|
+
return result;
|
|
3571
|
+
}
|
|
3572
|
+
const dedent = computeDedent(rest);
|
|
3573
|
+
for (const raw of rest) {
|
|
3574
|
+
if (!raw.trim()) {
|
|
3575
|
+
result.push("");
|
|
1638
3576
|
continue;
|
|
1639
3577
|
}
|
|
1640
|
-
|
|
3578
|
+
const withoutIndent = raw.slice(Math.min(dedent, raw.length)).trimEnd();
|
|
3579
|
+
result.push(cleanLine(`${childIndent}${withoutIndent}`));
|
|
1641
3580
|
}
|
|
3581
|
+
return result;
|
|
1642
3582
|
}
|
|
1643
|
-
function
|
|
1644
|
-
|
|
1645
|
-
for (const
|
|
1646
|
-
|
|
3583
|
+
function computeDedent(lines) {
|
|
3584
|
+
let min = Number.POSITIVE_INFINITY;
|
|
3585
|
+
for (const line of lines) {
|
|
3586
|
+
if (!line.trim()) continue;
|
|
3587
|
+
const indentMatch = line.match(/^\s*/);
|
|
3588
|
+
const indentLength = indentMatch ? indentMatch[0].length : 0;
|
|
3589
|
+
min = Math.min(min, indentLength);
|
|
3590
|
+
}
|
|
3591
|
+
return Number.isFinite(min) ? min : 0;
|
|
3592
|
+
}
|
|
3593
|
+
function formatFor(node, level, indentSize) {
|
|
3594
|
+
const indent = indentString(level, indentSize);
|
|
3595
|
+
const header = cleanLine(`${indent}@for ${node.itemName} in ${node.arrayExpr}`);
|
|
3596
|
+
const body = formatNodes(node.body, level + 1, indentSize);
|
|
3597
|
+
return body.length ? [header, ...body] : [header];
|
|
3598
|
+
}
|
|
3599
|
+
function formatConditional(node, level, indentSize) {
|
|
3600
|
+
const indent = indentString(level, indentSize);
|
|
3601
|
+
const lines = [];
|
|
3602
|
+
node.branches.forEach((branch, index) => {
|
|
3603
|
+
let directive;
|
|
3604
|
+
if (index === 0) {
|
|
3605
|
+
directive = `@if (${branch.test ?? ""})`;
|
|
3606
|
+
} else if (branch.test) {
|
|
3607
|
+
directive = `@elseIf (${branch.test})`;
|
|
3608
|
+
} else {
|
|
3609
|
+
directive = "@else";
|
|
3610
|
+
}
|
|
3611
|
+
lines.push(cleanLine(`${indent}${directive}`));
|
|
3612
|
+
const body = formatNodes(branch.body, level + 1, indentSize);
|
|
3613
|
+
lines.push(...body);
|
|
3614
|
+
});
|
|
3615
|
+
return lines;
|
|
3616
|
+
}
|
|
3617
|
+
function formatAttributes(attributes) {
|
|
3618
|
+
if (!attributes.length) {
|
|
3619
|
+
return "";
|
|
1647
3620
|
}
|
|
3621
|
+
const sorted = [...attributes].sort((a, b) => {
|
|
3622
|
+
if (a.name === b.name) return 0;
|
|
3623
|
+
if (a.name === "class") return -1;
|
|
3624
|
+
if (b.name === "class") return 1;
|
|
3625
|
+
return a.name.localeCompare(b.name);
|
|
3626
|
+
});
|
|
3627
|
+
return sorted.map((attr) => {
|
|
3628
|
+
if (attr.value === null) {
|
|
3629
|
+
return attr.name;
|
|
3630
|
+
}
|
|
3631
|
+
return `${attr.name}=${normalizeAttributeValue(attr.value)}`;
|
|
3632
|
+
}).join(" ");
|
|
1648
3633
|
}
|
|
1649
|
-
function
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
3634
|
+
function normalizeAttributeValue(value) {
|
|
3635
|
+
const trimmed = value.trim();
|
|
3636
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("<")) {
|
|
3637
|
+
return trimmed;
|
|
3638
|
+
}
|
|
3639
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
3640
|
+
return trimmed;
|
|
3641
|
+
}
|
|
3642
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
3643
|
+
const inner = trimmed.slice(1, -1).replace(/"/g, '\\"');
|
|
3644
|
+
return `"${inner}"`;
|
|
3645
|
+
}
|
|
3646
|
+
return trimmed;
|
|
3647
|
+
}
|
|
3648
|
+
function formatClassList(classes) {
|
|
3649
|
+
if (!classes.length) return "";
|
|
3650
|
+
return classes.map((cls) => `.${cls}`).join("");
|
|
3651
|
+
}
|
|
3652
|
+
function indentString(level, indentSize) {
|
|
3653
|
+
return " ".repeat(level * indentSize);
|
|
3654
|
+
}
|
|
3655
|
+
function ensureTrailingNewline(output) {
|
|
3656
|
+
const trimmed = output.replace(/\s+$/g, "");
|
|
3657
|
+
return trimmed.length ? `${trimmed}
|
|
3658
|
+
` : "\n";
|
|
3659
|
+
}
|
|
3660
|
+
function cleanLine(line) {
|
|
3661
|
+
return line.replace(/[ \t]+$/g, "");
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
// src/convert.ts
|
|
3665
|
+
import ts from "typescript";
|
|
3666
|
+
function convertTsxToCollie(source, options = {}) {
|
|
3667
|
+
const filename = options.filename ?? "input.tsx";
|
|
3668
|
+
const sourceFile = ts.createSourceFile(
|
|
3669
|
+
filename,
|
|
3670
|
+
source,
|
|
3671
|
+
ts.ScriptTarget.Latest,
|
|
3672
|
+
true,
|
|
3673
|
+
inferScriptKind(filename)
|
|
3674
|
+
);
|
|
3675
|
+
const warnings = [];
|
|
3676
|
+
const ctx = { sourceFile, warnings };
|
|
3677
|
+
const propDeclarations = collectPropDeclarations(sourceFile);
|
|
3678
|
+
const component = findComponentInfo(sourceFile, propDeclarations, ctx);
|
|
3679
|
+
if (!component) {
|
|
3680
|
+
throw new Error("Could not find a component that returns JSX in this file.");
|
|
3681
|
+
}
|
|
3682
|
+
const propsLines = buildPropsBlock(component, propDeclarations, ctx);
|
|
3683
|
+
const templateLines = convertJsxNode(component.jsxRoot, ctx, 0);
|
|
3684
|
+
if (!templateLines.length) {
|
|
3685
|
+
throw new Error("Unable to convert JSX tree to Collie template.");
|
|
3686
|
+
}
|
|
3687
|
+
const sections = [];
|
|
3688
|
+
if (propsLines.length) {
|
|
3689
|
+
sections.push(propsLines.join("\n"));
|
|
3690
|
+
}
|
|
3691
|
+
sections.push(templateLines.join("\n"));
|
|
3692
|
+
const collie = `${sections.join("\n\n").trimEnd()}
|
|
3693
|
+
`;
|
|
3694
|
+
return { collie, warnings };
|
|
3695
|
+
}
|
|
3696
|
+
function inferScriptKind(filename) {
|
|
3697
|
+
const dotIndex = filename.lastIndexOf(".");
|
|
3698
|
+
const ext = dotIndex === -1 ? "" : filename.slice(dotIndex).toLowerCase();
|
|
3699
|
+
if (ext === ".tsx") return ts.ScriptKind.TSX;
|
|
3700
|
+
if (ext === ".jsx") return ts.ScriptKind.JSX;
|
|
3701
|
+
if (ext === ".ts") return ts.ScriptKind.TS;
|
|
3702
|
+
return ts.ScriptKind.JS;
|
|
3703
|
+
}
|
|
3704
|
+
function collectPropDeclarations(sourceFile) {
|
|
3705
|
+
const map = /* @__PURE__ */ new Map();
|
|
3706
|
+
for (const statement of sourceFile.statements) {
|
|
3707
|
+
if (ts.isInterfaceDeclaration(statement) && statement.name) {
|
|
3708
|
+
map.set(statement.name.text, extractPropsFromMembers(statement.members, sourceFile));
|
|
3709
|
+
} else if (ts.isTypeAliasDeclaration(statement) && ts.isTypeLiteralNode(statement.type)) {
|
|
3710
|
+
map.set(statement.name.text, extractPropsFromMembers(statement.type.members, sourceFile));
|
|
1676
3711
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
3712
|
+
}
|
|
3713
|
+
return map;
|
|
3714
|
+
}
|
|
3715
|
+
function extractPropsFromMembers(members, sourceFile) {
|
|
3716
|
+
const fields = [];
|
|
3717
|
+
for (const member of members) {
|
|
3718
|
+
if (!ts.isPropertySignature(member) || member.name === void 0) {
|
|
3719
|
+
continue;
|
|
3720
|
+
}
|
|
3721
|
+
const name = getPropertyName(member.name, sourceFile);
|
|
3722
|
+
if (!name) {
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
const typeText = member.type ? member.type.getText(sourceFile).trim() : "any";
|
|
3726
|
+
fields.push({
|
|
3727
|
+
name,
|
|
3728
|
+
optional: Boolean(member.questionToken),
|
|
3729
|
+
typeText
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
return fields;
|
|
3733
|
+
}
|
|
3734
|
+
function findComponentInfo(sourceFile, declarations, ctx) {
|
|
3735
|
+
for (const statement of sourceFile.statements) {
|
|
3736
|
+
if (ts.isFunctionDeclaration(statement) && statement.body) {
|
|
3737
|
+
const jsx = findJsxReturn(statement.body);
|
|
3738
|
+
if (jsx) {
|
|
3739
|
+
const defaults = extractDefaultsFromParameters(statement.parameters, ctx);
|
|
3740
|
+
const propsInfo = resolvePropsFromParameters(statement.parameters, declarations, ctx);
|
|
3741
|
+
return {
|
|
3742
|
+
jsxRoot: jsx,
|
|
3743
|
+
propsTypeName: propsInfo.typeName,
|
|
3744
|
+
inlineProps: propsInfo.inline,
|
|
3745
|
+
defaults
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
3749
|
+
for (const decl of statement.declarationList.declarations) {
|
|
3750
|
+
const init = decl.initializer;
|
|
3751
|
+
if (!init) continue;
|
|
3752
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
3753
|
+
const jsx = init.body ? findJsxInFunctionBody(init.body) : void 0;
|
|
3754
|
+
if (!jsx) {
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
const defaults = extractDefaultsFromParameters(init.parameters, ctx);
|
|
3758
|
+
const propsInfo = resolvePropsFromParameters(init.parameters, declarations, ctx);
|
|
3759
|
+
if (!propsInfo.typeName && !propsInfo.inline && decl.type) {
|
|
3760
|
+
const inferred = resolvePropsFromTypeAnnotation(decl.type, sourceFile, declarations);
|
|
3761
|
+
if (inferred.typeName && !propsInfo.typeName) {
|
|
3762
|
+
propsInfo.typeName = inferred.typeName;
|
|
3763
|
+
}
|
|
3764
|
+
if (inferred.inline && !propsInfo.inline) {
|
|
3765
|
+
propsInfo.inline = inferred.inline;
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
return {
|
|
3769
|
+
jsxRoot: jsx,
|
|
3770
|
+
propsTypeName: propsInfo.typeName,
|
|
3771
|
+
inlineProps: propsInfo.inline,
|
|
3772
|
+
defaults
|
|
3773
|
+
};
|
|
1681
3774
|
}
|
|
1682
3775
|
}
|
|
1683
3776
|
}
|
|
1684
|
-
return;
|
|
1685
3777
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
3778
|
+
return null;
|
|
3779
|
+
}
|
|
3780
|
+
function resolvePropsFromParameters(parameters, declarations, ctx) {
|
|
3781
|
+
if (!parameters.length) {
|
|
3782
|
+
return {};
|
|
3783
|
+
}
|
|
3784
|
+
const param = parameters[0];
|
|
3785
|
+
if (param.type) {
|
|
3786
|
+
const inferred = resolvePropsFromTypeAnnotation(param.type, ctx.sourceFile, declarations);
|
|
3787
|
+
if (inferred.inline) {
|
|
3788
|
+
return inferred;
|
|
3789
|
+
}
|
|
3790
|
+
if (inferred.typeName) {
|
|
3791
|
+
return inferred;
|
|
1691
3792
|
}
|
|
1692
3793
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
3794
|
+
return {};
|
|
3795
|
+
}
|
|
3796
|
+
function resolvePropsFromTypeAnnotation(typeNode, sourceFile, declarations) {
|
|
3797
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
3798
|
+
const referenced = getTypeReferenceName(typeNode.typeName);
|
|
3799
|
+
if (referenced && declarations.has(referenced)) {
|
|
3800
|
+
return { typeName: referenced };
|
|
3801
|
+
}
|
|
3802
|
+
const typeArg = typeNode.typeArguments?.[0];
|
|
3803
|
+
if (typeArg) {
|
|
3804
|
+
if (ts.isTypeReferenceNode(typeArg)) {
|
|
3805
|
+
const nested = getTypeReferenceName(typeArg.typeName);
|
|
3806
|
+
if (nested && declarations.has(nested)) {
|
|
3807
|
+
return { typeName: nested };
|
|
3808
|
+
}
|
|
3809
|
+
} else if (ts.isTypeLiteralNode(typeArg)) {
|
|
3810
|
+
return { inline: extractPropsFromMembers(typeArg.members, sourceFile) };
|
|
3811
|
+
}
|
|
1696
3812
|
}
|
|
1697
3813
|
}
|
|
3814
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
3815
|
+
return { inline: extractPropsFromMembers(typeNode.members, sourceFile) };
|
|
3816
|
+
}
|
|
3817
|
+
return {};
|
|
1698
3818
|
}
|
|
1699
|
-
function
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
return null;
|
|
3819
|
+
function getTypeReferenceName(typeName) {
|
|
3820
|
+
if (ts.isIdentifier(typeName)) {
|
|
3821
|
+
return typeName.text;
|
|
1703
3822
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
const nextPart = line.slice(cursor);
|
|
1707
|
-
const isComponent = /^[A-Z]/.test(name);
|
|
1708
|
-
if (isComponent && nextPart.length > 0) {
|
|
1709
|
-
const trimmedNext = nextPart.trimStart();
|
|
1710
|
-
if (trimmedNext.length > 0 && !trimmedNext.startsWith("(")) {
|
|
1711
|
-
return null;
|
|
1712
|
-
}
|
|
3823
|
+
if (ts.isQualifiedName(typeName)) {
|
|
3824
|
+
return typeName.right.text;
|
|
1713
3825
|
}
|
|
1714
|
-
if (
|
|
1715
|
-
|
|
1716
|
-
if (nextChar !== "." && nextChar !== "(" && !/\s/.test(nextChar)) {
|
|
1717
|
-
return null;
|
|
1718
|
-
}
|
|
3826
|
+
if (ts.isPropertyAccessExpression(typeName)) {
|
|
3827
|
+
return getTypeReferenceName(typeName.name);
|
|
1719
3828
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const
|
|
1726
|
-
if (
|
|
1727
|
-
|
|
1728
|
-
diagnostics,
|
|
1729
|
-
"COLLIE004",
|
|
1730
|
-
"Class names must contain only letters, numbers, underscores, hyphens, or `$` (for aliases).",
|
|
1731
|
-
lineNumber,
|
|
1732
|
-
column + cursor,
|
|
1733
|
-
lineOffset
|
|
1734
|
-
);
|
|
1735
|
-
return null;
|
|
3829
|
+
return void 0;
|
|
3830
|
+
}
|
|
3831
|
+
function findJsxReturn(body) {
|
|
3832
|
+
for (const statement of body.statements) {
|
|
3833
|
+
if (ts.isReturnStatement(statement) && statement.expression) {
|
|
3834
|
+
const jsx = unwrapJsx(statement.expression);
|
|
3835
|
+
if (jsx) {
|
|
3836
|
+
return jsx;
|
|
1736
3837
|
}
|
|
1737
|
-
const className = classMatch[1];
|
|
1738
|
-
classes.push(className);
|
|
1739
|
-
classSpans.push(createSpan(lineNumber, column + cursor, className.length, lineOffset));
|
|
1740
|
-
cursor += className.length;
|
|
1741
3838
|
}
|
|
1742
3839
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
}
|
|
1749
|
-
attributes.push(...attrResult.attributes);
|
|
1750
|
-
cursor = attrResult.endIndex;
|
|
3840
|
+
return void 0;
|
|
3841
|
+
}
|
|
3842
|
+
function findJsxInFunctionBody(body) {
|
|
3843
|
+
if (ts.isBlock(body)) {
|
|
3844
|
+
return findJsxReturn(body);
|
|
1751
3845
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
3846
|
+
return unwrapJsx(body);
|
|
3847
|
+
}
|
|
3848
|
+
function unwrapJsx(expression) {
|
|
3849
|
+
let current = expression;
|
|
3850
|
+
while (ts.isParenthesizedExpression(current)) {
|
|
3851
|
+
current = current.expression;
|
|
1756
3852
|
}
|
|
1757
|
-
if (
|
|
1758
|
-
|
|
1759
|
-
cursor++;
|
|
1760
|
-
const guardExpr = line.slice(cursor).trim();
|
|
1761
|
-
if (!guardExpr) {
|
|
1762
|
-
pushDiag(
|
|
1763
|
-
diagnostics,
|
|
1764
|
-
"COLLIE601",
|
|
1765
|
-
"Guard expressions require a condition after '?'.",
|
|
1766
|
-
lineNumber,
|
|
1767
|
-
guardColumn,
|
|
1768
|
-
lineOffset
|
|
1769
|
-
);
|
|
1770
|
-
} else {
|
|
1771
|
-
guard = guardExpr;
|
|
1772
|
-
}
|
|
1773
|
-
cursor = line.length;
|
|
1774
|
-
} else {
|
|
1775
|
-
cursor = guardProbeStart;
|
|
3853
|
+
if (ts.isJsxElement(current) || ts.isJsxFragment(current) || ts.isJsxSelfClosingElement(current)) {
|
|
3854
|
+
return current;
|
|
1776
3855
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
3856
|
+
return void 0;
|
|
3857
|
+
}
|
|
3858
|
+
function extractDefaultsFromParameters(parameters, ctx) {
|
|
3859
|
+
const defaults = /* @__PURE__ */ new Map();
|
|
3860
|
+
if (!parameters.length) {
|
|
3861
|
+
return defaults;
|
|
1784
3862
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
if (guard) {
|
|
1793
|
-
component.guard = guard;
|
|
3863
|
+
const param = parameters[0];
|
|
3864
|
+
if (!ts.isObjectBindingPattern(param.name)) {
|
|
3865
|
+
return defaults;
|
|
3866
|
+
}
|
|
3867
|
+
for (const element of param.name.elements) {
|
|
3868
|
+
if (!element.initializer) {
|
|
3869
|
+
continue;
|
|
1794
3870
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
name,
|
|
1800
|
-
classes,
|
|
1801
|
-
attributes,
|
|
1802
|
-
children
|
|
1803
|
-
};
|
|
1804
|
-
if (classSpans.length) {
|
|
1805
|
-
element.classSpans = classSpans;
|
|
3871
|
+
const propName = getBindingElementPropName(element, ctx.sourceFile);
|
|
3872
|
+
if (!propName) {
|
|
3873
|
+
ctx.warnings.push("Skipping complex destructured default value.");
|
|
3874
|
+
continue;
|
|
1806
3875
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
3876
|
+
defaults.set(propName, element.initializer.getText(ctx.sourceFile).trim());
|
|
3877
|
+
}
|
|
3878
|
+
return defaults;
|
|
3879
|
+
}
|
|
3880
|
+
function getBindingElementPropName(element, sourceFile) {
|
|
3881
|
+
const prop = element.propertyName;
|
|
3882
|
+
if (prop) {
|
|
3883
|
+
if (ts.isIdentifier(prop) || ts.isStringLiteral(prop) || ts.isNumericLiteral(prop)) {
|
|
3884
|
+
return prop.text;
|
|
1809
3885
|
}
|
|
1810
|
-
return
|
|
3886
|
+
return prop.getText(sourceFile);
|
|
1811
3887
|
}
|
|
3888
|
+
if (ts.isIdentifier(element.name)) {
|
|
3889
|
+
return element.name.text;
|
|
3890
|
+
}
|
|
3891
|
+
return void 0;
|
|
1812
3892
|
}
|
|
1813
|
-
function
|
|
1814
|
-
if (
|
|
1815
|
-
return
|
|
3893
|
+
function getPropertyName(name, sourceFile) {
|
|
3894
|
+
if (ts.isIdentifier(name)) {
|
|
3895
|
+
return name.text;
|
|
1816
3896
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
3897
|
+
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
3898
|
+
return name.text;
|
|
3899
|
+
}
|
|
3900
|
+
return name.getText(sourceFile);
|
|
3901
|
+
}
|
|
3902
|
+
function buildPropsBlock(info, propDeclarations, ctx) {
|
|
3903
|
+
const fields = info.inlineProps ?? (info.propsTypeName ? propDeclarations.get(info.propsTypeName) ?? [] : void 0) ?? [];
|
|
3904
|
+
if (!fields.length && !info.defaults.size) {
|
|
3905
|
+
return [];
|
|
3906
|
+
}
|
|
3907
|
+
const lines = ["props"];
|
|
3908
|
+
if (fields.length) {
|
|
3909
|
+
for (const field of fields) {
|
|
3910
|
+
const def = info.defaults.get(field.name);
|
|
3911
|
+
let line = ` ${field.name}${field.optional ? "?" : ""}: ${field.typeText}`;
|
|
3912
|
+
if (def) {
|
|
3913
|
+
line += ` = ${def}`;
|
|
1830
3914
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
3915
|
+
lines.push(line);
|
|
3916
|
+
}
|
|
3917
|
+
} else {
|
|
3918
|
+
for (const [name, defValue] of info.defaults.entries()) {
|
|
3919
|
+
lines.push(` ${name}: any = ${defValue}`);
|
|
1833
3920
|
}
|
|
1834
|
-
cursor++;
|
|
1835
3921
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
lineNumber,
|
|
1842
|
-
column + startIndex,
|
|
1843
|
-
lineOffset
|
|
1844
|
-
);
|
|
1845
|
-
return null;
|
|
3922
|
+
return lines;
|
|
3923
|
+
}
|
|
3924
|
+
function convertJsxNode(node, ctx, indent) {
|
|
3925
|
+
if (ts.isJsxElement(node)) {
|
|
3926
|
+
return convertJsxElement(node, ctx, indent);
|
|
1846
3927
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
return { attributes: [], endIndex: cursor };
|
|
3928
|
+
if (ts.isJsxSelfClosingElement(node)) {
|
|
3929
|
+
return convertJsxSelfClosing(node, ctx, indent);
|
|
1850
3930
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
3931
|
+
if (ts.isJsxFragment(node)) {
|
|
3932
|
+
return convertJsxFragment(node, ctx, indent);
|
|
3933
|
+
}
|
|
3934
|
+
if (ts.isJsxText(node)) {
|
|
3935
|
+
return convertJsxText(node, ctx, indent);
|
|
3936
|
+
}
|
|
3937
|
+
if (ts.isJsxExpression(node)) {
|
|
3938
|
+
return convertJsxExpression(node, ctx, indent);
|
|
3939
|
+
}
|
|
3940
|
+
return [];
|
|
3941
|
+
}
|
|
3942
|
+
function convertJsxFragment(fragment, ctx, indent) {
|
|
3943
|
+
const lines = [];
|
|
3944
|
+
for (const child of fragment.children) {
|
|
3945
|
+
lines.push(...convertJsxNode(child, ctx, indent));
|
|
3946
|
+
}
|
|
3947
|
+
return lines;
|
|
3948
|
+
}
|
|
3949
|
+
function convertJsxElement(element, ctx, indent) {
|
|
3950
|
+
const line = buildElementLine(element.openingElement, ctx, indent);
|
|
3951
|
+
const children = [];
|
|
3952
|
+
for (const child of element.children) {
|
|
3953
|
+
children.push(...convertJsxNode(child, ctx, indent + 1));
|
|
3954
|
+
}
|
|
3955
|
+
if (!children.length) {
|
|
3956
|
+
return [line];
|
|
3957
|
+
}
|
|
3958
|
+
return [line, ...children];
|
|
3959
|
+
}
|
|
3960
|
+
function convertJsxSelfClosing(element, ctx, indent) {
|
|
3961
|
+
return [buildElementLine(element, ctx, indent)];
|
|
3962
|
+
}
|
|
3963
|
+
function buildElementLine(element, ctx, indent) {
|
|
3964
|
+
const indentStr = " ".repeat(indent);
|
|
3965
|
+
const tagName = getTagName(element.tagName, ctx);
|
|
3966
|
+
const { classSegments, attributes } = convertAttributes(element.attributes, ctx);
|
|
3967
|
+
const classes = classSegments.length ? classSegments.map((cls) => `.${cls}`).join("") : "";
|
|
3968
|
+
const attrString = attributes.length ? `(${attributes.join(" ")})` : "";
|
|
3969
|
+
return `${indentStr}${tagName}${classes}${attrString}`;
|
|
3970
|
+
}
|
|
3971
|
+
function getTagName(tag, ctx) {
|
|
3972
|
+
const fallback = tag.getText(ctx.sourceFile);
|
|
3973
|
+
if (ts.isIdentifier(tag)) {
|
|
3974
|
+
return tag.text;
|
|
3975
|
+
}
|
|
3976
|
+
if (ts.isPropertyAccessExpression(tag)) {
|
|
3977
|
+
const left = getTagName(tag.expression, ctx);
|
|
3978
|
+
return `${left}.${tag.name.text}`;
|
|
3979
|
+
}
|
|
3980
|
+
if (tag.kind === ts.SyntaxKind.ThisKeyword) {
|
|
3981
|
+
return "this";
|
|
3982
|
+
}
|
|
3983
|
+
if (ts.isJsxNamespacedName(tag)) {
|
|
3984
|
+
return `${tag.namespace.text}:${tag.name.text}`;
|
|
3985
|
+
}
|
|
3986
|
+
return fallback;
|
|
3987
|
+
}
|
|
3988
|
+
function convertAttributes(attributes, ctx) {
|
|
3989
|
+
const classSegments = [];
|
|
3990
|
+
const attrs = [];
|
|
3991
|
+
for (const attr of attributes.properties) {
|
|
3992
|
+
if (ts.isJsxAttribute(attr)) {
|
|
3993
|
+
const attrName = getAttributeName(attr.name, ctx);
|
|
3994
|
+
if (!attrName) {
|
|
3995
|
+
ctx.warnings.push("Skipping unsupported attribute name.");
|
|
3996
|
+
continue;
|
|
1861
3997
|
}
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
3998
|
+
if (attrName === "className" || attrName === "class") {
|
|
3999
|
+
const handled = handleClassAttribute(attr, ctx, classSegments, attrs);
|
|
4000
|
+
if (!handled) {
|
|
4001
|
+
attrs.push(formatAttribute(attrName === "className" ? "className" : attrName, attr.initializer, ctx));
|
|
4002
|
+
}
|
|
4003
|
+
continue;
|
|
1868
4004
|
}
|
|
4005
|
+
attrs.push(formatAttribute(attrName, attr.initializer, ctx));
|
|
4006
|
+
} else if (ts.isJsxSpreadAttribute(attr)) {
|
|
4007
|
+
ctx.warnings.push("Spread attributes are not supported and were skipped.");
|
|
1869
4008
|
}
|
|
1870
4009
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
4010
|
+
return { classSegments, attributes: attrs.filter(Boolean) };
|
|
4011
|
+
}
|
|
4012
|
+
function handleClassAttribute(attr, ctx, classSegments, attrs) {
|
|
4013
|
+
if (!attr.initializer) {
|
|
4014
|
+
return false;
|
|
1873
4015
|
}
|
|
1874
|
-
|
|
4016
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
4017
|
+
classSegments.push(...splitClassNames(attr.initializer.text));
|
|
4018
|
+
return true;
|
|
4019
|
+
}
|
|
4020
|
+
if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) {
|
|
4021
|
+
const expressionText = attr.initializer.expression.getText(ctx.sourceFile).trim();
|
|
4022
|
+
attrs.push(`className={${expressionText}}`);
|
|
4023
|
+
return true;
|
|
4024
|
+
}
|
|
4025
|
+
ctx.warnings.push("Unsupported class attribute value; leaving as-is.");
|
|
4026
|
+
return false;
|
|
1875
4027
|
}
|
|
1876
|
-
function
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
"COLLIE004",
|
|
1891
|
-
`Invalid attribute syntax: ${trimmed.slice(0, 30)}`,
|
|
1892
|
-
lineNumber,
|
|
1893
|
-
column,
|
|
1894
|
-
lineOffset
|
|
1895
|
-
);
|
|
4028
|
+
function splitClassNames(value) {
|
|
4029
|
+
return value.split(/\s+/).map((cls) => cls.trim()).filter(Boolean);
|
|
4030
|
+
}
|
|
4031
|
+
function formatAttribute(name, initializer, ctx) {
|
|
4032
|
+
if (!initializer) {
|
|
4033
|
+
return name;
|
|
4034
|
+
}
|
|
4035
|
+
if (ts.isStringLiteral(initializer)) {
|
|
4036
|
+
return `${name}="${initializer.text}"`;
|
|
4037
|
+
}
|
|
4038
|
+
if (ts.isJsxExpression(initializer)) {
|
|
4039
|
+
if (initializer.expression) {
|
|
4040
|
+
const expr = initializer.expression.getText(ctx.sourceFile).trim();
|
|
4041
|
+
return `${name}={${expr}}`;
|
|
1896
4042
|
}
|
|
4043
|
+
return name;
|
|
1897
4044
|
}
|
|
4045
|
+
ctx.warnings.push("Unsupported JSX attribute value; leaving as-is.");
|
|
4046
|
+
return name;
|
|
1898
4047
|
}
|
|
1899
|
-
function
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
}
|
|
4048
|
+
function getAttributeName(name, ctx) {
|
|
4049
|
+
if (ts.isIdentifier(name)) {
|
|
4050
|
+
return name.text;
|
|
4051
|
+
}
|
|
4052
|
+
if (ts.isJsxNamespacedName(name)) {
|
|
4053
|
+
return `${name.namespace.text}:${name.name.text}`;
|
|
4054
|
+
}
|
|
4055
|
+
return null;
|
|
4056
|
+
}
|
|
4057
|
+
function convertJsxText(textNode, ctx, indent) {
|
|
4058
|
+
const text = textNode.getText(ctx.sourceFile).replace(/\s+/g, " ").trim();
|
|
4059
|
+
if (!text) {
|
|
4060
|
+
return [];
|
|
4061
|
+
}
|
|
4062
|
+
return [`${" ".repeat(indent)}| ${text}`];
|
|
4063
|
+
}
|
|
4064
|
+
function convertJsxExpression(expressionNode, ctx, indent) {
|
|
4065
|
+
if (!expressionNode.expression) {
|
|
4066
|
+
return [];
|
|
4067
|
+
}
|
|
4068
|
+
const exprText = expressionNode.expression.getText(ctx.sourceFile).trim();
|
|
4069
|
+
if (!exprText) {
|
|
4070
|
+
return [];
|
|
4071
|
+
}
|
|
4072
|
+
return [`${" ".repeat(indent)}{{ ${exprText} }}`];
|
|
1906
4073
|
}
|
|
1907
4074
|
|
|
1908
4075
|
// src/index.ts
|
|
1909
4076
|
function parseCollie(source, options = {}) {
|
|
1910
|
-
const result = parse(source);
|
|
4077
|
+
const result = parse(source, { dialect: options.dialect });
|
|
1911
4078
|
if (!options.filename) {
|
|
1912
|
-
return
|
|
4079
|
+
return {
|
|
4080
|
+
templates: result.templates.map((template) => ({
|
|
4081
|
+
...template,
|
|
4082
|
+
diagnostics: normalizeDiagnostics2(template.diagnostics)
|
|
4083
|
+
})),
|
|
4084
|
+
diagnostics: normalizeDiagnostics2(result.diagnostics)
|
|
4085
|
+
};
|
|
4086
|
+
}
|
|
4087
|
+
return {
|
|
4088
|
+
templates: result.templates.map((template) => ({
|
|
4089
|
+
...template,
|
|
4090
|
+
diagnostics: normalizeDiagnostics2(template.diagnostics, options.filename)
|
|
4091
|
+
})),
|
|
4092
|
+
diagnostics: normalizeDiagnostics2(result.diagnostics, options.filename)
|
|
4093
|
+
};
|
|
4094
|
+
}
|
|
4095
|
+
function compileTemplate(template, options = {}) {
|
|
4096
|
+
const diagnostics = normalizeDiagnostics2(template.diagnostics, options.filename);
|
|
4097
|
+
const jsxRuntime = options.jsxRuntime ?? "automatic";
|
|
4098
|
+
const flavor = options.flavor ?? "tsx";
|
|
4099
|
+
let code = createStubRender(flavor);
|
|
4100
|
+
if (!hasErrors(diagnostics)) {
|
|
4101
|
+
code = generateRenderModule(template.ast, { jsxRuntime, flavor });
|
|
1913
4102
|
}
|
|
1914
|
-
|
|
4103
|
+
const meta = buildCompileMeta(template, options.filename);
|
|
4104
|
+
return { code, diagnostics, map: void 0, meta };
|
|
1915
4105
|
}
|
|
1916
4106
|
function compileToJsx(sourceOrAst, options = {}) {
|
|
1917
|
-
const document = normalizeDocument(sourceOrAst, options.filename);
|
|
1918
|
-
const diagnostics =
|
|
4107
|
+
const document = normalizeDocument(sourceOrAst, options.filename, options.dialect);
|
|
4108
|
+
const diagnostics = normalizeDiagnostics2(document.diagnostics, options.filename);
|
|
4109
|
+
const template = document.templates[0];
|
|
1919
4110
|
const componentName = options.componentNameHint ?? "CollieTemplate";
|
|
1920
4111
|
const jsxRuntime = options.jsxRuntime ?? "automatic";
|
|
1921
4112
|
let code = createStubComponent(componentName, "jsx");
|
|
1922
|
-
if (!hasErrors(diagnostics)) {
|
|
1923
|
-
|
|
4113
|
+
if (!hasErrors(diagnostics) && template) {
|
|
4114
|
+
const renderResult = compileTemplate(template, {
|
|
4115
|
+
filename: options.filename,
|
|
4116
|
+
jsxRuntime,
|
|
4117
|
+
flavor: "jsx"
|
|
4118
|
+
});
|
|
4119
|
+
code = wrapRenderModuleAsComponent(renderResult.code, componentName, "jsx");
|
|
1924
4120
|
}
|
|
1925
|
-
|
|
4121
|
+
const meta = buildCompileMeta(template, options.filename);
|
|
4122
|
+
return { code, diagnostics, map: void 0, meta };
|
|
1926
4123
|
}
|
|
1927
4124
|
function compileToTsx(sourceOrAst, options = {}) {
|
|
1928
|
-
const document = normalizeDocument(sourceOrAst, options.filename);
|
|
1929
|
-
const diagnostics =
|
|
4125
|
+
const document = normalizeDocument(sourceOrAst, options.filename, options.dialect);
|
|
4126
|
+
const diagnostics = normalizeDiagnostics2(document.diagnostics, options.filename);
|
|
4127
|
+
const template = document.templates[0];
|
|
1930
4128
|
const componentName = options.componentNameHint ?? "CollieTemplate";
|
|
1931
4129
|
const jsxRuntime = options.jsxRuntime ?? "automatic";
|
|
1932
4130
|
let code = createStubComponent(componentName, "tsx");
|
|
1933
|
-
if (!hasErrors(diagnostics)) {
|
|
1934
|
-
|
|
4131
|
+
if (!hasErrors(diagnostics) && template) {
|
|
4132
|
+
const renderResult = compileTemplate(template, {
|
|
4133
|
+
filename: options.filename,
|
|
4134
|
+
jsxRuntime,
|
|
4135
|
+
flavor: "tsx"
|
|
4136
|
+
});
|
|
4137
|
+
code = wrapRenderModuleAsComponent(renderResult.code, componentName, "tsx");
|
|
1935
4138
|
}
|
|
1936
|
-
|
|
4139
|
+
const meta = buildCompileMeta(template, options.filename);
|
|
4140
|
+
return { code, diagnostics, map: void 0, meta };
|
|
4141
|
+
}
|
|
4142
|
+
function convertCollieToTsx(source, options = {}) {
|
|
4143
|
+
const result = compileToTsx(source, options);
|
|
4144
|
+
return {
|
|
4145
|
+
tsx: result.code,
|
|
4146
|
+
diagnostics: result.diagnostics,
|
|
4147
|
+
meta: result.meta
|
|
4148
|
+
};
|
|
1937
4149
|
}
|
|
1938
4150
|
function compileToHtml(sourceOrAst, options = {}) {
|
|
1939
|
-
const document = normalizeDocument(sourceOrAst, options.filename);
|
|
1940
|
-
const diagnostics =
|
|
1941
|
-
const
|
|
1942
|
-
let code = createStubHtml(
|
|
1943
|
-
if (!hasErrors(diagnostics)) {
|
|
1944
|
-
code =
|
|
1945
|
-
}
|
|
1946
|
-
|
|
4151
|
+
const document = normalizeDocument(sourceOrAst, options.filename, options.dialect);
|
|
4152
|
+
const diagnostics = normalizeDiagnostics2(document.diagnostics, options.filename);
|
|
4153
|
+
const template = document.templates[0];
|
|
4154
|
+
let code = createStubHtml();
|
|
4155
|
+
if (!hasErrors(diagnostics) && template) {
|
|
4156
|
+
code = generateHtml(template.ast);
|
|
4157
|
+
}
|
|
4158
|
+
const meta = buildCompileMeta(template, options.filename);
|
|
4159
|
+
return { code, diagnostics, map: void 0, meta };
|
|
1947
4160
|
}
|
|
1948
4161
|
function compile(source, options = {}) {
|
|
1949
4162
|
return compileToJsx(source, options);
|
|
1950
4163
|
}
|
|
1951
|
-
function normalizeDocument(sourceOrAst, filename) {
|
|
4164
|
+
function normalizeDocument(sourceOrAst, filename, dialect) {
|
|
1952
4165
|
if (typeof sourceOrAst === "string") {
|
|
1953
|
-
return parseCollie(sourceOrAst, { filename });
|
|
4166
|
+
return parseCollie(sourceOrAst, { filename, dialect });
|
|
1954
4167
|
}
|
|
1955
4168
|
if (isCollieDocument(sourceOrAst)) {
|
|
1956
4169
|
if (!filename) {
|
|
1957
4170
|
return sourceOrAst;
|
|
1958
4171
|
}
|
|
1959
|
-
return
|
|
4172
|
+
return attachFilenameToDocument(sourceOrAst, filename);
|
|
1960
4173
|
}
|
|
1961
4174
|
if (isRootNode(sourceOrAst)) {
|
|
1962
|
-
|
|
4175
|
+
const id = sourceOrAst.id ?? sourceOrAst.rawId ?? "";
|
|
4176
|
+
const rawId = sourceOrAst.rawId ?? sourceOrAst.id ?? "";
|
|
4177
|
+
const template = {
|
|
4178
|
+
id,
|
|
4179
|
+
rawId,
|
|
4180
|
+
span: sourceOrAst.idTokenSpan,
|
|
4181
|
+
ast: sourceOrAst,
|
|
4182
|
+
diagnostics: []
|
|
4183
|
+
};
|
|
4184
|
+
return { templates: [template], diagnostics: [] };
|
|
1963
4185
|
}
|
|
1964
4186
|
throw new TypeError("Collie compiler expected source text, a parsed document, or a root node.");
|
|
1965
4187
|
}
|
|
@@ -1967,7 +4189,7 @@ function isRootNode(value) {
|
|
|
1967
4189
|
return !!value && typeof value === "object" && value.type === "Root";
|
|
1968
4190
|
}
|
|
1969
4191
|
function isCollieDocument(value) {
|
|
1970
|
-
return !!value && typeof value === "object" &&
|
|
4192
|
+
return !!value && typeof value === "object" && Array.isArray(value.templates) && Array.isArray(value.diagnostics);
|
|
1971
4193
|
}
|
|
1972
4194
|
function hasErrors(diagnostics) {
|
|
1973
4195
|
return diagnostics.some((diag) => diag.severity === "error");
|
|
@@ -1983,20 +4205,86 @@ function createStubComponent(name, flavor) {
|
|
|
1983
4205
|
}
|
|
1984
4206
|
return [`export default function ${name}(props) {`, " return null;", "}"].join("\n");
|
|
1985
4207
|
}
|
|
1986
|
-
function
|
|
1987
|
-
|
|
4208
|
+
function createStubRender(flavor) {
|
|
4209
|
+
if (flavor === "tsx") {
|
|
4210
|
+
return [
|
|
4211
|
+
"export type Props = Record<string, never>;",
|
|
4212
|
+
"export function render(props: any) {",
|
|
4213
|
+
" return null;",
|
|
4214
|
+
"}"
|
|
4215
|
+
].join("\n");
|
|
4216
|
+
}
|
|
4217
|
+
return ["export function render(props) {", " return null;", "}"].join("\n");
|
|
4218
|
+
}
|
|
4219
|
+
function wrapRenderModuleAsComponent(renderModule, name, flavor) {
|
|
4220
|
+
const signature = flavor === "tsx" ? `export default function ${name}(props: Props) {` : `export default function ${name}(props) {`;
|
|
4221
|
+
const wrapper = [signature, " return render(props);", "}"].join("\n");
|
|
4222
|
+
return `${renderModule}
|
|
4223
|
+
|
|
4224
|
+
${wrapper}`;
|
|
4225
|
+
}
|
|
4226
|
+
function createStubHtml() {
|
|
4227
|
+
return "";
|
|
4228
|
+
}
|
|
4229
|
+
function buildCompileMeta(template, filename) {
|
|
4230
|
+
const meta = {};
|
|
4231
|
+
if (filename) {
|
|
4232
|
+
meta.filename = filename;
|
|
4233
|
+
}
|
|
4234
|
+
if (template?.rawId) {
|
|
4235
|
+
meta.rawId = template.rawId;
|
|
4236
|
+
}
|
|
4237
|
+
if (template?.id) {
|
|
4238
|
+
meta.id = template.id;
|
|
4239
|
+
}
|
|
4240
|
+
if (template?.span) {
|
|
4241
|
+
meta.span = template.span;
|
|
4242
|
+
}
|
|
4243
|
+
return meta.id || meta.rawId || meta.filename ? meta : void 0;
|
|
1988
4244
|
}
|
|
1989
|
-
function
|
|
4245
|
+
function attachFilenameToDocument(document, filename) {
|
|
1990
4246
|
if (!filename) {
|
|
1991
|
-
return
|
|
4247
|
+
return document;
|
|
1992
4248
|
}
|
|
1993
|
-
return
|
|
4249
|
+
return {
|
|
4250
|
+
templates: document.templates.map((template) => ({
|
|
4251
|
+
...template,
|
|
4252
|
+
diagnostics: normalizeDiagnostics2(template.diagnostics, filename)
|
|
4253
|
+
})),
|
|
4254
|
+
diagnostics: normalizeDiagnostics2(document.diagnostics, filename)
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
function normalizeDiagnostics2(diagnostics, filename) {
|
|
4258
|
+
return diagnostics.map((diag) => {
|
|
4259
|
+
const filePath = diag.filePath ?? diag.file ?? filename;
|
|
4260
|
+
const file = diag.file ?? filename;
|
|
4261
|
+
const range = diag.range ?? diag.span;
|
|
4262
|
+
if (filePath === diag.filePath && file === diag.file && range === diag.range) {
|
|
4263
|
+
return diag;
|
|
4264
|
+
}
|
|
4265
|
+
return {
|
|
4266
|
+
...diag,
|
|
4267
|
+
filePath,
|
|
4268
|
+
file,
|
|
4269
|
+
range
|
|
4270
|
+
};
|
|
4271
|
+
});
|
|
1994
4272
|
}
|
|
1995
4273
|
export {
|
|
4274
|
+
applyFixes,
|
|
1996
4275
|
compile,
|
|
4276
|
+
compileTemplate,
|
|
1997
4277
|
compileToHtml,
|
|
1998
4278
|
compileToJsx,
|
|
1999
4279
|
compileToTsx,
|
|
4280
|
+
convertCollieToTsx,
|
|
4281
|
+
convertTsxToCollie,
|
|
4282
|
+
defineConfig,
|
|
4283
|
+
fixAllFromDiagnostics,
|
|
4284
|
+
formatCollie,
|
|
4285
|
+
loadAndNormalizeConfig,
|
|
4286
|
+
loadConfig,
|
|
4287
|
+
normalizeConfig,
|
|
2000
4288
|
parseCollie as parse,
|
|
2001
4289
|
parseCollie
|
|
2002
4290
|
};
|