@bendyline/squisq 1.4.1 → 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 (254) 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-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
  4. package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
  5. package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
  6. package/dist/chunk-2OIBZYKP.js.map +1 -0
  7. package/dist/chunk-2UQOHYE2.js +45 -0
  8. package/dist/chunk-2UQOHYE2.js.map +1 -0
  9. package/dist/chunk-6ATE2PSM.js +344 -0
  10. package/dist/chunk-6ATE2PSM.js.map +1 -0
  11. package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
  12. package/dist/chunk-A4O7GIWE.js.map +1 -0
  13. package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
  14. package/dist/chunk-DLXZMS5K.js.map +1 -0
  15. package/dist/chunk-ET53IIEP.js +1 -0
  16. package/dist/chunk-ET53IIEP.js.map +1 -0
  17. package/dist/chunk-HWVFJAAH.js +362 -0
  18. package/dist/chunk-HWVFJAAH.js.map +1 -0
  19. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  20. package/dist/chunk-IMSCRLLV.js.map +1 -0
  21. package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
  22. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  23. package/dist/chunk-LDQ2HJIX.js +596 -0
  24. package/dist/chunk-LDQ2HJIX.js.map +1 -0
  25. package/dist/chunk-LH7I6SH7.js +7208 -0
  26. package/dist/chunk-LH7I6SH7.js.map +1 -0
  27. package/dist/chunk-PGGGKYWG.js +1 -0
  28. package/dist/chunk-PGGGKYWG.js.map +1 -0
  29. package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
  30. package/dist/chunk-Q3ROPT5H.js.map +1 -0
  31. package/dist/chunk-QE7TWGVT.js +178 -0
  32. package/dist/chunk-QE7TWGVT.js.map +1 -0
  33. package/dist/chunk-RUDYOTA6.js +345 -0
  34. package/dist/chunk-RUDYOTA6.js.map +1 -0
  35. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  36. package/dist/chunk-T5UK6YOB.js.map +1 -0
  37. package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
  38. package/dist/chunk-TEEEILMP.js.map +1 -0
  39. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  40. package/dist/chunk-TQWLI6S2.js.map +1 -0
  41. package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
  42. package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
  43. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  44. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  45. package/dist/chunk-ZLQKNOZQ.js +1 -0
  46. package/dist/chunk-ZLQKNOZQ.js.map +1 -0
  47. package/dist/doc/index.d.ts +5 -5
  48. package/dist/doc/index.js +135 -8
  49. package/dist/generate/index.d.ts +3 -74
  50. package/dist/generate/index.js +2 -7
  51. package/dist/icons/index.d.ts +3 -26
  52. package/dist/icons/index.js +13 -0
  53. package/dist/icons/inlineIconMarker.d.ts +1 -0
  54. package/dist/icons/inlineIconMarker.js +15 -0
  55. package/dist/icons/inlineIconMarker.js.map +1 -0
  56. package/dist/imageEdit/index.d.ts +12 -6
  57. package/dist/imageEdit/index.js +4 -1
  58. package/dist/index.d.ts +14 -11
  59. package/dist/index.js +265 -37
  60. package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
  61. package/dist/jsonForm/index.d.ts +42 -1
  62. package/dist/jsonForm/index.js +9 -1
  63. package/dist/markdown/index.d.ts +228 -3
  64. package/dist/markdown/index.js +42 -7
  65. package/dist/recommend/index.d.ts +1 -1
  66. package/dist/recommend/index.js +3 -2
  67. package/dist/schemas/index.d.ts +28 -8
  68. package/dist/schemas/index.js +36 -8
  69. package/dist/spatial/index.d.ts +9 -4
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.d.ts +2 -2
  72. package/dist/storage/index.js +1 -1
  73. package/dist/story/index.d.ts +1021 -41
  74. package/dist/story/index.js +135 -8
  75. package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  76. package/dist/timing/index.js +3 -5
  77. package/dist/transform/index.d.ts +48 -10
  78. package/dist/transform/index.js +17 -481
  79. package/dist/transform/index.js.map +1 -1
  80. package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
  81. package/dist/versions/index.d.ts +1 -1
  82. package/package.json +6 -1
  83. package/src/__tests__/animationUtils.test.ts +40 -0
  84. package/src/__tests__/annotationCoercion.test.ts +243 -0
  85. package/src/__tests__/applyRenderStyle.test.ts +146 -0
  86. package/src/__tests__/atmosphereLayers.test.ts +108 -0
  87. package/src/__tests__/attrTokens.test.ts +162 -0
  88. package/src/__tests__/contentExtractor.test.ts +60 -0
  89. package/src/__tests__/coverBlock.test.ts +46 -1
  90. package/src/__tests__/customTemplateSample.test.ts +144 -0
  91. package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
  92. package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
  93. package/src/__tests__/diagramBlock.test.ts +122 -0
  94. package/src/__tests__/diagramLayout.test.ts +134 -0
  95. package/src/__tests__/drawing.test.ts +109 -0
  96. package/src/__tests__/drawingBlock.test.ts +124 -0
  97. package/src/__tests__/drawingLayout.test.ts +171 -0
  98. package/src/__tests__/geohash.test.ts +6 -5
  99. package/src/__tests__/getLayers.test.ts +6 -2
  100. package/src/__tests__/getLayersFallback.test.ts +103 -0
  101. package/src/__tests__/imageEditExportSvg.test.ts +70 -0
  102. package/src/__tests__/imageEditVersions.test.ts +72 -0
  103. package/src/__tests__/inlineIconMarker.test.ts +36 -0
  104. package/src/__tests__/inputDescriptors.test.ts +162 -0
  105. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  106. package/src/__tests__/layoutBlock.test.ts +83 -0
  107. package/src/__tests__/layoutLayout.test.ts +169 -0
  108. package/src/__tests__/markdown.test.ts +87 -0
  109. package/src/__tests__/markdownSanitize.test.ts +79 -0
  110. package/src/__tests__/markdownToDoc.test.ts +391 -2
  111. package/src/__tests__/mediaAnnotations.test.ts +91 -0
  112. package/src/__tests__/mediaSchedule.test.ts +118 -0
  113. package/src/__tests__/pandocAttrParse.test.ts +187 -0
  114. package/src/__tests__/resolveDocTheme.test.ts +55 -0
  115. package/src/__tests__/rootExports.test.ts +43 -0
  116. package/src/__tests__/shapeGeometry.test.ts +89 -0
  117. package/src/__tests__/shapeVocabulary.test.ts +133 -0
  118. package/src/__tests__/structuredData.test.ts +249 -0
  119. package/src/__tests__/templateAnnotationParse.test.ts +108 -0
  120. package/src/__tests__/templateMetadata.test.ts +31 -0
  121. package/src/__tests__/templates.test.ts +257 -4
  122. package/src/__tests__/themeCompile.test.ts +42 -0
  123. package/src/__tests__/themeValidator.test.ts +6 -0
  124. package/src/__tests__/transformV2.test.ts +137 -0
  125. package/src/__tests__/transitionNormalize.test.ts +61 -0
  126. package/src/__tests__/validateDoc.test.ts +260 -0
  127. package/src/doc/annotationBlocks.ts +117 -0
  128. package/src/doc/customTemplatesFrontmatter.ts +314 -0
  129. package/src/doc/customThemesFrontmatter.ts +95 -0
  130. package/src/doc/docToMarkdown.ts +104 -5
  131. package/src/doc/getLayers.ts +49 -17
  132. package/src/doc/index.ts +47 -1
  133. package/src/doc/markdownToDoc.ts +411 -13
  134. package/src/doc/mediaAnnotations.ts +100 -0
  135. package/src/doc/resolveDocTheme.ts +55 -0
  136. package/src/doc/standaloneAnnotation.ts +58 -0
  137. package/src/doc/structuredData.ts +301 -0
  138. package/src/doc/templateInputs.ts +240 -0
  139. package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
  140. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
  141. package/src/doc/templates/accentImage.ts +30 -6
  142. package/src/doc/templates/captionUtils.ts +23 -0
  143. package/src/doc/templates/comparisonBar.ts +33 -18
  144. package/src/doc/templates/coverBlock.ts +35 -5
  145. package/src/doc/templates/customTemplate.ts +51 -0
  146. package/src/doc/templates/dataTable.ts +37 -19
  147. package/src/doc/templates/dateEvent.ts +41 -20
  148. package/src/doc/templates/definitionCard.ts +26 -18
  149. package/src/doc/templates/diagramBlock.ts +204 -0
  150. package/src/doc/templates/diagramLayout.ts +159 -0
  151. package/src/doc/templates/drawingBlock.ts +325 -0
  152. package/src/doc/templates/drawingLayout.ts +539 -0
  153. package/src/doc/templates/factCard.ts +54 -26
  154. package/src/doc/templates/fallbackBlock.ts +123 -0
  155. package/src/doc/templates/featureBlock.ts +20 -13
  156. package/src/doc/templates/fullBleedQuote.ts +18 -8
  157. package/src/doc/templates/imageWithCaption.ts +30 -15
  158. package/src/doc/templates/index.ts +225 -46
  159. package/src/doc/templates/inputDescriptors.ts +432 -0
  160. package/src/doc/templates/layoutBlock.ts +43 -0
  161. package/src/doc/templates/layoutLayout.ts +243 -0
  162. package/src/doc/templates/listBlock.ts +56 -30
  163. package/src/doc/templates/mapBlock.ts +15 -12
  164. package/src/doc/templates/metadata.ts +120 -0
  165. package/src/doc/templates/persistentLayers.ts +129 -11
  166. package/src/doc/templates/photoGrid.ts +22 -34
  167. package/src/doc/templates/pullQuote.ts +30 -12
  168. package/src/doc/templates/quoteBlock.ts +54 -29
  169. package/src/doc/templates/sectionHeader.ts +25 -11
  170. package/src/doc/templates/statHighlight.ts +39 -22
  171. package/src/doc/templates/titleBlock.ts +59 -21
  172. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
  173. package/src/doc/templates/tokens/resolveTokens.ts +453 -0
  174. package/src/doc/templates/twoColumn.ts +48 -30
  175. package/src/doc/templates/videoPullQuote.ts +22 -12
  176. package/src/doc/templates/videoWithCaption.ts +21 -15
  177. package/src/doc/utils/animationUtils.ts +267 -3
  178. package/src/doc/utils/applyRenderStyle.ts +113 -0
  179. package/src/doc/utils/imageTreatment.ts +70 -0
  180. package/src/doc/utils/nearest.ts +59 -0
  181. package/src/doc/utils/shapeGeometry.ts +464 -0
  182. package/src/doc/utils/themeUtils.ts +75 -1
  183. package/src/doc/validate.ts +575 -0
  184. package/src/generate/contentExtractor.ts +148 -35
  185. package/src/generate/index.ts +0 -3
  186. package/src/icons/index.ts +8 -0
  187. package/src/icons/inlineIconMarker.ts +73 -0
  188. package/src/imageEdit/export.ts +85 -2
  189. package/src/imageEdit/index.ts +1 -0
  190. package/src/imageEdit/persistence.ts +51 -0
  191. package/src/index.ts +2 -0
  192. package/src/jsonForm/index.ts +2 -0
  193. package/src/jsonForm/tokens.ts +78 -0
  194. package/src/markdown/annotationCoercion.ts +295 -0
  195. package/src/markdown/attrTokens.ts +241 -0
  196. package/src/markdown/convert.ts +229 -26
  197. package/src/markdown/index.ts +37 -0
  198. package/src/markdown/sanitize.ts +318 -0
  199. package/src/markdown/stringify.ts +96 -6
  200. package/src/markdown/types.ts +66 -0
  201. package/src/markdown/utils.ts +111 -2
  202. package/src/recommend/templates.ts +24 -0
  203. package/src/schemas/BlockTemplates.ts +133 -4
  204. package/src/schemas/CustomTemplates.ts +126 -0
  205. package/src/schemas/Doc.ts +387 -65
  206. package/src/schemas/ImageEditDoc.ts +11 -4
  207. package/src/schemas/Media.ts +169 -0
  208. package/src/schemas/Theme.ts +23 -1
  209. package/src/schemas/Transitions.ts +224 -0
  210. package/src/schemas/colorUtils.ts +33 -0
  211. package/src/schemas/fontStacks.ts +5 -2
  212. package/src/schemas/index.ts +3 -0
  213. package/src/schemas/themeCompile.ts +24 -5
  214. package/src/schemas/themeValidator.ts +49 -11
  215. package/src/schemas/themes/cinematic.json +69 -12
  216. package/src/schemas/themes/documentary.json +58 -11
  217. package/src/schemas/themes/gezellig.json +60 -11
  218. package/src/schemas/themes/magazine.json +54 -9
  219. package/src/schemas/themes/morning-light.json +52 -9
  220. package/src/schemas/themes/standard-dark.json +48 -13
  221. package/src/schemas/themes/standard.json +44 -12
  222. package/src/schemas/themes/tech-dark.json +65 -12
  223. package/src/spatial/Geohash.ts +52 -21
  224. package/src/storage/ContentContainer.ts +6 -2
  225. package/src/transform/applyTransform.ts +43 -4
  226. package/src/transform/index.ts +1 -0
  227. package/src/transform/registry.ts +44 -13
  228. package/src/transform/styles/dataDriven.ts +2 -1
  229. package/src/transform/styles/documentary.ts +2 -1
  230. package/src/transform/styles/magazine.ts +3 -1
  231. package/src/transform/styles/minimal.ts +2 -1
  232. package/src/transform/styles/narrative.ts +3 -1
  233. package/src/transform/templateSelector.ts +87 -11
  234. package/src/transform/types.ts +35 -2
  235. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  236. package/dist/chunk-3E5F2XMR.js.map +0 -1
  237. package/dist/chunk-3K5OG6KI.js.map +0 -1
  238. package/dist/chunk-4X3JQXNM.js +0 -35
  239. package/dist/chunk-4X3JQXNM.js.map +0 -1
  240. package/dist/chunk-57CVQCFX.js.map +0 -1
  241. package/dist/chunk-5WFRKURX.js.map +0 -1
  242. package/dist/chunk-7UDSRZKG.js.map +0 -1
  243. package/dist/chunk-B4EEON3N.js.map +0 -1
  244. package/dist/chunk-FR2RBTKO.js.map +0 -1
  245. package/dist/chunk-H3AXU4MJ.js +0 -179
  246. package/dist/chunk-H3AXU4MJ.js.map +0 -1
  247. package/dist/chunk-K32VJONL.js.map +0 -1
  248. package/dist/chunk-KSWJR66U.js +0 -3706
  249. package/dist/chunk-KSWJR66U.js.map +0 -1
  250. package/dist/chunk-MYIH7FWD.js +0 -145
  251. package/dist/chunk-MYIH7FWD.js.map +0 -1
  252. package/dist/chunk-ZTX4PFFT.js.map +0 -1
  253. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  254. package/src/generate/slideshowGenerator.ts +0 -273
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Frontmatter serialization for user-defined custom themes.
3
+ *
4
+ * The theme analog of {@link ./customTemplatesFrontmatter.ts}. Custom Theme
5
+ * definitions live in the document's YAML frontmatter under the key
6
+ * `squisq-custom-themes`, stored as a single **compact JSON** object keyed
7
+ * by theme id:
8
+ *
9
+ * ```yaml
10
+ * squisq-custom-themes: {"my-brand":{"schemaVersion":"1","id":"my-brand",…}}
11
+ * ```
12
+ *
13
+ * Squisq's frontmatter parser is line-based; a JSON object literal
14
+ * round-trips through it verbatim (single line, no leading quote to strip),
15
+ * so the value is written **unquoted** and stays human-readable and diffable
16
+ * — the same mechanism the custom-templates codec relies on.
17
+ *
18
+ * Only one theme is *active* per doc (selected by id via the `squisq-theme`
19
+ * frontmatter key / `Doc.themeId`), but the payload is a keyed map — parallel
20
+ * to custom templates — so a doc can carry a small catalog and switch between
21
+ * them, and `resolveThemeForDoc` can resolve any of them doc-scoped.
22
+ *
23
+ * Malformed entries are dropped (via `validateTheme`) rather than failing the
24
+ * whole doc load, matching the tolerant behavior of the templates reader.
25
+ */
26
+
27
+ import type { Theme } from '../schemas/Theme.js';
28
+ import { FRONTMATTER_CUSTOM_THEMES_KEY } from '../schemas/Theme.js';
29
+ import { validateTheme } from '../schemas/themeValidator.js';
30
+
31
+ /** Re-export for callers that need the canonical key spelling. */
32
+ export { FRONTMATTER_CUSTOM_THEMES_KEY };
33
+
34
+ /**
35
+ * Normalize the raw frontmatter value into an array of candidate objects.
36
+ * Accepts, in order: a JSON string (current, from the line-based parser),
37
+ * an already-parsed object map (keyed by id), and an array (in case a richer
38
+ * YAML parser delivers one). Returns undefined when absent/unparseable.
39
+ */
40
+ function normalizeCandidates(raw: unknown): unknown[] | undefined {
41
+ if (raw === undefined || raw === null) return undefined;
42
+ let value: unknown = raw;
43
+ if (typeof value === 'string') {
44
+ const trimmed = value.trim();
45
+ if (!trimmed) return undefined;
46
+ try {
47
+ value = JSON.parse(trimmed);
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ if (Array.isArray(value)) return value;
53
+ if (value && typeof value === 'object') {
54
+ return Object.values(value as Record<string, unknown>);
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ /**
60
+ * Read the custom themes inlined into a document's frontmatter. Returns
61
+ * undefined when the key is absent or holds no valid theme, so callers can
62
+ * omit the field from the Doc.
63
+ */
64
+ export function readCustomThemesFromFrontmatter(
65
+ frontmatter: Record<string, unknown> | undefined,
66
+ ): Theme[] | undefined {
67
+ if (!frontmatter) return undefined;
68
+ const candidates = normalizeCandidates(frontmatter[FRONTMATTER_CUSTOM_THEMES_KEY]);
69
+ if (!candidates) return undefined;
70
+ const out: Theme[] = [];
71
+ for (const entry of candidates) {
72
+ const result = validateTheme(entry);
73
+ if (result.valid && result.theme) out.push(result.theme);
74
+ }
75
+ return out.length > 0 ? out : undefined;
76
+ }
77
+
78
+ /**
79
+ * Encode a list of custom themes into the compact JSON object described in
80
+ * the module header (keyed by theme id). Returns undefined when the input is
81
+ * empty so callers can leave the key off the output.
82
+ */
83
+ export function writeCustomThemesToFrontmatter(
84
+ themes: readonly Theme[] | undefined,
85
+ options?: { pretty?: boolean },
86
+ ): string | undefined {
87
+ if (!themes || themes.length === 0) return undefined;
88
+ const map: Record<string, Theme> = {};
89
+ for (const theme of themes) map[theme.id] = theme;
90
+ // Pretty output is multi-line, so it serializes as a YAML literal block
91
+ // scalar (see the markdown stringify frontmatter path) and reads back via
92
+ // `JSON.parse`. Default stays compact (single line) so existing docs and
93
+ // snapshots are byte-unchanged.
94
+ return JSON.stringify(map, null, options?.pretty ? 2 : undefined);
95
+ }
@@ -23,7 +23,21 @@
23
23
  */
24
24
 
25
25
  import type { Doc, Block } from '../schemas/Doc.js';
26
- import type { MarkdownDocument, MarkdownBlockNode, MarkdownHeading } from '../markdown/types.js';
26
+ import type {
27
+ MarkdownDocument,
28
+ MarkdownBlockNode,
29
+ MarkdownHeading,
30
+ MarkdownParagraph,
31
+ } from '../markdown/types.js';
32
+ import { serializeAnnotation } from '../markdown/attrTokens.js';
33
+ import {
34
+ FRONTMATTER_CUSTOM_TEMPLATES_KEY,
35
+ writeCustomTemplatesToFrontmatter,
36
+ } from './customTemplatesFrontmatter.js';
37
+ import {
38
+ FRONTMATTER_CUSTOM_THEMES_KEY,
39
+ writeCustomThemesToFrontmatter,
40
+ } from './customThemesFrontmatter.js';
27
41
 
28
42
  /**
29
43
  * Convert a Doc with heading-driven blocks back to a MarkdownDocument.
@@ -47,6 +61,12 @@ export function docToMarkdown(doc: Doc): MarkdownDocument {
47
61
  if (block.sourceHeading) {
48
62
  const heading = ensureAnnotation(block, block.sourceHeading);
49
63
  children.push(heading);
64
+ } else if (block.standaloneAnnotation) {
65
+ // Heading-less standalone block: re-emit its `{[…]}` annotation as a
66
+ // paragraph before its contents. The serializer's quoting is paired
67
+ // with the tokenizer, and stringifyMarkdown un-escapes `{[…]}` spans,
68
+ // so the annotation round-trips.
69
+ children.push(synthesizeAnnotationParagraph(block));
50
70
  }
51
71
 
52
72
  // Emit body content
@@ -66,33 +86,112 @@ export function docToMarkdown(doc: Doc): MarkdownDocument {
66
86
  emitBlock(block);
67
87
  }
68
88
 
89
+ // Merge `Doc.customTemplates` into the outgoing frontmatter so the
90
+ // markdown remains the source-of-truth: edits made via the editor's
91
+ // TemplateDesigner round-trip through `docToMarkdown` →
92
+ // `stringifyMarkdown` and are visible to anyone reading the file.
93
+ const customPayload = writeCustomTemplatesToFrontmatter(doc.customTemplates);
94
+ let frontmatter = doc.frontmatter;
95
+ if (customPayload) {
96
+ frontmatter = { ...(frontmatter ?? {}), [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: customPayload };
97
+ } else if (frontmatter && FRONTMATTER_CUSTOM_TEMPLATES_KEY in frontmatter) {
98
+ // The doc has no custom templates anymore — drop the stale
99
+ // frontmatter key so the markdown reflects the current state.
100
+ const { [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: _drop, ...rest } = frontmatter;
101
+ void _drop;
102
+ frontmatter = rest;
103
+ }
104
+
105
+ // Mirror the same round-trip for `Doc.customThemes` (the theme analog).
106
+ const customThemesPayload = writeCustomThemesToFrontmatter(doc.customThemes);
107
+ if (customThemesPayload) {
108
+ frontmatter = { ...(frontmatter ?? {}), [FRONTMATTER_CUSTOM_THEMES_KEY]: customThemesPayload };
109
+ } else if (frontmatter && FRONTMATTER_CUSTOM_THEMES_KEY in frontmatter) {
110
+ const { [FRONTMATTER_CUSTOM_THEMES_KEY]: _dropTheme, ...rest } = frontmatter;
111
+ void _dropTheme;
112
+ frontmatter = rest;
113
+ }
114
+
69
115
  return {
70
116
  type: 'document',
71
117
  children,
118
+ ...(frontmatter && Object.keys(frontmatter).length > 0 ? { frontmatter } : {}),
72
119
  };
73
120
  }
74
121
 
122
+ /**
123
+ * Build the synthesized annotation paragraph for a heading-less standalone
124
+ * block: `{[templateName key=value …]}` from its `sourceAnnotation.template`
125
+ * and current `templateOverrides` (edits round-trip through the latter).
126
+ */
127
+ function synthesizeAnnotationParagraph(block: Block): MarkdownParagraph {
128
+ const text = serializeAnnotation(block.sourceAnnotation?.template, block.templateOverrides);
129
+ return { type: 'paragraph', children: [{ type: 'text', value: text }] };
130
+ }
131
+
75
132
  /**
76
133
  * Ensure the heading's `templateAnnotation` reflects the block's
77
134
  * template and templateOverrides. Returns a (possibly cloned) heading.
78
135
  */
79
136
  function ensureAnnotation(block: Block, heading: MarkdownHeading): MarkdownHeading {
80
- // If the heading already has an annotation, trust it (it came from parsing)
81
- if (heading.templateAnnotation) return heading;
137
+ const attrs = ensureTransitionAttributes(block, heading);
82
138
 
83
139
  // If the block has a non-default template or overrides, inject an annotation
84
- const hasExplicitTemplate = block.template && block.template !== 'sectionHeader';
140
+ const hasExplicitTemplate =
141
+ block.template && block.template !== 'sectionHeader' && block.autoTemplate !== true;
85
142
  const hasOverrides = block.templateOverrides && Object.keys(block.templateOverrides).length > 0;
86
143
 
87
- if (!hasExplicitTemplate && !hasOverrides) return heading;
144
+ // If the heading already has an annotation, trust it (it came from parsing).
145
+ // Transition attrs may still have been injected above if the block changed.
146
+ if (heading.templateAnnotation) {
147
+ return attrs === heading.attributes ? heading : { ...heading, attributes: attrs };
148
+ }
149
+
150
+ if (!hasExplicitTemplate && !hasOverrides) {
151
+ return attrs === heading.attributes ? heading : { ...heading, attributes: attrs };
152
+ }
88
153
 
89
154
  // Clone to avoid mutating the original
90
155
  return {
91
156
  ...heading,
92
157
  children: [...heading.children],
158
+ attributes: attrs,
93
159
  templateAnnotation: {
94
160
  template: block.template ?? 'sectionHeader',
95
161
  ...(hasOverrides ? { params: block.templateOverrides } : {}),
96
162
  },
97
163
  };
98
164
  }
165
+
166
+ function ensureTransitionAttributes(
167
+ block: Block,
168
+ heading: MarkdownHeading,
169
+ ): MarkdownHeading['attributes'] {
170
+ const transition = block.transition;
171
+ if (!transition) return heading.attributes;
172
+
173
+ // The transition is deliberately written to BOTH channels, which serve
174
+ // different consumers:
175
+ // - `params` is what `stringifyMarkdown` serializes into the Pandoc `{…}`
176
+ // attribute text (serializePandocAttributes reads only id/classes/params).
177
+ // - `blockMeta` is what `markdownToDoc` reads back directly (makeBlock,
178
+ // pinnedHeadingMeta) — an in-memory Doc → MarkdownDocument → Doc round
179
+ // trip never re-parses params, so a stale/missing `blockMeta.transition`
180
+ // would lose or resurrect the wrong transition.
181
+ const attrs = heading.attributes ?? {};
182
+ return {
183
+ ...attrs,
184
+ params: {
185
+ ...(attrs.params ?? {}),
186
+ transition: transition.type,
187
+ ...(transition.duration !== undefined
188
+ ? { transitionDuration: String(transition.duration) }
189
+ : {}),
190
+ ...(transition.direction ? { transitionDirection: transition.direction } : {}),
191
+ },
192
+ blockMeta: {
193
+ ...(attrs.blockMeta ?? {}),
194
+ transition,
195
+ },
196
+ };
197
+ }
@@ -35,7 +35,10 @@ import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
35
35
  import { createTemplateContext, isTemplateBlock } from '../schemas/BlockTemplates.js';
36
36
  import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
37
37
  import { templateRegistry, resolveTemplateName } from './templates/index.js';
38
- import { expandPersistentLayers } from './templates/persistentLayers.js';
38
+ import { coerceTemplateParams } from './templates/inputDescriptors.js';
39
+ import { expandPersistentLayers, wrapWithPersistentLayers } from './templates/persistentLayers.js';
40
+ import { applyRenderStyleToLayers } from './utils/applyRenderStyle.js';
41
+ import { fallbackBlockLayers } from './templates/fallbackBlock.js';
39
42
 
40
43
  // ============================================
41
44
  // RenderContext
@@ -109,9 +112,30 @@ export function getLayers(block: DocBlock, context: RenderContext = {}): Layer[]
109
112
  // and the layer list comes back empty.
110
113
  if (isTemplateBlock(block)) {
111
114
  const resolved = resolveTemplateName(block.template);
115
+ const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
112
116
  if (resolved in templateRegistry) {
113
117
  const templateName = resolved as keyof typeof templateRegistry;
114
- const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
118
+ // Aggregate templates (e.g. `diagram`) consume the block's children.
119
+ const maybeChildren = (block as Block).children;
120
+ if (maybeChildren && maybeChildren.length > 0) {
121
+ templateCtx.children = maybeChildren;
122
+ }
123
+ // Effective template input: the block's own fields, then structured
124
+ // body data (```json data fences, GFM tables), then `{[…]}` string
125
+ // overrides — the same merge order buildPreviewDoc uses. Inline overrides
126
+ // are coerced to their typed shape (`center="47.6,-122.3"` → `{lat,lng}`,
127
+ // `zoom=9` → `9`) so pure-inline annotations render without any change to
128
+ // the template functions. `block.templateOverrides` itself stays raw
129
+ // strings for lossless round-tripping — only this ephemeral input is coerced.
130
+ const { templateData, templateOverrides } = block as Block;
131
+ const input =
132
+ templateData || templateOverrides
133
+ ? ({
134
+ ...block,
135
+ ...templateData,
136
+ ...coerceTemplateParams(block.template, templateOverrides ?? {}).input,
137
+ } as TemplateBlock)
138
+ : block;
115
139
  let layers: Layer[];
116
140
  try {
117
141
  // Each registry entry accepts its specific TemplateBlock variant; the
@@ -120,21 +144,34 @@ export function getLayers(block: DocBlock, context: RenderContext = {}): Layer[]
120
144
  input: TemplateBlock,
121
145
  ctx: TemplateContext,
122
146
  ) => Layer[];
123
- layers = templateFn(block, templateCtx);
147
+ layers = templateFn(input, templateCtx);
124
148
  if (!Array.isArray(layers)) {
125
- console.error(`Template ${templateName} did not return an array, got:`, typeof layers);
126
- layers = [];
149
+ console.warn(`Template ${templateName} did not return an array, got:`, typeof layers);
150
+ layers = fallbackBlockLayers(block, templateCtx, `Template "${block.template}" failed`);
127
151
  }
128
152
  } catch (err: unknown) {
129
- console.error(`Error expanding template ${templateName}:`, err);
130
- layers = [];
153
+ console.warn(`Error expanding template ${templateName}:`, err);
154
+ layers = fallbackBlockLayers(block, templateCtx, `Template "${block.template}" failed`);
131
155
  }
132
156
 
157
+ // Theme motion defaults apply to template-generated layers only
158
+ // (mirrors expandDocBlocks — the two paths must agree).
159
+ layers = applyRenderStyleToLayers(layers, block as Block, theme);
160
+
133
161
  return injectPersistentLayers(layers, block, context);
134
162
  }
163
+
164
+ // Unknown template — graceful-degradation guarantee: render the
165
+ // block's heading + body text as a plain card with a visible notice
166
+ // instead of a blank slide.
167
+ return injectPersistentLayers(
168
+ fallbackBlockLayers(block, templateCtx, `Unknown template "${block.template}"`),
169
+ block,
170
+ context,
171
+ );
135
172
  }
136
173
 
137
- // 3. Fallback: no layers and no known template
174
+ // 3. Fallback: no layers and no template requested
138
175
  return injectPersistentLayers([], block, context);
139
176
  }
140
177
 
@@ -150,14 +187,9 @@ function injectPersistentLayers(layers: Layer[], block: DocBlock, context: Rende
150
187
  const { persistentLayers } = context;
151
188
  if (!persistentLayers) return layers;
152
189
 
153
- const bottomLayers = expandPersistentLayers(persistentLayers.bottomLayers);
154
- const topLayers = expandPersistentLayers(persistentLayers.topLayers);
155
-
156
- if (bottomLayers.length === 0 && topLayers.length === 0) return layers;
157
-
158
- const templateBlock = block as TemplateBlock;
159
- const useBottom = templateBlock.useBottomLayer !== false;
160
- const useTop = templateBlock.useTopLayer !== false;
190
+ const theme = context.theme ?? DEFAULT_THEME;
191
+ const bottomLayers = expandPersistentLayers(persistentLayers.bottomLayers, theme);
192
+ const topLayers = expandPersistentLayers(persistentLayers.topLayers, theme);
161
193
 
162
- return [...(useBottom ? bottomLayers : []), ...layers, ...(useTop ? topLayers : [])];
194
+ return wrapWithPersistentLayers(layers, block as TemplateBlock, bottomLayers, topLayers);
163
195
  }
package/src/doc/index.ts CHANGED
@@ -1,9 +1,55 @@
1
1
  export * from './templates/index.js';
2
2
  export * from './utils/animationUtils.js';
3
3
  export * from './utils/themeUtils.js';
4
- export { markdownToDoc, flattenBlocks, countBlocks, getBlockDepth } from './markdownToDoc.js';
4
+ export * from './utils/applyRenderStyle.js';
5
+ export * from './utils/imageTreatment.js';
6
+ export * from './templateInputs.js';
7
+ export * from './utils/shapeGeometry.js';
8
+ export {
9
+ markdownToDoc,
10
+ flattenBlocks,
11
+ flattenRenderableBlocks,
12
+ countBlocks,
13
+ getBlockDepth,
14
+ } from './markdownToDoc.js';
5
15
  export type { MarkdownToDocOptions } from './markdownToDoc.js';
6
16
  export { docToMarkdown } from './docToMarkdown.js';
17
+ export {
18
+ encodeLayersForFrontmatter,
19
+ decodeLayersFromFrontmatter,
20
+ readCustomTemplatesFromFrontmatter,
21
+ writeCustomTemplatesToFrontmatter,
22
+ FRONTMATTER_CUSTOM_TEMPLATES_KEY,
23
+ } from './customTemplatesFrontmatter.js';
24
+ export {
25
+ readCustomThemesFromFrontmatter,
26
+ writeCustomThemesToFrontmatter,
27
+ FRONTMATTER_CUSTOM_THEMES_KEY,
28
+ } from './customThemesFrontmatter.js';
29
+ export { resolveThemeForDoc } from './resolveDocTheme.js';
7
30
  export { getLayers } from './getLayers.js';
8
31
  export type { RenderContext } from './getLayers.js';
9
32
  export { resolveAudioMapping, scoreTextSimilarity } from './audioMapping.js';
33
+ export {
34
+ isDataFence,
35
+ parseDataFence,
36
+ replaceDataFence,
37
+ parseYamlSubset,
38
+ findFirstTable,
39
+ extractTableData,
40
+ } from './structuredData.js';
41
+ export type { DataFenceParseResult, ExtractedTableData } from './structuredData.js';
42
+ export { validateMarkdownSource, validateMarkdownDoc } from './validate.js';
43
+ export type { ValidateOptions, MarkdownValidationResult } from './validate.js';
44
+ export { fallbackBlockLayers } from './templates/fallbackBlock.js';
45
+ export {
46
+ BASE_INPUT_DESCRIPTORS,
47
+ TEMPLATE_INPUT_DESCRIPTORS,
48
+ coerceTemplateParams,
49
+ lintTemplateParams,
50
+ } from './templates/inputDescriptors.js';
51
+ export type {
52
+ InputCoercion,
53
+ TemplateInputDescriptor,
54
+ TemplateParamFinding,
55
+ } from './templates/inputDescriptors.js';