@bendyline/squisq 1.5.0 → 1.5.1

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.
Files changed (124) hide show
  1. package/README.md +14 -1
  2. package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
  3. package/dist/{Doc-BsMkmzH8.d.ts → Doc-BBVGq1_9.d.ts} +94 -15
  4. package/dist/{ImageEditDoc-DvoeHMP9.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +1 -1
  5. package/dist/{chunk-KZNXZ5SM.js → chunk-6ATE2PSM.js} +18 -7
  6. package/dist/chunk-6ATE2PSM.js.map +1 -0
  7. package/dist/{chunk-7MQGNR76.js → chunk-A4O7GIWE.js} +2 -27
  8. package/dist/chunk-A4O7GIWE.js.map +1 -0
  9. package/dist/chunk-ET53IIEP.js +1 -0
  10. package/dist/chunk-ET53IIEP.js.map +1 -0
  11. package/dist/{chunk-OKJOQXK4.js → chunk-HWVFJAAH.js} +38 -2
  12. package/dist/chunk-HWVFJAAH.js.map +1 -0
  13. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  14. package/dist/chunk-IMSCRLLV.js.map +1 -0
  15. package/dist/{chunk-4AKUQPEE.js → chunk-KKNUBQ6Y.js} +1 -1
  16. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  17. package/dist/{chunk-TVSGLUID.js → chunk-LDQ2HJIX.js} +3 -3
  18. package/dist/{chunk-4ZFRZBU2.js → chunk-LH7I6SH7.js} +777 -151
  19. package/dist/chunk-LH7I6SH7.js.map +1 -0
  20. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  21. package/dist/chunk-T5UK6YOB.js.map +1 -0
  22. package/dist/{chunk-OH43RPTP.js → chunk-TEEEILMP.js} +12 -2
  23. package/dist/chunk-TEEEILMP.js.map +1 -0
  24. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  25. package/dist/chunk-TQWLI6S2.js.map +1 -0
  26. package/dist/{chunk-LRCV7N7F.js → chunk-UY7KGQ5R.js} +2 -2
  27. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  28. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  29. package/dist/doc/index.d.ts +4 -4
  30. package/dist/doc/index.js +16 -7
  31. package/dist/generate/index.d.ts +2 -80
  32. package/dist/generate/index.js +2 -7
  33. package/dist/imageEdit/index.d.ts +4 -4
  34. package/dist/index.d.ts +8 -8
  35. package/dist/index.js +32 -23
  36. package/dist/jsonForm/index.d.ts +42 -1
  37. package/dist/jsonForm/index.js +9 -1
  38. package/dist/markdown/index.d.ts +18 -2
  39. package/dist/markdown/index.js +5 -3
  40. package/dist/recommend/index.js +2 -2
  41. package/dist/schemas/index.d.ts +4 -4
  42. package/dist/schemas/index.js +3 -5
  43. package/dist/storage/index.d.ts +2 -2
  44. package/dist/storage/index.js +1 -1
  45. package/dist/story/index.d.ts +125 -11
  46. package/dist/story/index.js +16 -7
  47. package/dist/{themeLibrary-DQQKuRMx.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  48. package/dist/timing/index.js +3 -5
  49. package/dist/transform/index.d.ts +2 -2
  50. package/dist/transform/index.js +3 -3
  51. package/dist/versions/index.d.ts +1 -1
  52. package/package.json +1 -1
  53. package/src/__tests__/contentExtractor.test.ts +60 -0
  54. package/src/__tests__/coverBlock.test.ts +46 -1
  55. package/src/__tests__/customTemplateSample.test.ts +63 -2
  56. package/src/__tests__/customTemplatesFrontmatter.test.ts +31 -0
  57. package/src/__tests__/customThemesFrontmatter.test.ts +27 -0
  58. package/src/__tests__/inputDescriptors.test.ts +162 -0
  59. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  60. package/src/__tests__/markdown.test.ts +64 -0
  61. package/src/__tests__/markdownSanitize.test.ts +15 -0
  62. package/src/__tests__/markdownToDoc.test.ts +168 -0
  63. package/src/__tests__/mediaAnnotations.test.ts +27 -0
  64. package/src/__tests__/structuredData.test.ts +73 -3
  65. package/src/__tests__/templateMetadata.test.ts +11 -1
  66. package/src/__tests__/templates.test.ts +112 -0
  67. package/src/__tests__/validateDoc.test.ts +160 -0
  68. package/src/doc/annotationBlocks.ts +117 -0
  69. package/src/doc/customTemplatesFrontmatter.ts +7 -1
  70. package/src/doc/customThemesFrontmatter.ts +6 -1
  71. package/src/doc/docToMarkdown.ts +31 -1
  72. package/src/doc/getLayers.ts +11 -2
  73. package/src/doc/index.ts +12 -0
  74. package/src/doc/markdownToDoc.ts +80 -3
  75. package/src/doc/mediaAnnotations.ts +15 -33
  76. package/src/doc/standaloneAnnotation.ts +58 -0
  77. package/src/doc/structuredData.ts +92 -21
  78. package/src/doc/templates/__tests__/customTemplate.test.ts +21 -2
  79. package/src/doc/templates/comparisonBar.ts +8 -1
  80. package/src/doc/templates/coverBlock.ts +29 -2
  81. package/src/doc/templates/customTemplate.ts +10 -4
  82. package/src/doc/templates/factCard.ts +4 -2
  83. package/src/doc/templates/imageWithCaption.ts +10 -8
  84. package/src/doc/templates/index.ts +12 -2
  85. package/src/doc/templates/inputDescriptors.ts +432 -0
  86. package/src/doc/templates/photoGrid.ts +16 -39
  87. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +267 -1
  88. package/src/doc/templates/tokens/resolveTokens.ts +0 -0
  89. package/src/doc/templates/videoWithCaption.ts +15 -14
  90. package/src/doc/utils/nearest.ts +59 -0
  91. package/src/doc/validate.ts +153 -54
  92. package/src/generate/contentExtractor.ts +148 -35
  93. package/src/generate/index.ts +0 -3
  94. package/src/jsonForm/index.ts +2 -0
  95. package/src/jsonForm/tokens.ts +78 -0
  96. package/src/markdown/annotationCoercion.ts +1 -1
  97. package/src/markdown/attrTokens.ts +19 -0
  98. package/src/markdown/index.ts +2 -1
  99. package/src/markdown/sanitize.ts +22 -2
  100. package/src/markdown/stringify.ts +12 -3
  101. package/src/markdown/utils.ts +62 -1
  102. package/src/schemas/CustomTemplates.ts +56 -4
  103. package/src/schemas/Doc.ts +39 -41
  104. package/src/schemas/themeCompile.ts +1 -1
  105. package/src/storage/ContentContainer.ts +6 -2
  106. package/src/transform/types.ts +1 -1
  107. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  108. package/dist/chunk-3E5F2XMR.js.map +0 -1
  109. package/dist/chunk-4AKUQPEE.js.map +0 -1
  110. package/dist/chunk-4X3JQXNM.js +0 -35
  111. package/dist/chunk-4X3JQXNM.js.map +0 -1
  112. package/dist/chunk-4ZFRZBU2.js.map +0 -1
  113. package/dist/chunk-7MQGNR76.js.map +0 -1
  114. package/dist/chunk-FBKP5CAO.js +0 -145
  115. package/dist/chunk-FBKP5CAO.js.map +0 -1
  116. package/dist/chunk-FR2RBTKO.js.map +0 -1
  117. package/dist/chunk-K32VJONL.js.map +0 -1
  118. package/dist/chunk-KZNXZ5SM.js.map +0 -1
  119. package/dist/chunk-OH43RPTP.js.map +0 -1
  120. package/dist/chunk-OKJOQXK4.js.map +0 -1
  121. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  122. package/src/generate/slideshowGenerator.ts +0 -280
  123. /package/dist/{chunk-TVSGLUID.js.map → chunk-LDQ2HJIX.js.map} +0 -0
  124. /package/dist/{chunk-LRCV7N7F.js.map → chunk-UY7KGQ5R.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  pickAutoTemplate,
3
3
  profileBlockContents
4
- } from "./chunk-LRCV7N7F.js";
4
+ } from "./chunk-UY7KGQ5R.js";
5
5
  import {
6
6
  SeededRandom,
7
7
  hashString
@@ -24,7 +24,7 @@ import {
24
24
  resolveTheme,
25
25
  validateTheme,
26
26
  withAlpha
27
- } from "./chunk-4AKUQPEE.js";
27
+ } from "./chunk-KKNUBQ6Y.js";
28
28
  import {
29
29
  VIEWPORT_PRESETS,
30
30
  createTemplateContext,
@@ -34,25 +34,28 @@ import {
34
34
  scaledFontSize2 as scaledFontSize
35
35
  } from "./chunk-DLXZMS5K.js";
36
36
  import {
37
+ KNOWN_BLOCK_META_KEYS,
37
38
  coerceAnnotationValues,
38
39
  matchTrailingTemplateAnnotation,
39
40
  parseMarkdown,
41
+ parseNumber,
40
42
  parseTimeSeconds,
43
+ serializeAnnotation,
41
44
  splitKeyValueToken,
42
45
  tokenizeAttrTokens
43
- } from "./chunk-OH43RPTP.js";
46
+ } from "./chunk-TEEEILMP.js";
44
47
  import {
45
48
  extractPlainText,
46
49
  getChildren,
47
50
  readFrontmatterThemeId,
48
51
  walkMarkdownTree
49
- } from "./chunk-OKJOQXK4.js";
52
+ } from "./chunk-HWVFJAAH.js";
50
53
  import {
51
54
  resolveBlockTransition
52
55
  } from "./chunk-QE7TWGVT.js";
53
56
  import {
54
57
  estimateReadingTime
55
- } from "./chunk-4X3JQXNM.js";
58
+ } from "./chunk-VWUFZ6ZG.js";
56
59
 
57
60
  // src/doc/templates/persistentLayers.ts
