@collie-lang/compiler 1.0.0 → 1.2.0

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