@bendyline/squisq 1.5.0 → 1.5.2

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 (135) 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-4ZFRZBU2.js → chunk-5TYS5V3G.js} +866 -192
  6. package/dist/chunk-5TYS5V3G.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-OH43RPTP.js → chunk-BEQODJWV.js} +20 -3
  10. package/dist/chunk-BEQODJWV.js.map +1 -0
  11. package/dist/chunk-ET53IIEP.js +1 -0
  12. package/dist/chunk-ET53IIEP.js.map +1 -0
  13. package/dist/{chunk-KZNXZ5SM.js → chunk-GPLTCSXO.js} +18 -7
  14. package/dist/chunk-GPLTCSXO.js.map +1 -0
  15. package/dist/{chunk-OKJOQXK4.js → chunk-HWVFJAAH.js} +38 -2
  16. package/dist/chunk-HWVFJAAH.js.map +1 -0
  17. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  18. package/dist/chunk-IMSCRLLV.js.map +1 -0
  19. package/dist/{chunk-4AKUQPEE.js → chunk-KKNUBQ6Y.js} +1 -1
  20. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  21. package/dist/{chunk-TVSGLUID.js → chunk-LDQ2HJIX.js} +3 -3
  22. package/dist/{chunk-RUDYOTA6.js → chunk-LTN6I7QW.js} +56 -1
  23. package/dist/chunk-LTN6I7QW.js.map +1 -0
  24. package/dist/{chunk-2OIBZYKP.js → chunk-P3K7NLHF.js} +2 -2
  25. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  26. package/dist/chunk-T5UK6YOB.js.map +1 -0
  27. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  28. package/dist/chunk-TQWLI6S2.js.map +1 -0
  29. package/dist/{chunk-LRCV7N7F.js → chunk-UY7KGQ5R.js} +2 -2
  30. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  31. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  32. package/dist/doc/index.d.ts +4 -4
  33. package/dist/doc/index.js +24 -9
  34. package/dist/generate/index.d.ts +2 -80
  35. package/dist/generate/index.js +2 -7
  36. package/dist/imageEdit/index.d.ts +4 -4
  37. package/dist/imageEdit/index.js +2 -2
  38. package/dist/index.d.ts +8 -8
  39. package/dist/index.js +41 -26
  40. package/dist/jsonForm/index.d.ts +42 -1
  41. package/dist/jsonForm/index.js +9 -1
  42. package/dist/markdown/index.d.ts +18 -2
  43. package/dist/markdown/index.js +5 -3
  44. package/dist/recommend/index.js +2 -2
  45. package/dist/schemas/index.d.ts +4 -4
  46. package/dist/schemas/index.js +3 -5
  47. package/dist/storage/index.d.ts +2 -2
  48. package/dist/storage/index.js +1 -1
  49. package/dist/story/index.d.ts +151 -13
  50. package/dist/story/index.js +24 -9
  51. package/dist/{themeLibrary-DQQKuRMx.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  52. package/dist/timing/index.js +3 -5
  53. package/dist/transform/index.d.ts +2 -2
  54. package/dist/transform/index.js +3 -3
  55. package/dist/versions/index.d.ts +1 -1
  56. package/package.json +1 -1
  57. package/src/__tests__/contentExtractor.test.ts +60 -0
  58. package/src/__tests__/coverBlock.test.ts +79 -3
  59. package/src/__tests__/customTemplateSample.test.ts +63 -2
  60. package/src/__tests__/customTemplatesFrontmatter.test.ts +31 -0
  61. package/src/__tests__/customThemesFrontmatter.test.ts +27 -0
  62. package/src/__tests__/inputDescriptors.test.ts +162 -0
  63. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  64. package/src/__tests__/markdown.test.ts +64 -0
  65. package/src/__tests__/markdownSanitize.test.ts +15 -0
  66. package/src/__tests__/markdownToDoc.test.ts +173 -2
  67. package/src/__tests__/mediaAnnotations.test.ts +27 -0
  68. package/src/__tests__/shapeGeometry.test.ts +37 -0
  69. package/src/__tests__/structuredData.test.ts +73 -3
  70. package/src/__tests__/templateMetadata.test.ts +11 -1
  71. package/src/__tests__/templates.test.ts +112 -0
  72. package/src/__tests__/validateDoc.test.ts +160 -0
  73. package/src/doc/annotationBlocks.ts +117 -0
  74. package/src/doc/customTemplatesFrontmatter.ts +7 -1
  75. package/src/doc/customThemesFrontmatter.ts +6 -1
  76. package/src/doc/docToMarkdown.ts +78 -29
  77. package/src/doc/getLayers.ts +11 -2
  78. package/src/doc/index.ts +12 -0
  79. package/src/doc/markdownToDoc.ts +80 -3
  80. package/src/doc/mediaAnnotations.ts +15 -33
  81. package/src/doc/standaloneAnnotation.ts +58 -0
  82. package/src/doc/structuredData.ts +92 -21
  83. package/src/doc/templates/__tests__/customTemplate.test.ts +21 -2
  84. package/src/doc/templates/comparisonBar.ts +8 -1
  85. package/src/doc/templates/coverBlock.ts +78 -15
  86. package/src/doc/templates/customTemplate.ts +10 -4
  87. package/src/doc/templates/diagramBlock.ts +2 -2
  88. package/src/doc/templates/drawingBlock.ts +2 -2
  89. package/src/doc/templates/factCard.ts +4 -2
  90. package/src/doc/templates/imageWithCaption.ts +10 -8
  91. package/src/doc/templates/index.ts +12 -2
  92. package/src/doc/templates/inputDescriptors.ts +432 -0
  93. package/src/doc/templates/photoGrid.ts +16 -39
  94. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +267 -1
  95. package/src/doc/templates/tokens/resolveTokens.ts +0 -0
  96. package/src/doc/templates/videoWithCaption.ts +15 -14
  97. package/src/doc/utils/nearest.ts +59 -0
  98. package/src/doc/utils/shapeGeometry.ts +90 -2
  99. package/src/doc/validate.ts +153 -54
  100. package/src/generate/contentExtractor.ts +148 -35
  101. package/src/generate/index.ts +0 -3
  102. package/src/jsonForm/index.ts +2 -0
  103. package/src/jsonForm/tokens.ts +78 -0
  104. package/src/markdown/annotationCoercion.ts +1 -1
  105. package/src/markdown/attrTokens.ts +19 -0
  106. package/src/markdown/convert.ts +11 -1
  107. package/src/markdown/index.ts +2 -1
  108. package/src/markdown/sanitize.ts +22 -2
  109. package/src/markdown/stringify.ts +12 -3
  110. package/src/markdown/utils.ts +62 -1
  111. package/src/schemas/CustomTemplates.ts +56 -4
  112. package/src/schemas/Doc.ts +39 -41
  113. package/src/schemas/themeCompile.ts +1 -1
  114. package/src/storage/ContentContainer.ts +6 -2
  115. package/src/transform/types.ts +1 -1
  116. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  117. package/dist/chunk-3E5F2XMR.js.map +0 -1
  118. package/dist/chunk-4AKUQPEE.js.map +0 -1
  119. package/dist/chunk-4X3JQXNM.js +0 -35
  120. package/dist/chunk-4X3JQXNM.js.map +0 -1
  121. package/dist/chunk-4ZFRZBU2.js.map +0 -1
  122. package/dist/chunk-7MQGNR76.js.map +0 -1
  123. package/dist/chunk-FBKP5CAO.js +0 -145
  124. package/dist/chunk-FBKP5CAO.js.map +0 -1
  125. package/dist/chunk-FR2RBTKO.js.map +0 -1
  126. package/dist/chunk-K32VJONL.js.map +0 -1
  127. package/dist/chunk-KZNXZ5SM.js.map +0 -1
  128. package/dist/chunk-OH43RPTP.js.map +0 -1
  129. package/dist/chunk-OKJOQXK4.js.map +0 -1
  130. package/dist/chunk-RUDYOTA6.js.map +0 -1
  131. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  132. package/src/generate/slideshowGenerator.ts +0 -280
  133. /package/dist/{chunk-TVSGLUID.js.map → chunk-LDQ2HJIX.js.map} +0 -0
  134. /package/dist/{chunk-2OIBZYKP.js.map → chunk-P3K7NLHF.js.map} +0 -0
  135. /package/dist/{chunk-LRCV7N7F.js.map → chunk-UY7KGQ5R.js.map} +0 -0
@@ -1,17 +1,17 @@
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
8
8
  } from "./chunk-FVTQSNR7.js";
