@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/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 "&amp;";
322
+ case "<":
323
+ return "&lt;";
324
+ case ">":
325
+ return "&gt;";
326
+ case "{":
327
+ return "&#123;";
328
+ case "}":
329
+ return "&#125;";
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 "&amp;";
558
+ case "<":
559
+ return "&lt;";
560
+ case ">":
561
+ return "&gt;";
562
+ case "{":
563
+ return "&#123;";
564
+ case "}":
565
+ return "&#125;";
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 "&amp;";',
583
+ ' case "<":',
584
+ ' return "&lt;";',
585
+ ' case ">":',
586
+ ' return "&gt;";',
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 "&amp;";',
601
+ ' case "<":',
602
+ ' return "&lt;";',
603
+ ' case ">":',
604
+ ' return "&gt;";',
605
+ ` case '"':`,
606
+ ' return "&quot;";',
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