58
61
  var GRADIENT_PRESETS = {
@@ -530,6 +533,10 @@ function applyRenderStyleToLayers(layers, block, theme) {
530
533
  }
531
534
 
532
535
  // src/doc/templates/tokens/resolveTokens.ts
536
+ var TOKEN_RE = /\{(\w+)(?::([^}|]+))?(?:\|([^}]*))?\}/g;
537
+ var ESCAPE_RE = /\{\{([^{}]*)\}\}/g;
538
+ var ESCAPED_OPEN = " ESC_OPEN ";
539
+ var ESCAPED_CLOSE = " ESC_CLOSE ";
533
540
  function collectImages(block) {
534
541
  if (!block.contents) return [];
535
542
  const out = [];
@@ -547,6 +554,21 @@ function collectImages(block) {
547
554
  }
548
555
  return out;
549
556
  }
557
+ function collectListItems(block) {
558
+ if (!block.contents) return [];
559
+ const out = [];
560
+ for (const node of block.contents) {
561
+ walkMarkdownTree(node, (n) => {
562
+ if (n.type === "listItem") {
563
+ const text = extractPlainText(n).replace(/\s+/g, " ").trim();
564
+ if (text) out.push(text);
565
+ return true;
566
+ }
567
+ return;
568
+ });
569
+ }
570
+ return out;
571
+ }
550
572
  function getBlockBodyText(block) {
551
573
  if (!block.contents) return "";
552
574
  return block.contents.map((node) => extractPlainText(node).trim()).filter((s) => s.length > 0).join(" ");
@@ -555,22 +577,26 @@ function getChildrenSummary(block) {
555
577
  if (!block.children || block.children.length === 0) return "";
556
578
  return block.children.map((c) => c.title?.trim()).filter((t) => Boolean(t)).join(", ");
557
579
  }
558
- function substituteText(text, block, images) {
559
- const ESCAPED_OPEN = "\0ESC_OPEN\0";
560
- const ESCAPED_CLOSE = "\0ESC_CLOSE\0";
561
- const escaped = text.replace(/\{\{([^{}]*)\}\}/g, (_match, inner) => {
562
- return ESCAPED_OPEN + inner + ESCAPED_CLOSE;
563
- });
580
+ function escapeBraces(text) {
581
+ return text.replace(ESCAPE_RE, (_match, inner) => ESCAPED_OPEN + inner + ESCAPED_CLOSE);
582
+ }
583
+ function unescapeBraces(text) {
584
+ return text.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
585
+ }
586
+ function substitute(text, resolve) {
587
+ const escaped = escapeBraces(text);
564
588
  const tokenized = escaped.replace(
565
- /\{(\w+)(?::([^}]+))?\}/g,
566
- (match, name, arg) => {
567
- const value = resolveToken(name, arg, block, images);
568
- return value ?? match;
589
+ TOKEN_RE,
590
+ (match, name, arg, def) => {
591
+ const value = resolve(name, arg);
592
+ if (value == null) return match;
593
+ if (value === "" && def !== void 0) return def;
594
+ return value;
569
595
  }
570
596
  );
571
- return tokenized.replace(new RegExp(ESCAPED_OPEN, "g"), "{").replace(new RegExp(ESCAPED_CLOSE, "g"), "}");
597
+ return unescapeBraces(tokenized);
572
598
  }
573
- function resolveToken(name, arg, block, images) {
599
+ function resolveToken(name, arg, block, images, item) {
574
600
  switch (name) {
575
601
  case "title":
576
602
  return block.title ?? "";
@@ -578,49 +604,148 @@ function resolveToken(name, arg, block, images) {
578
604
  return getBlockBodyText(block);
579
605
  case "children":
580
606
  return getChildrenSummary(block);
607
+ case "attr": {
608
+ if (!arg) return "";
609
+ const raw = block.templateOverrides?.[arg] ?? block.templateData?.[arg] ?? block.metadata?.[arg];
610
+ return raw == null ? "" : String(raw);
611
+ }
581
612
  case "image": {
582
613
  const idx = arg ? parseInt(arg, 10) : 0;
583
614
  if (!Number.isFinite(idx) || idx < 0 || idx >= images.length) return "";
584
615
  return images[idx].alt;
585
616
  }
617
+ case "item": {
618
+ if (!item) return null;
619
+ if (!arg) return item.text;
620
+ if (arg === "src") return item.src ?? "";
621
+ if (arg === "label") return item.label ?? "";
622
+ return "";
623
+ }
624
+ case "index":
625
+ return item ? String(item.index) : null;
586
626
  default:
587
627
  return null;
588
628
  }
589
629
  }
590
- function resolveImageSrc(src, images) {
591
- if (!/\{image:?[^}]*\}/.test(src)) return src;
630
+ function substituteText(text, block, images, item) {
631
+ return substitute(text, (name, arg) => resolveToken(name, arg, block, images, item));
632
+ }
633
+ function resolveImageSrc(src, block, images, item) {
634
+ if (!src.includes("{")) return src;
592
635
  let dropped = false;
593
- const resolved = src.replace(/\{image(?::([^}]+))?\}/g, (_match, arg) => {
594
- const idx = arg ? parseInt(arg, 10) : 0;
595
- if (!Number.isFinite(idx) || idx < 0 || idx >= images.length) {
596
- dropped = true;
597
- return "";
636
+ const escaped = escapeBraces(src);
637
+ const tokenized = escaped.replace(
638
+ TOKEN_RE,
639
+ (match, name, arg, def) => {
640
+ if (name === "image") {
641
+ const idx = arg ? parseInt(arg, 10) : 0;
642
+ const ok = Number.isFinite(idx) && idx >= 0 && idx < images.length;
643
+ if (ok) return images[idx].url;
644
+ if (def !== void 0) return def;
645
+ dropped = true;
646
+ return "";
647
+ }
648
+ const value = resolveToken(name, arg, block, images, item);
649
+ if (value == null) return match;
650
+ if (value === "" && def !== void 0) return def;
651
+ return value;
598
652
  }
599
- return images[idx].url;
600
- });
601
- return dropped ? null : resolved;
653
+ );
654
+ if (dropped) return null;
655
+ return unescapeBraces(tokenized);
656
+ }
657
+ function parseMagnitude(value) {
658
+ if (typeof value === "number") return value;
659
+ if (typeof value === "string") {
660
+ const m = value.match(/-?\d*\.?\d+/);
661
+ if (m) return parseFloat(m[0]);
662
+ }
663
+ return 0;
664
+ }
665
+ function offsetCoord(base, delta) {
666
+ if (base == null) return delta;
667
+ if (typeof base === "number") return base + delta;
668
+ const m = base.match(/^(\s*)(-?\d*\.?\d+)(.*)$/);
669
+ if (m) {
670
+ const next = parseFloat(m[2]) + delta;
671
+ return `${m[1]}${next}${m[3]}`;
672
+ }
673
+ return base;
674
+ }
675
+ function collectRepeatItems(source, block, images) {
676
+ switch (source) {
677
+ case "images":
678
+ return images.map((img, i) => ({
679
+ text: img.alt,
680
+ src: img.url,
681
+ label: img.alt,
682
+ index: i + 1
683
+ }));
684
+ case "children":
685
+ return (block.children ?? []).map((child, i) => ({
686
+ text: child.title ?? "",
687
+ label: child.title ?? "",
688
+ index: i + 1
689
+ }));
690
+ case "listItems":
691
+ return collectListItems(block).map((text, i) => ({ text, label: text, index: i + 1 }));
692
+ default:
693
+ return [];
694
+ }
695
+ }
696
+ function expandRepeat(layer, block, images) {
697
+ const repeat = layer.repeat;
698
+ if (!repeat) {
699
+ const resolved = resolveLayer(layer, block, images, void 0);
700
+ return resolved ? [resolved] : [];
701
+ }
702
+ const items = collectRepeatItems(repeat.source, block, images);
703
+ const direction = repeat.direction ?? "column";
704
+ const gap = repeat.gap ?? 24;
705
+ const limit = repeat.max != null && repeat.max >= 0 ? Math.min(repeat.max, items.length) : items.length;
706
+ const out = [];
707
+ let cursor = 0;
708
+ for (let i = 0; i < limit; i++) {
709
+ const position = { ...layer.position };
710
+ if (direction === "row") {
711
+ position.x = offsetCoord(position.x, cursor);
712
+ } else {
713
+ position.y = offsetCoord(position.y, cursor);
714
+ }
715
+ const clone = { ...layer, position, id: `${layer.id}-${i}` };
716
+ delete clone.repeat;
717
+ const resolved = resolveLayer(clone, block, images, items[i]);
718
+ if (resolved) out.push(resolved);
719
+ const dim = direction === "row" ? layer.position.width : layer.position.height;
720
+ cursor += parseMagnitude(dim) + gap;
721
+ }
722
+ return out;
602
723
  }
603
724
  function resolveTokens(layers, block) {
604
725
  const images = collectImages(block);
605
726
  const out = [];
606
727
  for (const layer of layers) {
607
- const resolved = resolveLayer(layer, block, images);
608
- if (resolved) out.push(resolved);
728
+ if (layer.repeat) {
729
+ out.push(...expandRepeat(layer, block, images));
730
+ } else {
731
+ const resolved = resolveLayer(layer, block, images, void 0);
732
+ if (resolved) out.push(resolved);
733
+ }
609
734
  }
610
735
  return out;
611
736
  }
612
- function resolveLayer(layer, block, images) {
737
+ function resolveLayer(layer, block, images, item) {
613
738
  switch (layer.type) {
614
739
  case "text":
615
740
  return {
616
741
  ...layer,
617
742
  content: {
618
743
  ...layer.content,
619
- text: substituteText(layer.content.text, block, images)
744
+ text: substituteText(layer.content.text, block, images, item)
620
745
  }
621
746
  };
622
747
  case "image": {
623
- const nextSrc = resolveImageSrc(layer.content.src, images);
748
+ const nextSrc = resolveImageSrc(layer.content.src, block, images, item);
624
749
  if (nextSrc == null) return null;
625
750
  const next = {
626
751
  ...layer,
@@ -630,7 +755,7 @@ function resolveLayer(layer, block, images) {
630
755
  }
631
756
  };
632
757
  if (layer.content.alt && layer.content.alt.includes("{")) {
633
- next.content.alt = substituteText(layer.content.alt, block, images);
758
+ next.content.alt = substituteText(layer.content.alt, block, images, item);
634
759
  }
635
760
  return next;
636
761
  }
@@ -641,12 +766,305 @@ function resolveLayer(layer, block, images) {
641
766
 
642
767
  // src/doc/templates/customTemplate.ts
643
768
  function makeCustomTemplateFn(def) {
644
- return (_input, context) => {
645
- if (!context.block) return def.layers.slice();
646
- return resolveTokens(def.layers, context.block);
769
+ return (input, context) => {
770
+ const block = context.block ?? input;
771
+ if (!block) return def.layers.slice();
772
+ return resolveTokens(def.layers, block);
647
773
  };
648
774
  }
649
775
 
776
+ // src/doc/utils/nearest.ts
777
+ function levenshtein(a, b) {
778
+ if (a === b) return 0;
779
+ const m = a.length;
780
+ const n = b.length;
781
+ if (m === 0) return n;
782
+ if (n === 0) return m;
783
+ let prev = new Array(n + 1);
784
+ let curr = new Array(n + 1);
785
+ for (let j = 0; j <= n; j++) prev[j] = j;
786
+ for (let i = 1; i <= m; i++) {
787
+ curr[0] = i;
788
+ for (let j = 1; j <= n; j++) {
789
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
790
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
791
+ }
792
+ [prev, curr] = [curr, prev];
793
+ }
794
+ return prev[n];
795
+ }
796
+ function nearestName(input, candidates, options) {
797
+ const maxDistance = options?.maxDistance ?? 2;
798
+ let best;
799
+ let bestDist = Infinity;
800
+ const lower = input.toLowerCase();
801
+ for (const candidate of candidates) {
802
+ const dist = levenshtein(lower, candidate.toLowerCase());
803
+ if (dist < bestDist) {
804
+ bestDist = dist;
805
+ best = candidate;
806
+ }
807
+ }
808
+ if (best && (bestDist <= maxDistance || bestDist <= Math.ceil(input.length * 0.4))) {
809
+ return options?.map ? options.map(best) : best;
810
+ }
811
+ return void 0;
812
+ }
813
+
814
+ // src/doc/templates/inputDescriptors.ts
815
+ var AMBIENT_MOTION_VALUES = ["zoomIn", "zoomOut", "panLeft", "panRight"];
816
+ var CAPTION_POSITION_VALUES = ["bottom", "top", "center"];
817
+ var MAP_STYLE_VALUES = ["terrain", "satellite", "road", "toner", "watercolor"];
818
+ var DATE_EVENT_MOOD_VALUES = ["neutral", "somber", "celebratory"];
819
+ var COLOR_SCHEME_HINT = "theme color-scheme name \u2014 e.g. 'blue', 'green'";
820
+ var BASE_INPUT_DESCRIPTORS = [
821
+ {
822
+ key: "imageTreatment",
823
+ description: "Per-block photographic grade override ('none' opts out)",
824
+ values: ["none", "mono", "duotone", "warm", "cool"]
825
+ },
826
+ {
827
+ key: "useBottomLayer",
828
+ description: "Show the doc's bottom persistent layers",
829
+ coerce: "boolean"
830
+ },
831
+ { key: "useTopLayer", description: "Show the doc's top persistent layers", coerce: "boolean" }
832
+ ];
833
+ var TEMPLATE_INPUT_DESCRIPTORS = {
834
+ title: [
835
+ { key: "title", description: "Main title text" },
836
+ { key: "subtitle", description: "Subtitle or tagline" },
837
+ { key: "backgroundColor", description: "Background color", valueHint: "CSS color" }
838
+ ],
839
+ statHighlight: [
840
+ { key: "stat", description: 'The statistic (e.g. "89%", "2x")' },
841
+ { key: "description", description: "What the stat means" },
842
+ { key: "detail", description: "Additional detail or context" },
843
+ { key: "colorScheme", description: "Color scheme for the stat", valueHint: COLOR_SCHEME_HINT }
844
+ ],
845
+ quote: [
846
+ { key: "quote", description: "The quote text" },
847
+ { key: "attribution", description: "Attribution (author, source)" }
848
+ ],
849
+ pullQuote: [
850
+ { key: "text", description: "Quote text" },
851
+ { key: "attribution", description: "Attribution" }
852
+ ],
853
+ twoColumn: [
854
+ {
855
+ key: "left",
856
+ description: 'Left column, as "label|sublabel"',
857
+ coerce: "labeledPair",
858
+ required: true
859
+ },
860
+ {
861
+ key: "right",
862
+ description: 'Right column, as "label|sublabel"',
863
+ coerce: "labeledPair",
864
+ required: true
865
+ },
866
+ { key: "header", description: "Optional header above the columns" },
867
+ { key: "leftColor", description: "Left column color scheme", valueHint: COLOR_SCHEME_HINT },
868
+ { key: "rightColor", description: "Right column color scheme", valueHint: COLOR_SCHEME_HINT }
869
+ ],
870
+ dateEvent: [
871
+ { key: "date", description: 'The date (e.g. "July 14, 1974")' },
872
+ { key: "description", description: "What happened" },
873
+ { key: "footer", description: "Optional footer text" },
874
+ { key: "mood", description: "Emotional tone", values: DATE_EVENT_MOOD_VALUES }
875
+ ],
876
+ imageWithCaption: [
877
+ { key: "imageSrc", description: "Path to the image file", required: true },
878
+ { key: "imageAlt", description: "Alt text for accessibility" },
879
+ { key: "caption", description: "Caption text" },
880
+ { key: "captionPosition", description: "Caption placement", values: CAPTION_POSITION_VALUES },
881
+ { key: "ambientMotion", description: "Slow zoom/pan effect", values: AMBIENT_MOTION_VALUES },
882
+ { key: "isTitle", description: "Style the caption as a title", coerce: "boolean" },
883
+ { key: "subtitle", description: "Subtitle (only when isTitle)" },
884
+ { key: "imageCredit", description: "Photo credit / artist name" },
885
+ { key: "imageLicense", description: "License identifier" }
886
+ ],
887
+ leftFeature: FEATURE_DESCRIPTORS(),
888
+ rightFeature: FEATURE_DESCRIPTORS(),
889
+ map: [
890
+ {
891
+ key: "center",
892
+ description: 'Map center as "lat,lng"',
893
+ coerce: "latLng",
894
+ required: true
895
+ },
896
+ { key: "zoom", description: "Zoom level (4-16)", coerce: "number" },
897
+ { key: "mapStyle", description: "Map tile style", values: MAP_STYLE_VALUES },
898
+ { key: "title", description: "Optional title overlay" },
899
+ { key: "caption", description: "Optional caption" }
900
+ ],
901
+ photoGrid: [
902
+ { key: "images", description: "Comma-separated image paths", coerce: "stringList" },
903
+ { key: "caption", description: "Optional caption below the grid" },
904
+ {
905
+ key: "ambientMotion",
906
+ description: "Slow zoom/pan on the largest image",
907
+ values: AMBIENT_MOTION_VALUES
908
+ }
909
+ ],
910
+ comparisonBar: [
911
+ { key: "leftLabel", description: "Left bar label", required: true },
912
+ { key: "leftValue", description: "Left bar numeric value", coerce: "number", required: true },
913
+ { key: "rightLabel", description: "Right bar label", required: true },
914
+ { key: "rightValue", description: "Right bar numeric value", coerce: "number", required: true },
915
+ { key: "unit", description: 'Unit label (e.g. "km")' },
916
+ { key: "colorScheme", description: "Color scheme", valueHint: COLOR_SCHEME_HINT }
917
+ ],
918
+ dataTable: [
919
+ { key: "title", description: "Optional title above the table" },
920
+ { key: "headers", description: "Comma-separated header cells", coerce: "stringList" },
921
+ // `rows`/`align` are structured (string[][] / alignment[]) and can't be
922
+ // expressed as a single inline string — declared so they aren't flagged
923
+ // as unknown when supplied via a data fence's overriding params.
924
+ { key: "rows", description: "Table rows (supply via a data fence)" },
925
+ { key: "align", description: "Per-column alignment (supply via a data fence)" },
926
+ { key: "colorScheme", description: "Header color scheme", valueHint: COLOR_SCHEME_HINT }
927
+ ],
928
+ videoWithCaption: [
929
+ { key: "videoSrc", description: "Path to the video file", required: true },
930
+ { key: "posterSrc", description: "Poster frame path" },
931
+ { key: "videoAlt", description: "Alt text for accessibility" },
932
+ { key: "clipStart", description: "Clip start time (seconds)", coerce: "number" },
933
+ { key: "clipEnd", description: "Clip end time (seconds)", coerce: "number" },
934
+ { key: "caption", description: "Caption text" },
935
+ { key: "captionPosition", description: "Caption placement", values: CAPTION_POSITION_VALUES },
936
+ { key: "videoCredit", description: "Video credit / artist name" },
937
+ { key: "videoLicense", description: "License identifier" }
938
+ ]
939
+ };
940
+ function FEATURE_DESCRIPTORS() {
941
+ return [
942
+ { key: "imageSrc", description: "Path to the feature image", required: true },
943
+ { key: "imageAlt", description: "Alt text for accessibility" },
944
+ { key: "imageWidth", description: "Explicit display width in px", coerce: "number" },
945
+ { key: "imageHeight", description: "Explicit display height in px", coerce: "number" },
946
+ { key: "title", description: "Heading text next to the image" },
947
+ { key: "body", description: "Body text below the title" }
948
+ ];
949
+ }
950
+ function descriptorsFor(canonical) {
951
+ const specific = TEMPLATE_INPUT_DESCRIPTORS[canonical];
952
+ if (!specific) return void 0;
953
+ return [...specific, ...BASE_INPUT_DESCRIPTORS];
954
+ }
955
+ function descriptorMap(descriptors) {
956
+ return new Map(descriptors.map((d) => [d.key, d]));
957
+ }
958
+ function coerceTemplateParams(template, params) {
959
+ const input = {};
960
+ const warnings = [];
961
+ const descriptors = template ? descriptorsFor(resolveTemplateName(template)) : void 0;
962
+ const byKey = descriptors ? descriptorMap(descriptors) : void 0;
963
+ for (const [key, raw] of Object.entries(params)) {
964
+ const coerce = byKey?.get(key)?.coerce ?? "string";
965
+ const { value, warning } = coerceValue(coerce, raw);
966
+ if (warning) {
967
+ warnings.push(`"${key}": ${warning}`);
968
+ input[key] = raw;
969
+ } else {
970
+ input[key] = value;
971
+ }
972
+ }
973
+ return { input, warnings };
974
+ }
975
+ function coerceValue(coerce, raw) {
976
+ switch (coerce) {
977
+ case "number": {
978
+ const n = parseNumber(raw);
979
+ return n == null ? { value: raw, warning: `not a number (${JSON.stringify(raw)})` } : { value: n };
980
+ }
981
+ case "boolean":
982
+ return coerceBoolean(raw);
983
+ case "latLng":
984
+ return coerceLatLng(raw);
985
+ case "labeledPair":
986
+ return { value: coerceLabeledPair(raw) };
987
+ case "stringList":
988
+ return { value: coerceStringList(raw) };
989
+ case "string":
990
+ default:
991
+ return { value: raw };
992
+ }
993
+ }
994
+ function coerceBoolean(raw) {
995
+ const t = raw.trim().toLowerCase();
996
+ if (t === "true" || t === "") return { value: true };
997
+ if (t === "false") return { value: false };
998
+ return { value: raw, warning: `not a boolean (${JSON.stringify(raw)}) \u2014 expected true or false` };
999
+ }
1000
+ function coerceLatLng(raw) {
1001
+ const parts = raw.split(",");
1002
+ if (parts.length === 2) {
1003
+ const lat = parseNumber(parts[0]);
1004
+ const lng = parseNumber(parts[1]);
1005
+ if (lat != null && lng != null) return { value: { lat, lng } };
1006
+ }
1007
+ return { value: raw, warning: `not a "lat,lng" pair (${JSON.stringify(raw)})` };
1008
+ }
1009
+ function coerceLabeledPair(raw) {
1010
+ const sep = raw.indexOf("|");
1011
+ if (sep < 0) return { label: raw.trim() };
1012
+ const label = raw.slice(0, sep).trim();
1013
+ const sublabel = raw.slice(sep + 1).trim();
1014
+ return sublabel ? { label, sublabel } : { label };
1015
+ }
1016
+ function coerceStringList(raw) {
1017
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1018
+ }
1019
+ function lintTemplateParams(template, params) {
1020
+ const canonical = resolveTemplateName(template);
1021
+ const specific = TEMPLATE_INPUT_DESCRIPTORS[canonical];
1022
+ if (!specific) return [];
1023
+ const findings = [];
1024
+ const allDescriptors = [...specific, ...BASE_INPUT_DESCRIPTORS];
1025
+ const byKey = descriptorMap(allDescriptors);
1026
+ const knownKeys = /* @__PURE__ */ new Set([...byKey.keys(), ...Object.keys(KNOWN_BLOCK_META_KEYS)]);
1027
+ for (const [key, raw] of Object.entries(params)) {
1028
+ const descriptor = byKey.get(key);
1029
+ if (!descriptor) {
1030
+ if (knownKeys.has(key)) continue;
1031
+ const suggestion = nearestName(key, knownKeys);
1032
+ findings.push({
1033
+ kind: "unknown-input",
1034
+ key,
1035
+ message: `Unknown input "${key}" for template "${canonical}"` + (suggestion ? `. Did you mean "${suggestion}"?` : ""),
1036
+ ...suggestion ? { suggestion } : {}
1037
+ });
1038
+ continue;
1039
+ }
1040
+ if (descriptor.values) {
1041
+ const v = raw.trim();
1042
+ if (v !== "" && !descriptor.values.includes(v)) {
1043
+ findings.push({
1044
+ kind: "invalid-input-value",
1045
+ key,
1046
+ message: `Invalid value ${JSON.stringify(raw)} for "${key}" \u2014 expected one of: ${descriptor.values.join(", ")}`
1047
+ });
1048
+ }
1049
+ } else if (descriptor.coerce && descriptor.coerce !== "string") {
1050
+ const { warning } = coerceValue(descriptor.coerce, raw);
1051
+ if (warning) {
1052
+ findings.push({ kind: "invalid-input-value", key, message: `"${key}": ${warning}` });
1053
+ }
1054
+ }
1055
+ }
1056
+ for (const descriptor of specific) {
1057
+ if (descriptor.required && params[descriptor.key] == null) {
1058
+ findings.push({
1059
+ kind: "missing-input",
1060
+ key: descriptor.key,
1061
+ message: `Missing required input "${descriptor.key}" for template "${canonical}"`
1062
+ });
1063
+ }
1064
+ }
1065
+ return findings;
1066
+ }
1067
+
650
1068
  // src/doc/templates/metadata.ts
651
1069
  var TEMPLATE_METADATA = {
652
1070
  title: {
@@ -1440,7 +1858,7 @@ function factCard(input, context) {
1440
1858
  const { theme, viewport } = context;
1441
1859
  const accentLayout = accentImage ? getAccentLayout(accentImage.position) : DEFAULT_LAYOUT;
1442
1860
  const factFontSize = themedFontSize(56, context, true);
1443
- const explainFontSize = themedFontSize(28, context, false);
1861
+ const explainFontSize = themedFontSize(34, context, false);
1444
1862
  const sourceFontSize = themedFontSize(20, context, false);
1445
1863
  const layers = [createBackgroundLayer("bg", themedSurfaceGradient(context, 170))];
1446
1864
  if (accentImage) {
@@ -1854,7 +2272,7 @@ function imageWithCaption(input, context) {
1854
2272
  const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
1855
2273
  const { theme, layout } = context;
1856
2274
  const treatment = themedImageTreatment(context, input.imageTreatment);
1857
- const captionFontSize = themedFontSize(36, context, false);
2275
+ const captionFontSize = themedFontSize(30, context, false);
1858
2276
  const titleFontSize = themedFontSize(96, context, true);
1859
2277
  const subtitleFontSize = themedFontSize(36, context, false);
1860
2278
  const legacyKenBurns = input.kenBurns;
@@ -1941,13 +2359,13 @@ function imageWithCaption(input, context) {
1941
2359
  id: "caption-gradient",
1942
2360
  content: {
1943
2361
  shape: "rect",
1944
- fill: `linear-gradient(0deg, ${withAlpha(bg, 0.8)} 0%, ${withAlpha(bg, 0.35)} 60%, ${withAlpha(bg, 0)} 100%)`
2362
+ fill: `linear-gradient(90deg, ${withAlpha(bg, 0)}, ${withAlpha(bg, 0.78)} 16%, ${withAlpha(bg, 0.78)} 84%, ${withAlpha(bg, 0)})`
1945
2363
  },
1946
2364
  position: {
1947
2365
  x: 0,
1948
- y: "65%",
2366
+ y: "68%",
1949
2367
  width: "100%",
1950
- height: "35%"
2368
+ height: "16%"
1951
2369
  }
1952
2370
  });
1953
2371
  layers.push({
@@ -1960,14 +2378,16 @@ function imageWithCaption(input, context) {
1960
2378
  fontFamily: getThemeFont(context, "body"),
1961
2379
  color: theme.colors.text,
1962
2380
  textAlign: "center",
1963
- shadow: shouldUseShadow(context)
2381
+ shadow: shouldUseShadow(context),
2382
+ lineHeight: 1.18,
2383
+ maxLines: 2
1964
2384
  }
1965
2385
  },
1966
2386
  position: {
1967
2387
  x: "50%",
1968
- y: "82%",
2388
+ y: "74%",
1969
2389
  anchor: "center",
1970
- width: layout.maxTextWidth
2390
+ width: "78%"
1971
2391
  },
1972
2392
  animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
1973
2393
  });
@@ -2351,10 +2771,8 @@ function photoGrid(input, context) {
2351
2771
  position: { x: 0, y: 0, width: "100%", height: "100%" }
2352
2772
  }
2353
2773
  ];
2354
- const captionHeight = caption ? layout.stackColumns ? 16 : 14 : 0;
2355
- const gridHeight = 100 - captionHeight;
2774
+ const gridHeight = 100;
2356
2775
  const positions = getGridPositions(images.length, gridHeight, layout.stackColumns);
2357
- const altFontSize = themedFontSize(24, context, false);
2358
2776
  for (let i = 0; i < Math.min(images.length, 4); i++) {
2359
2777
  const img = images[i];
2360
2778
  const pos = positions[i];
@@ -2364,22 +2782,6 @@ function photoGrid(input, context) {
2364
2782
  content: { shape: "rect", fill: theme.colors.background },
2365
2783
  position: { x: `${pos.x}%`, y: `${pos.y}%`, width: `${pos.w}%`, height: `${pos.h}%` }
2366
2784
  });
2367
- if (img.alt) {
2368
- layers.push({
2369
- type: "text",
2370
- id: `grid-alt-${i}`,
2371
- content: {
2372
- text: img.alt,
2373
- style: {
2374
- fontSize: altFontSize,
2375
- fontFamily: getThemeFont(context, "body"),
2376
- color: theme.colors.textMuted,
2377
- textAlign: "center"
2378
- }
2379
- },
2380
- position: { x: `${pos.x + pos.w / 2}%`, y: `${pos.y + pos.h / 2}%`, anchor: "center" }
2381
- });
2382
- }
2383
2785
  layers.push({
2384
2786
  type: "image",
2385
2787
  id: `grid-img-${i}`,
@@ -2402,15 +2804,19 @@ function photoGrid(input, context) {
2402
2804
  });
2403
2805
  }
2404
2806
  if (caption) {
2405
- const captionFontSize = themedFontSize(layout.stackColumns ? 28 : 32, context, false);
2807
+ const captionFontSize = themedFontSize(layout.stackColumns ? 24 : 26, context, false);
2808
+ const captionY = layout.stackColumns ? "78%" : "74%";
2809
+ const captionBgY = layout.stackColumns ? "72%" : "68%";
2810
+ const captionHeight = layout.stackColumns ? "13%" : "16%";
2811
+ const bg = theme.colors.background;
2406
2812
  layers.push({
2407
2813
  type: "shape",
2408
2814
  id: "caption-bg",
2409
2815
  content: {
2410
2816
  shape: "rect",
2411
- fill: `linear-gradient(${withAlpha(theme.colors.background, 0)}, ${withAlpha(theme.colors.background, 0.85)})`
2817
+ fill: `linear-gradient(90deg, ${withAlpha(bg, 0)}, ${withAlpha(bg, 0.78)} 16%, ${withAlpha(bg, 0.78)} 84%, ${withAlpha(bg, 0)})`
2412
2818
  },
2413
- position: { x: 0, y: `${gridHeight - 4}%`, width: "100%", height: `${captionHeight + 4}%` }
2819
+ position: { x: 0, y: captionBgY, width: "100%", height: captionHeight }
2414
2820
  });
2415
2821
  layers.push({
2416
2822
  type: "text",
@@ -2422,14 +2828,16 @@ function photoGrid(input, context) {
2422
2828
  fontFamily: getThemeFont(context, "body"),
2423
2829
  color: theme.colors.text,
2424
2830
  textAlign: "center",
2425
- shadow: shouldUseShadow(context)
2831
+ shadow: true,
2832
+ lineHeight: 1.18,
2833
+ maxLines: layout.stackColumns ? 2 : 1
2426
2834
  }
2427
2835
  },
2428
2836
  position: {
2429
2837
  x: "50%",
2430
- y: `${gridHeight + captionHeight / 2}%`,
2838
+ y: captionY,
2431
2839
  anchor: "center",
2432
- width: "90%"
2840
+ width: "78%"
2433
2841
  },
2434
2842
  animation: { type: "fadeIn", duration: 1, delay: 0.5 }
2435
2843
  });
@@ -2585,12 +2993,15 @@ function definitionCard(input, context) {
2585
2993
 
2586
2994
  // src/doc/templates/comparisonBar.ts
2587
2995
  function comparisonBar(input, context) {
2588
- const { leftLabel, leftValue, rightLabel, rightValue, unit, colorScheme = "blue" } = input;
2996
+ const { leftLabel, rightLabel, unit, colorScheme = "blue" } = input;
2589
2997
  const { theme, viewport } = context;
2590
2998
  const colors = resolveColorScheme(context, colorScheme);
2999
+ const leftValue = Number.isFinite(input.leftValue) ? input.leftValue : 0;
3000
+ const rightValue = Number.isFinite(input.rightValue) ? input.rightValue : 0;
2591
3001
  const labelFontSize = themedFontSize(28, context, false);
2592
3002
  const valueFontSize = themedFontSize(48, context, true);
2593
3003
  const formatValue = (v) => {
3004
+ if (!Number.isFinite(v)) return "\u2014";
2594
3005
  if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
2595
3006
  if (v >= 1e3) return `${(v / 1e3).toFixed(v >= 1e4 ? 0 : 1)}K`;
2596
3007
  return v.toLocaleString();
@@ -2841,8 +3252,8 @@ function videoWithCaption(input, context) {
2841
3252
  videoLicense
2842
3253
  } = input;
2843
3254
  const caption = rawCaption ? cleanCaption(rawCaption) : rawCaption;
2844
- const { theme, layout } = context;
2845
- const captionFontSize = themedFontSize(36, context, false);
3255
+ const { theme } = context;
3256
+ const captionFontSize = themedFontSize(30, context, false);
2846
3257
  const creditFontSize = themedFontSize(16, context, false);
2847
3258
  const layers = [
2848
3259
  // Background video clip
@@ -2865,22 +3276,22 @@ function videoWithCaption(input, context) {
2865
3276
  ];
2866
3277
  if (caption) {
2867
3278
  const bg = theme.colors.background;
2868
- const scrimAngle = captionPosition === "top" ? 180 : 0;
3279
+ const captionBgY = captionPosition === "top" ? "7%" : captionPosition === "center" ? "42%" : "68%";
3280
+ const captionY = captionPosition === "top" ? "13%" : captionPosition === "center" ? "48%" : "74%";
2869
3281
  layers.push({
2870
3282
  type: "shape",
2871
3283
  id: "caption-gradient",
2872
3284
  content: {
2873
3285
  shape: "rect",
2874
- fill: `linear-gradient(${scrimAngle}deg, ${withAlpha(bg, 0.8)} 0%, ${withAlpha(bg, 0.35)} 60%, ${withAlpha(bg, 0)} 100%)`
3286
+ fill: `linear-gradient(90deg, ${withAlpha(bg, 0)}, ${withAlpha(bg, 0.78)} 16%, ${withAlpha(bg, 0.78)} 84%, ${withAlpha(bg, 0)})`
2875
3287
  },
2876
3288
  position: {
2877
3289
  x: 0,
2878
- y: captionPosition === "top" ? 0 : "65%",
3290
+ y: captionBgY,
2879
3291
  width: "100%",
2880
- height: "35%"
3292
+ height: "16%"
2881
3293
  }
2882
3294
  });
2883
- const captionY = captionPosition === "top" ? "15%" : captionPosition === "center" ? "50%" : "82%";
2884
3295
  layers.push({
2885
3296
  type: "text",
2886
3297
  id: "caption",
@@ -2891,14 +3302,16 @@ function videoWithCaption(input, context) {
2891
3302
  fontFamily: getThemeFont(context, "body"),
2892
3303
  color: theme.colors.text,
2893
3304
  textAlign: "center",
2894
- shadow: shouldUseShadow(context)
3305
+ shadow: shouldUseShadow(context),
3306
+ lineHeight: 1.18,
3307
+ maxLines: 2
2895
3308
  }
2896
3309
  },
2897
3310
  position: {
2898
3311
  x: "50%",
2899
3312
  y: captionY,
2900
3313
  anchor: "center",
2901
- width: layout.maxTextWidth
3314
+ width: "78%"
2902
3315
  },
2903
3316
  animation: { type: "fadeIn", duration: 1.5, delay: 0.5 }
2904
3317
  });
@@ -4050,11 +4463,20 @@ function emptyHint2(input, context) {
4050
4463
  }
4051
4464
 
4052
4465
  // src/doc/templates/coverBlock.ts
4466
+ var COVER_TITLE_BASE_PX = 120;
4467
+ var COVER_TITLE_COMFORTABLE_CHARS = 26;
4468
+ var COVER_TITLE_MIN_BASE_PX = 62;
4469
+ function fitCoverTitleSize(title) {
4470
+ const len = title.trim().length;
4471
+ if (len <= COVER_TITLE_COMFORTABLE_CHARS) return COVER_TITLE_BASE_PX;
4472
+ const scaled = COVER_TITLE_BASE_PX * Math.sqrt(COVER_TITLE_COMFORTABLE_CHARS / len);
4473
+ return Math.max(COVER_TITLE_MIN_BASE_PX, Math.round(scaled));
4474
+ }
4053
4475
  function coverBlock(input, context) {
4054
4476
  const treatment = themedImageTreatment(context, input.imageTreatment);
4055
4477
  const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
4056
4478
  const { theme, layout } = context;
4057
- const titleFontSize = themedFontSize(120, context, true);
4479
+ const titleFontSize = themedFontSize(fitCoverTitleSize(title), context, true);
4058
4480
  const subtitleFontSize = themedFontSize(40, context, false);
4059
4481
  const layers = [];
4060
4482
  if (heroSrc) {
@@ -4243,7 +4665,11 @@ function expandTemplateBlock(templateBlock, context, registry = templateRegistry
4243
4665
  };
4244
4666
  }
4245
4667
  const { templateData, templateOverrides } = templateBlock;
4246
- const input = templateData || templateOverrides ? { ...templateBlock, ...templateData, ...templateOverrides } : templateBlock;
4668
+ const input = templateData || templateOverrides ? {
4669
+ ...templateBlock,
4670
+ ...templateData,
4671
+ ...coerceTemplateParams(templateBlock.template, templateOverrides ?? {}).input
4672
+ } : templateBlock;
4247
4673
  let layers;
4248
4674
  try {
4249
4675
  layers = templateFn(input, context);
@@ -5047,7 +5473,7 @@ function readCustomTemplatesFromFrontmatter(frontmatter) {
5047
5473
  }
5048
5474
  return out.length > 0 ? out : void 0;
5049
5475
  }
5050
- function writeCustomTemplatesToFrontmatter(templates) {
5476
+ function writeCustomTemplatesToFrontmatter(templates, options) {
5051
5477
  if (!templates || templates.length === 0) return void 0;
5052
5478
  const map = {};
5053
5479
  for (const def of templates) {
@@ -5057,7 +5483,7 @@ function writeCustomTemplatesToFrontmatter(templates) {
5057
5483
  entry.ly = renameKeys(def.layers, LONG_TO_SHORT);
5058
5484
  map[def.name] = entry;
5059
5485
  }
5060
- return JSON.stringify(map);
5486
+ return JSON.stringify(map, null, options?.pretty ? 2 : void 0);
5061
5487
  }
5062
5488
  function readViewport(raw) {
5063
5489
  if (raw && typeof raw === "object") {
@@ -5139,11 +5565,11 @@ function readCustomThemesFromFrontmatter(frontmatter) {
5139
5565
  }
5140
5566
  return out.length > 0 ? out : void 0;
5141
5567
  }
5142
- function writeCustomThemesToFrontmatter(themes) {
5568
+ function writeCustomThemesToFrontmatter(themes, options) {
5143
5569
  if (!themes || themes.length === 0) return void 0;
5144
5570
  const map = {};
5145
5571
  for (const theme of themes) map[theme.id] = theme;
5146
- return JSON.stringify(map);
5572
+ return JSON.stringify(map, null, options?.pretty ? 2 : void 0);
5147
5573
  }
5148
5574
 
5149
5575
  // src/doc/structuredData.ts
@@ -5172,6 +5598,17 @@ function parseDataFence(node) {
5172
5598
  return { error: `invalid YAML: ${err instanceof Error ? err.message : String(err)}` };
5173
5599
  }
5174
5600
  }
5601
+ function replaceDataFence(contents, data) {
5602
+ const value = JSON.stringify(data, null, 2);
5603
+ const index = contents.findIndex((node) => node.type === "code" && isDataFence(node));
5604
+ if (index < 0) {
5605
+ const fence = { type: "code", lang: "json", meta: "data", value };
5606
+ return [...contents, fence];
5607
+ }
5608
+ const next = [...contents];
5609
+ next[index] = { ...contents[index], value };
5610
+ return next;
5611
+ }
5175
5612
  function parseScalar(raw) {
5176
5613
  const trimmed = raw.trim();
5177
5614
  if (trimmed.length >= 2) {
@@ -5217,25 +5654,59 @@ function parseInlineValue(raw) {
5217
5654
  function parseYamlSubset(src) {
5218
5655
  const result = {};
5219
5656
  const lines = src.split("\n");
5220
- let pendingListKey = null;
5657
+ let pendingKey = null;
5658
+ let pendingKind = null;
5659
+ let childIndent = -1;
5221
5660
  for (let i = 0; i < lines.length; i++) {
5222
5661
  const line = lines[i];
5223
5662
  const trimmed = line.trim();
5224
5663
  if (!trimmed || trimmed.startsWith("#")) continue;
5225
- const indented = /^\s/.test(line);
5226
- if (indented && trimmed.startsWith("- ")) {
5227
- if (!pendingListKey) {
5228
- throw new Error(`line ${i + 1}: list item without a preceding "key:" line`);
5664
+ const indent = line.length - line.trimStart().length;
5665
+ if (indent > 0) {
5666
+ if (!pendingKey) {
5667
+ throw new Error(`line ${i + 1}: unexpected indented line without a preceding "key:" line`);
5668
+ }
5669
+ if (childIndent < 0) childIndent = indent;
5670
+ if (indent > childIndent) {
5671
+ throw new Error(
5672
+ `line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
5673
+ );
5674
+ }
5675
+ if (trimmed.startsWith("- ")) {
5676
+ if (pendingKind === "map") {
5677
+ throw new Error(
5678
+ `line ${i + 1}: cannot mix list items and mapping keys under "${pendingKey}"`
5679
+ );
5680
+ }
5681
+ pendingKind = "list";
5682
+ result[pendingKey].push(parseInlineValue(trimmed.slice(2)));
5683
+ continue;
5684
+ }
5685
+ if (pendingKind === "list") {
5686
+ throw new Error(
5687
+ `line ${i + 1}: cannot mix mapping keys and list items under "${pendingKey}"`
5688
+ );
5689
+ }
5690
+ const subColon = trimmed.indexOf(":");
5691
+ if (subColon < 1) {
5692
+ throw new Error(`line ${i + 1}: expected "key: value"`);
5693
+ }
5694
+ const subValue = trimmed.slice(subColon + 1).trim();
5695
+ if (subValue === "") {
5696
+ throw new Error(
5697
+ `line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
5698
+ );
5229
5699
  }
5230
- result[pendingListKey].push(parseInlineValue(trimmed.slice(2)));
5700
+ if (pendingKind !== "map") {
5701
+ pendingKind = "map";
5702
+ result[pendingKey] = {};
5703
+ }
5704
+ result[pendingKey][trimmed.slice(0, subColon).trim()] = parseInlineValue(subValue);
5231
5705
  continue;
5232
5706
  }
5233
- if (indented) {
5234
- throw new Error(
5235
- `line ${i + 1}: nested mappings are not supported \u2014 use a \`\`\`json data fence for nested input`
5236
- );
5237
- }
5238
- pendingListKey = null;
5707
+ pendingKey = null;
5708
+ pendingKind = null;
5709
+ childIndent = -1;
5239
5710
  const colonIdx = trimmed.indexOf(":");
5240
5711
  if (colonIdx < 1) {
5241
5712
  throw new Error(`line ${i + 1}: expected "key: value"`);
@@ -5244,7 +5715,7 @@ function parseYamlSubset(src) {
5244
5715
  const rawValue = trimmed.slice(colonIdx + 1).trim();
5245
5716
  if (rawValue === "") {
5246
5717
  result[key] = [];
5247
- pendingListKey = key;
5718
+ pendingKey = key;
5248
5719
  } else {
5249
5720
  result[key] = parseInlineValue(rawValue);
5250
5721
  }
@@ -5264,8 +5735,7 @@ function extractTableData(table) {
5264
5735
  };
5265
5736
  }
5266
5737
 
5267
- // src/doc/mediaAnnotations.ts
5268
- var MEDIA_TEMPLATES = /* @__PURE__ */ new Set(["audio", "video", "media"]);
5738
+ // src/doc/standaloneAnnotation.ts
5269
5739
  function parseStandaloneAnnotation(node) {
5270
5740
  if (node.type !== "paragraph") return null;
5271
5741
  const children = node.children ?? [];
@@ -5284,6 +5754,12 @@ function parseStandaloneAnnotation(node) {
5284
5754
  }
5285
5755
  return { template, params };
5286
5756
  }
5757
+
5758
+ // src/doc/mediaAnnotations.ts
5759
+ var MEDIA_TEMPLATES = /* @__PURE__ */ new Set(["audio", "video", "media"]);
5760
+ function isMediaAnnotationName(name) {
5761
+ return name != null && MEDIA_TEMPLATES.has(name);
5762
+ }
5287
5763
  function time(raw) {
5288
5764
  if (raw == null) return null;
5289
5765
  return parseTimeSeconds(raw);
@@ -5295,7 +5771,13 @@ function toMediaClip(parsed, id) {
5295
5771
  if (!src) return null;
5296
5772
  const kind = template === "video" ? "video" : "audio";
5297
5773
  const anchor = params.anchor === "document" || params.span === "document" ? "document" : "block";
5298
- const clip = { id, src, kind, startAt: time(params.startAt) ?? 0, anchor };
5774
+ const clip = {
5775
+ id,
5776
+ src,
5777
+ kind,
5778
+ startAt: time(params.startAt ?? params.startTime) ?? 0,
5779
+ anchor
5780
+ };
5299
5781
  const clipStart = time(params.clipStart);
5300
5782
  const clipEnd = time(params.clipEnd);
5301
5783
  if (clipStart != null) clip.clipStart = clipStart;
@@ -5323,6 +5805,58 @@ function extractMediaFromContents(contents, makeId) {
5323
5805
  return { media, documentMedia, remaining };
5324
5806
  }
5325
5807
 
5808
+ // src/doc/annotationBlocks.ts
5809
+ function applyBlockMeta(block, m) {
5810
+ if (m.x != null) block.x = m.x;
5811
+ if (m.y != null) block.y = m.y;
5812
+ if (m.startTime != null) block.startTime = m.startTime;
5813
+ if (m.duration != null) block.duration = m.duration;
5814
+ if (m.connectsTo) block.connectsTo = m.connectsTo;
5815
+ if (m.transition) block.transition = m.transition;
5816
+ }
5817
+ function makeStandaloneBlock(parsed, options) {
5818
+ const rawTemplate = parsed.template;
5819
+ const params = parsed.params;
5820
+ const hasParams = Object.keys(params).length > 0;
5821
+ const block = {
5822
+ id: options.makeId(parsed),
5823
+ startTime: 0,
5824
+ duration: options.defaultDuration,
5825
+ audioSegment: 0,
5826
+ template: resolveTemplateName(rawTemplate),
5827
+ standaloneAnnotation: true,
5828
+ // Keep the raw (un-resolved) name so validation can report the exact
5829
+ // requested name — symmetric with `sourceHeading.templateAnnotation`.
5830
+ sourceAnnotation: { template: rawTemplate, ...hasParams ? { params } : {} },
5831
+ // Only carry a display title when the author supplied one.
5832
+ ...params.title ? { title: params.title } : {}
5833
+ };
5834
+ if (hasParams) {
5835
+ block.templateOverrides = params;
5836
+ applyBlockMeta(block, coerceAnnotationValues(params).blockMeta);
5837
+ }
5838
+ return block;
5839
+ }
5840
+ function extractTemplateBlocksFromContents(contents, options) {
5841
+ const leading = [];
5842
+ const blocks = [];
5843
+ let current = null;
5844
+ for (const node of contents ?? []) {
5845
+ const parsed = parseStandaloneAnnotation(node);
5846
+ const isTemplateAnnotation = parsed != null && parsed.template != null && !isMediaAnnotationName(parsed.template);
5847
+ if (isTemplateAnnotation) {
5848
+ const block = makeStandaloneBlock(parsed, options);
5849
+ blocks.push(block);
5850
+ current = block;
5851
+ } else if (current) {
5852
+ (current.contents ?? (current.contents = [])).push(node);
5853
+ } else {
5854
+ leading.push(node);
5855
+ }
5856
+ }
5857
+ return { leading, blocks };
5858
+ }
5859
+
5326
5860
  // src/doc/markdownToDoc.ts
5327
5861
  var SLUG_NON_WORD_RE = /[^\w\s-]/g;
5328
5862
  var SLUG_SPACES_RE = /[\s_]+/g;
@@ -5345,9 +5879,15 @@ function createIdGenerator() {
5345
5879
  generate.reserve = (id) => {
5346
5880
  used.set(id, Math.max(used.get(id) ?? 0, 1));
5347
5881
  };
5882
+ generate.fromText = (text) => {
5883
+ const base = slugify(text);
5884
+ const count = used.get(base) ?? 0;
5885
+ used.set(base, count + 1);
5886
+ return count === 0 ? base : `${base}-${count + 1}`;
5887
+ };
5348
5888
  return generate;
5349
5889
  }
5350
- function applyBlockMeta(block, m) {
5890
+ function applyBlockMeta2(block, m) {
5351
5891
  if (m.x != null) block.x = m.x;
5352
5892
  if (m.y != null) block.y = m.y;
5353
5893
  if (m.startTime != null) block.startTime = m.startTime;
@@ -5361,13 +5901,37 @@ function pinnedHeadingMeta(heading) {
5361
5901
  const fromPandoc = heading.attributes?.blockMeta ?? {};
5362
5902
  return { ...fromAnnotation, ...fromPandoc };
5363
5903
  }
5904
+ function pinnedMeta(block) {
5905
+ if (block.sourceHeading) return pinnedHeadingMeta(block.sourceHeading);
5906
+ if (block.sourceAnnotation?.params) {
5907
+ return coerceAnnotationValues(block.sourceAnnotation.params).blockMeta;
5908
+ }
5909
+ return {};
5910
+ }
5911
+ function expandStandaloneAnnotations(blocks, makeId, defaultDuration) {
5912
+ const out = [];
5913
+ for (const block of blocks) {
5914
+ if (block.children && block.children.length > 0) {
5915
+ block.children = expandStandaloneAnnotations(block.children, makeId, defaultDuration);
5916
+ }
5917
+ const { leading, blocks: produced } = extractTemplateBlocksFromContents(block.contents, {
5918
+ makeId,
5919
+ defaultDuration
5920
+ });
5921
+ if (produced.length > 0) {
5922
+ block.contents = leading;
5923
+ }
5924
+ out.push(block, ...produced);
5925
+ }
5926
+ return out;
5927
+ }
5364
5928
  function markdownToDoc(markdownDoc, options) {
5365
5929
  const articleId = options?.articleId ?? "markdown-doc";
5366
5930
  const defaultTemplate = options?.defaultTemplate ?? "sectionHeader";
5367
5931
  const defaultDuration = options?.defaultDuration ?? 5;
5368
5932
  const idGenerator = options?.generateId ? null : createIdGenerator();
5369
5933
  const generateId = options?.generateId ?? idGenerator;
5370
- const rootBlocks = [];
5934
+ let rootBlocks = [];
5371
5935
  const diagnostics = [];
5372
5936
  let headingIndex = 0;
5373
5937
  const stack = [];
@@ -5400,12 +5964,12 @@ function markdownToDoc(markdownDoc, options) {
5400
5964
  };
5401
5965
  if (annotation?.params) {
5402
5966
  block.templateOverrides = annotation.params;
5403
- applyBlockMeta(block, coerceAnnotationValues(annotation.params).blockMeta);
5967
+ applyBlockMeta2(block, coerceAnnotationValues(annotation.params).blockMeta);
5404
5968
  }
5405
5969
  const attrs = heading?.attributes;
5406
5970
  if (attrs) {
5407
5971
  if (attrs.blockMeta) {
5408
- applyBlockMeta(block, attrs.blockMeta);
5972
+ applyBlockMeta2(block, attrs.blockMeta);
5409
5973
  }
5410
5974
  if (attrs.classes && attrs.classes.length > 0) {
5411
5975
  block.classes = attrs.classes;
@@ -5471,6 +6035,14 @@ function markdownToDoc(markdownDoc, options) {
5471
6035
  if (rootBlocks.length > 0 && !rootBlocks[0].sourceHeading && (rootBlocks[0].contents?.length ?? 0) === 0 && (rootBlocks[0].children?.length ?? 0) === 0 && !rootBlocks[0].media) {
5472
6036
  rootBlocks.shift();
5473
6037
  }
6038
+ const makeStandaloneId = (parsed) => {
6039
+ const base = parsed.params.title ?? parsed.template ?? "block";
6040
+ return idGenerator ? idGenerator.fromText(base) : slugify(base);
6041
+ };
6042
+ rootBlocks = expandStandaloneAnnotations(rootBlocks, makeStandaloneId, defaultDuration);
6043
+ if (rootBlocks.length > 0 && !rootBlocks[0].sourceHeading && !rootBlocks[0].standaloneAnnotation && (rootBlocks[0].contents?.length ?? 0) === 0 && (rootBlocks[0].children?.length ?? 0) === 0 && !rootBlocks[0].media) {
6044
+ rootBlocks.shift();
6045
+ }
5474
6046
  const allBlocks = flattenBlocks(rootBlocks);
5475
6047
  for (const block of allBlocks) {
5476
6048
  applyStructuredData(block, diagnostics);
@@ -5508,7 +6080,7 @@ function markdownToDoc(markdownDoc, options) {
5508
6080
  const minDuration = 3;
5509
6081
  const phrases = [];
5510
6082
  for (const block of allBlocks) {
5511
- if (pinnedHeadingMeta(block.sourceHeading).duration != null) continue;
6083
+ if (pinnedMeta(block).duration != null) continue;
5512
6084
  const bodyText3 = getBlockBodyText2(block);
5513
6085
  if (bodyText3.length > 0) {
5514
6086
  const estimate = estimateReadingTime(bodyText3);
@@ -5519,7 +6091,7 @@ function markdownToDoc(markdownDoc, options) {
5519
6091
  }
5520
6092
  let currentTime = 0;
5521
6093
  for (const block of allBlocks) {
5522
- const explicitStart = pinnedHeadingMeta(block.sourceHeading).startTime;
6094
+ const explicitStart = pinnedMeta(block).startTime;
5523
6095
  if (explicitStart == null) {
5524
6096
  block.startTime = currentTime;
5525
6097
  }
@@ -5765,6 +6337,8 @@ function docToMarkdown(doc) {
5765
6337
  if (block.sourceHeading) {
5766
6338
  const heading = ensureAnnotation(block, block.sourceHeading);
5767
6339
  children.push(heading);
6340
+ } else if (block.standaloneAnnotation) {
6341
+ children.push(synthesizeAnnotationParagraph(block));
5768
6342
  }
5769
6343
  if (block.contents) {
5770
6344
  children.push(...block.contents);
@@ -5801,6 +6375,10 @@ function docToMarkdown(doc) {
5801
6375
  ...frontmatter && Object.keys(frontmatter).length > 0 ? { frontmatter } : {}
5802
6376
  };
5803
6377
  }
6378
+ function synthesizeAnnotationParagraph(block) {
6379
+ const text = serializeAnnotation(block.sourceAnnotation?.template, block.templateOverrides);
6380
+ return { type: "paragraph", children: [{ type: "text", value: text }] };
6381
+ }
5804
6382
  function ensureAnnotation(block, heading) {
5805
6383
  const attrs = ensureTransitionAttributes(block, heading);
5806
6384
  const hasExplicitTemplate = block.template && block.template !== "sectionHeader" && block.autoTemplate !== true;
@@ -5950,7 +6528,11 @@ function getLayers(block, context = {}) {
5950
6528
  templateCtx.children = maybeChildren;
5951
6529
  }
5952
6530
  const { templateData, templateOverrides } = block;
5953
- const input = templateData || templateOverrides ? { ...block, ...templateData, ...templateOverrides } : block;
6531
+ const input = templateData || templateOverrides ? {
6532
+ ...block,
6533
+ ...templateData,
6534
+ ...coerceTemplateParams(block.template, templateOverrides ?? {}).input
6535
+ } : block;
5954
6536
  let layers;
5955
6537
  try {
5956
6538
  const templateFn = templateRegistry[templateName];
@@ -6215,9 +6797,13 @@ function validateMarkdownDoc(markdownDoc, options) {
6215
6797
  const diagnostics = [...doc.diagnostics ?? []];
6216
6798
  const blocks = flattenBlocks(doc.blocks);
6217
6799
  const knownTemplates = collectKnownTemplates(doc, options);
6800
+ const customNames = new Set(doc.customTemplates?.map((t) => t.name) ?? []);
6218
6801
  const visit = (block, parent) => {
6219
6802
  checkTemplate(block, parent, knownTemplates, diagnostics);
6220
6803
  checkAttributeCoercion(block, parent, diagnostics);
6804
+ checkConflictingAnnotationKeys(block, diagnostics);
6805
+ checkTemplateInputs(block, parent, customNames, diagnostics);
6806
+ checkPossibleDataFences(block, diagnostics);
6221
6807
  if (isDrawingBlock(block)) checkDrawingConnectors(block, diagnostics);
6222
6808
  for (const child of block.children ?? []) visit(child, block);
6223
6809
  };
@@ -6234,6 +6820,7 @@ function validateMarkdownDoc(markdownDoc, options) {
6234
6820
  diagnostics,
6235
6821
  errorCount: diagnostics.filter((d) => d.severity === "error").length,
6236
6822
  warningCount: diagnostics.filter((d) => d.severity === "warning").length,
6823
+ infoCount: diagnostics.filter((d) => d.severity === "info").length,
6237
6824
  doc
6238
6825
  };
6239
6826
  }
@@ -6249,11 +6836,13 @@ function isDrawingBlock(block) {
6249
6836
  return resolveTemplateName(block.template ?? "") === "drawing";
6250
6837
  }
6251
6838
  function checkTemplate(block, parent, known, diagnostics) {
6252
- const requested = block.sourceHeading?.templateAnnotation?.template;
6839
+ const requested = block.sourceHeading?.templateAnnotation?.template ?? block.sourceAnnotation?.template;
6253
6840
  if (!requested) return;
6254
6841
  if (parent && isDrawingBlock(parent)) {
6255
6842
  if (isShapeName(requested)) return;
6256
- const suggestion2 = nearestName(requested, new Set(SHAPE_NAMES));
6843
+ const suggestion2 = nearestName(requested, new Set(SHAPE_NAMES), {
6844
+ map: resolveTemplateName
6845
+ });
6257
6846
  diagnostics.push({
6258
6847
  severity: "warning",
6259
6848
  code: "unknown-shape",
@@ -6274,7 +6863,7 @@ function checkTemplate(block, parent, known, diagnostics) {
6274
6863
  return;
6275
6864
  }
6276
6865
  if (known.has(requested) || known.has(resolveTemplateName(requested))) return;
6277
- const suggestion = nearestName(requested, known);
6866
+ const suggestion = nearestName(requested, known, { map: resolveTemplateName });
6278
6867
  diagnostics.push({
6279
6868
  severity: "warning",
6280
6869
  code: "unknown-template",
@@ -6283,6 +6872,33 @@ function checkTemplate(block, parent, known, diagnostics) {
6283
6872
  ...lineOf2(block)
6284
6873
  });
6285
6874
  }
6875
+ function checkTemplateInputs(block, parent, customNames, diagnostics) {
6876
+ const requested = block.sourceHeading?.templateAnnotation?.template;
6877
+ if (!requested) return;
6878
+ if (parent && isDrawingBlock(parent)) return;
6879
+ if (isShapeName(requested)) return;
6880
+ const canonical = resolveTemplateName(requested);
6881
+ if (customNames.has(requested) || customNames.has(canonical)) return;
6882
+ const params = block.sourceHeading?.templateAnnotation?.params ?? {};
6883
+ for (const finding of lintTemplateParams(canonical, params)) {
6884
+ if (finding.kind === "missing-input" && isInputSatisfiable(canonical, block, finding.key)) {
6885
+ continue;
6886
+ }
6887
+ diagnostics.push({
6888
+ severity: "warning",
6889
+ code: finding.kind,
6890
+ message: `Block "${block.id}": ${finding.message}`,
6891
+ blockId: block.id,
6892
+ ...lineOf2(block)
6893
+ });
6894
+ }
6895
+ }
6896
+ function isInputSatisfiable(canonical, block, key) {
6897
+ if (block.templateData && key in block.templateData) return true;
6898
+ if (block[key] != null) return true;
6899
+ const derived = deriveTemplateInputs(canonical, block.title ?? "", block.contents);
6900
+ return derived != null && key in derived;
6901
+ }
6286
6902
  var NUMERIC_SHAPE_KEYS = ["x", "y", "width", "height", "w", "h", "strokeWidth", "borderRadius"];
6287
6903
  function checkAttributeCoercion(block, parent, diagnostics) {
6288
6904
  const params = block.sourceHeading?.attributes?.params;
@@ -6316,6 +6932,23 @@ function checkAttributeCoercion(block, parent, diagnostics) {
6316
6932
  }
6317
6933
  }
6318
6934
  }
6935
+ function checkConflictingAnnotationKeys(block, diagnostics) {
6936
+ const annotationParams = block.sourceHeading?.templateAnnotation?.params;
6937
+ const pandocParams = block.sourceHeading?.attributes?.params;
6938
+ if (!annotationParams || !pandocParams) return;
6939
+ for (const key of Object.keys(KNOWN_BLOCK_META_KEYS)) {
6940
+ const fromAnnotation = annotationParams[key];
6941
+ const fromPandoc = pandocParams[key];
6942
+ if (fromAnnotation == null || fromPandoc == null || fromAnnotation === fromPandoc) continue;
6943
+ diagnostics.push({
6944
+ severity: "info",
6945
+ code: "conflicting-annotation-key",
6946
+ message: `"${key}" is set in both {[\u2026]} (${fromAnnotation}) and {\u2026} (${fromPandoc}) \u2014 the {\u2026} value wins.`,
6947
+ blockId: block.id,
6948
+ ...lineOf2(block)
6949
+ });
6950
+ }
6951
+ }
6319
6952
  function checkDrawingConnectors(block, diagnostics) {
6320
6953
  const children = block.children ?? [];
6321
6954
  const ids = new Set(children.map((c) => c.id));
@@ -6339,9 +6972,28 @@ function checkDrawingConnectors(block, diagnostics) {
6339
6972
  }
6340
6973
  }
6341
6974
  }
6975
+ function checkPossibleDataFences(block, diagnostics) {
6976
+ const annotated = !!(block.sourceHeading?.templateAnnotation?.template || block.sourceAnnotation?.template);
6977
+ if (!annotated) return;
6978
+ for (const node of block.contents ?? []) {
6979
+ if (node.type !== "code") continue;
6980
+ const lang = node.lang?.toLowerCase();
6981
+ if (lang !== "json" && lang !== "yaml" && lang !== "yml") continue;
6982
+ if (isDataFence(node)) continue;
6983
+ const result = parseDataFence(node);
6984
+ if (!result.data || Object.keys(result.data).length === 0) continue;
6985
+ diagnostics.push({
6986
+ severity: "info",
6987
+ code: "possible-data-fence",
6988
+ message: `This ${lang} block renders as code \u2014 add "data" to the fence (\`\`\`${lang} data) if it's meant as template input.`,
6989
+ blockId: block.id,
6990
+ ...node.position ? { line: node.position.start.line } : lineOf2(block)
6991
+ });
6992
+ }
6993
+ }
6342
6994
  var ANNOTATION_LIKE_RE = /\{\[/;
6343
6995
  function checkUnrecognizedAnnotations(markdownDoc, diagnostics) {
6344
- const walk = (node, headingLine) => {
6996
+ const walk = (node, headingLine, topLevel) => {
6345
6997
  if (node.type === "heading") {
6346
6998
  const heading = node;
6347
6999
  const text = extractPlainText(heading);
@@ -6355,22 +7007,26 @@ function checkUnrecognizedAnnotations(markdownDoc, diagnostics) {
6355
7007
  }
6356
7008
  return;
6357
7009
  }
7010
+ if (node.type === "paragraph" && topLevel) {
7011
+ const parsed = parseStandaloneAnnotation(node);
7012
+ if (parsed && parsed.template != null) return;
7013
+ }
6358
7014
  if (node.type === "text" && ANNOTATION_LIKE_RE.test(node.value)) {
6359
7015
  diagnostics.push({
6360
7016
  severity: "warning",
6361
7017
  code: "unparsed-annotation",
6362
- message: `Body text contains "{[\u2026]}" \u2014 template annotations are only recognized on headings (or the token is not a known inline icon)`,
7018
+ message: `Body text contains "{[\u2026]}" \u2014 template annotations are recognized on headings and as standalone paragraphs (or the token is not a known inline icon)`,
6363
7019
  ...node.position ? { line: node.position.start.line } : headingLine != null ? { line: headingLine } : {}
6364
7020
  });
6365
7021
  return;
6366
7022
  }
6367
7023
  if (node.type === "code") return;
6368
7024
  for (const child of getChildren(node)) {
6369
- walk(child, headingLine);
7025
+ walk(child, headingLine, false);
6370
7026
  }
6371
7027
  };
6372
7028
  for (const child of markdownDoc.children) {
6373
- walk(child, child.position?.start.line);
7029
+ walk(child, child.position?.start.line, true);
6374
7030
  }
6375
7031
  }
6376
7032
  function normalizeAssetCheck(assets) {
@@ -6433,41 +7089,6 @@ function walkHtmlImages(nodes, report) {
6433
7089
  walkHtmlImages(node.children, report);
6434
7090
  }
6435
7091
  }
6436
- function nearestName(input, candidates) {
6437
- let best;
6438
- let bestDist = Infinity;
6439
- const lower = input.toLowerCase();
6440
- for (const candidate of candidates) {
6441
- const dist = levenshtein(lower, candidate.toLowerCase());
6442
- if (dist < bestDist) {
6443
- bestDist = dist;
6444
- best = candidate;
6445
- }
6446
- }
6447
- if (best && (bestDist <= 2 || bestDist <= Math.ceil(input.length * 0.4))) {
6448
- return resolveTemplateName(best);
6449
- }
6450
- return void 0;
6451
- }
6452
- function levenshtein(a, b) {
6453
- if (a === b) return 0;
6454
- const m = a.length;
6455
- const n = b.length;
6456
- if (m === 0) return n;
6457
- if (n === 0) return m;
6458
- let prev = new Array(n + 1);
6459
- let curr = new Array(n + 1);
6460
- for (let j = 0; j <= n; j++) prev[j] = j;
6461
- for (let i = 1; i <= m; i++) {
6462
- curr[0] = i;
6463
- for (let j = 1; j <= n; j++) {
6464
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
6465
- curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
6466
- }
6467
- [prev, curr] = [curr, prev];
6468
- }
6469
- return prev[n];
6470
- }
6471
7092
  function lineOf2(block) {
6472
7093
  const line = block.sourceHeading?.position?.start.line;
6473
7094
  return line != null ? { line } : {};
@@ -6484,6 +7105,10 @@ export {
6484
7105
  wrapWithPersistentLayers,
6485
7106
  themeWantsAmbientMotion,
6486
7107
  applyRenderStyleToLayers,
7108
+ BASE_INPUT_DESCRIPTORS,
7109
+ TEMPLATE_INPUT_DESCRIPTORS,
7110
+ coerceTemplateParams,
7111
+ lintTemplateParams,
6487
7112
  TEMPLATE_METADATA,
6488
7113
  resolveColorScheme,
6489
7114
  getThemeFont,
@@ -6562,6 +7187,7 @@ export {
6562
7187
  writeCustomThemesToFrontmatter,
6563
7188
  isDataFence,
6564
7189
  parseDataFence,
7190
+ replaceDataFence,
6565
7191
  parseYamlSubset,
6566
7192
  findFirstTable,
6567
7193
  extractTableData,
@@ -6579,4 +7205,4 @@ export {
6579
7205
  validateMarkdownSource,
6580
7206
  validateMarkdownDoc
6581
7207
  };
6582
- //# sourceMappingURL=chunk-4ZFRZBU2.js.map
7208
+ //# sourceMappingURL=chunk-LH7I6SH7.js.map