@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.cjs CHANGED
@@ -334,175 +334,149 @@ function escapeText(value) {
334
334
  }
335
335
 
336
336
  // src/html-codegen.ts
337
- function generateHtmlModule(root, options) {
337
+ function generateHtml(root, options = {}) {
338
+ const indent = options.indent ?? " ";
338
339
  const aliasEnv = buildClassAliasEnvironment2(root.classAliases);
339
- const htmlExpression = emitNodesString(root.children, aliasEnv);
340
- const propsType = emitJsDocPropsType2(root.props);
341
- const propsDestructure = emitPropsDestructure2(root.props);
342
- const parts = [];
343
- if (root.clientComponent) {
344
- parts.push(`"use client";`);
345
- }
346
- parts.push(...createHtmlHelpers());
347
- parts.push(propsType);
348
- parts.push(`/** @param {Props} props */`);
349
- parts.push(`/** @returns {string} */`);
350
- const lines = [`export default function ${options.componentName}(props = {}) {`];
351
- if (propsDestructure) {
352
- lines.push(` ${propsDestructure}`);
353
- }
354
- lines.push(` const __collie_html = ${htmlExpression};`);
355
- lines.push(" return __collie_html;", "}");
356
- parts.push(lines.join("\n"));
357
- return parts.join("\n\n");
358
- }
359
- function emitNodesString(children, aliasEnv) {
360
- if (children.length === 0) {
361
- return '""';
340
+ const rendered = emitNodes(root.children, aliasEnv, indent, 0);
341
+ return rendered.trimEnd();
342
+ }
343
+ function emitNodes(children, aliasEnv, indent, depth) {
344
+ let html = "";
345
+ for (const child of children) {
346
+ const chunk = emitNode(child, aliasEnv, indent, depth);
347
+ if (chunk) {
348
+ html += chunk;
349
+ }
362
350
  }
363
- const segments = children.map((child) => emitNodeString(child, aliasEnv)).filter(Boolean);
364
- return concatSegments(segments);
351
+ return html;
365
352
  }
366
- function emitNodeString(node, aliasEnv) {
353
+ function emitNode(node, aliasEnv, indent, depth) {
367
354
  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
355
  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);
356
+ return emitElement2(node, aliasEnv, indent, depth);
357
+ case "Text":
358
+ return emitTextBlock(node, indent, depth);
382
359
  default:
383
- return '""';
360
+ return "";
384
361
  }
385
362
  }
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 ? ">" : " />")]);
363
+ function emitElement2(node, aliasEnv, indent, depth) {
364
+ const indentText = indent.repeat(depth);
365
+ const classNames = expandClasses2(node.classes, aliasEnv);
366
+ const attrs = renderAttributes(node.attributes, classNames);
367
+ const openTag = `<${node.name}${attrs}>`;
390
368
  if (node.children.length === 0) {
391
- return start;
392
- }
393
- const children = emitNodesString(node.children, aliasEnv);
394
- const end = literal(`</${node.name}>`);
395
- return concatSegments([start, children, end]);
396
- }
397
- function emitComponent2(node, aliasEnv) {
398
- const attributeSegments = emitAttributeSegments(node.attributes, []);
399
- const hasChildren = node.children.length > 0 || (node.slots?.length ?? 0) > 0;
400
- const closingToken = hasChildren ? ">" : " />";
401
- const start = concatSegments([literal(`<${node.name}`), ...attributeSegments, literal(closingToken)]);
402
- if (!hasChildren) {
403
- return start;
404
- }
405
- const childSegments = [];
406
- if (node.children.length) {
407
- childSegments.push(emitNodesString(node.children, aliasEnv));
369
+ return `${indentText}${openTag}</${node.name}>
370
+ `;
371
+ }
372
+ if (node.children.length === 1 && node.children[0].type === "Text") {
373
+ const inline = emitInlineText(node.children[0]);
374
+ if (inline !== null) {
375
+ return `${indentText}${openTag}${inline}</${node.name}>
376
+ `;
377
+ }
408
378
  }
409
- for (const slot of node.slots ?? []) {
410
- childSegments.push(emitSlotTemplate(slot, aliasEnv));
379
+ const children = emitNodes(node.children, aliasEnv, indent, depth + 1);
380
+ if (!children) {
381
+ return `${indentText}${openTag}</${node.name}>
382
+ `;
411
383
  }
412
- const children = concatSegments(childSegments);
413
- const end = literal(`</${node.name}>`);
414
- return concatSegments([start, children, end]);
415
- }
416
- function emitSlotTemplate(slot, aliasEnv) {
417
- const start = literal(`<template slot="${slot.name}">`);
418
- const body = emitNodesString(slot.children, aliasEnv);
419
- const end = literal("</template>");
420
- return concatSegments([start, body, end]);
384
+ return `${indentText}${openTag}
385
+ ${children}${indentText}</${node.name}>
386
+ `;
421
387
  }
422
- function emitConditional(node, aliasEnv) {
423
- if (node.branches.length === 0) {
424
- return '""';
388
+ function renderAttributes(attributes, classNames) {
389
+ const segments = [];
390
+ if (classNames.length) {
391
+ segments.push(`class="${escapeAttributeValue(classNames.join(" "))}"`);
425
392
  }
426
- const first = node.branches[0];
427
- if (node.branches.length === 1 && first.test) {
428
- return `(${first.test}) ? ${emitBranch(first, aliasEnv)} : ""`;
393
+ for (const attr of attributes) {
394
+ if (attr.value === null) {
395
+ segments.push(attr.name);
396
+ continue;
397
+ }
398
+ const literal = extractStaticAttributeValue(attr.value);
399
+ if (literal === null) {
400
+ continue;
401
+ }
402
+ const name = attr.name === "className" ? "class" : attr.name;
403
+ segments.push(`${name}="${escapeAttributeValue(literal)}"`);
429
404
  }
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}`;
405
+ if (!segments.length) {
406
+ return "";
437
407
  }
438
- return fallback;
439
- }
440
- function emitBranch(branch, aliasEnv) {
441
- return emitNodesString(branch.body, aliasEnv);
408
+ return " " + segments.join(" ");
442
409
  }
443
- function emitFor(node, aliasEnv) {
444
- const body = emitNodesString(node.body, aliasEnv);
445
- return `(${node.arrayExpr}).map((${node.itemName}) => ${body}).join("")`;
410
+ function emitTextBlock(node, indent, depth) {
411
+ const inline = emitInlineText(node);
412
+ if (inline === null || inline.trim().length === 0) {
413
+ return "";
414
+ }
415
+ return `${indent.repeat(depth)}${inline}
416
+ `;
446
417
  }
447
- function emitTextNode(node) {
418
+ function emitInlineText(node) {
448
419
  if (!node.parts.length) {
449
- return '""';
420
+ return "";
450
421
  }
451
- const segments = node.parts.map((part) => {
452
- if (part.type === "text") {
453
- return literal(escapeStaticText(part.value));
422
+ let text = "";
423
+ for (const part of node.parts) {
424
+ if (part.type !== "text") {
425
+ return null;
454
426
  }
455
- return `__collie_escapeHtml(${part.value})`;
456
- });
457
- return concatSegments(segments);
427
+ text += escapeStaticText(part.value);
428
+ }
429
+ return text;
458
430
  }
459
- function emitAttributeSegments(attributes, classNames) {
460
- const segments = [];
461
- if (classNames.length) {
462
- segments.push(literal(` class="${classNames.join(" ")}"`));
431
+ function extractStaticAttributeValue(raw) {
432
+ const trimmed = raw.trim();
433
+ if (trimmed.length < 2) {
434
+ return null;
463
435
  }
464
- for (const attr of attributes) {
465
- if (attr.value === null) {
466
- segments.push(literal(` ${attr.name}`));
436
+ const quote = trimmed[0];
437
+ if (quote !== '"' && quote !== "'" || trimmed[trimmed.length - 1] !== quote) {
438
+ return null;
439
+ }
440
+ const body = trimmed.slice(1, -1);
441
+ let result = "";
442
+ let escaping = false;
443
+ for (const char of body) {
444
+ if (escaping) {
445
+ result += unescapeChar(char, quote);
446
+ escaping = false;
467
447
  continue;
468
448
  }
469
- const expr = attributeExpression(attr.value);
470
- segments.push(
471
- [
472
- "(() => {",
473
- ` const __collie_attr = ${expr};`,
474
- ` return __collie_attr == null ? "" : ${literal(` ${attr.name}="`)} + __collie_escapeAttr(__collie_attr) + ${literal(`"`)};`,
475
- "})()"
476
- ].join(" ")
477
- );
478
- }
479
- return segments;
480
- }
481
- function attributeExpression(raw) {
482
- const trimmed = raw.trim();
483
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
484
- return trimmed.slice(1, -1).trim();
449
+ if (char === "\\") {
450
+ escaping = true;
451
+ } else {
452
+ result += char;
453
+ }
485
454
  }
486
- return trimmed;
487
- }
488
- function wrapWithGuard2(rendered, guard) {
489
- if (!guard) {
490
- return rendered;
455
+ if (escaping) {
456
+ result += "\\";
491
457
  }
492
- return `(${guard}) ? ${rendered} : ""`;
493
- }
494
- function literal(text) {
495
- return JSON.stringify(text);
458
+ return result;
496
459
  }
497
- function concatSegments(segments) {
498
- const filtered = segments.filter((segment) => segment && segment !== '""');
499
- if (!filtered.length) {
500
- return '""';
501
- }
502
- if (filtered.length === 1) {
503
- return filtered[0];
460
+ function unescapeChar(char, quote) {
461
+ switch (char) {
462
+ case "n":
463
+ return "\n";
464
+ case "r":
465
+ return "\r";
466
+ case "t":
467
+ return " ";
468
+ case "\\":
469
+ return "\\";
470
+ case '"':
471
+ return '"';
472
+ case "'":
473
+ return "'";
474
+ default:
475
+ if (char === quote) {
476
+ return quote;
477
+ }
478
+ return char;
504
479
  }
505
- return filtered.join(" + ");
506
480
  }
507
481
  function buildClassAliasEnvironment2(decl) {
508
482
  const env = /* @__PURE__ */ new Map();
@@ -530,26 +504,6 @@ function expandClasses2(classes, aliasEnv) {
530
504
  }
531
505
  return result;
532
506
  }
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
507
  function escapeStaticText(value) {
554
508
  return value.replace(/[&<>{}]/g, (char) => {
555
509
  switch (char) {
@@ -568,47 +522,21 @@ function escapeStaticText(value) {
568
522
  }
569
523
  });
570
524
  }
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
- ];
525
+ function escapeAttributeValue(value) {
526
+ return value.replace(/["&<>]/g, (char) => {
527
+ switch (char) {
528
+ case "&":
529
+ return "&amp;";
530
+ case "<":
531
+ return "&lt;";
532
+ case ">":
533
+ return "&gt;";
534
+ case '"':
535
+ return "&quot;";
536
+ default:
537
+ return char;
538
+ }
539
+ });
612
540
  }
613
541
 
614
542
  // src/diagnostics.ts
@@ -1969,10 +1897,9 @@ function compileToTsx(sourceOrAst, options = {}) {
1969
1897
  function compileToHtml(sourceOrAst, options = {}) {
1970
1898
  const document = normalizeDocument(sourceOrAst, options.filename);
1971
1899
  const diagnostics = options.filename ? attachFilename(document.diagnostics, options.filename) : document.diagnostics;
1972
- const componentName = options.componentNameHint ?? "CollieTemplate";
1973
- let code = createStubHtml(componentName);
1900
+ let code = createStubHtml();
1974
1901
  if (!hasErrors(diagnostics)) {
1975
- code = generateHtmlModule(document.root, { componentName });
1902
+ code = generateHtml(document.root);
1976
1903
  }
1977
1904
  return { code, diagnostics, map: void 0 };
1978
1905
  }
@@ -2014,8 +1941,8 @@ function createStubComponent(name, flavor) {
2014
1941
  }
2015
1942
  return [`export default function ${name}(props) {`, " return null;", "}"].join("\n");
2016
1943
  }
2017
- function createStubHtml(name) {
2018
- return [`export default function ${name}(props = {}) {`, ' return "";', "}"].join("\n");
1944
+ function createStubHtml() {
1945
+ return "";
2019
1946
  }
2020
1947
  function attachFilename(diagnostics, filename) {
2021
1948
  if (!filename) {