@collie-lang/compiler 1.0.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/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/index.cjs +2035 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +2003 -0
- package/dist/index.js.map +1 -0
- package/package.json +35 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2035 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
compile: () => compile,
|
|
24
|
+
compileToHtml: () => compileToHtml,
|
|
25
|
+
compileToJsx: () => compileToJsx,
|
|
26
|
+
compileToTsx: () => compileToTsx,
|
|
27
|
+
parse: () => parseCollie,
|
|
28
|
+
parseCollie: () => parseCollie
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/codegen.ts
|
|
33
|
+
function generateModule(root, options) {
|
|
34
|
+
const { componentName, jsxRuntime, flavor } = options;
|
|
35
|
+
const isTsx = flavor === "tsx";
|
|
36
|
+
const aliasEnv = buildClassAliasEnvironment(root.classAliases);
|
|
37
|
+
const jsx = renderRootChildren(root.children, aliasEnv);
|
|
38
|
+
const propsDestructure = emitPropsDestructure(root.props);
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (root.clientComponent) {
|
|
41
|
+
parts.push(`"use client";`);
|
|
42
|
+
}
|
|
43
|
+
if (jsxRuntime === "classic" && templateUsesJsx(root)) {
|
|
44
|
+
parts.push(`import React from "react";`);
|
|
45
|
+
}
|
|
46
|
+
parts.push(emitPropsType(root.props, flavor));
|
|
47
|
+
if (!isTsx) {
|
|
48
|
+
parts.push(`/** @param {Props} props */`);
|
|
49
|
+
}
|
|
50
|
+
const functionLines = [
|
|
51
|
+
isTsx ? `export default function ${componentName}(props: Props) {` : `export default function ${componentName}(props) {`
|
|
52
|
+
];
|
|
53
|
+
if (propsDestructure) {
|
|
54
|
+
functionLines.push(` ${propsDestructure}`);
|
|
55
|
+
}
|
|
56
|
+
functionLines.push(` return ${jsx};`, `}`);
|
|
57
|
+
parts.push(functionLines.join("\n"));
|
|
58
|
+
return parts.join("\n\n");
|
|
59
|
+
}
|
|
60
|
+
function buildClassAliasEnvironment(decl) {
|
|
61
|
+
const env = /* @__PURE__ */ new Map();
|
|
62
|
+
if (!decl) {
|
|
63
|
+
return env;
|
|
64
|
+
}
|
|
65
|
+
for (const alias of decl.aliases) {
|
|
66
|
+
env.set(alias.name, alias.classes);
|
|
67
|
+
}
|
|
68
|
+
return env;
|
|
69
|
+
}
|
|
70
|
+
function renderRootChildren(children, aliasEnv) {
|
|
71
|
+
return emitNodesExpression(children, aliasEnv);
|
|
72
|
+
}
|
|
73
|
+
function templateUsesJsx(root) {
|
|
74
|
+
if (root.children.length === 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (root.children.length > 1) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return nodeUsesJsx(root.children[0]);
|
|
81
|
+
}
|
|
82
|
+
function nodeUsesJsx(node) {
|
|
83
|
+
if (node.type === "Element" || node.type === "Text" || node.type === "Component") {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (node.type === "Expression" || node.type === "JSXPassthrough") {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (node.type === "Conditional") {
|
|
90
|
+
return node.branches.some((branch) => branchUsesJsx(branch));
|
|
91
|
+
}
|
|
92
|
+
if (node.type === "For") {
|
|
93
|
+
return node.body.some((child) => nodeUsesJsx(child));
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function branchUsesJsx(branch) {
|
|
98
|
+
if (!branch.body.length) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return branch.body.some((child) => nodeUsesJsx(child));
|
|
102
|
+
}
|
|
103
|
+
function emitNodeInJsx(node, aliasEnv) {
|
|
104
|
+
if (node.type === "Text") {
|
|
105
|
+
return emitText(node);
|
|
106
|
+
}
|
|
107
|
+
if (node.type === "Expression") {
|
|
108
|
+
return `{${node.value}}`;
|
|
109
|
+
}
|
|
110
|
+
if (node.type === "JSXPassthrough") {
|
|
111
|
+
return `{${node.expression}}`;
|
|
112
|
+
}
|
|
113
|
+
if (node.type === "Conditional") {
|
|
114
|
+
return `{${emitConditionalExpression(node, aliasEnv)}}`;
|
|
115
|
+
}
|
|
116
|
+
if (node.type === "For") {
|
|
117
|
+
return `{${emitForExpression(node, aliasEnv)}}`;
|
|
118
|
+
}
|
|
119
|
+
if (node.type === "Component") {
|
|
120
|
+
return wrapWithGuard(emitComponent(node, aliasEnv), node.guard, "jsx");
|
|
121
|
+
}
|
|
122
|
+
return wrapWithGuard(emitElement(node, aliasEnv), node.guard, "jsx");
|
|
123
|
+
}
|
|
124
|
+
function emitElement(node, aliasEnv) {
|
|
125
|
+
const expanded = expandClasses(node.classes, aliasEnv);
|
|
126
|
+
const classAttr = expanded.length ? ` className="${expanded.join(" ")}"` : "";
|
|
127
|
+
const attrs = emitAttributes(node.attributes, aliasEnv);
|
|
128
|
+
const allAttrs = classAttr + attrs;
|
|
129
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv);
|
|
130
|
+
if (children.length > 0) {
|
|
131
|
+
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
132
|
+
} else {
|
|
133
|
+
return `<${node.name}${allAttrs} />`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function emitComponent(node, aliasEnv) {
|
|
137
|
+
const attrs = emitAttributes(node.attributes, aliasEnv);
|
|
138
|
+
const slotProps = emitSlotProps(node, aliasEnv);
|
|
139
|
+
const allAttrs = `${attrs}${slotProps}`;
|
|
140
|
+
const children = emitChildrenWithSpacing(node.children, aliasEnv);
|
|
141
|
+
if (children.length > 0) {
|
|
142
|
+
return `<${node.name}${allAttrs}>${children}</${node.name}>`;
|
|
143
|
+
} else {
|
|
144
|
+
return `<${node.name}${allAttrs} />`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function emitChildrenWithSpacing(children, aliasEnv) {
|
|
148
|
+
if (children.length === 0) {
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
const parts = [];
|
|
152
|
+
for (let i = 0; i < children.length; i++) {
|
|
153
|
+
const child = children[i];
|
|
154
|
+
const emitted = emitNodeInJsx(child, aliasEnv);
|
|
155
|
+
parts.push(emitted);
|
|
156
|
+
if (i < children.length - 1) {
|
|
157
|
+
const nextChild = children[i + 1];
|
|
158
|
+
const needsSpace = child.type === "Text" && (nextChild.type === "Element" || nextChild.type === "Component" || nextChild.type === "Expression" || nextChild.type === "JSXPassthrough");
|
|
159
|
+
if (needsSpace) {
|
|
160
|
+
parts.push(" ");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return parts.join("");
|
|
165
|
+
}
|
|
166
|
+
function emitAttributes(attributes, aliasEnv) {
|
|
167
|
+
if (attributes.length === 0) {
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
return attributes.map((attr) => {
|
|
171
|
+
if (attr.value === null) {
|
|
172
|
+
return ` ${attr.name}`;
|
|
173
|
+
}
|
|
174
|
+
return ` ${attr.name}=${attr.value}`;
|
|
175
|
+
}).join("");
|
|
176
|
+
}
|
|
177
|
+
function emitSlotProps(node, aliasEnv) {
|
|
178
|
+
if (!node.slots || node.slots.length === 0) {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
return node.slots.map((slot) => {
|
|
182
|
+
const expr = emitNodesExpression(slot.children, aliasEnv);
|
|
183
|
+
return ` ${slot.name}={${expr}}`;
|
|
184
|
+
}).join("");
|
|
185
|
+
}
|
|
186
|
+
function wrapWithGuard(rendered, guard, context) {
|
|
187
|
+
if (!guard) {
|
|
188
|
+
return rendered;
|
|
189
|
+
}
|
|
190
|
+
const expression = `(${guard}) && ${rendered}`;
|
|
191
|
+
return context === "jsx" ? `{${expression}}` : expression;
|
|
192
|
+
}
|
|
193
|
+
function emitForExpression(node, aliasEnv) {
|
|
194
|
+
const body = emitNodesExpression(node.body, aliasEnv);
|
|
195
|
+
return `${node.arrayExpr}.map((${node.itemName}) => ${body})`;
|
|
196
|
+
}
|
|
197
|
+
function expandClasses(classes, aliasEnv) {
|
|
198
|
+
const result = [];
|
|
199
|
+
for (const cls of classes) {
|
|
200
|
+
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
201
|
+
if (!match) {
|
|
202
|
+
result.push(cls);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const aliasClasses = aliasEnv.get(match[1]);
|
|
206
|
+
if (!aliasClasses) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
result.push(...aliasClasses);
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
function emitText(node) {
|
|
214
|
+
if (!node.parts.length) {
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
return node.parts.map((part) => {
|
|
218
|
+
if (part.type === "text") {
|
|
219
|
+
return escapeText(part.value);
|
|
220
|
+
}
|
|
221
|
+
return `{${part.value}}`;
|
|
222
|
+
}).join("");
|
|
223
|
+
}
|
|
224
|
+
function emitConditionalExpression(node, aliasEnv) {
|
|
225
|
+
if (!node.branches.length) {
|
|
226
|
+
return "null";
|
|
227
|
+
}
|
|
228
|
+
const first = node.branches[0];
|
|
229
|
+
if (node.branches.length === 1 && first.test) {
|
|
230
|
+
return `(${first.test}) && ${emitBranchExpression(first, aliasEnv)}`;
|
|
231
|
+
}
|
|
232
|
+
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
233
|
+
let fallback = hasElse ? emitBranchExpression(node.branches[node.branches.length - 1], aliasEnv) : "null";
|
|
234
|
+
const startIndex = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
235
|
+
if (startIndex < 0) {
|
|
236
|
+
return fallback;
|
|
237
|
+
}
|
|
238
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
239
|
+
const branch = node.branches[i];
|
|
240
|
+
const test = branch.test ?? "false";
|
|
241
|
+
fallback = `(${test}) ? ${emitBranchExpression(branch, aliasEnv)} : ${fallback}`;
|
|
242
|
+
}
|
|
243
|
+
return fallback;
|
|
244
|
+
}
|
|
245
|
+
function emitBranchExpression(branch, aliasEnv) {
|
|
246
|
+
return emitNodesExpression(branch.body, aliasEnv);
|
|
247
|
+
}
|
|
248
|
+
function emitNodesExpression(children, aliasEnv) {
|
|
249
|
+
if (children.length === 0) {
|
|
250
|
+
return "null";
|
|
251
|
+
}
|
|
252
|
+
if (children.length === 1) {
|
|
253
|
+
return emitSingleNodeExpression(children[0], aliasEnv);
|
|
254
|
+
}
|
|
255
|
+
return `<>${children.map((child) => emitNodeInJsx(child, aliasEnv)).join("")}</>`;
|
|
256
|
+
}
|
|
257
|
+
function emitSingleNodeExpression(node, aliasEnv) {
|
|
258
|
+
if (node.type === "Expression") {
|
|
259
|
+
return node.value;
|
|
260
|
+
}
|
|
261
|
+
if (node.type === "JSXPassthrough") {
|
|
262
|
+
return node.expression;
|
|
263
|
+
}
|
|
264
|
+
if (node.type === "Conditional") {
|
|
265
|
+
return emitConditionalExpression(node, aliasEnv);
|
|
266
|
+
}
|
|
267
|
+
if (node.type === "For") {
|
|
268
|
+
return emitForExpression(node, aliasEnv);
|
|
269
|
+
}
|
|
270
|
+
if (node.type === "Element") {
|
|
271
|
+
return wrapWithGuard(emitElement(node, aliasEnv), node.guard, "expression");
|
|
272
|
+
}
|
|
273
|
+
if (node.type === "Component") {
|
|
274
|
+
return wrapWithGuard(emitComponent(node, aliasEnv), node.guard, "expression");
|
|
275
|
+
}
|
|
276
|
+
if (node.type === "Text") {
|
|
277
|
+
return `<>${emitNodeInJsx(node, aliasEnv)}</>`;
|
|
278
|
+
}
|
|
279
|
+
return emitNodeInJsx(node, aliasEnv);
|
|
280
|
+
}
|
|
281
|
+
function emitPropsType(props, flavor) {
|
|
282
|
+
if (flavor === "tsx") {
|
|
283
|
+
return emitTsPropsType(props);
|
|
284
|
+
}
|
|
285
|
+
return emitJsDocPropsType(props);
|
|
286
|
+
}
|
|
287
|
+
function emitJsDocPropsType(props) {
|
|
288
|
+
if (!props) {
|
|
289
|
+
return "/** @typedef {any} Props */";
|
|
290
|
+
}
|
|
291
|
+
if (!props.fields.length) {
|
|
292
|
+
return "/** @typedef {{}} Props */";
|
|
293
|
+
}
|
|
294
|
+
const fields = props.fields.map((field) => {
|
|
295
|
+
const optional = field.optional ? "?" : "";
|
|
296
|
+
return `${field.name}${optional}: ${field.typeText}`;
|
|
297
|
+
}).join("; ");
|
|
298
|
+
return `/** @typedef {{ ${fields} }} Props */`;
|
|
299
|
+
}
|
|
300
|
+
function emitTsPropsType(props) {
|
|
301
|
+
if (!props || props.fields.length === 0) {
|
|
302
|
+
return "export type Props = Record<string, never>;";
|
|
303
|
+
}
|
|
304
|
+
const lines = props.fields.map((field) => {
|
|
305
|
+
const optional = field.optional ? "?" : "";
|
|
306
|
+
return ` ${field.name}${optional}: ${field.typeText};`;
|
|
307
|
+
});
|
|
308
|
+
return ["export interface Props {", ...lines, "}"].join("\n");
|
|
309
|
+
}
|
|
310
|
+
function emitPropsDestructure(props) {
|
|
311
|
+
if (!props || props.fields.length === 0) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const names = props.fields.map((field) => field.name);
|
|
315
|
+
return `const { ${names.join(", ")} } = props;`;
|
|
316
|
+
}
|
|
317
|
+
function escapeText(value) {
|
|
318
|
+
return value.replace(/[&<>{}]/g, (char) => {
|
|
319
|
+
switch (char) {
|
|
320
|
+
case "&":
|
|
321
|
+
return "&";
|
|
322
|
+
case "<":
|
|
323
|
+
return "<";
|
|
324
|
+
case ">":
|
|
325
|
+
return ">";
|
|
326
|
+
case "{":
|
|
327
|
+
return "{";
|
|
328
|
+
case "}":
|
|
329
|
+
return "}";
|
|
330
|
+
default:
|
|
331
|
+
return char;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/html-codegen.ts
|
|
337
|
+
function generateHtmlModule(root, options) {
|
|
338
|
+
const aliasEnv = buildClassAliasEnvironment2(root.classAliases);
|
|
339
|
+
const htmlExpression = emitNodesString(root.children, aliasEnv);
|
|
340
|
+
const propsType = emitJsDocPropsType2(root.props);
|
|
341
|
+
const propsDestructure = emitPropsDestructure2(root.props);
|
|
342
|
+
const parts = [];
|
|
343
|
+
if (root.clientComponent) {
|
|
344
|
+
parts.push(`"use client";`);
|
|
345
|
+
}
|
|
346
|
+
parts.push(...createHtmlHelpers());
|
|
347
|
+
parts.push(propsType);
|
|
348
|
+
parts.push(`/** @param {Props} props */`);
|
|
349
|
+
parts.push(`/** @returns {string} */`);
|
|
350
|
+
const lines = [`export default function ${options.componentName}(props = {}) {`];
|
|
351
|
+
if (propsDestructure) {
|
|
352
|
+
lines.push(` ${propsDestructure}`);
|
|
353
|
+
}
|
|
354
|
+
lines.push(` const __collie_html = ${htmlExpression};`);
|
|
355
|
+
lines.push(" return __collie_html;", "}");
|
|
356
|
+
parts.push(lines.join("\n"));
|
|
357
|
+
return parts.join("\n\n");
|
|
358
|
+
}
|
|
359
|
+
function emitNodesString(children, aliasEnv) {
|
|
360
|
+
if (children.length === 0) {
|
|
361
|
+
return '""';
|
|
362
|
+
}
|
|
363
|
+
const segments = children.map((child) => emitNodeString(child, aliasEnv)).filter(Boolean);
|
|
364
|
+
return concatSegments(segments);
|
|
365
|
+
}
|
|
366
|
+
function emitNodeString(node, aliasEnv) {
|
|
367
|
+
switch (node.type) {
|
|
368
|
+
case "Text":
|
|
369
|
+
return emitTextNode(node);
|
|
370
|
+
case "Expression":
|
|
371
|
+
return `__collie_escapeHtml(${node.value})`;
|
|
372
|
+
case "JSXPassthrough":
|
|
373
|
+
return `String(${node.expression})`;
|
|
374
|
+
case "Element":
|
|
375
|
+
return wrapWithGuard2(emitElement2(node, aliasEnv), node.guard);
|
|
376
|
+
case "Component":
|
|
377
|
+
return wrapWithGuard2(emitComponent2(node, aliasEnv), node.guard);
|
|
378
|
+
case "Conditional":
|
|
379
|
+
return emitConditional(node, aliasEnv);
|
|
380
|
+
case "For":
|
|
381
|
+
return emitFor(node, aliasEnv);
|
|
382
|
+
default:
|
|
383
|
+
return '""';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function emitElement2(node, aliasEnv) {
|
|
387
|
+
const classSegments = expandClasses2(node.classes, aliasEnv);
|
|
388
|
+
const attributeSegments = emitAttributeSegments(node.attributes, classSegments);
|
|
389
|
+
const start = concatSegments([literal(`<${node.name}`), ...attributeSegments, literal(node.children.length > 0 ? ">" : " />")]);
|
|
390
|
+
if (node.children.length === 0) {
|
|
391
|
+
return start;
|
|
392
|
+
}
|
|
393
|
+
const children = emitNodesString(node.children, aliasEnv);
|
|
394
|
+
const end = literal(`</${node.name}>`);
|
|
395
|
+
return concatSegments([start, children, end]);
|
|
396
|
+
}
|
|
397
|
+
function emitComponent2(node, aliasEnv) {
|
|
398
|
+
const attributeSegments = emitAttributeSegments(node.attributes, []);
|
|
399
|
+
const hasChildren = node.children.length > 0 || (node.slots?.length ?? 0) > 0;
|
|
400
|
+
const closingToken = hasChildren ? ">" : " />";
|
|
401
|
+
const start = concatSegments([literal(`<${node.name}`), ...attributeSegments, literal(closingToken)]);
|
|
402
|
+
if (!hasChildren) {
|
|
403
|
+
return start;
|
|
404
|
+
}
|
|
405
|
+
const childSegments = [];
|
|
406
|
+
if (node.children.length) {
|
|
407
|
+
childSegments.push(emitNodesString(node.children, aliasEnv));
|
|
408
|
+
}
|
|
409
|
+
for (const slot of node.slots ?? []) {
|
|
410
|
+
childSegments.push(emitSlotTemplate(slot, aliasEnv));
|
|
411
|
+
}
|
|
412
|
+
const children = concatSegments(childSegments);
|
|
413
|
+
const end = literal(`</${node.name}>`);
|
|
414
|
+
return concatSegments([start, children, end]);
|
|
415
|
+
}
|
|
416
|
+
function emitSlotTemplate(slot, aliasEnv) {
|
|
417
|
+
const start = literal(`<template slot="${slot.name}">`);
|
|
418
|
+
const body = emitNodesString(slot.children, aliasEnv);
|
|
419
|
+
const end = literal("</template>");
|
|
420
|
+
return concatSegments([start, body, end]);
|
|
421
|
+
}
|
|
422
|
+
function emitConditional(node, aliasEnv) {
|
|
423
|
+
if (node.branches.length === 0) {
|
|
424
|
+
return '""';
|
|
425
|
+
}
|
|
426
|
+
const first = node.branches[0];
|
|
427
|
+
if (node.branches.length === 1 && first.test) {
|
|
428
|
+
return `(${first.test}) ? ${emitBranch(first, aliasEnv)} : ""`;
|
|
429
|
+
}
|
|
430
|
+
const hasElse = node.branches[node.branches.length - 1].test === void 0;
|
|
431
|
+
let fallback = hasElse ? emitBranch(node.branches[node.branches.length - 1], aliasEnv) : '""';
|
|
432
|
+
const limit = hasElse ? node.branches.length - 2 : node.branches.length - 1;
|
|
433
|
+
for (let i = limit; i >= 0; i--) {
|
|
434
|
+
const branch = node.branches[i];
|
|
435
|
+
const test = branch.test ?? "false";
|
|
436
|
+
fallback = `(${test}) ? ${emitBranch(branch, aliasEnv)} : ${fallback}`;
|
|
437
|
+
}
|
|
438
|
+
return fallback;
|
|
439
|
+
}
|
|
440
|
+
function emitBranch(branch, aliasEnv) {
|
|
441
|
+
return emitNodesString(branch.body, aliasEnv);
|
|
442
|
+
}
|
|
443
|
+
function emitFor(node, aliasEnv) {
|
|
444
|
+
const body = emitNodesString(node.body, aliasEnv);
|
|
445
|
+
return `(${node.arrayExpr}).map((${node.itemName}) => ${body}).join("")`;
|
|
446
|
+
}
|
|
447
|
+
function emitTextNode(node) {
|
|
448
|
+
if (!node.parts.length) {
|
|
449
|
+
return '""';
|
|
450
|
+
}
|
|
451
|
+
const segments = node.parts.map((part) => {
|
|
452
|
+
if (part.type === "text") {
|
|
453
|
+
return literal(escapeStaticText(part.value));
|
|
454
|
+
}
|
|
455
|
+
return `__collie_escapeHtml(${part.value})`;
|
|
456
|
+
});
|
|
457
|
+
return concatSegments(segments);
|
|
458
|
+
}
|
|
459
|
+
function emitAttributeSegments(attributes, classNames) {
|
|
460
|
+
const segments = [];
|
|
461
|
+
if (classNames.length) {
|
|
462
|
+
segments.push(literal(` class="${classNames.join(" ")}"`));
|
|
463
|
+
}
|
|
464
|
+
for (const attr of attributes) {
|
|
465
|
+
if (attr.value === null) {
|
|
466
|
+
segments.push(literal(` ${attr.name}`));
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const expr = attributeExpression(attr.value);
|
|
470
|
+
segments.push(
|
|
471
|
+
[
|
|
472
|
+
"(() => {",
|
|
473
|
+
` const __collie_attr = ${expr};`,
|
|
474
|
+
` return __collie_attr == null ? "" : ${literal(` ${attr.name}="`)} + __collie_escapeAttr(__collie_attr) + ${literal(`"`)};`,
|
|
475
|
+
"})()"
|
|
476
|
+
].join(" ")
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
return segments;
|
|
480
|
+
}
|
|
481
|
+
function attributeExpression(raw) {
|
|
482
|
+
const trimmed = raw.trim();
|
|
483
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
484
|
+
return trimmed.slice(1, -1).trim();
|
|
485
|
+
}
|
|
486
|
+
return trimmed;
|
|
487
|
+
}
|
|
488
|
+
function wrapWithGuard2(rendered, guard) {
|
|
489
|
+
if (!guard) {
|
|
490
|
+
return rendered;
|
|
491
|
+
}
|
|
492
|
+
return `(${guard}) ? ${rendered} : ""`;
|
|
493
|
+
}
|
|
494
|
+
function literal(text) {
|
|
495
|
+
return JSON.stringify(text);
|
|
496
|
+
}
|
|
497
|
+
function concatSegments(segments) {
|
|
498
|
+
const filtered = segments.filter((segment) => segment && segment !== '""');
|
|
499
|
+
if (!filtered.length) {
|
|
500
|
+
return '""';
|
|
501
|
+
}
|
|
502
|
+
if (filtered.length === 1) {
|
|
503
|
+
return filtered[0];
|
|
504
|
+
}
|
|
505
|
+
return filtered.join(" + ");
|
|
506
|
+
}
|
|
507
|
+
function buildClassAliasEnvironment2(decl) {
|
|
508
|
+
const env = /* @__PURE__ */ new Map();
|
|
509
|
+
if (!decl) {
|
|
510
|
+
return env;
|
|
511
|
+
}
|
|
512
|
+
for (const alias of decl.aliases) {
|
|
513
|
+
env.set(alias.name, alias.classes);
|
|
514
|
+
}
|
|
515
|
+
return env;
|
|
516
|
+
}
|
|
517
|
+
function expandClasses2(classes, aliasEnv) {
|
|
518
|
+
const result = [];
|
|
519
|
+
for (const cls of classes) {
|
|
520
|
+
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
521
|
+
if (!match) {
|
|
522
|
+
result.push(cls);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const aliasClasses = aliasEnv.get(match[1]);
|
|
526
|
+
if (!aliasClasses) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
result.push(...aliasClasses);
|
|
530
|
+
}
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
function emitJsDocPropsType2(props) {
|
|
534
|
+
if (!props) {
|
|
535
|
+
return "/** @typedef {any} Props */";
|
|
536
|
+
}
|
|
537
|
+
if (!props.fields.length) {
|
|
538
|
+
return "/** @typedef {{}} Props */";
|
|
539
|
+
}
|
|
540
|
+
const fields = props.fields.map((field) => {
|
|
541
|
+
const optional = field.optional ? "?" : "";
|
|
542
|
+
return `${field.name}${optional}: ${field.typeText}`;
|
|
543
|
+
}).join("; ");
|
|
544
|
+
return `/** @typedef {{ ${fields} }} Props */`;
|
|
545
|
+
}
|
|
546
|
+
function emitPropsDestructure2(props) {
|
|
547
|
+
if (!props || props.fields.length === 0) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
const names = props.fields.map((field) => field.name);
|
|
551
|
+
return `const { ${names.join(", ")} } = props;`;
|
|
552
|
+
}
|
|
553
|
+
function escapeStaticText(value) {
|
|
554
|
+
return value.replace(/[&<>{}]/g, (char) => {
|
|
555
|
+
switch (char) {
|
|
556
|
+
case "&":
|
|
557
|
+
return "&";
|
|
558
|
+
case "<":
|
|
559
|
+
return "<";
|
|
560
|
+
case ">":
|
|
561
|
+
return ">";
|
|
562
|
+
case "{":
|
|
563
|
+
return "{";
|
|
564
|
+
case "}":
|
|
565
|
+
return "}";
|
|
566
|
+
default:
|
|
567
|
+
return char;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function createHtmlHelpers() {
|
|
572
|
+
return [
|
|
573
|
+
"function __collie_escapeHtml(value) {",
|
|
574
|
+
" if (value === null || value === undefined) {",
|
|
575
|
+
' return "";',
|
|
576
|
+
" }",
|
|
577
|
+
" return String(value).replace(/[&<>]/g, __collie_escapeHtmlChar);",
|
|
578
|
+
"}",
|
|
579
|
+
"function __collie_escapeHtmlChar(char) {",
|
|
580
|
+
" switch (char) {",
|
|
581
|
+
' case "&":',
|
|
582
|
+
' return "&";',
|
|
583
|
+
' case "<":',
|
|
584
|
+
' return "<";',
|
|
585
|
+
' case ">":',
|
|
586
|
+
' return ">";',
|
|
587
|
+
" default:",
|
|
588
|
+
" return char;",
|
|
589
|
+
" }",
|
|
590
|
+
"}",
|
|
591
|
+
"function __collie_escapeAttr(value) {",
|
|
592
|
+
" if (value === null || value === undefined) {",
|
|
593
|
+
' return "";',
|
|
594
|
+
" }",
|
|
595
|
+
' return String(value).replace(/["&<>]/g, __collie_escapeAttrChar);',
|
|
596
|
+
"}",
|
|
597
|
+
"function __collie_escapeAttrChar(char) {",
|
|
598
|
+
" switch (char) {",
|
|
599
|
+
' case "&":',
|
|
600
|
+
' return "&";',
|
|
601
|
+
' case "<":',
|
|
602
|
+
' return "<";',
|
|
603
|
+
' case ">":',
|
|
604
|
+
' return ">";',
|
|
605
|
+
` case '"':`,
|
|
606
|
+
' return """;',
|
|
607
|
+
" default:",
|
|
608
|
+
" return char;",
|
|
609
|
+
" }",
|
|
610
|
+
"}"
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/diagnostics.ts
|
|
615
|
+
function createSpan(line, col, length, lineOffset) {
|
|
616
|
+
const startOffset = lineOffset + col - 1;
|
|
617
|
+
return {
|
|
618
|
+
start: { line, col, offset: startOffset },
|
|
619
|
+
end: { line, col: col + length, offset: startOffset + length }
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/parser.ts
|
|
624
|
+
function getIndentLevel(line) {
|
|
625
|
+
const match = line.match(/^\s*/);
|
|
626
|
+
return match ? match[0].length / 2 : 0;
|
|
627
|
+
}
|
|
628
|
+
function parse(source) {
|
|
629
|
+
const diagnostics = [];
|
|
630
|
+
const root = { type: "Root", children: [] };
|
|
631
|
+
const stack = [{ node: root, level: -1 }];
|
|
632
|
+
let propsBlockLevel = null;
|
|
633
|
+
let classesBlockLevel = null;
|
|
634
|
+
let sawTopLevelTemplateNode = false;
|
|
635
|
+
const conditionalChains = /* @__PURE__ */ new Map();
|
|
636
|
+
const branchLocations = [];
|
|
637
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
638
|
+
const lines = normalized.split("\n");
|
|
639
|
+
let offset = 0;
|
|
640
|
+
let i = 0;
|
|
641
|
+
while (i < lines.length) {
|
|
642
|
+
const rawLine = lines[i];
|
|
643
|
+
const lineNumber = i + 1;
|
|
644
|
+
const lineOffset = offset;
|
|
645
|
+
offset += rawLine.length + 1;
|
|
646
|
+
i++;
|
|
647
|
+
if (/^\s*$/.test(rawLine)) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const tabIndex = rawLine.indexOf(" ");
|
|
651
|
+
if (tabIndex !== -1) {
|
|
652
|
+
pushDiag(
|
|
653
|
+
diagnostics,
|
|
654
|
+
"COLLIE001",
|
|
655
|
+
"Tabs are not allowed; use spaces for indentation.",
|
|
656
|
+
lineNumber,
|
|
657
|
+
tabIndex + 1,
|
|
658
|
+
lineOffset
|
|
659
|
+
);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const indentMatch = rawLine.match(/^\s*/) ?? [""];
|
|
663
|
+
const indent = indentMatch[0].length;
|
|
664
|
+
const lineContent = rawLine.slice(indent);
|
|
665
|
+
const trimmed = lineContent.trimEnd();
|
|
666
|
+
if (indent % 2 !== 0) {
|
|
667
|
+
pushDiag(
|
|
668
|
+
diagnostics,
|
|
669
|
+
"COLLIE002",
|
|
670
|
+
"Indentation must be multiples of two spaces.",
|
|
671
|
+
lineNumber,
|
|
672
|
+
indent + 1,
|
|
673
|
+
lineOffset
|
|
674
|
+
);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
let level = indent / 2;
|
|
678
|
+
if (propsBlockLevel !== null && level <= propsBlockLevel) {
|
|
679
|
+
propsBlockLevel = null;
|
|
680
|
+
}
|
|
681
|
+
if (classesBlockLevel !== null && level <= classesBlockLevel) {
|
|
682
|
+
classesBlockLevel = null;
|
|
683
|
+
}
|
|
684
|
+
const top = stack[stack.length - 1];
|
|
685
|
+
const isInPropsBlock = propsBlockLevel !== null && level > propsBlockLevel;
|
|
686
|
+
const isInClassesBlock = classesBlockLevel !== null && level > classesBlockLevel;
|
|
687
|
+
if (level > top.level + 1 && !isInPropsBlock && !isInClassesBlock) {
|
|
688
|
+
pushDiag(
|
|
689
|
+
diagnostics,
|
|
690
|
+
"COLLIE003",
|
|
691
|
+
"Indentation jumped more than one level.",
|
|
692
|
+
lineNumber,
|
|
693
|
+
indent + 1,
|
|
694
|
+
lineOffset
|
|
695
|
+
);
|
|
696
|
+
level = top.level + 1;
|
|
697
|
+
}
|
|
698
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
|
699
|
+
stack.pop();
|
|
700
|
+
}
|
|
701
|
+
cleanupConditionalChains(conditionalChains, level);
|
|
702
|
+
const isElseIfLine = /^@elseIf\b/.test(trimmed);
|
|
703
|
+
const isElseLine = /^@else\b/.test(trimmed) && !isElseIfLine;
|
|
704
|
+
if (!isElseIfLine && !isElseLine) {
|
|
705
|
+
conditionalChains.delete(level);
|
|
706
|
+
}
|
|
707
|
+
if (trimmed === "classes") {
|
|
708
|
+
if (level !== 0) {
|
|
709
|
+
pushDiag(
|
|
710
|
+
diagnostics,
|
|
711
|
+
"COLLIE301",
|
|
712
|
+
"Classes block must be at the top level.",
|
|
713
|
+
lineNumber,
|
|
714
|
+
indent + 1,
|
|
715
|
+
lineOffset,
|
|
716
|
+
trimmed.length
|
|
717
|
+
);
|
|
718
|
+
} else if (sawTopLevelTemplateNode) {
|
|
719
|
+
pushDiag(
|
|
720
|
+
diagnostics,
|
|
721
|
+
"COLLIE302",
|
|
722
|
+
"Classes block must appear before any template nodes.",
|
|
723
|
+
lineNumber,
|
|
724
|
+
indent + 1,
|
|
725
|
+
lineOffset,
|
|
726
|
+
trimmed.length
|
|
727
|
+
);
|
|
728
|
+
} else {
|
|
729
|
+
if (!root.classAliases) {
|
|
730
|
+
root.classAliases = { aliases: [] };
|
|
731
|
+
}
|
|
732
|
+
classesBlockLevel = level;
|
|
733
|
+
}
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (trimmed === "props") {
|
|
737
|
+
if (level !== 0) {
|
|
738
|
+
pushDiag(
|
|
739
|
+
diagnostics,
|
|
740
|
+
"COLLIE102",
|
|
741
|
+
"Props block must be at the top level.",
|
|
742
|
+
lineNumber,
|
|
743
|
+
indent + 1,
|
|
744
|
+
lineOffset,
|
|
745
|
+
trimmed.length
|
|
746
|
+
);
|
|
747
|
+
} else if (sawTopLevelTemplateNode || root.props) {
|
|
748
|
+
pushDiag(
|
|
749
|
+
diagnostics,
|
|
750
|
+
"COLLIE101",
|
|
751
|
+
"Props block must appear before any template nodes.",
|
|
752
|
+
lineNumber,
|
|
753
|
+
indent + 1,
|
|
754
|
+
lineOffset,
|
|
755
|
+
trimmed.length
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
root.props = { fields: [] };
|
|
759
|
+
propsBlockLevel = level;
|
|
760
|
+
}
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (trimmed === "@client") {
|
|
764
|
+
if (level !== 0) {
|
|
765
|
+
pushDiag(
|
|
766
|
+
diagnostics,
|
|
767
|
+
"COLLIE401",
|
|
768
|
+
"@client must appear at the top level before any other blocks.",
|
|
769
|
+
lineNumber,
|
|
770
|
+
indent + 1,
|
|
771
|
+
lineOffset,
|
|
772
|
+
trimmed.length
|
|
773
|
+
);
|
|
774
|
+
} else if (sawTopLevelTemplateNode) {
|
|
775
|
+
pushDiag(
|
|
776
|
+
diagnostics,
|
|
777
|
+
"COLLIE401",
|
|
778
|
+
"@client must appear before any template nodes.",
|
|
779
|
+
lineNumber,
|
|
780
|
+
indent + 1,
|
|
781
|
+
lineOffset,
|
|
782
|
+
trimmed.length
|
|
783
|
+
);
|
|
784
|
+
} else if (root.clientComponent) {
|
|
785
|
+
pushDiag(
|
|
786
|
+
diagnostics,
|
|
787
|
+
"COLLIE402",
|
|
788
|
+
"@client can only appear once per file.",
|
|
789
|
+
lineNumber,
|
|
790
|
+
indent + 1,
|
|
791
|
+
lineOffset,
|
|
792
|
+
trimmed.length
|
|
793
|
+
);
|
|
794
|
+
} else {
|
|
795
|
+
root.clientComponent = true;
|
|
796
|
+
}
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (propsBlockLevel !== null && level > propsBlockLevel) {
|
|
800
|
+
if (level !== propsBlockLevel + 1) {
|
|
801
|
+
pushDiag(
|
|
802
|
+
diagnostics,
|
|
803
|
+
"COLLIE102",
|
|
804
|
+
"Props lines must be indented two spaces under the props header.",
|
|
805
|
+
lineNumber,
|
|
806
|
+
indent + 1,
|
|
807
|
+
lineOffset
|
|
808
|
+
);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
const field = parsePropsField(trimmed, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
812
|
+
if (field && root.props) {
|
|
813
|
+
root.props.fields.push(field);
|
|
814
|
+
}
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (classesBlockLevel !== null && level > classesBlockLevel) {
|
|
818
|
+
if (level !== classesBlockLevel + 1) {
|
|
819
|
+
pushDiag(
|
|
820
|
+
diagnostics,
|
|
821
|
+
"COLLIE303",
|
|
822
|
+
"Classes lines must be indented two spaces under the classes header.",
|
|
823
|
+
lineNumber,
|
|
824
|
+
indent + 1,
|
|
825
|
+
lineOffset
|
|
826
|
+
);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const alias = parseClassAliasLine(trimmed, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
830
|
+
if (alias && root.classAliases) {
|
|
831
|
+
root.classAliases.aliases.push(alias);
|
|
832
|
+
}
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const parent = stack[stack.length - 1].node;
|
|
836
|
+
if (trimmed.startsWith("@for")) {
|
|
837
|
+
const forHeader = parseForHeader(
|
|
838
|
+
lineContent,
|
|
839
|
+
lineNumber,
|
|
840
|
+
indent + 1,
|
|
841
|
+
lineOffset,
|
|
842
|
+
diagnostics
|
|
843
|
+
);
|
|
844
|
+
if (!forHeader) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const forNode = {
|
|
848
|
+
type: "For",
|
|
849
|
+
itemName: forHeader.itemName,
|
|
850
|
+
arrayExpr: forHeader.arrayExpr,
|
|
851
|
+
body: []
|
|
852
|
+
};
|
|
853
|
+
addChildToParent(parent, forNode);
|
|
854
|
+
if (parent === root) {
|
|
855
|
+
sawTopLevelTemplateNode = true;
|
|
856
|
+
}
|
|
857
|
+
stack.push({ node: forNode, level });
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (trimmed.startsWith("@if")) {
|
|
861
|
+
const header = parseConditionalHeader(
|
|
862
|
+
"if",
|
|
863
|
+
lineContent,
|
|
864
|
+
lineNumber,
|
|
865
|
+
indent + 1,
|
|
866
|
+
lineOffset,
|
|
867
|
+
diagnostics
|
|
868
|
+
);
|
|
869
|
+
if (!header) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const chain = { type: "Conditional", branches: [] };
|
|
873
|
+
const branch = { test: header.test, body: [] };
|
|
874
|
+
chain.branches.push(branch);
|
|
875
|
+
addChildToParent(parent, chain);
|
|
876
|
+
if (parent === root) {
|
|
877
|
+
sawTopLevelTemplateNode = true;
|
|
878
|
+
}
|
|
879
|
+
conditionalChains.set(level, { node: chain, level, hasElse: false });
|
|
880
|
+
branchLocations.push({
|
|
881
|
+
branch,
|
|
882
|
+
line: lineNumber,
|
|
883
|
+
column: indent + 1,
|
|
884
|
+
lineOffset,
|
|
885
|
+
length: header.directiveLength
|
|
886
|
+
});
|
|
887
|
+
if (header.inlineBody) {
|
|
888
|
+
const inlineNode = parseInlineNode(
|
|
889
|
+
header.inlineBody,
|
|
890
|
+
lineNumber,
|
|
891
|
+
header.inlineColumn ?? indent + 1,
|
|
892
|
+
lineOffset,
|
|
893
|
+
diagnostics
|
|
894
|
+
);
|
|
895
|
+
if (inlineNode) {
|
|
896
|
+
branch.body.push(inlineNode);
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
stack.push({ node: createConditionalBranchContext(chain, branch), level });
|
|
900
|
+
}
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (isElseIfLine) {
|
|
904
|
+
const chain = conditionalChains.get(level);
|
|
905
|
+
if (!chain) {
|
|
906
|
+
pushDiag(
|
|
907
|
+
diagnostics,
|
|
908
|
+
"COLLIE205",
|
|
909
|
+
"@elseIf must follow an @if at the same indentation level.",
|
|
910
|
+
lineNumber,
|
|
911
|
+
indent + 1,
|
|
912
|
+
lineOffset,
|
|
913
|
+
trimmed.length
|
|
914
|
+
);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (chain.hasElse) {
|
|
918
|
+
pushDiag(
|
|
919
|
+
diagnostics,
|
|
920
|
+
"COLLIE207",
|
|
921
|
+
"@elseIf cannot appear after an @else in the same chain.",
|
|
922
|
+
lineNumber,
|
|
923
|
+
indent + 1,
|
|
924
|
+
lineOffset,
|
|
925
|
+
trimmed.length
|
|
926
|
+
);
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const header = parseConditionalHeader(
|
|
930
|
+
"elseIf",
|
|
931
|
+
lineContent,
|
|
932
|
+
lineNumber,
|
|
933
|
+
indent + 1,
|
|
934
|
+
lineOffset,
|
|
935
|
+
diagnostics
|
|
936
|
+
);
|
|
937
|
+
if (!header) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const branch = { test: header.test, body: [] };
|
|
941
|
+
chain.node.branches.push(branch);
|
|
942
|
+
branchLocations.push({
|
|
943
|
+
branch,
|
|
944
|
+
line: lineNumber,
|
|
945
|
+
column: indent + 1,
|
|
946
|
+
lineOffset,
|
|
947
|
+
length: header.directiveLength
|
|
948
|
+
});
|
|
949
|
+
if (header.inlineBody) {
|
|
950
|
+
const inlineNode = parseInlineNode(
|
|
951
|
+
header.inlineBody,
|
|
952
|
+
lineNumber,
|
|
953
|
+
header.inlineColumn ?? indent + 1,
|
|
954
|
+
lineOffset,
|
|
955
|
+
diagnostics
|
|
956
|
+
);
|
|
957
|
+
if (inlineNode) {
|
|
958
|
+
branch.body.push(inlineNode);
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
stack.push({ node: createConditionalBranchContext(chain.node, branch), level });
|
|
962
|
+
}
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (isElseLine) {
|
|
966
|
+
const chain = conditionalChains.get(level);
|
|
967
|
+
if (!chain) {
|
|
968
|
+
pushDiag(
|
|
969
|
+
diagnostics,
|
|
970
|
+
"COLLIE206",
|
|
971
|
+
"@else must follow an @if at the same indentation level.",
|
|
972
|
+
lineNumber,
|
|
973
|
+
indent + 1,
|
|
974
|
+
lineOffset,
|
|
975
|
+
trimmed.length
|
|
976
|
+
);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (chain.hasElse) {
|
|
980
|
+
pushDiag(
|
|
981
|
+
diagnostics,
|
|
982
|
+
"COLLIE203",
|
|
983
|
+
"An @if chain can only have one @else branch.",
|
|
984
|
+
lineNumber,
|
|
985
|
+
indent + 1,
|
|
986
|
+
lineOffset,
|
|
987
|
+
trimmed.length
|
|
988
|
+
);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
const header = parseElseHeader(lineContent, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
992
|
+
if (!header) {
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
const branch = { test: void 0, body: [] };
|
|
996
|
+
chain.node.branches.push(branch);
|
|
997
|
+
chain.hasElse = true;
|
|
998
|
+
branchLocations.push({
|
|
999
|
+
branch,
|
|
1000
|
+
line: lineNumber,
|
|
1001
|
+
column: indent + 1,
|
|
1002
|
+
lineOffset,
|
|
1003
|
+
length: header.directiveLength
|
|
1004
|
+
});
|
|
1005
|
+
if (header.inlineBody) {
|
|
1006
|
+
const inlineNode = parseInlineNode(
|
|
1007
|
+
header.inlineBody,
|
|
1008
|
+
lineNumber,
|
|
1009
|
+
header.inlineColumn ?? indent + 1,
|
|
1010
|
+
lineOffset,
|
|
1011
|
+
diagnostics
|
|
1012
|
+
);
|
|
1013
|
+
if (inlineNode) {
|
|
1014
|
+
branch.body.push(inlineNode);
|
|
1015
|
+
}
|
|
1016
|
+
} else {
|
|
1017
|
+
stack.push({ node: createConditionalBranchContext(chain.node, branch), level });
|
|
1018
|
+
}
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const slotMatch = trimmed.match(/^@([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
1022
|
+
if (slotMatch) {
|
|
1023
|
+
const slotName = slotMatch[1];
|
|
1024
|
+
if (!isComponentNode(parent)) {
|
|
1025
|
+
pushDiag(
|
|
1026
|
+
diagnostics,
|
|
1027
|
+
"COLLIE501",
|
|
1028
|
+
`Slot '${slotName}' must be a direct child of a component.`,
|
|
1029
|
+
lineNumber,
|
|
1030
|
+
indent + 1,
|
|
1031
|
+
lineOffset,
|
|
1032
|
+
trimmed.length
|
|
1033
|
+
);
|
|
1034
|
+
stack.push({ node: createStandaloneSlotContext(slotName), level });
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (!parent.slots) {
|
|
1038
|
+
parent.slots = [];
|
|
1039
|
+
}
|
|
1040
|
+
const existing = parent.slots.find((slot) => slot.name === slotName);
|
|
1041
|
+
const slotBlock = existing ?? {
|
|
1042
|
+
type: "Slot",
|
|
1043
|
+
name: slotName,
|
|
1044
|
+
children: []
|
|
1045
|
+
};
|
|
1046
|
+
if (!existing) {
|
|
1047
|
+
parent.slots.push(slotBlock);
|
|
1048
|
+
} else {
|
|
1049
|
+
pushDiag(
|
|
1050
|
+
diagnostics,
|
|
1051
|
+
"COLLIE503",
|
|
1052
|
+
`Duplicate slot '${slotName}' inside ${parent.name}.`,
|
|
1053
|
+
lineNumber,
|
|
1054
|
+
indent + 1,
|
|
1055
|
+
lineOffset,
|
|
1056
|
+
trimmed.length
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
stack.push({ node: createSlotContext(parent, slotBlock), level });
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (trimmed.startsWith("@")) {
|
|
1063
|
+
pushDiag(
|
|
1064
|
+
diagnostics,
|
|
1065
|
+
"COLLIE502",
|
|
1066
|
+
"Invalid slot syntax. Use @slotName on its own line.",
|
|
1067
|
+
lineNumber,
|
|
1068
|
+
indent + 1,
|
|
1069
|
+
lineOffset,
|
|
1070
|
+
trimmed.length
|
|
1071
|
+
);
|
|
1072
|
+
const fallbackName = trimmed.slice(1).split(/\s+/)[0] || "slot";
|
|
1073
|
+
stack.push({ node: createStandaloneSlotContext(fallbackName), level });
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (lineContent.startsWith("=")) {
|
|
1077
|
+
const payload = lineContent.slice(1).trim();
|
|
1078
|
+
if (payload.endsWith("(") || payload.endsWith("<") || i < lines.length && level < getIndentLevel(lines[i])) {
|
|
1079
|
+
let jsxContent = payload;
|
|
1080
|
+
const jsxStartLine = i;
|
|
1081
|
+
while (i < lines.length) {
|
|
1082
|
+
const nextRaw = lines[i];
|
|
1083
|
+
const nextIndent = getIndentLevel(nextRaw);
|
|
1084
|
+
const nextTrimmed = nextRaw.trim();
|
|
1085
|
+
if (nextIndent > level && nextTrimmed.length > 0) {
|
|
1086
|
+
jsxContent += "\n" + nextRaw;
|
|
1087
|
+
i++;
|
|
1088
|
+
} else if (nextIndent === level && /^[)\]}]+$/.test(nextTrimmed)) {
|
|
1089
|
+
jsxContent += "\n" + nextRaw;
|
|
1090
|
+
i++;
|
|
1091
|
+
break;
|
|
1092
|
+
} else {
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
const jsxNode2 = {
|
|
1097
|
+
type: "JSXPassthrough",
|
|
1098
|
+
expression: jsxContent
|
|
1099
|
+
};
|
|
1100
|
+
addChildToParent(parent, jsxNode2);
|
|
1101
|
+
if (parent === root) {
|
|
1102
|
+
sawTopLevelTemplateNode = true;
|
|
1103
|
+
}
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const jsxNode = parseJSXPassthrough(lineContent, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
1107
|
+
if (jsxNode) {
|
|
1108
|
+
addChildToParent(parent, jsxNode);
|
|
1109
|
+
if (parent === root) {
|
|
1110
|
+
sawTopLevelTemplateNode = true;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
if (lineContent.startsWith("|")) {
|
|
1116
|
+
const textNode = parseTextLine(lineContent, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
1117
|
+
if (textNode) {
|
|
1118
|
+
addChildToParent(parent, textNode);
|
|
1119
|
+
if (parent === root) {
|
|
1120
|
+
sawTopLevelTemplateNode = true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (lineContent.startsWith("{{")) {
|
|
1126
|
+
const exprNode = parseExpressionLine(lineContent, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
1127
|
+
if (exprNode) {
|
|
1128
|
+
addChildToParent(parent, exprNode);
|
|
1129
|
+
if (parent === root) {
|
|
1130
|
+
sawTopLevelTemplateNode = true;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
let fullLine = trimmed;
|
|
1136
|
+
let multilineEnd = i;
|
|
1137
|
+
if (trimmed.includes("(") && !trimmed.includes(")")) {
|
|
1138
|
+
let parenDepth = (trimmed.match(/\(/g) || []).length - (trimmed.match(/\)/g) || []).length;
|
|
1139
|
+
while (multilineEnd < lines.length && parenDepth > 0) {
|
|
1140
|
+
const nextRaw = lines[multilineEnd];
|
|
1141
|
+
multilineEnd++;
|
|
1142
|
+
fullLine += "\n" + nextRaw;
|
|
1143
|
+
parenDepth += (nextRaw.match(/\(/g) || []).length - (nextRaw.match(/\)/g) || []).length;
|
|
1144
|
+
}
|
|
1145
|
+
i = multilineEnd;
|
|
1146
|
+
}
|
|
1147
|
+
const element = parseElement(fullLine, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
1148
|
+
if (!element) {
|
|
1149
|
+
const textNode = parseTextPayload(trimmed, lineNumber, indent + 1, lineOffset, diagnostics);
|
|
1150
|
+
if (textNode && textNode.parts.length > 0) {
|
|
1151
|
+
addChildToParent(parent, textNode);
|
|
1152
|
+
if (parent === root) {
|
|
1153
|
+
sawTopLevelTemplateNode = true;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
addChildToParent(parent, element);
|
|
1159
|
+
if (parent === root) {
|
|
1160
|
+
sawTopLevelTemplateNode = true;
|
|
1161
|
+
}
|
|
1162
|
+
stack.push({ node: element, level });
|
|
1163
|
+
}
|
|
1164
|
+
if (root.classAliases) {
|
|
1165
|
+
validateClassAliasDefinitions(root.classAliases, diagnostics);
|
|
1166
|
+
}
|
|
1167
|
+
validateClassAliasUsages(root, diagnostics);
|
|
1168
|
+
for (const info of branchLocations) {
|
|
1169
|
+
if (info.branch.body.length === 0) {
|
|
1170
|
+
pushDiag(
|
|
1171
|
+
diagnostics,
|
|
1172
|
+
"COLLIE208",
|
|
1173
|
+
"Conditional branches must include an inline body or indented block.",
|
|
1174
|
+
info.line,
|
|
1175
|
+
info.column,
|
|
1176
|
+
info.lineOffset,
|
|
1177
|
+
info.length || 3
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return { root, diagnostics };
|
|
1182
|
+
}
|
|
1183
|
+
function cleanupConditionalChains(state, level) {
|
|
1184
|
+
for (const key of Array.from(state.keys())) {
|
|
1185
|
+
if (key > level) {
|
|
1186
|
+
state.delete(key);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
function addChildToParent(parent, child) {
|
|
1191
|
+
if (isForParent(parent)) {
|
|
1192
|
+
parent.body.push(child);
|
|
1193
|
+
} else {
|
|
1194
|
+
parent.children.push(child);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function isForParent(parent) {
|
|
1198
|
+
return "type" in parent && parent.type === "For";
|
|
1199
|
+
}
|
|
1200
|
+
function isComponentNode(parent) {
|
|
1201
|
+
return "type" in parent && parent.type === "Component";
|
|
1202
|
+
}
|
|
1203
|
+
function parseConditionalHeader(kind, lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
1204
|
+
const trimmed = lineContent.trimEnd();
|
|
1205
|
+
const pattern = kind === "if" ? /^@if\s*\((.*)\)(.*)$/ : /^@elseIf\s*\((.*)\)(.*)$/;
|
|
1206
|
+
const match = trimmed.match(pattern);
|
|
1207
|
+
if (!match) {
|
|
1208
|
+
pushDiag(
|
|
1209
|
+
diagnostics,
|
|
1210
|
+
"COLLIE201",
|
|
1211
|
+
kind === "if" ? "Invalid @if syntax. Use @if (condition)." : "Invalid @elseIf syntax. Use @elseIf (condition).",
|
|
1212
|
+
lineNumber,
|
|
1213
|
+
column,
|
|
1214
|
+
lineOffset,
|
|
1215
|
+
trimmed.length || 3
|
|
1216
|
+
);
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
const test = match[1].trim();
|
|
1220
|
+
if (!test) {
|
|
1221
|
+
pushDiag(
|
|
1222
|
+
diagnostics,
|
|
1223
|
+
"COLLIE201",
|
|
1224
|
+
kind === "if" ? "@if condition cannot be empty." : "@elseIf condition cannot be empty.",
|
|
1225
|
+
lineNumber,
|
|
1226
|
+
column,
|
|
1227
|
+
lineOffset,
|
|
1228
|
+
trimmed.length || 3
|
|
1229
|
+
);
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
const remainderRaw = match[2] ?? "";
|
|
1233
|
+
const inlineBody = remainderRaw.trim();
|
|
1234
|
+
const remainderOffset = trimmed.length - remainderRaw.length;
|
|
1235
|
+
const leadingWhitespace = remainderRaw.length - inlineBody.length;
|
|
1236
|
+
const inlineColumn = inlineBody.length > 0 ? column + remainderOffset + leadingWhitespace : void 0;
|
|
1237
|
+
return {
|
|
1238
|
+
test,
|
|
1239
|
+
inlineBody: inlineBody.length ? inlineBody : void 0,
|
|
1240
|
+
inlineColumn,
|
|
1241
|
+
directiveLength: trimmed.length || 3
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function parseElseHeader(lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
1245
|
+
const trimmed = lineContent.trimEnd();
|
|
1246
|
+
const match = trimmed.match(/^@else\b(.*)$/);
|
|
1247
|
+
if (!match) {
|
|
1248
|
+
pushDiag(
|
|
1249
|
+
diagnostics,
|
|
1250
|
+
"COLLIE203",
|
|
1251
|
+
"Invalid @else syntax.",
|
|
1252
|
+
lineNumber,
|
|
1253
|
+
column,
|
|
1254
|
+
lineOffset,
|
|
1255
|
+
trimmed.length || 4
|
|
1256
|
+
);
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
const remainderRaw = match[1] ?? "";
|
|
1260
|
+
const inlineBody = remainderRaw.trim();
|
|
1261
|
+
const remainderOffset = trimmed.length - remainderRaw.length;
|
|
1262
|
+
const leadingWhitespace = remainderRaw.length - inlineBody.length;
|
|
1263
|
+
const inlineColumn = inlineBody.length > 0 ? column + remainderOffset + leadingWhitespace : void 0;
|
|
1264
|
+
return {
|
|
1265
|
+
inlineBody: inlineBody.length ? inlineBody : void 0,
|
|
1266
|
+
inlineColumn,
|
|
1267
|
+
directiveLength: trimmed.length || 4
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function parseForHeader(lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
1271
|
+
const trimmed = lineContent.trimEnd();
|
|
1272
|
+
const match = trimmed.match(/^@for\s+([A-Za-z_][A-Za-z0-9_]*)\s+in\s+(.+)$/);
|
|
1273
|
+
if (!match) {
|
|
1274
|
+
pushDiag(
|
|
1275
|
+
diagnostics,
|
|
1276
|
+
"COLLIE210",
|
|
1277
|
+
"Invalid @for syntax. Use @for itemName in arrayExpr.",
|
|
1278
|
+
lineNumber,
|
|
1279
|
+
column,
|
|
1280
|
+
lineOffset,
|
|
1281
|
+
trimmed.length || 4
|
|
1282
|
+
);
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
const itemName = match[1];
|
|
1286
|
+
const arrayExprRaw = match[2];
|
|
1287
|
+
if (!itemName || !arrayExprRaw) {
|
|
1288
|
+
pushDiag(
|
|
1289
|
+
diagnostics,
|
|
1290
|
+
"COLLIE210",
|
|
1291
|
+
"Invalid @for syntax. Use @for itemName in arrayExpr.",
|
|
1292
|
+
lineNumber,
|
|
1293
|
+
column,
|
|
1294
|
+
lineOffset,
|
|
1295
|
+
trimmed.length || 4
|
|
1296
|
+
);
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
const arrayExpr = arrayExprRaw.trim();
|
|
1300
|
+
if (!arrayExpr) {
|
|
1301
|
+
pushDiag(
|
|
1302
|
+
diagnostics,
|
|
1303
|
+
"COLLIE210",
|
|
1304
|
+
"@for array expression cannot be empty.",
|
|
1305
|
+
lineNumber,
|
|
1306
|
+
column,
|
|
1307
|
+
lineOffset,
|
|
1308
|
+
trimmed.length || 4
|
|
1309
|
+
);
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
return { itemName, arrayExpr };
|
|
1313
|
+
}
|
|
1314
|
+
function parseInlineNode(source, lineNumber, column, lineOffset, diagnostics) {
|
|
1315
|
+
const trimmed = source.trim();
|
|
1316
|
+
if (!trimmed) {
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
if (trimmed.startsWith("|")) {
|
|
1320
|
+
return parseTextLine(trimmed, lineNumber, column, lineOffset, diagnostics);
|
|
1321
|
+
}
|
|
1322
|
+
if (trimmed.startsWith("{{")) {
|
|
1323
|
+
return parseExpressionLine(trimmed, lineNumber, column, lineOffset, diagnostics);
|
|
1324
|
+
}
|
|
1325
|
+
if (trimmed.startsWith("@")) {
|
|
1326
|
+
pushDiag(
|
|
1327
|
+
diagnostics,
|
|
1328
|
+
"COLLIE209",
|
|
1329
|
+
"Inline conditional bodies may only contain elements, text, or expressions.",
|
|
1330
|
+
lineNumber,
|
|
1331
|
+
column,
|
|
1332
|
+
lineOffset,
|
|
1333
|
+
trimmed.length
|
|
1334
|
+
);
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
return parseElement(trimmed, lineNumber, column, lineOffset, diagnostics);
|
|
1338
|
+
}
|
|
1339
|
+
function createConditionalBranchContext(owner, branch) {
|
|
1340
|
+
return {
|
|
1341
|
+
kind: "ConditionalBranch",
|
|
1342
|
+
owner,
|
|
1343
|
+
branch,
|
|
1344
|
+
children: branch.body
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
function createSlotContext(owner, slot) {
|
|
1348
|
+
return {
|
|
1349
|
+
kind: "Slot",
|
|
1350
|
+
owner,
|
|
1351
|
+
slot,
|
|
1352
|
+
children: slot.children
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function createStandaloneSlotContext(name) {
|
|
1356
|
+
const owner = {
|
|
1357
|
+
type: "Component",
|
|
1358
|
+
name: "__invalid_slot__",
|
|
1359
|
+
attributes: [],
|
|
1360
|
+
children: []
|
|
1361
|
+
};
|
|
1362
|
+
const slot = { type: "Slot", name, children: [] };
|
|
1363
|
+
return createSlotContext(owner, slot);
|
|
1364
|
+
}
|
|
1365
|
+
function parseTextLine(lineContent, lineNumber, column, lineOffset, diagnostics) {
|
|
1366
|
+
const trimmed = lineContent.trimEnd();
|
|
1367
|
+
let payload = trimmed;
|
|
1368
|
+
let payloadColumn = column;
|
|
1369
|
+
if (payload.startsWith("|")) {
|
|
1370
|
+
payload = payload.slice(1);
|
|
1371
|
+
payloadColumn += 1;
|
|
1372
|
+
if (payload.startsWith(" ")) {
|
|
1373
|
+
payload = payload.slice(1);
|
|
1374
|
+
payloadColumn += 1;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return parseTextPayload(payload, lineNumber, payloadColumn, lineOffset, diagnostics);
|
|
1378
|
+
}
|
|
1379
|
+
function parseTextPayload(payload, lineNumber, payloadColumn, lineOffset, diagnostics) {
|
|
1380
|
+
const parts = [];
|
|
1381
|
+
let cursor = 0;
|
|
1382
|
+
let textBuffer = "";
|
|
1383
|
+
const flushText = () => {
|
|
1384
|
+
if (textBuffer.length) {
|
|
1385
|
+
parts.push({ type: "text", value: textBuffer });
|
|
1386
|
+
textBuffer = "";
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
while (cursor < payload.length) {
|
|
1390
|
+
const ch = payload[cursor];
|
|
1391
|
+
if (ch === "{") {
|
|
1392
|
+
flushText();
|
|
1393
|
+
if (payload[cursor + 1] === "{") {
|
|
1394
|
+
const exprStart2 = cursor;
|
|
1395
|
+
const exprEnd2 = payload.indexOf("}}", cursor + 2);
|
|
1396
|
+
if (exprEnd2 === -1) {
|
|
1397
|
+
pushDiag(
|
|
1398
|
+
diagnostics,
|
|
1399
|
+
"COLLIE005",
|
|
1400
|
+
"Inline expression must end with }}.",
|
|
1401
|
+
lineNumber,
|
|
1402
|
+
payloadColumn + exprStart2,
|
|
1403
|
+
lineOffset
|
|
1404
|
+
);
|
|
1405
|
+
textBuffer += payload.slice(exprStart2);
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
const inner2 = payload.slice(exprStart2 + 2, exprEnd2).trim();
|
|
1409
|
+
if (!inner2) {
|
|
1410
|
+
pushDiag(
|
|
1411
|
+
diagnostics,
|
|
1412
|
+
"COLLIE005",
|
|
1413
|
+
"Inline expression cannot be empty.",
|
|
1414
|
+
lineNumber,
|
|
1415
|
+
payloadColumn + exprStart2,
|
|
1416
|
+
lineOffset,
|
|
1417
|
+
exprEnd2 - exprStart2
|
|
1418
|
+
);
|
|
1419
|
+
} else {
|
|
1420
|
+
parts.push({ type: "expr", value: inner2 });
|
|
1421
|
+
}
|
|
1422
|
+
cursor = exprEnd2 + 2;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
const exprStart = cursor;
|
|
1426
|
+
const exprEnd = payload.indexOf("}", cursor + 1);
|
|
1427
|
+
if (exprEnd === -1) {
|
|
1428
|
+
pushDiag(
|
|
1429
|
+
diagnostics,
|
|
1430
|
+
"COLLIE005",
|
|
1431
|
+
"Inline expression must end with }.",
|
|
1432
|
+
lineNumber,
|
|
1433
|
+
payloadColumn + exprStart,
|
|
1434
|
+
lineOffset
|
|
1435
|
+
);
|
|
1436
|
+
textBuffer += payload.slice(exprStart);
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
const inner = payload.slice(exprStart + 1, exprEnd).trim();
|
|
1440
|
+
if (!inner) {
|
|
1441
|
+
pushDiag(
|
|
1442
|
+
diagnostics,
|
|
1443
|
+
"COLLIE005",
|
|
1444
|
+
"Inline expression cannot be empty.",
|
|
1445
|
+
lineNumber,
|
|
1446
|
+
payloadColumn + exprStart,
|
|
1447
|
+
lineOffset,
|
|
1448
|
+
exprEnd - exprStart
|
|
1449
|
+
);
|
|
1450
|
+
} else {
|
|
1451
|
+
parts.push({ type: "expr", value: inner });
|
|
1452
|
+
}
|
|
1453
|
+
cursor = exprEnd + 1;
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (ch === "}") {
|
|
1457
|
+
flushText();
|
|
1458
|
+
if (payload[cursor + 1] === "}") {
|
|
1459
|
+
pushDiag(
|
|
1460
|
+
diagnostics,
|
|
1461
|
+
"COLLIE005",
|
|
1462
|
+
"Inline expression closing }} must follow an opening {{.",
|
|
1463
|
+
lineNumber,
|
|
1464
|
+
payloadColumn + cursor,
|
|
1465
|
+
lineOffset,
|
|
1466
|
+
2
|
|
1467
|
+
);
|
|
1468
|
+
cursor += 2;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
pushDiag(
|
|
1472
|
+
diagnostics,
|
|
1473
|
+
"COLLIE005",
|
|
1474
|
+
"Inline expression closing } must follow an opening {.",
|
|
1475
|
+
lineNumber,
|
|
1476
|
+
payloadColumn + cursor,
|
|
1477
|
+
lineOffset
|
|
1478
|
+
);
|
|
1479
|
+
cursor += 1;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
textBuffer += ch;
|
|
1483
|
+
cursor += 1;
|
|
1484
|
+
}
|
|
1485
|
+
flushText();
|
|
1486
|
+
return { type: "Text", parts };
|
|
1487
|
+
}
|
|
1488
|
+
function parseExpressionLine(line, lineNumber, column, lineOffset, diagnostics) {
|
|
1489
|
+
const trimmed = line.trimEnd();
|
|
1490
|
+
const closeIndex = trimmed.indexOf("}}");
|
|
1491
|
+
if (closeIndex === -1) {
|
|
1492
|
+
pushDiag(
|
|
1493
|
+
diagnostics,
|
|
1494
|
+
"COLLIE005",
|
|
1495
|
+
"Expression lines must end with }}.",
|
|
1496
|
+
lineNumber,
|
|
1497
|
+
column,
|
|
1498
|
+
lineOffset
|
|
1499
|
+
);
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
if (trimmed.slice(closeIndex + 2).trim().length) {
|
|
1503
|
+
pushDiag(
|
|
1504
|
+
diagnostics,
|
|
1505
|
+
"COLLIE005",
|
|
1506
|
+
"Expression lines cannot contain text after the closing }}.",
|
|
1507
|
+
lineNumber,
|
|
1508
|
+
column + closeIndex + 2,
|
|
1509
|
+
lineOffset
|
|
1510
|
+
);
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
const inner = trimmed.slice(2, closeIndex).trim();
|
|
1514
|
+
if (!inner) {
|
|
1515
|
+
pushDiag(
|
|
1516
|
+
diagnostics,
|
|
1517
|
+
"COLLIE005",
|
|
1518
|
+
"Expression cannot be empty.",
|
|
1519
|
+
lineNumber,
|
|
1520
|
+
column,
|
|
1521
|
+
lineOffset,
|
|
1522
|
+
closeIndex + 2
|
|
1523
|
+
);
|
|
1524
|
+
return null;
|
|
1525
|
+
}
|
|
1526
|
+
return { type: "Expression", value: inner };
|
|
1527
|
+
}
|
|
1528
|
+
function parseJSXPassthrough(line, lineNumber, column, lineOffset, diagnostics) {
|
|
1529
|
+
if (!line.startsWith("=")) {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
const payload = line.slice(1).trim();
|
|
1533
|
+
if (!payload) {
|
|
1534
|
+
pushDiag(
|
|
1535
|
+
diagnostics,
|
|
1536
|
+
"COLLIE005",
|
|
1537
|
+
"JSX passthrough expression cannot be empty.",
|
|
1538
|
+
lineNumber,
|
|
1539
|
+
column,
|
|
1540
|
+
lineOffset
|
|
1541
|
+
);
|
|
1542
|
+
return null;
|
|
1543
|
+
}
|
|
1544
|
+
return { type: "JSXPassthrough", expression: payload };
|
|
1545
|
+
}
|
|
1546
|
+
function parsePropsField(line, lineNumber, column, lineOffset, diagnostics) {
|
|
1547
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)(\??)\s*:\s*(.+)$/);
|
|
1548
|
+
if (!match) {
|
|
1549
|
+
pushDiag(
|
|
1550
|
+
diagnostics,
|
|
1551
|
+
"COLLIE102",
|
|
1552
|
+
"Props lines must be in the form `name[:?] Type`.",
|
|
1553
|
+
lineNumber,
|
|
1554
|
+
column,
|
|
1555
|
+
lineOffset,
|
|
1556
|
+
Math.max(line.length, 1)
|
|
1557
|
+
);
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
const [, name, optionalFlag, typePart] = match;
|
|
1561
|
+
const typeText = typePart.trim();
|
|
1562
|
+
if (!typeText) {
|
|
1563
|
+
pushDiag(
|
|
1564
|
+
diagnostics,
|
|
1565
|
+
"COLLIE102",
|
|
1566
|
+
"Props lines must provide a type after the colon.",
|
|
1567
|
+
lineNumber,
|
|
1568
|
+
column,
|
|
1569
|
+
lineOffset,
|
|
1570
|
+
Math.max(line.length, 1)
|
|
1571
|
+
);
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
name,
|
|
1576
|
+
optional: optionalFlag === "?",
|
|
1577
|
+
typeText
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
function parseClassAliasLine(line, lineNumber, column, lineOffset, diagnostics) {
|
|
1581
|
+
const match = line.match(/^([^=]+?)\s*=\s*(.+)$/);
|
|
1582
|
+
if (!match) {
|
|
1583
|
+
pushDiag(
|
|
1584
|
+
diagnostics,
|
|
1585
|
+
"COLLIE304",
|
|
1586
|
+
"Classes lines must be in the form `name = class.tokens`.",
|
|
1587
|
+
lineNumber,
|
|
1588
|
+
column,
|
|
1589
|
+
lineOffset,
|
|
1590
|
+
Math.max(line.length, 1)
|
|
1591
|
+
);
|
|
1592
|
+
return null;
|
|
1593
|
+
}
|
|
1594
|
+
const rawName = match[1].trim();
|
|
1595
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(rawName)) {
|
|
1596
|
+
pushDiag(
|
|
1597
|
+
diagnostics,
|
|
1598
|
+
"COLLIE305",
|
|
1599
|
+
`Class alias name '${rawName}' must be a valid identifier.`,
|
|
1600
|
+
lineNumber,
|
|
1601
|
+
column,
|
|
1602
|
+
lineOffset,
|
|
1603
|
+
Math.max(rawName.length, 1)
|
|
1604
|
+
);
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
const rhs = match[2];
|
|
1608
|
+
const rhsIndex = line.indexOf(rhs);
|
|
1609
|
+
const rhsColumn = rhsIndex >= 0 ? column + rhsIndex : column;
|
|
1610
|
+
const classes = parseAliasClasses(rhs, lineNumber, rhsColumn, lineOffset, diagnostics);
|
|
1611
|
+
if (!classes.length) {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
const nameIndex = line.indexOf(rawName);
|
|
1615
|
+
const nameColumn = nameIndex >= 0 ? column + nameIndex : column;
|
|
1616
|
+
const span = createSpan(lineNumber, nameColumn, rawName.length, lineOffset);
|
|
1617
|
+
return { name: rawName, classes, span };
|
|
1618
|
+
}
|
|
1619
|
+
function parseAliasClasses(rhs, lineNumber, column, lineOffset, diagnostics) {
|
|
1620
|
+
const trimmed = rhs.trim();
|
|
1621
|
+
if (!trimmed) {
|
|
1622
|
+
pushDiag(
|
|
1623
|
+
diagnostics,
|
|
1624
|
+
"COLLIE304",
|
|
1625
|
+
"Classes lines must provide one or more class tokens after '='.",
|
|
1626
|
+
lineNumber,
|
|
1627
|
+
column,
|
|
1628
|
+
lineOffset,
|
|
1629
|
+
Math.max(rhs.length, 1)
|
|
1630
|
+
);
|
|
1631
|
+
return [];
|
|
1632
|
+
}
|
|
1633
|
+
const withoutDotPrefix = trimmed.startsWith(".") ? trimmed.slice(1) : trimmed;
|
|
1634
|
+
const parts = withoutDotPrefix.split(".");
|
|
1635
|
+
const classes = [];
|
|
1636
|
+
for (const part of parts) {
|
|
1637
|
+
const token = part.trim();
|
|
1638
|
+
if (!token) {
|
|
1639
|
+
pushDiag(
|
|
1640
|
+
diagnostics,
|
|
1641
|
+
"COLLIE304",
|
|
1642
|
+
"Classes lines must provide one or more class tokens after '='.",
|
|
1643
|
+
lineNumber,
|
|
1644
|
+
column,
|
|
1645
|
+
lineOffset,
|
|
1646
|
+
Math.max(rhs.length, 1)
|
|
1647
|
+
);
|
|
1648
|
+
return [];
|
|
1649
|
+
}
|
|
1650
|
+
classes.push(token);
|
|
1651
|
+
}
|
|
1652
|
+
return classes;
|
|
1653
|
+
}
|
|
1654
|
+
function validateClassAliasDefinitions(classAliases, diagnostics) {
|
|
1655
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1656
|
+
for (const alias of classAliases.aliases) {
|
|
1657
|
+
const previous = seen.get(alias.name);
|
|
1658
|
+
if (previous) {
|
|
1659
|
+
if (alias.span) {
|
|
1660
|
+
diagnostics.push({
|
|
1661
|
+
severity: "error",
|
|
1662
|
+
code: "COLLIE306",
|
|
1663
|
+
message: `Duplicate class alias '${alias.name}'.`,
|
|
1664
|
+
span: alias.span
|
|
1665
|
+
});
|
|
1666
|
+
} else {
|
|
1667
|
+
pushDiag(diagnostics, "COLLIE306", `Duplicate class alias '${alias.name}'.`, 1, 1, 0);
|
|
1668
|
+
}
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
seen.set(alias.name, alias);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function validateClassAliasUsages(root, diagnostics) {
|
|
1675
|
+
const defined = new Set(root.classAliases?.aliases.map((alias) => alias.name) ?? []);
|
|
1676
|
+
for (const child of root.children) {
|
|
1677
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function validateNodeClassAliases(node, defined, diagnostics) {
|
|
1681
|
+
if (node.type === "Element" || node.type === "Component") {
|
|
1682
|
+
const spans = node.type === "Element" ? node.classSpans ?? [] : [];
|
|
1683
|
+
const classes = node.type === "Element" ? node.classes : [];
|
|
1684
|
+
classes.forEach((cls, index) => {
|
|
1685
|
+
const match = cls.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
1686
|
+
if (!match) {
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const aliasName = match[1];
|
|
1690
|
+
if (defined.has(aliasName)) {
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const span = spans[index];
|
|
1694
|
+
if (span) {
|
|
1695
|
+
diagnostics.push({
|
|
1696
|
+
severity: "error",
|
|
1697
|
+
code: "COLLIE307",
|
|
1698
|
+
message: `Undefined class alias '${aliasName}'.`,
|
|
1699
|
+
span
|
|
1700
|
+
});
|
|
1701
|
+
} else {
|
|
1702
|
+
pushDiag(diagnostics, "COLLIE307", `Undefined class alias '${aliasName}'.`, 1, 1, 0);
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
for (const child of node.children) {
|
|
1706
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
1707
|
+
}
|
|
1708
|
+
if (node.type === "Component" && node.slots) {
|
|
1709
|
+
for (const slot of node.slots) {
|
|
1710
|
+
for (const child of slot.children) {
|
|
1711
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (node.type === "Conditional") {
|
|
1718
|
+
for (const branch of node.branches) {
|
|
1719
|
+
for (const child of branch.body) {
|
|
1720
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
if (node.type === "For") {
|
|
1725
|
+
for (const child of node.body) {
|
|
1726
|
+
validateNodeClassAliases(child, defined, diagnostics);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function parseElement(line, lineNumber, column, lineOffset, diagnostics) {
|
|
1731
|
+
const nameMatch = line.match(/^([A-Za-z][A-Za-z0-9_]*)/);
|
|
1732
|
+
if (!nameMatch) {
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
const name = nameMatch[1];
|
|
1736
|
+
let cursor = name.length;
|
|
1737
|
+
const nextPart = line.slice(cursor);
|
|
1738
|
+
const isComponent = /^[A-Z]/.test(name);
|
|
1739
|
+
if (isComponent && nextPart.length > 0) {
|
|
1740
|
+
const trimmedNext = nextPart.trimStart();
|
|
1741
|
+
if (trimmedNext.length > 0 && !trimmedNext.startsWith("(")) {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (cursor < line.length) {
|
|
1746
|
+
const nextChar = line[cursor];
|
|
1747
|
+
if (nextChar !== "." && nextChar !== "(" && !/\s/.test(nextChar)) {
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const classes = [];
|
|
1752
|
+
const classSpans = [];
|
|
1753
|
+
if (!isComponent) {
|
|
1754
|
+
while (cursor < line.length && line[cursor] === ".") {
|
|
1755
|
+
cursor++;
|
|
1756
|
+
const classMatch = line.slice(cursor).match(/^([A-Za-z0-9_$-]+)/);
|
|
1757
|
+
if (!classMatch) {
|
|
1758
|
+
pushDiag(
|
|
1759
|
+
diagnostics,
|
|
1760
|
+
"COLLIE004",
|
|
1761
|
+
"Class names must contain only letters, numbers, underscores, hyphens, or `$` (for aliases).",
|
|
1762
|
+
lineNumber,
|
|
1763
|
+
column + cursor,
|
|
1764
|
+
lineOffset
|
|
1765
|
+
);
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
const className = classMatch[1];
|
|
1769
|
+
classes.push(className);
|
|
1770
|
+
classSpans.push(createSpan(lineNumber, column + cursor, className.length, lineOffset));
|
|
1771
|
+
cursor += className.length;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const attributes = [];
|
|
1775
|
+
if (cursor < line.length && line[cursor] === "(") {
|
|
1776
|
+
const attrResult = parseAttributes(line, cursor, lineNumber, column, lineOffset, diagnostics);
|
|
1777
|
+
if (!attrResult) {
|
|
1778
|
+
return null;
|
|
1779
|
+
}
|
|
1780
|
+
attributes.push(...attrResult.attributes);
|
|
1781
|
+
cursor = attrResult.endIndex;
|
|
1782
|
+
}
|
|
1783
|
+
let guard;
|
|
1784
|
+
const guardProbeStart = cursor;
|
|
1785
|
+
while (cursor < line.length && /\s/.test(line[cursor])) {
|
|
1786
|
+
cursor++;
|
|
1787
|
+
}
|
|
1788
|
+
if (cursor < line.length && line[cursor] === "?") {
|
|
1789
|
+
const guardColumn = column + cursor;
|
|
1790
|
+
cursor++;
|
|
1791
|
+
const guardExpr = line.slice(cursor).trim();
|
|
1792
|
+
if (!guardExpr) {
|
|
1793
|
+
pushDiag(
|
|
1794
|
+
diagnostics,
|
|
1795
|
+
"COLLIE601",
|
|
1796
|
+
"Guard expressions require a condition after '?'.",
|
|
1797
|
+
lineNumber,
|
|
1798
|
+
guardColumn,
|
|
1799
|
+
lineOffset
|
|
1800
|
+
);
|
|
1801
|
+
} else {
|
|
1802
|
+
guard = guardExpr;
|
|
1803
|
+
}
|
|
1804
|
+
cursor = line.length;
|
|
1805
|
+
} else {
|
|
1806
|
+
cursor = guardProbeStart;
|
|
1807
|
+
}
|
|
1808
|
+
let rest = line.slice(cursor).trimStart();
|
|
1809
|
+
const children = [];
|
|
1810
|
+
if (rest.length > 0) {
|
|
1811
|
+
const textNode = parseTextPayload(rest, lineNumber, column + cursor + (line.slice(cursor).length - rest.length), lineOffset, diagnostics);
|
|
1812
|
+
if (textNode) {
|
|
1813
|
+
children.push(textNode);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (isComponent) {
|
|
1817
|
+
const component = {
|
|
1818
|
+
type: "Component",
|
|
1819
|
+
name,
|
|
1820
|
+
attributes,
|
|
1821
|
+
children
|
|
1822
|
+
};
|
|
1823
|
+
if (guard) {
|
|
1824
|
+
component.guard = guard;
|
|
1825
|
+
}
|
|
1826
|
+
return component;
|
|
1827
|
+
} else {
|
|
1828
|
+
const element = {
|
|
1829
|
+
type: "Element",
|
|
1830
|
+
name,
|
|
1831
|
+
classes,
|
|
1832
|
+
attributes,
|
|
1833
|
+
children
|
|
1834
|
+
};
|
|
1835
|
+
if (classSpans.length) {
|
|
1836
|
+
element.classSpans = classSpans;
|
|
1837
|
+
}
|
|
1838
|
+
if (guard) {
|
|
1839
|
+
element.guard = guard;
|
|
1840
|
+
}
|
|
1841
|
+
return element;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function parseAttributes(line, startIndex, lineNumber, column, lineOffset, diagnostics) {
|
|
1845
|
+
if (line[startIndex] !== "(") {
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
const attributes = [];
|
|
1849
|
+
let cursor = startIndex + 1;
|
|
1850
|
+
let depth = 1;
|
|
1851
|
+
let attrBuffer = "";
|
|
1852
|
+
while (cursor < line.length && depth > 0) {
|
|
1853
|
+
const ch = line[cursor];
|
|
1854
|
+
if (ch === "(") {
|
|
1855
|
+
depth++;
|
|
1856
|
+
attrBuffer += ch;
|
|
1857
|
+
} else if (ch === ")") {
|
|
1858
|
+
depth--;
|
|
1859
|
+
if (depth > 0) {
|
|
1860
|
+
attrBuffer += ch;
|
|
1861
|
+
}
|
|
1862
|
+
} else {
|
|
1863
|
+
attrBuffer += ch;
|
|
1864
|
+
}
|
|
1865
|
+
cursor++;
|
|
1866
|
+
}
|
|
1867
|
+
if (depth !== 0) {
|
|
1868
|
+
pushDiag(
|
|
1869
|
+
diagnostics,
|
|
1870
|
+
"COLLIE004",
|
|
1871
|
+
"Unclosed attribute parentheses.",
|
|
1872
|
+
lineNumber,
|
|
1873
|
+
column + startIndex,
|
|
1874
|
+
lineOffset
|
|
1875
|
+
);
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
const trimmedAttrs = attrBuffer.trim();
|
|
1879
|
+
if (trimmedAttrs.length === 0) {
|
|
1880
|
+
return { attributes: [], endIndex: cursor };
|
|
1881
|
+
}
|
|
1882
|
+
const attrLines = trimmedAttrs.split("\n");
|
|
1883
|
+
let currentAttr = "";
|
|
1884
|
+
for (const attrLine of attrLines) {
|
|
1885
|
+
const trimmedLine = attrLine.trim();
|
|
1886
|
+
if (trimmedLine.length === 0) continue;
|
|
1887
|
+
const eqIndex = trimmedLine.indexOf("=");
|
|
1888
|
+
if (eqIndex > 0 && /^[A-Za-z][A-Za-z0-9_-]*\s*=/.test(trimmedLine)) {
|
|
1889
|
+
if (currentAttr) {
|
|
1890
|
+
parseAndAddAttribute(currentAttr, attributes, diagnostics, lineNumber, column, lineOffset);
|
|
1891
|
+
currentAttr = "";
|
|
1892
|
+
}
|
|
1893
|
+
currentAttr = trimmedLine;
|
|
1894
|
+
} else {
|
|
1895
|
+
if (currentAttr) {
|
|
1896
|
+
currentAttr += " " + trimmedLine;
|
|
1897
|
+
} else {
|
|
1898
|
+
currentAttr = trimmedLine;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (currentAttr) {
|
|
1903
|
+
parseAndAddAttribute(currentAttr, attributes, diagnostics, lineNumber, column, lineOffset);
|
|
1904
|
+
}
|
|
1905
|
+
return { attributes, endIndex: cursor };
|
|
1906
|
+
}
|
|
1907
|
+
function parseAndAddAttribute(attrStr, attributes, diagnostics, lineNumber, column, lineOffset) {
|
|
1908
|
+
const trimmed = attrStr.trim();
|
|
1909
|
+
const match = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*=\s*(.+)$/s);
|
|
1910
|
+
if (match) {
|
|
1911
|
+
const attrName = match[1];
|
|
1912
|
+
const attrValue = match[2].trim();
|
|
1913
|
+
attributes.push({ name: attrName, value: attrValue });
|
|
1914
|
+
} else {
|
|
1915
|
+
const nameMatch = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*)$/);
|
|
1916
|
+
if (nameMatch) {
|
|
1917
|
+
attributes.push({ name: nameMatch[1], value: null });
|
|
1918
|
+
} else {
|
|
1919
|
+
pushDiag(
|
|
1920
|
+
diagnostics,
|
|
1921
|
+
"COLLIE004",
|
|
1922
|
+
`Invalid attribute syntax: ${trimmed.slice(0, 30)}`,
|
|
1923
|
+
lineNumber,
|
|
1924
|
+
column,
|
|
1925
|
+
lineOffset
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
function pushDiag(diagnostics, code, message, line, column, lineOffset, length = 1) {
|
|
1931
|
+
diagnostics.push({
|
|
1932
|
+
severity: "error",
|
|
1933
|
+
code,
|
|
1934
|
+
message,
|
|
1935
|
+
span: createSpan(line, column, Math.max(length, 1), lineOffset)
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/index.ts
|
|
1940
|
+
function parseCollie(source, options = {}) {
|
|
1941
|
+
const result = parse(source);
|
|
1942
|
+
if (!options.filename) {
|
|
1943
|
+
return result;
|
|
1944
|
+
}
|
|
1945
|
+
return { root: result.root, diagnostics: attachFilename(result.diagnostics, options.filename) };
|
|
1946
|
+
}
|
|
1947
|
+
function compileToJsx(sourceOrAst, options = {}) {
|
|
1948
|
+
const document = normalizeDocument(sourceOrAst, options.filename);
|
|
1949
|
+
const diagnostics = options.filename ? attachFilename(document.diagnostics, options.filename) : document.diagnostics;
|
|
1950
|
+
const componentName = options.componentNameHint ?? "CollieTemplate";
|
|
1951
|
+
const jsxRuntime = options.jsxRuntime ?? "automatic";
|
|
1952
|
+
let code = createStubComponent(componentName, "jsx");
|
|
1953
|
+
if (!hasErrors(diagnostics)) {
|
|
1954
|
+
code = generateModule(document.root, { componentName, jsxRuntime, flavor: "jsx" });
|
|
1955
|
+
}
|
|
1956
|
+
return { code, diagnostics, map: void 0 };
|
|
1957
|
+
}
|
|
1958
|
+
function compileToTsx(sourceOrAst, options = {}) {
|
|
1959
|
+
const document = normalizeDocument(sourceOrAst, options.filename);
|
|
1960
|
+
const diagnostics = options.filename ? attachFilename(document.diagnostics, options.filename) : document.diagnostics;
|
|
1961
|
+
const componentName = options.componentNameHint ?? "CollieTemplate";
|
|
1962
|
+
const jsxRuntime = options.jsxRuntime ?? "automatic";
|
|
1963
|
+
let code = createStubComponent(componentName, "tsx");
|
|
1964
|
+
if (!hasErrors(diagnostics)) {
|
|
1965
|
+
code = generateModule(document.root, { componentName, jsxRuntime, flavor: "tsx" });
|
|
1966
|
+
}
|
|
1967
|
+
return { code, diagnostics, map: void 0 };
|
|
1968
|
+
}
|
|
1969
|
+
function compileToHtml(sourceOrAst, options = {}) {
|
|
1970
|
+
const document = normalizeDocument(sourceOrAst, options.filename);
|
|
1971
|
+
const diagnostics = options.filename ? attachFilename(document.diagnostics, options.filename) : document.diagnostics;
|
|
1972
|
+
const componentName = options.componentNameHint ?? "CollieTemplate";
|
|
1973
|
+
let code = createStubHtml(componentName);
|
|
1974
|
+
if (!hasErrors(diagnostics)) {
|
|
1975
|
+
code = generateHtmlModule(document.root, { componentName });
|
|
1976
|
+
}
|
|
1977
|
+
return { code, diagnostics, map: void 0 };
|
|
1978
|
+
}
|
|
1979
|
+
function compile(source, options = {}) {
|
|
1980
|
+
return compileToJsx(source, options);
|
|
1981
|
+
}
|
|
1982
|
+
function normalizeDocument(sourceOrAst, filename) {
|
|
1983
|
+
if (typeof sourceOrAst === "string") {
|
|
1984
|
+
return parseCollie(sourceOrAst, { filename });
|
|
1985
|
+
}
|
|
1986
|
+
if (isCollieDocument(sourceOrAst)) {
|
|
1987
|
+
if (!filename) {
|
|
1988
|
+
return sourceOrAst;
|
|
1989
|
+
}
|
|
1990
|
+
return { root: sourceOrAst.root, diagnostics: attachFilename(sourceOrAst.diagnostics, filename) };
|
|
1991
|
+
}
|
|
1992
|
+
if (isRootNode(sourceOrAst)) {
|
|
1993
|
+
return { root: sourceOrAst, diagnostics: [] };
|
|
1994
|
+
}
|
|
1995
|
+
throw new TypeError("Collie compiler expected source text, a parsed document, or a root node.");
|
|
1996
|
+
}
|
|
1997
|
+
function isRootNode(value) {
|
|
1998
|
+
return !!value && typeof value === "object" && value.type === "Root";
|
|
1999
|
+
}
|
|
2000
|
+
function isCollieDocument(value) {
|
|
2001
|
+
return !!value && typeof value === "object" && isRootNode(value.root) && Array.isArray(value.diagnostics);
|
|
2002
|
+
}
|
|
2003
|
+
function hasErrors(diagnostics) {
|
|
2004
|
+
return diagnostics.some((diag) => diag.severity === "error");
|
|
2005
|
+
}
|
|
2006
|
+
function createStubComponent(name, flavor) {
|
|
2007
|
+
if (flavor === "tsx") {
|
|
2008
|
+
return [
|
|
2009
|
+
"export type Props = Record<string, never>;",
|
|
2010
|
+
`export default function ${name}(props: Props) {`,
|
|
2011
|
+
" return null;",
|
|
2012
|
+
"}"
|
|
2013
|
+
].join("\n");
|
|
2014
|
+
}
|
|
2015
|
+
return [`export default function ${name}(props) {`, " return null;", "}"].join("\n");
|
|
2016
|
+
}
|
|
2017
|
+
function createStubHtml(name) {
|
|
2018
|
+
return [`export default function ${name}(props = {}) {`, ' return "";', "}"].join("\n");
|
|
2019
|
+
}
|
|
2020
|
+
function attachFilename(diagnostics, filename) {
|
|
2021
|
+
if (!filename) {
|
|
2022
|
+
return diagnostics;
|
|
2023
|
+
}
|
|
2024
|
+
return diagnostics.map((diag) => diag.file ? diag : { ...diag, file: filename });
|
|
2025
|
+
}
|
|
2026
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2027
|
+
0 && (module.exports = {
|
|
2028
|
+
compile,
|
|
2029
|
+
compileToHtml,
|
|
2030
|
+
compileToJsx,
|
|
2031
|
+
compileToTsx,
|
|
2032
|
+
parse,
|
|
2033
|
+
parseCollie
|
|
2034
|
+
});
|
|
2035
|
+
//# sourceMappingURL=index.cjs.map
|