@collie-lang/compiler 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -303,175 +303,149 @@ function escapeText(value) {
303
303
  }
304
304
 
305
305
  // src/html-codegen.ts
306
- function generateHtmlModule(root, options) {
306
+ function generateHtml(root, options = {}) {
307
+ const indent = options.indent ?? " ";
307
308
  const aliasEnv = buildClassAliasEnvironment2(root.classAliases);
308
- const htmlExpression = emitNodesString(root.children, aliasEnv);
309
- const propsType = emitJsDocPropsType2(root.props);
310
- const propsDestructure = emitPropsDestructure2(root.props);
311
- const parts = [];
312
- if (root.clientComponent) {
313
- parts.push(`"use client";`);
314
- }
315
- parts.push(...createHtmlHelpers());
316
- parts.push(propsType);
317
- parts.push(`/** @param {Props} props */`);
318
- parts.push(`/** @returns {string} */`);
319
- const lines = [`export default function ${options.componentName}(props = {}) {`];
320
- if (propsDestructure) {
321
- lines.push(` ${propsDestructure}`);
322
- }
323
- lines.push(` const __collie_html = ${htmlExpression};`);
324
- lines.push(" return __collie_html;", "}");
325
- parts.push(lines.join("\n"));
326
- return parts.join("\n\n");
327
- }
328
- function emitNodesString(children, aliasEnv) {
329
- if (children.length === 0) {
330
- return '""';
309
+ const rendered = emitNodes(root.children, aliasEnv, indent, 0);
310
+ return rendered.trimEnd();
311
+ }
312
+ function emitNodes(children, aliasEnv, indent, depth) {
313
+ let html = "";
314
+ for (const child of children) {
315
+ const chunk = emitNode(child, aliasEnv, indent, depth);
316
+ if (chunk) {
317
+ html += chunk;
318
+ }
331
319
  }
332
- const segments = children.map((child) => emitNodeString(child, aliasEnv)).filter(Boolean);
333
- return concatSegments(segments);
320
+ return html;
334
321
  }
335
- function emitNodeString(node, aliasEnv) {
322
+ function emitNode(node, aliasEnv, indent, depth) {
336
323
  switch (node.type) {
337
- case "Text":
338
- return emitTextNode(node);
339
- case "Expression":
340
- return `__collie_escapeHtml(${node.value})`;
341
- case "JSXPassthrough":
342
- return `String(${node.expression})`;
343
324
  case "Element":
344
- return wrapWithGuard2(emitElement2(node, aliasEnv), node.guard);
345
- case "Component":
346
- return wrapWithGuard2(emitComponent2(node, aliasEnv), node.guard);
347
- case "Conditional":
348
- return emitConditional(node, aliasEnv);
349
- case "For":
350
- return emitFor(node, aliasEnv);
325
+ return emitElement2(node, aliasEnv, indent, depth);
326
+ case "Text":
327
+ return emitTextBlock(node, indent, depth);
351
328
  default:
352
- return '""';
329
+ return "";
353
330
  }
354
331
  }
355
- function emitElement2(node, aliasEnv) {
356
- const classSegments = expandClasses2(node.classes, aliasEnv);
357
- const attributeSegments = emitAttributeSegments(node.attributes, classSegments);
358
- const start = concatSegments([literal(`<${node.name}`), ...attributeSegments, literal(node.children.length > 0 ? ">" : " />")]);
332
+ function emitElement2(node, aliasEnv, indent, depth) {
333
+ const indentText = indent.repeat(depth);
334
+ const classNames = expandClasses2(node.classes, aliasEnv);
335
+ const attrs = renderAttributes(node.attributes, classNames);
336
+ const openTag = `<${node.name}${attrs}>`;
359
337
  if (node.children.length === 0) {
360
- return start;
361
- }
362
- const children = emitNodesString(node.children, aliasEnv);
363
- const end = literal(`</${node.name}>`);
364
- return concatSegments([start, children, end]);
365
- }
366
- function emitComponent2(node, aliasEnv) {
367
- const attributeSegments = emitAttributeSegments(node.attributes, []);
368
- const hasChildren = node.children.length > 0 || (node.slots?.length ?? 0) > 0;
369
- const closingToken = hasChildren ? ">" : " />";
370
- const start = concatSegments([literal(`<${node.name}`), ...attributeSegments, literal(closingToken)]);
371
- if (!hasChildren) {
372
- return start;
373
- }
374
- const childSegments = [];
375
- if (node.children.length) {
376
- childSegments.push(emitNodesString(node.children, aliasEnv));
338
+ return `${indentText}${openTag}</${node.name}>
339
+ `;
340
+ }
341
+ if (node.children.length === 1 && node.children[0].type === "Text") {
342
+ const inline = emitInlineText(node.children[0]);
343
+ if (inline !== null) {
344
+ return `${indentText}${openTag}${inline}</${node.name}>
345
+ `;
346
+ }
377
347
  }
378
- for (const slot of node.slots ?? []) {
379
- childSegments.push(emitSlotTemplate(slot, aliasEnv));
348
+ const children = emitNodes(node.children, aliasEnv, indent, depth + 1);
349
+ if (!children) {
350
+ return `${indentText}${openTag}</${node.name}>
351
+ `;
380
352
  }
381
- const children = concatSegments(childSegments);
382
- const end = literal(`</${node.name}>`);
383
- return concatSegments([start, children, end]);
384
- }
385
- function emitSlotTemplate(slot, aliasEnv) {
386
- const start = literal(`<template slot="${slot.name}">`);
387
- const body = emitNodesString(slot.children, aliasEnv);
388
- const end = literal("</template>");
389
- return concatSegments([start, body, end]);
353
+ return `${indentText}${openTag}
354
+ ${children}${indentText}</${node.name}>
355
+ `;
390
356
  }
391
- function emitConditional(node, aliasEnv) {
392
- if (node.branches.length === 0) {
393
- return '""';
357
+ function renderAttributes(attributes, classNames) {
358
+ const segments = [];
359
+ if (classNames.length) {
360
+ segments.push(`class="${escapeAttributeValue(classNames.join(" "))}"`);
394
361
  }
395
- const first = node.branches[0];
396
- if (node.branches.length === 1 && first.test) {
397
- return `(${first.test}) ? ${emitBranch(first, aliasEnv)} : ""`;
362
+ for (const attr of attributes) {
363
+ if (attr.value === null) {
364
+ segments.push(attr.name);
365
+ continue;
366
+ }
367
+ const literal = extractStaticAttributeValue(attr.value);
368
+ if (literal === null) {
369
+ continue;
370
+ }
371
+ const name = attr.name === "className" ? "class" : attr.name;
372
+ segments.push(`${name}="${escapeAttributeValue(literal)}"`);
398
373
  }
399
- const hasElse = node.branches[node.branches.length - 1].test === void 0;
400
- let fallback = hasElse ? emitBranch(node.branches[node.branches.length - 1], aliasEnv) : '""';
401
- const limit = hasElse ? node.branches.length - 2 : node.branches.length - 1;
402
- for (let i = limit; i >= 0; i--) {
403
- const branch = node.branches[i];
404
- const test = branch.test ?? "false";
405
- fallback = `(${test}) ? ${emitBranch(branch, aliasEnv)} : ${fallback}`;
374
+ if (!segments.length) {
375
+ return "";
406
376
  }
407
- return fallback;
408
- }
409
- function emitBranch(branch, aliasEnv) {
410
- return emitNodesString(branch.body, aliasEnv);
377
+ return " " + segments.join(" ");
411
378
  }
412
- function emitFor(node, aliasEnv) {
413
- const body = emitNodesString(node.body, aliasEnv);
414
- return `(${node.arrayExpr}).map((${node.itemName}) => ${body}).join("")`;
379
+ function emitTextBlock(node, indent, depth) {
380
+ const inline = emitInlineText(node);
381
+ if (inline === null || inline.trim().length === 0) {
382
+ return "";
383
+ }
384
+ return `${indent.repeat(depth)}${inline}
385
+ `;
415
386
  }
416
- function emitTextNode(node) {
387
+ function emitInlineText(node) {
417
388
  if (!node.parts.length) {
418
- return '""';
389
+ return "";
419
390
  }
420
- const segments = node.parts.map((part) => {
421
- if (part.type === "text") {
422
- return literal(escapeStaticText(part.value));
391
+ let text = "";
392
+ for (const part of node.parts) {
393
+ if (part.type !== "text") {
394
+ return null;
423
395
  }
424
- return `__collie_escapeHtml(${part.value})`;
425
- });
426
- return concatSegments(segments);
396
+ text += escapeStaticText(part.value);
397
+ }
398
+ return text;
427
399
  }
428
- function emitAttributeSegments(attributes, classNames) {
429
- const segments = [];
430
- if (classNames.length) {
431
- segments.push(literal(` class="${classNames.join(" ")}"`));
400
+ function extractStaticAttributeValue(raw) {
401
+ const trimmed = raw.trim();
402
+ if (trimmed.length < 2) {
403
+ return null;
432
404
  }
433
- for (const attr of attributes) {
434
- if (attr.value === null) {
435
- segments.push(literal(` ${attr.name}`));
405
+ const quote = trimmed[0];
406
+ if (quote !== '"' && quote !== "'" || trimmed[trimmed.length - 1] !== quote) {
407
+ return null;
408
+ }
409
+ const body = trimmed.slice(1, -1);
410
+ let result = "";
411
+ let escaping = false;
412
+ for (const char of body) {
413
+ if (escaping) {
414
+ result += unescapeChar(char, quote);
415
+ escaping = false;
436
416
  continue;
437
417
  }
438
- const expr = attributeExpression(attr.value);
439
- segments.push(
440
- [
441
- "(() => {",
442
- ` const __collie_attr = ${expr};`,
443
- ` return __collie_attr == null ? "" : ${literal(` ${attr.name}="`)} + __collie_escapeAttr(__collie_attr) + ${literal(`"`)};`,
444
- "})()"
445
- ].join(" ")
446
- );
447
- }
448
- return segments;
449
- }
450
- function attributeExpression(raw) {
451
- const trimmed = raw.trim();
452
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
453
- return trimmed.slice(1, -1).trim();
418
+ if (char === "\\") {
419
+ escaping = true;
420
+ } else {
421
+ result += char;
422
+ }
454
423
  }
455
- return trimmed;
456
- }
457
- function wrapWithGuard2(rendered, guard) {
458
- if (!guard) {
459
- return rendered;
424
+ if (escaping) {
425
+ result += "\\";
460
426
  }
461
- return `(${guard}) ? ${rendered} : ""`;
462
- }
463
- function literal(text) {
464
- return JSON.stringify(text);
427
+ return result;
465
428
  }
466
- function concatSegments(segments) {
467
- const filtered = segments.filter((segment) => segment && segment !== '""');
468
- if (!filtered.length) {
469
- return '""';
470
- }
471
- if (filtered.length === 1) {
472
- return filtered[0];
429
+ function unescapeChar(char, quote) {
430
+ switch (char) {
431
+ case "n":
432
+ return "\n";
433
+ case "r":
434
+ return "\r";
435
+ case "t":
436
+ return " ";
437
+ case "\\":
438
+ return "\\";
439
+ case '"':
440
+ return '"';
441
+ case "'":
442
+ return "'";
443
+ default:
444
+ if (char === quote) {
445
+ return quote;
446
+ }
447
+ return char;
473
448
  }
474
- return filtered.join(" + ");
475
449
  }
476
450
  function buildClassAliasEnvironment2(decl) {
477
451
  const env = /* @__PURE__ */ new Map();
@@ -499,26 +473,6 @@ function expandClasses2(classes, aliasEnv) {
499
473
  }
500
474
  return result;
501
475
  }
502
- function emitJsDocPropsType2(props) {
503
- if (!props) {
504
- return "/** @typedef {any} Props */";
505
- }
506
- if (!props.fields.length) {
507
- return "/** @typedef {{}} Props */";
508
- }
509
- const fields = props.fields.map((field) => {
510
- const optional = field.optional ? "?" : "";
511
- return `${field.name}${optional}: ${field.typeText}`;
512
- }).join("; ");
513
- return `/** @typedef {{ ${fields} }} Props */`;
514
- }
515
- function emitPropsDestructure2(props) {
516
- if (!props || props.fields.length === 0) {
517
- return null;
518
- }
519
- const names = props.fields.map((field) => field.name);
520
- return `const { ${names.join(", ")} } = props;`;
521
- }
522
476
  function escapeStaticText(value) {
523
477
  return value.replace(/[&<>{}]/g, (char) => {
524
478
  switch (char) {
@@ -537,47 +491,21 @@ function escapeStaticText(value) {
537
491
  }
538
492
  });
539
493
  }
540
- function createHtmlHelpers() {
541
- return [
542
- "function __collie_escapeHtml(value) {",
543
- " if (value === null || value === undefined) {",
544
- ' return "";',
545
- " }",
546
- " return String(value).replace(/[&<>]/g, __collie_escapeHtmlChar);",
547
- "}",
548
- "function __collie_escapeHtmlChar(char) {",
549
- " switch (char) {",
550
- ' case "&":',
551
- ' return "&amp;";',
552
- ' case "<":',
553
- ' return "&lt;";',
554
- ' case ">":',
555
- ' return "&gt;";',
556
- " default:",
557
- " return char;",
558
- " }",
559
- "}",
560
- "function __collie_escapeAttr(value) {",
561
- " if (value === null || value === undefined) {",
562
- ' return "";',
563
- " }",
564
- ' return String(value).replace(/["&<>]/g, __collie_escapeAttrChar);',
565
- "}",
566
- "function __collie_escapeAttrChar(char) {",
567
- " switch (char) {",
568
- ' case "&":',
569
- ' return "&amp;";',
570
- ' case "<":',
571
- ' return "&lt;";',
572
- ' case ">":',
573
- ' return "&gt;";',
574
- ` case '"':`,
575
- ' return "&quot;";',
576
- " default:",
577
- " return char;",
578
- " }",
579
- "}"
580
- ];
494
+ function escapeAttributeValue(value) {
495
+ return value.replace(/["&<>]/g, (char) => {
496
+ switch (char) {
497
+ case "&":
498
+ return "&amp;";
499
+ case "<":
500
+ return "&lt;";
501
+ case ">":
502
+ return "&gt;";
503
+ case '"':
504
+ return "&quot;";
505
+ default:
506
+ return char;
507
+ }
508
+ });
581
509
  }
582
510
 
583
511
  // src/diagnostics.ts
@@ -1938,10 +1866,9 @@ function compileToTsx(sourceOrAst, options = {}) {
1938
1866
  function compileToHtml(sourceOrAst, options = {}) {
1939
1867
  const document = normalizeDocument(sourceOrAst, options.filename);
1940
1868
  const diagnostics = options.filename ? attachFilename(document.diagnostics, options.filename) : document.diagnostics;
1941
- const componentName = options.componentNameHint ?? "CollieTemplate";
1942
- let code = createStubHtml(componentName);
1869
+ let code = createStubHtml();
1943
1870
  if (!hasErrors(diagnostics)) {
1944
- code = generateHtmlModule(document.root, { componentName });
1871
+ code = generateHtml(document.root);
1945
1872
  }
1946
1873
  return { code, diagnostics, map: void 0 };
1947
1874
  }
@@ -1983,8 +1910,8 @@ function createStubComponent(name, flavor) {
1983
1910
  }
1984
1911
  return [`export default function ${name}(props) {`, " return null;", "}"].join("\n");
1985
1912
  }
1986
- function createStubHtml(name) {
1987
- return [`export default function ${name}(props = {}) {`, ' return "";', "}"].join("\n");
1913
+ function createStubHtml() {
1914
+ return "";
1988
1915
  }
1989
1916
  function attachFilename(diagnostics, filename) {
1990
1917
  if (!filename) {