9
9
  import {
10
- clipEndpoints,
11
10
  connectorPath,
12
11
  lineStyleDasharray,
13
- shapePath
14
- } from "./chunk-RUDYOTA6.js";
12
+ shapePath,
13
+ snapEndpoints
14
+ } from "./chunk-LTN6I7QW.js";
15
15
  import {
16
16
  DEFAULT_THEME,
17
17
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
@@ -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-BEQODJWV.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
  });
@@ -3259,7 +3672,7 @@ function diagramBlock(input, context) {
3259
3672
  const a = positions.get(edge.source);
3260
3673
  const b = positions.get(edge.target);
3261
3674
  if (!a || !b) continue;
3262
- const { start, end } = clipEndpoints(a, b);
3675
+ const { start, end } = snapEndpoints(a, b);
3263
3676
  const pathLayer = {
3264
3677
  type: "path",
3265
3678
  id: `edge-${edge.id}`,
@@ -3695,7 +4108,7 @@ function drawingBlock(input, context) {
3695
4108
  const a = boxes.get(conn.from);
3696
4109
  const b = boxes.get(conn.to);
3697
4110
  if (!a || !b) continue;
3698
- const { start, end } = clipEndpoints(a, b);
4111
+ const { start, end } = snapEndpoints(a, b);
3699
4112
  const path = {
3700
4113
  type: "path",
3701
4114
  id: `connector-${conn.id}`,
@@ -4050,11 +4463,25 @@ 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
+ }
4475
+ function isStandardLightCover(context) {
4476
+ const { theme } = context;
4477
+ return theme.id === "standard" && relativeLuminance(theme.colors.background) > 0.85;
4478
+ }
4053
4479
  function coverBlock(input, context) {
4054
4480
  const treatment = themedImageTreatment(context, input.imageTreatment);
4055
4481
  const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
4056
4482
  const { theme, layout } = context;
4057
- const titleFontSize = themedFontSize(120, context, true);
4483
+ const standardLightCover = !heroSrc && isStandardLightCover(context);
4484
+ const titleFontSize = themedFontSize(fitCoverTitleSize(title), context, true);
4058
4485
  const subtitleFontSize = themedFontSize(40, context, false);
4059
4486
  const layers = [];
4060
4487
  if (heroSrc) {
@@ -4085,21 +4512,44 @@ function coverBlock(input, context) {
4085
4512
  }
4086
4513
  );
4087
4514
  } else {
4088
- layers.push({
4089
- type: "shape",
4090
- id: "cover-bg",
4091
- content: {
4092
- shape: "rect",
4093
- fill: `radial-gradient(ellipse at 50% 40%, ${theme.colors.primary} 0%, ${theme.colors.background} 100%)`
4094
- },
4095
- position: { x: 0, y: 0, width: "100%", height: "100%" }
4096
- });
4515
+ if (standardLightCover) {
4516
+ layers.push(
4517
+ {
4518
+ type: "shape",
4519
+ id: "cover-bg",
4520
+ content: {
4521
+ shape: "rect",
4522
+ fill: theme.colors.background
4523
+ },
4524
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
4525
+ },
4526
+ {
4527
+ type: "shape",
4528
+ id: "cover-bg-tint",
4529
+ content: {
4530
+ shape: "rect",
4531
+ fill: `radial-gradient(ellipse at 50% 40%, ${withAlpha(theme.colors.primary, 0.16)} 0%, ${withAlpha(theme.colors.primary, 0.07)} 38%, ${withAlpha(theme.colors.primary, 0)} 78%)`
4532
+ },
4533
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
4534
+ }
4535
+ );
4536
+ } else {
4537
+ layers.push({
4538
+ type: "shape",
4539
+ id: "cover-bg",
4540
+ content: {
4541
+ shape: "rect",
4542
+ fill: `radial-gradient(ellipse at 50% 40%, ${theme.colors.primary} 0%, ${theme.colors.background} 100%)`
4543
+ },
4544
+ position: { x: 0, y: 0, width: "100%", height: "100%" }
4545
+ });
4546
+ }
4097
4547
  layers.push({
4098
4548
  type: "shape",
4099
4549
  id: "cover-accent",
4100
4550
  content: {
4101
4551
  shape: "rect",
4102
- fill: "rgba(255, 255, 255, 0.2)"
4552
+ fill: standardLightCover ? withAlpha(theme.colors.primary, 0.24) : "rgba(255, 255, 255, 0.2)"
4103
4553
  },
4104
4554
  position: {
4105
4555
  x: "35%",
@@ -4120,7 +4570,7 @@ function coverBlock(input, context) {
4120
4570
  fontWeight: "bold",
4121
4571
  color: theme.colors.text,
4122
4572
  textAlign: "center",
4123
- shadow: true
4573
+ shadow: !standardLightCover
4124
4574
  }
4125
4575
  },
4126
4576
  position: {
@@ -4141,7 +4591,8 @@ function coverBlock(input, context) {
4141
4591
  style: {
4142
4592
  fontSize: subtitleFontSize,
4143
4593
  fontFamily: getThemeFont(context, "body"),
4144
- color: theme.colors.textMuted,
4594
+ fontWeight: standardLightCover ? "bold" : "normal",
4595
+ color: standardLightCover ? withAlpha(theme.colors.text, 0.78) : theme.colors.textMuted,
4145
4596
  textAlign: "center",
4146
4597
  lineHeight: 1.5
4147
4598
  }
@@ -4243,7 +4694,11 @@ function expandTemplateBlock(templateBlock, context, registry = templateRegistry
4243
4694
  };
4244
4695
  }
4245
4696
  const { templateData, templateOverrides } = templateBlock;
4246
- const input = templateData || templateOverrides ? { ...templateBlock, ...templateData, ...templateOverrides } : templateBlock;
4697
+ const input = templateData || templateOverrides ? {
4698
+ ...templateBlock,
4699
+ ...templateData,
4700
+ ...coerceTemplateParams(templateBlock.template, templateOverrides ?? {}).input
4701
+ } : templateBlock;
4247
4702
  let layers;
4248
4703
  try {
4249
4704
  layers = templateFn(input, context);
@@ -5047,7 +5502,7 @@ function readCustomTemplatesFromFrontmatter(frontmatter) {
5047
5502
  }
5048
5503
  return out.length > 0 ? out : void 0;
5049
5504
  }
5050
- function writeCustomTemplatesToFrontmatter(templates) {
5505
+ function writeCustomTemplatesToFrontmatter(templates, options) {
5051
5506
  if (!templates || templates.length === 0) return void 0;
5052
5507
  const map = {};
5053
5508
  for (const def of templates) {
@@ -5057,7 +5512,7 @@ function writeCustomTemplatesToFrontmatter(templates) {
5057
5512
  entry.ly = renameKeys(def.layers, LONG_TO_SHORT);
5058
5513
  map[def.name] = entry;
5059
5514
  }
5060
- return JSON.stringify(map);
5515
+ return JSON.stringify(map, null, options?.pretty ? 2 : void 0);
5061
5516
  }
5062
5517
  function readViewport(raw) {
5063
5518
  if (raw && typeof raw === "object") {
@@ -5139,11 +5594,11 @@ function readCustomThemesFromFrontmatter(frontmatter) {
5139
5594
  }
5140
5595
  return out.length > 0 ? out : void 0;
5141
5596
  }
5142
- function writeCustomThemesToFrontmatter(themes) {
5597
+ function writeCustomThemesToFrontmatter(themes, options) {
5143
5598
  if (!themes || themes.length === 0) return void 0;
5144
5599
  const map = {};
5145
5600
  for (const theme of themes) map[theme.id] = theme;
5146
- return JSON.stringify(map);
5601
+ return JSON.stringify(map, null, options?.pretty ? 2 : void 0);
5147
5602
  }
5148
5603
 
5149
5604
  // src/doc/structuredData.ts
@@ -5172,6 +5627,17 @@ function parseDataFence(node) {
5172
5627
  return { error: `invalid YAML: ${err instanceof Error ? err.message : String(err)}` };
5173
5628
  }
5174
5629
  }
5630
+ function replaceDataFence(contents, data) {
5631
+ const value = JSON.stringify(data, null, 2);
5632
+ const index = contents.findIndex((node) => node.type === "code" && isDataFence(node));
5633
+ if (index < 0) {
5634
+ const fence = { type: "code", lang: "json", meta: "data", value };
5635
+ return [...contents, fence];
5636
+ }
5637
+ const next = [...contents];
5638
+ next[index] = { ...contents[index], value };
5639
+ return next;
5640
+ }
5175
5641
  function parseScalar(raw) {
5176
5642
  const trimmed = raw.trim();
5177
5643
  if (trimmed.length >= 2) {
@@ -5217,25 +5683,59 @@ function parseInlineValue(raw) {
5217
5683
  function parseYamlSubset(src) {
5218
5684
  const result = {};
5219
5685
  const lines = src.split("\n");
5220
- let pendingListKey = null;
5686
+ let pendingKey = null;
5687
+ let pendingKind = null;
5688
+ let childIndent = -1;
5221
5689
  for (let i = 0; i < lines.length; i++) {
5222
5690
  const line = lines[i];
5223
5691
  const trimmed = line.trim();
5224
5692
  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`);
5693
+ const indent = line.length - line.trimStart().length;
5694
+ if (indent > 0) {
5695
+ if (!pendingKey) {
5696
+ throw new Error(`line ${i + 1}: unexpected indented line without a preceding "key:" line`);
5697
+ }
5698
+ if (childIndent < 0) childIndent = indent;
5699
+ if (indent > childIndent) {
5700
+ throw new Error(
5701
+ `line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
5702
+ );
5229
5703
  }
5230
- result[pendingListKey].push(parseInlineValue(trimmed.slice(2)));
5704
+ if (trimmed.startsWith("- ")) {
5705
+ if (pendingKind === "map") {
5706
+ throw new Error(
5707
+ `line ${i + 1}: cannot mix list items and mapping keys under "${pendingKey}"`
5708
+ );
5709
+ }
5710
+ pendingKind = "list";
5711
+ result[pendingKey].push(parseInlineValue(trimmed.slice(2)));
5712
+ continue;
5713
+ }
5714
+ if (pendingKind === "list") {
5715
+ throw new Error(
5716
+ `line ${i + 1}: cannot mix mapping keys and list items under "${pendingKey}"`
5717
+ );
5718
+ }
5719
+ const subColon = trimmed.indexOf(":");
5720
+ if (subColon < 1) {
5721
+ throw new Error(`line ${i + 1}: expected "key: value"`);
5722
+ }
5723
+ const subValue = trimmed.slice(subColon + 1).trim();
5724
+ if (subValue === "") {
5725
+ throw new Error(
5726
+ `line ${i + 1}: nesting deeper than one level is not supported \u2014 use a \`\`\`json data fence for nested input`
5727
+ );
5728
+ }
5729
+ if (pendingKind !== "map") {
5730
+ pendingKind = "map";
5731
+ result[pendingKey] = {};
5732
+ }
5733
+ result[pendingKey][trimmed.slice(0, subColon).trim()] = parseInlineValue(subValue);
5231
5734
  continue;
5232
5735
  }
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;
5736
+ pendingKey = null;
5737
+ pendingKind = null;
5738
+ childIndent = -1;
5239
5739
  const colonIdx = trimmed.indexOf(":");
5240
5740
  if (colonIdx < 1) {
5241
5741
  throw new Error(`line ${i + 1}: expected "key: value"`);
@@ -5244,7 +5744,7 @@ function parseYamlSubset(src) {
5244
5744
  const rawValue = trimmed.slice(colonIdx + 1).trim();
5245
5745
  if (rawValue === "") {
5246
5746
  result[key] = [];
5247
- pendingListKey = key;
5747
+ pendingKey = key;
5248
5748
  } else {
5249
5749
  result[key] = parseInlineValue(rawValue);
5250
5750
  }
@@ -5264,8 +5764,7 @@ function extractTableData(table) {
5264
5764
  };
5265
5765
  }
5266
5766
 
5267
- // src/doc/mediaAnnotations.ts
5268
- var MEDIA_TEMPLATES = /* @__PURE__ */ new Set(["audio", "video", "media"]);
5767
+ // src/doc/standaloneAnnotation.ts
5269
5768
  function parseStandaloneAnnotation(node) {
5270
5769
  if (node.type !== "paragraph") return null;
5271
5770
  const children = node.children ?? [];
@@ -5284,6 +5783,12 @@ function parseStandaloneAnnotation(node) {
5284
5783
  }
5285
5784
  return { template, params };
5286
5785
  }
5786
+
5787
+ // src/doc/mediaAnnotations.ts
5788
+ var MEDIA_TEMPLATES = /* @__PURE__ */ new Set(["audio", "video", "media"]);
5789
+ function isMediaAnnotationName(name) {
5790
+ return name != null && MEDIA_TEMPLATES.has(name);
5791
+ }
5287
5792
  function time(raw) {
5288
5793
  if (raw == null) return null;
5289
5794
  return parseTimeSeconds(raw);
@@ -5295,7 +5800,13 @@ function toMediaClip(parsed, id) {
5295
5800
  if (!src) return null;
5296
5801
  const kind = template === "video" ? "video" : "audio";
5297
5802
  const anchor = params.anchor === "document" || params.span === "document" ? "document" : "block";
5298
- const clip = { id, src, kind, startAt: time(params.startAt) ?? 0, anchor };
5803
+ const clip = {
5804
+ id,
5805
+ src,
5806
+ kind,
5807
+ startAt: time(params.startAt ?? params.startTime) ?? 0,
5808
+ anchor
5809
+ };
5299
5810
  const clipStart = time(params.clipStart);
5300
5811
  const clipEnd = time(params.clipEnd);
5301
5812
  if (clipStart != null) clip.clipStart = clipStart;
@@ -5323,6 +5834,58 @@ function extractMediaFromContents(contents, makeId) {
5323
5834
  return { media, documentMedia, remaining };
5324
5835
  }
5325
5836
 
5837
+ // src/doc/annotationBlocks.ts
5838
+ function applyBlockMeta(block, m) {
5839
+ if (m.x != null) block.x = m.x;
5840
+ if (m.y != null) block.y = m.y;
5841
+ if (m.startTime != null) block.startTime = m.startTime;
5842
+ if (m.duration != null) block.duration = m.duration;
5843
+ if (m.connectsTo) block.connectsTo = m.connectsTo;
5844
+ if (m.transition) block.transition = m.transition;
5845
+ }
5846
+ function makeStandaloneBlock(parsed, options) {
5847
+ const rawTemplate = parsed.template;
5848
+ const params = parsed.params;
5849
+ const hasParams = Object.keys(params).length > 0;
5850
+ const block = {
5851
+ id: options.makeId(parsed),
5852
+ startTime: 0,
5853
+ duration: options.defaultDuration,
5854
+ audioSegment: 0,
5855
+ template: resolveTemplateName(rawTemplate),
5856
+ standaloneAnnotation: true,
5857
+ // Keep the raw (un-resolved) name so validation can report the exact
5858
+ // requested name — symmetric with `sourceHeading.templateAnnotation`.
5859
+ sourceAnnotation: { template: rawTemplate, ...hasParams ? { params } : {} },
5860
+ // Only carry a display title when the author supplied one.
5861
+ ...params.title ? { title: params.title } : {}
5862
+ };
5863
+ if (hasParams) {
5864
+ block.templateOverrides = params;
5865
+ applyBlockMeta(block, coerceAnnotationValues(params).blockMeta);
5866
+ }
5867
+ return block;
5868
+ }
5869
+ function extractTemplateBlocksFromContents(contents, options) {
5870
+ const leading = [];
5871
+ const blocks = [];
5872
+ let current = null;
5873
+ for (const node of contents ?? []) {
5874
+ const parsed = parseStandaloneAnnotation(node);
5875
+ const isTemplateAnnotation = parsed != null && parsed.template != null && !isMediaAnnotationName(parsed.template);
5876
+ if (isTemplateAnnotation) {
5877
+ const block = makeStandaloneBlock(parsed, options);
5878
+ blocks.push(block);
5879
+ current = block;
5880
+ } else if (current) {
5881
+ (current.contents ?? (current.contents = [])).push(node);
5882
+ } else {
5883
+ leading.push(node);
5884
+ }
5885
+ }
5886
+ return { leading, blocks };
5887
+ }
5888
+
5326
5889
  // src/doc/markdownToDoc.ts
5327
5890
  var SLUG_NON_WORD_RE = /[^\w\s-]/g;
5328
5891
  var SLUG_SPACES_RE = /[\s_]+/g;
@@ -5345,9 +5908,15 @@ function createIdGenerator() {
5345
5908
  generate.reserve = (id) => {
5346
5909
  used.set(id, Math.max(used.get(id) ?? 0, 1));
5347
5910
  };
5911
+ generate.fromText = (text) => {
5912
+ const base = slugify(text);
5913
+ const count = used.get(base) ?? 0;
5914
+ used.set(base, count + 1);
5915
+ return count === 0 ? base : `${base}-${count + 1}`;
5916
+ };
5348
5917
  return generate;
5349
5918
  }
5350
- function applyBlockMeta(block, m) {
5919
+ function applyBlockMeta2(block, m) {
5351
5920
  if (m.x != null) block.x = m.x;
5352
5921
  if (m.y != null) block.y = m.y;
5353
5922
  if (m.startTime != null) block.startTime = m.startTime;
@@ -5361,13 +5930,37 @@ function pinnedHeadingMeta(heading) {
5361
5930
  const fromPandoc = heading.attributes?.blockMeta ?? {};
5362
5931
  return { ...fromAnnotation, ...fromPandoc };
5363
5932
  }
5933
+ function pinnedMeta(block) {
5934
+ if (block.sourceHeading) return pinnedHeadingMeta(block.sourceHeading);
5935
+ if (block.sourceAnnotation?.params) {
5936
+ return coerceAnnotationValues(block.sourceAnnotation.params).blockMeta;
5937
+ }
5938
+ return {};
5939
+ }
5940
+ function expandStandaloneAnnotations(blocks, makeId, defaultDuration) {
5941
+ const out = [];
5942
+ for (const block of blocks) {
5943
+ if (block.children && block.children.length > 0) {
5944
+ block.children = expandStandaloneAnnotations(block.children, makeId, defaultDuration);
5945
+ }
5946
+ const { leading, blocks: produced } = extractTemplateBlocksFromContents(block.contents, {
5947
+ makeId,
5948
+ defaultDuration
5949
+ });
5950
+ if (produced.length > 0) {
5951
+ block.contents = leading;
5952
+ }
5953
+ out.push(block, ...produced);
5954
+ }
5955
+ return out;
5956
+ }
5364
5957
  function markdownToDoc(markdownDoc, options) {
5365
5958
  const articleId = options?.articleId ?? "markdown-doc";
5366
5959
  const defaultTemplate = options?.defaultTemplate ?? "sectionHeader";
5367
5960
  const defaultDuration = options?.defaultDuration ?? 5;
5368
5961
  const idGenerator = options?.generateId ? null : createIdGenerator();
5369
5962
  const generateId = options?.generateId ?? idGenerator;
5370
- const rootBlocks = [];
5963
+ let rootBlocks = [];
5371
5964
  const diagnostics = [];
5372
5965
  let headingIndex = 0;
5373
5966
  const stack = [];
@@ -5400,12 +5993,12 @@ function markdownToDoc(markdownDoc, options) {
5400
5993
  };
5401
5994
  if (annotation?.params) {
5402
5995
  block.templateOverrides = annotation.params;
5403
- applyBlockMeta(block, coerceAnnotationValues(annotation.params).blockMeta);
5996
+ applyBlockMeta2(block, coerceAnnotationValues(annotation.params).blockMeta);
5404
5997
  }
5405
5998
  const attrs = heading?.attributes;
5406
5999
  if (attrs) {
5407
6000
  if (attrs.blockMeta) {
5408
- applyBlockMeta(block, attrs.blockMeta);
6001
+ applyBlockMeta2(block, attrs.blockMeta);
5409
6002
  }
5410
6003
  if (attrs.classes && attrs.classes.length > 0) {
5411
6004
  block.classes = attrs.classes;
@@ -5471,6 +6064,14 @@ function markdownToDoc(markdownDoc, options) {
5471
6064
  if (rootBlocks.length > 0 && !rootBlocks[0].sourceHeading && (rootBlocks[0].contents?.length ?? 0) === 0 && (rootBlocks[0].children?.length ?? 0) === 0 && !rootBlocks[0].media) {
5472
6065
  rootBlocks.shift();
5473
6066
  }
6067
+ const makeStandaloneId = (parsed) => {
6068
+ const base = parsed.params.title ?? parsed.template ?? "block";
6069
+ return idGenerator ? idGenerator.fromText(base) : slugify(base);
6070
+ };
6071
+ rootBlocks = expandStandaloneAnnotations(rootBlocks, makeStandaloneId, defaultDuration);
6072
+ 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) {
6073
+ rootBlocks.shift();
6074
+ }
5474
6075
  const allBlocks = flattenBlocks(rootBlocks);
5475
6076
  for (const block of allBlocks) {
5476
6077
  applyStructuredData(block, diagnostics);
@@ -5508,7 +6109,7 @@ function markdownToDoc(markdownDoc, options) {
5508
6109
  const minDuration = 3;
5509
6110
  const phrases = [];
5510
6111
  for (const block of allBlocks) {
5511
- if (pinnedHeadingMeta(block.sourceHeading).duration != null) continue;
6112
+ if (pinnedMeta(block).duration != null) continue;
5512
6113
  const bodyText3 = getBlockBodyText2(block);
5513
6114
  if (bodyText3.length > 0) {
5514
6115
  const estimate = estimateReadingTime(bodyText3);
@@ -5519,7 +6120,7 @@ function markdownToDoc(markdownDoc, options) {
5519
6120
  }
5520
6121
  let currentTime = 0;
5521
6122
  for (const block of allBlocks) {
5522
- const explicitStart = pinnedHeadingMeta(block.sourceHeading).startTime;
6123
+ const explicitStart = pinnedMeta(block).startTime;
5523
6124
  if (explicitStart == null) {
5524
6125
  block.startTime = currentTime;
5525
6126
  }
@@ -5759,12 +6360,15 @@ function buildStartBlock(markdownDoc, rootBlocks) {
5759
6360
  }
5760
6361
 
5761
6362
  // src/doc/docToMarkdown.ts
6363
+ var TRANSITION_PARAM_KEYS = ["transition", "transitionDuration", "transitionDirection"];
5762
6364
  function docToMarkdown(doc) {
5763
6365
  const children = [];
5764
6366
  function emitBlock(block) {
5765
6367
  if (block.sourceHeading) {
5766
6368
  const heading = ensureAnnotation(block, block.sourceHeading);
5767
6369
  children.push(heading);
6370
+ } else if (block.standaloneAnnotation) {
6371
+ children.push(synthesizeAnnotationParagraph(block));
5768
6372
  }
5769
6373
  if (block.contents) {
5770
6374
  children.push(...block.contents);
@@ -5801,43 +6405,65 @@ function docToMarkdown(doc) {
5801
6405
  ...frontmatter && Object.keys(frontmatter).length > 0 ? { frontmatter } : {}
5802
6406
  };
5803
6407
  }
6408
+ function synthesizeAnnotationParagraph(block) {
6409
+ const text = serializeAnnotation(block.sourceAnnotation?.template, block.templateOverrides);
6410
+ return { type: "paragraph", children: [{ type: "text", value: text }] };
6411
+ }
5804
6412
  function ensureAnnotation(block, heading) {
5805
- const attrs = ensureTransitionAttributes(block, heading);
6413
+ const { attributes, transitionParams } = ensureTransitionMetadata(block, heading);
5806
6414
  const hasExplicitTemplate = block.template && block.template !== "sectionHeader" && block.autoTemplate !== true;
5807
6415
  const hasOverrides = block.templateOverrides && Object.keys(block.templateOverrides).length > 0;
5808
- if (heading.templateAnnotation) {
5809
- return attrs === heading.attributes ? heading : { ...heading, attributes: attrs };
6416
+ let templateAnnotation = heading.templateAnnotation;
6417
+ if (!templateAnnotation && (hasExplicitTemplate || hasOverrides || transitionParams)) {
6418
+ templateAnnotation = {
6419
+ ...hasExplicitTemplate ? { template: block.template ?? "sectionHeader" } : {},
6420
+ ...hasOverrides ? { params: block.templateOverrides } : {}
6421
+ };
5810
6422
  }
5811
- if (!hasExplicitTemplate && !hasOverrides) {
5812
- return attrs === heading.attributes ? heading : { ...heading, attributes: attrs };
6423
+ if (transitionParams) {
6424
+ templateAnnotation = {
6425
+ ...templateAnnotation ?? {},
6426
+ params: {
6427
+ ...templateAnnotation?.params ?? {},
6428
+ ...transitionParams
6429
+ }
6430
+ };
5813
6431
  }
5814
- return {
5815
- ...heading,
5816
- children: [...heading.children],
5817
- attributes: attrs,
5818
- templateAnnotation: {
5819
- template: block.template ?? "sectionHeader",
5820
- ...hasOverrides ? { params: block.templateOverrides } : {}
5821
- }
5822
- };
6432
+ if (attributes === heading.attributes && templateAnnotation === heading.templateAnnotation) {
6433
+ return heading;
6434
+ }
6435
+ return { ...heading, children: [...heading.children], attributes, templateAnnotation };
5823
6436
  }
5824
- function ensureTransitionAttributes(block, heading) {
6437
+ function ensureTransitionMetadata(block, heading) {
5825
6438
  const transition = block.transition;
5826
- if (!transition) return heading.attributes;
5827
- const attrs = heading.attributes ?? {};
5828
- return {
6439
+ if (!transition) {
6440
+ return { attributes: heading.attributes, transitionParams: void 0 };
6441
+ }
6442
+ const attrs = removeTransitionParams(heading.attributes) ?? {};
6443
+ const attributes = {
5829
6444
  ...attrs,
5830
- params: {
5831
- ...attrs.params ?? {},
5832
- transition: transition.type,
5833
- ...transition.duration !== void 0 ? { transitionDuration: String(transition.duration) } : {},
5834
- ...transition.direction ? { transitionDirection: transition.direction } : {}
5835
- },
5836
6445
  blockMeta: {
5837
6446
  ...attrs.blockMeta ?? {},
5838
6447
  transition
5839
6448
  }
5840
6449
  };
6450
+ return {
6451
+ attributes,
6452
+ transitionParams: {
6453
+ transition: transition.type,
6454
+ ...transition.duration !== void 0 ? { transitionDuration: String(transition.duration) } : {},
6455
+ ...transition.direction ? { transitionDirection: transition.direction } : {}
6456
+ }
6457
+ };
6458
+ }
6459
+ function removeTransitionParams(attrs) {
6460
+ if (!attrs?.params) return attrs;
6461
+ const params = { ...attrs.params };
6462
+ for (const key of TRANSITION_PARAM_KEYS) delete params[key];
6463
+ return {
6464
+ ...attrs,
6465
+ ...Object.keys(params).length > 0 ? { params } : { params: void 0 }
6466
+ };
5841
6467
  }
5842
6468
 
5843
6469
  // src/doc/resolveDocTheme.ts
@@ -5950,7 +6576,11 @@ function getLayers(block, context = {}) {
5950
6576
  templateCtx.children = maybeChildren;
5951
6577
  }
5952
6578
  const { templateData, templateOverrides } = block;
5953
- const input = templateData || templateOverrides ? { ...block, ...templateData, ...templateOverrides } : block;
6579
+ const input = templateData || templateOverrides ? {
6580
+ ...block,
6581
+ ...templateData,
6582
+ ...coerceTemplateParams(block.template, templateOverrides ?? {}).input
6583
+ } : block;
5954
6584
  let layers;
5955
6585
  try {
5956
6586
  const templateFn = templateRegistry[templateName];
@@ -6215,9 +6845,13 @@ function validateMarkdownDoc(markdownDoc, options) {
6215
6845
  const diagnostics = [...doc.diagnostics ?? []];
6216
6846
  const blocks = flattenBlocks(doc.blocks);
6217
6847
  const knownTemplates = collectKnownTemplates(doc, options);
6848
+ const customNames = new Set(doc.customTemplates?.map((t) => t.name) ?? []);
6218
6849
  const visit = (block, parent) => {
6219
6850
  checkTemplate(block, parent, knownTemplates, diagnostics);
6220
6851
  checkAttributeCoercion(block, parent, diagnostics);
6852
+ checkConflictingAnnotationKeys(block, diagnostics);
6853
+ checkTemplateInputs(block, parent, customNames, diagnostics);
6854
+ checkPossibleDataFences(block, diagnostics);
6221
6855
  if (isDrawingBlock(block)) checkDrawingConnectors(block, diagnostics);
6222
6856
  for (const child of block.children ?? []) visit(child, block);
6223
6857
  };
@@ -6234,6 +6868,7 @@ function validateMarkdownDoc(markdownDoc, options) {
6234
6868
  diagnostics,
6235
6869
  errorCount: diagnostics.filter((d) => d.severity === "error").length,
6236
6870
  warningCount: diagnostics.filter((d) => d.severity === "warning").length,
6871
+ infoCount: diagnostics.filter((d) => d.severity === "info").length,
6237
6872
  doc
6238
6873
  };
6239
6874
  }
@@ -6249,11 +6884,13 @@ function isDrawingBlock(block) {
6249
6884
  return resolveTemplateName(block.template ?? "") === "drawing";
6250
6885
  }
6251
6886
  function checkTemplate(block, parent, known, diagnostics) {
6252
- const requested = block.sourceHeading?.templateAnnotation?.template;
6887
+ const requested = block.sourceHeading?.templateAnnotation?.template ?? block.sourceAnnotation?.template;
6253
6888
  if (!requested) return;
6254
6889
  if (parent && isDrawingBlock(parent)) {
6255
6890
  if (isShapeName(requested)) return;
6256
- const suggestion2 = nearestName(requested, new Set(SHAPE_NAMES));
6891
+ const suggestion2 = nearestName(requested, new Set(SHAPE_NAMES), {
6892
+ map: resolveTemplateName
6893
+ });
6257
6894
  diagnostics.push({
6258
6895
  severity: "warning",
6259
6896
  code: "unknown-shape",
@@ -6274,7 +6911,7 @@ function checkTemplate(block, parent, known, diagnostics) {
6274
6911
  return;
6275
6912
  }
6276
6913
  if (known.has(requested) || known.has(resolveTemplateName(requested))) return;
6277
- const suggestion = nearestName(requested, known);
6914
+ const suggestion = nearestName(requested, known, { map: resolveTemplateName });
6278
6915
  diagnostics.push({
6279
6916
  severity: "warning",
6280
6917
  code: "unknown-template",
@@ -6283,6 +6920,33 @@ function checkTemplate(block, parent, known, diagnostics) {
6283
6920
  ...lineOf2(block)
6284
6921
  });
6285
6922
  }
6923
+ function checkTemplateInputs(block, parent, customNames, diagnostics) {
6924
+ const requested = block.sourceHeading?.templateAnnotation?.template;
6925
+ if (!requested) return;
6926
+ if (parent && isDrawingBlock(parent)) return;
6927
+ if (isShapeName(requested)) return;
6928
+ const canonical = resolveTemplateName(requested);
6929
+ if (customNames.has(requested) || customNames.has(canonical)) return;
6930
+ const params = block.sourceHeading?.templateAnnotation?.params ?? {};
6931
+ for (const finding of lintTemplateParams(canonical, params)) {
6932
+ if (finding.kind === "missing-input" && isInputSatisfiable(canonical, block, finding.key)) {
6933
+ continue;
6934
+ }
6935
+ diagnostics.push({
6936
+ severity: "warning",
6937
+ code: finding.kind,
6938
+ message: `Block "${block.id}": ${finding.message}`,
6939
+ blockId: block.id,
6940
+ ...lineOf2(block)
6941
+ });
6942
+ }
6943
+ }
6944
+ function isInputSatisfiable(canonical, block, key) {
6945
+ if (block.templateData && key in block.templateData) return true;
6946
+ if (block[key] != null) return true;
6947
+ const derived = deriveTemplateInputs(canonical, block.title ?? "", block.contents);
6948
+ return derived != null && key in derived;
6949
+ }
6286
6950
  var NUMERIC_SHAPE_KEYS = ["x", "y", "width", "height", "w", "h", "strokeWidth", "borderRadius"];
6287
6951
  function checkAttributeCoercion(block, parent, diagnostics) {
6288
6952
  const params = block.sourceHeading?.attributes?.params;
@@ -6316,6 +6980,23 @@ function checkAttributeCoercion(block, parent, diagnostics) {
6316
6980
  }
6317
6981
  }
6318
6982
  }
6983
+ function checkConflictingAnnotationKeys(block, diagnostics) {
6984
+ const annotationParams = block.sourceHeading?.templateAnnotation?.params;
6985
+ const pandocParams = block.sourceHeading?.attributes?.params;
6986
+ if (!annotationParams || !pandocParams) return;
6987
+ for (const key of Object.keys(KNOWN_BLOCK_META_KEYS)) {
6988
+ const fromAnnotation = annotationParams[key];
6989
+ const fromPandoc = pandocParams[key];
6990
+ if (fromAnnotation == null || fromPandoc == null || fromAnnotation === fromPandoc) continue;
6991
+ diagnostics.push({
6992
+ severity: "info",
6993
+ code: "conflicting-annotation-key",
6994
+ message: `"${key}" is set in both {[\u2026]} (${fromAnnotation}) and {\u2026} (${fromPandoc}) \u2014 the {\u2026} value wins.`,
6995
+ blockId: block.id,
6996
+ ...lineOf2(block)
6997
+ });
6998
+ }
6999
+ }
6319
7000
  function checkDrawingConnectors(block, diagnostics) {
6320
7001
  const children = block.children ?? [];
6321
7002
  const ids = new Set(children.map((c) => c.id));
@@ -6339,9 +7020,28 @@ function checkDrawingConnectors(block, diagnostics) {
6339
7020
  }
6340
7021
  }
6341
7022
  }
7023
+ function checkPossibleDataFences(block, diagnostics) {
7024
+ const annotated = !!(block.sourceHeading?.templateAnnotation?.template || block.sourceAnnotation?.template);
7025
+ if (!annotated) return;
7026
+ for (const node of block.contents ?? []) {
7027
+ if (node.type !== "code") continue;
7028
+ const lang = node.lang?.toLowerCase();
7029
+ if (lang !== "json" && lang !== "yaml" && lang !== "yml") continue;
7030
+ if (isDataFence(node)) continue;
7031
+ const result = parseDataFence(node);
7032
+ if (!result.data || Object.keys(result.data).length === 0) continue;
7033
+ diagnostics.push({
7034
+ severity: "info",
7035
+ code: "possible-data-fence",
7036
+ message: `This ${lang} block renders as code \u2014 add "data" to the fence (\`\`\`${lang} data) if it's meant as template input.`,
7037
+ blockId: block.id,
7038
+ ...node.position ? { line: node.position.start.line } : lineOf2(block)
7039
+ });
7040
+ }
7041
+ }
6342
7042
  var ANNOTATION_LIKE_RE = /\{\[/;
6343
7043
  function checkUnrecognizedAnnotations(markdownDoc, diagnostics) {
6344
- const walk = (node, headingLine) => {
7044
+ const walk = (node, headingLine, topLevel) => {
6345
7045
  if (node.type === "heading") {
6346
7046
  const heading = node;
6347
7047
  const text = extractPlainText(heading);
@@ -6355,22 +7055,26 @@ function checkUnrecognizedAnnotations(markdownDoc, diagnostics) {
6355
7055
  }
6356
7056
  return;
6357
7057
  }
7058
+ if (node.type === "paragraph" && topLevel) {
7059
+ const parsed = parseStandaloneAnnotation(node);
7060
+ if (parsed && parsed.template != null) return;
7061
+ }
6358
7062
  if (node.type === "text" && ANNOTATION_LIKE_RE.test(node.value)) {
6359
7063
  diagnostics.push({
6360
7064
  severity: "warning",
6361
7065
  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)`,
7066
+ 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
7067
  ...node.position ? { line: node.position.start.line } : headingLine != null ? { line: headingLine } : {}
6364
7068
  });
6365
7069
  return;
6366
7070
  }
6367
7071
  if (node.type === "code") return;
6368
7072
  for (const child of getChildren(node)) {
6369
- walk(child, headingLine);
7073
+ walk(child, headingLine, false);
6370
7074
  }
6371
7075
  };
6372
7076
  for (const child of markdownDoc.children) {
6373
- walk(child, child.position?.start.line);
7077
+ walk(child, child.position?.start.line, true);
6374
7078
  }
6375
7079
  }
6376
7080
  function normalizeAssetCheck(assets) {
@@ -6433,41 +7137,6 @@ function walkHtmlImages(nodes, report) {
6433
7137
  walkHtmlImages(node.children, report);
6434
7138
  }
6435
7139
  }
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
7140
  function lineOf2(block) {
6472
7141
  const line = block.sourceHeading?.position?.start.line;
6473
7142
  return line != null ? { line } : {};
@@ -6484,6 +7153,10 @@ export {
6484
7153
  wrapWithPersistentLayers,
6485
7154
  themeWantsAmbientMotion,
6486
7155
  applyRenderStyleToLayers,
7156
+ BASE_INPUT_DESCRIPTORS,
7157
+ TEMPLATE_INPUT_DESCRIPTORS,
7158
+ coerceTemplateParams,
7159
+ lintTemplateParams,
6487
7160
  TEMPLATE_METADATA,
6488
7161
  resolveColorScheme,
6489
7162
  getThemeFont,
@@ -6562,6 +7235,7 @@ export {
6562
7235
  writeCustomThemesToFrontmatter,
6563
7236
  isDataFence,
6564
7237
  parseDataFence,
7238
+ replaceDataFence,
6565
7239
  parseYamlSubset,
6566
7240
  findFirstTable,
6567
7241
  extractTableData,
@@ -6579,4 +7253,4 @@ export {
6579
7253
  validateMarkdownSource,
6580
7254
  validateMarkdownDoc
6581
7255
  };
6582
- //# sourceMappingURL=chunk-4ZFRZBU2.js.map
7256
+ //# sourceMappingURL=chunk-5TYS5V3G.js.map