@collie-lang/compiler 1.0.0 → 1.2.0

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