@bendyline/squisq 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  6. package/dist/chunk-3E5F2XMR.js.map +1 -0
  7. package/dist/chunk-4X3JQXNM.js +35 -0
  8. package/dist/chunk-4X3JQXNM.js.map +1 -0
  9. package/dist/{chunk-2OIBZYKP.js → chunk-5DKBFXPY.js} +31 -121
  10. package/dist/chunk-5DKBFXPY.js.map +1 -0
  11. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  12. package/dist/chunk-5J4HKRJF.js.map +1 -0
  13. package/dist/chunk-5UYI3RJB.js +2503 -0
  14. package/dist/chunk-5UYI3RJB.js.map +1 -0
  15. package/dist/{chunk-TEEEILMP.js → chunk-66QO7XFB.js} +19 -292
  16. package/dist/chunk-66QO7XFB.js.map +1 -0
  17. package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
  18. package/dist/chunk-6PJRB2CF.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-FVJUUTEM.js +74 -0
  22. package/dist/chunk-FVJUUTEM.js.map +1 -0
  23. package/dist/{chunk-LH7I6SH7.js → chunk-GF3BVNLU.js} +6360 -5723
  24. package/dist/chunk-GF3BVNLU.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
  30. package/dist/chunk-LYNJEOAC.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
  36. package/dist/chunk-QTSJOY54.js.map +1 -0
  37. package/dist/chunk-QVANWDPH.js +2358 -0
  38. package/dist/chunk-QVANWDPH.js.map +1 -0
  39. package/dist/{chunk-6ATE2PSM.js → chunk-R2GC6RLD.js} +7 -7
  40. package/dist/chunk-R2GC6RLD.js.map +1 -0
  41. package/dist/chunk-R2YZTCLO.js +292 -0
  42. package/dist/chunk-R2YZTCLO.js.map +1 -0
  43. package/dist/chunk-S7A5D7ZJ.js +174 -0
  44. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  45. package/dist/chunk-T3TI7GSZ.js +884 -0
  46. package/dist/chunk-T3TI7GSZ.js.map +1 -0
  47. package/dist/{chunk-RUDYOTA6.js → chunk-TUWMK62B.js} +110 -4
  48. package/dist/chunk-TUWMK62B.js.map +1 -0
  49. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  50. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +134 -34
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +253 -81
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/coverBlock.test.ts +33 -2
  97. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  98. package/src/__tests__/dataTable.test.ts +25 -15
  99. package/src/__tests__/diagramBlock.test.ts +3 -3
  100. package/src/__tests__/diagramText.test.ts +26 -0
  101. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  102. package/src/__tests__/drawingBlock.test.ts +3 -3
  103. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  104. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  105. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  106. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  107. package/src/__tests__/getLayers.test.ts +32 -18
  108. package/src/__tests__/getLayersFallback.test.ts +8 -8
  109. package/src/__tests__/imageEditVersions.test.ts +2 -0
  110. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  111. package/src/__tests__/layoutBlock.test.ts +3 -3
  112. package/src/__tests__/majorApiTypes.test.ts +18 -0
  113. package/src/__tests__/markdown.test.ts +41 -0
  114. package/src/__tests__/markdownToDoc.test.ts +5 -2
  115. package/src/__tests__/matchFontFamily.test.ts +114 -0
  116. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  117. package/src/__tests__/materializeTestUtils.ts +17 -0
  118. package/src/__tests__/narrationAlign.test.ts +150 -0
  119. package/src/__tests__/narrationFeatures.test.ts +71 -0
  120. package/src/__tests__/narrationNuclei.test.ts +45 -0
  121. package/src/__tests__/narrationPacing.test.ts +189 -0
  122. package/src/__tests__/narrationScript.test.ts +149 -0
  123. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  124. package/src/__tests__/narrationSidecar.test.ts +88 -0
  125. package/src/__tests__/narrationSyllables.test.ts +65 -0
  126. package/src/__tests__/narrationTestSignals.ts +167 -0
  127. package/src/__tests__/narrationVad.test.ts +66 -0
  128. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  129. package/src/__tests__/rootExports.test.ts +18 -0
  130. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  131. package/src/__tests__/shapeGeometry.test.ts +49 -0
  132. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  133. package/src/__tests__/templateInputs.test.ts +69 -0
  134. package/src/__tests__/templateMetadata.test.ts +2 -1
  135. package/src/__tests__/templates.test.ts +143 -17
  136. package/src/__tests__/themeCompile.test.ts +49 -22
  137. package/src/__tests__/transformProvenance.test.ts +160 -0
  138. package/src/__tests__/transformV2.test.ts +40 -14
  139. package/src/__tests__/treeviewBattle.test.ts +76 -0
  140. package/src/__tests__/treeviewDetect.test.ts +168 -0
  141. package/src/__tests__/treeviewParse.test.ts +118 -0
  142. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  143. package/src/__tests__/treeviewRender.test.ts +91 -0
  144. package/src/__tests__/versions.test.ts +44 -0
  145. package/src/doc/applyNarrationTiming.ts +244 -0
  146. package/src/doc/asciiDiagram/chars.ts +210 -0
  147. package/src/doc/asciiDiagram/detect.ts +175 -0
  148. package/src/doc/asciiDiagram/index.ts +38 -0
  149. package/src/doc/asciiDiagram/mapping.ts +225 -0
  150. package/src/doc/asciiDiagram/parse.ts +1095 -0
  151. package/src/doc/asciiDiagram/render.ts +963 -0
  152. package/src/doc/asciiDiagram/repair.ts +400 -0
  153. package/src/doc/asciiDiagram/types.ts +65 -0
  154. package/src/doc/asciiTimeline/detect.ts +138 -0
  155. package/src/doc/asciiTimeline/index.ts +26 -0
  156. package/src/doc/asciiTimeline/mapping.ts +43 -0
  157. package/src/doc/asciiTimeline/parse.ts +870 -0
  158. package/src/doc/asciiTimeline/render.ts +427 -0
  159. package/src/doc/asciiTimeline/types.ts +73 -0
  160. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  161. package/src/doc/audioMapping.ts +60 -46
  162. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  163. package/src/doc/docToMarkdown.ts +126 -39
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +135 -1
  166. package/src/doc/materializeBlockLayers.ts +304 -0
  167. package/src/doc/mediaAnnotations.ts +20 -0
  168. package/src/doc/resolveDocTheme.ts +8 -7
  169. package/src/doc/templateInputs.ts +185 -2
  170. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  171. package/src/doc/templates/coverBlock.ts +49 -13
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/drawingBlock.ts +2 -2
  174. package/src/doc/templates/fallbackBlock.ts +1 -1
  175. package/src/doc/templates/index.ts +134 -279
  176. package/src/doc/templates/inputDescriptors.ts +20 -1
  177. package/src/doc/templates/metadata.ts +10 -0
  178. package/src/doc/templates/persistentLayers.ts +12 -126
  179. package/src/doc/templates/registry.ts +88 -0
  180. package/src/doc/templates/templateNames.ts +22 -0
  181. package/src/doc/templates/timelineBlock.ts +613 -0
  182. package/src/doc/templates/treeBlock.ts +131 -0
  183. package/src/doc/treeview/chars.ts +72 -0
  184. package/src/doc/treeview/detect.ts +145 -0
  185. package/src/doc/treeview/index.ts +30 -0
  186. package/src/doc/treeview/mapping.ts +95 -0
  187. package/src/doc/treeview/parse.ts +201 -0
  188. package/src/doc/treeview/render.ts +41 -0
  189. package/src/doc/treeview/types.ts +43 -0
  190. package/src/doc/utils/applyRenderStyle.ts +2 -9
  191. package/src/doc/utils/diagramText.ts +78 -0
  192. package/src/doc/utils/shapeGeometry.ts +157 -7
  193. package/src/doc/validate.ts +1 -1
  194. package/src/generate/templateMapper.ts +7 -0
  195. package/src/imageEdit/export.ts +6 -5
  196. package/src/imageEdit/versionPaths.ts +13 -26
  197. package/src/imageEdit/versions.ts +21 -100
  198. package/src/index.ts +1 -0
  199. package/src/internal/immutable.ts +58 -0
  200. package/src/jsonForm/index.ts +9 -1
  201. package/src/jsonForm/pointer.ts +68 -23
  202. package/src/markdown/attrTokens.ts +7 -5
  203. package/src/markdown/convert.ts +11 -1
  204. package/src/markdown/index.ts +1 -0
  205. package/src/markdown/stringify.ts +4 -9
  206. package/src/markdown/utils.ts +36 -14
  207. package/src/narration/align.ts +514 -0
  208. package/src/narration/features.ts +231 -0
  209. package/src/narration/index.ts +86 -0
  210. package/src/narration/nuclei.ts +118 -0
  211. package/src/narration/pacing.ts +211 -0
  212. package/src/narration/script.ts +239 -0
  213. package/src/narration/session.ts +100 -0
  214. package/src/narration/sidecar.ts +187 -0
  215. package/src/narration/syllables.ts +85 -0
  216. package/src/narration/trace.ts +50 -0
  217. package/src/narration/types.ts +299 -0
  218. package/src/narration/vad.ts +131 -0
  219. package/src/recommend/numberHighlight.ts +25 -0
  220. package/src/recommend/templates.ts +108 -12
  221. package/src/schemas/BlockTemplates.ts +156 -7
  222. package/src/schemas/Doc.ts +72 -9
  223. package/src/schemas/Media.ts +12 -2
  224. package/src/schemas/Theme.ts +45 -25
  225. package/src/schemas/Viewport.ts +2 -2
  226. package/src/schemas/fontStacks.ts +79 -0
  227. package/src/schemas/themeCompile.ts +15 -0
  228. package/src/schemas/themeConstants.ts +2 -0
  229. package/src/schemas/themeLibrary.ts +25 -19
  230. package/src/schemas/themeValidator.ts +1 -1
  231. package/src/spatial/Geohash.ts +0 -3
  232. package/src/storage/ContentContainer.ts +17 -9
  233. package/src/storage/ScopedContentContainer.ts +45 -15
  234. package/src/transform/applyTransform.ts +5 -5
  235. package/src/transform/index.ts +3 -1
  236. package/src/transform/registry.ts +169 -49
  237. package/src/transform/templateSelector.ts +21 -2
  238. package/src/transform/timingAllocator.ts +152 -21
  239. package/src/transform/types.ts +14 -1
  240. package/src/versions/gitignore.ts +34 -0
  241. package/src/versions/index.ts +2 -0
  242. package/src/versions/operations.ts +18 -104
  243. package/src/versions/paths.ts +13 -28
  244. package/src/versions/snapshotStore.ts +202 -0
  245. package/src/versions/types.ts +3 -1
  246. package/dist/chunk-2OIBZYKP.js.map +0 -1
  247. package/dist/chunk-6ATE2PSM.js.map +0 -1
  248. package/dist/chunk-A4O7GIWE.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-HWVFJAAH.js.map +0 -1
  251. package/dist/chunk-IMSCRLLV.js.map +0 -1
  252. package/dist/chunk-JELIRVDP.js +0 -24
  253. package/dist/chunk-JELIRVDP.js.map +0 -1
  254. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  255. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  256. package/dist/chunk-LH7I6SH7.js.map +0 -1
  257. package/dist/chunk-RUDYOTA6.js.map +0 -1
  258. package/dist/chunk-T5UK6YOB.js.map +0 -1
  259. package/dist/chunk-TEEEILMP.js.map +0 -1
  260. package/dist/chunk-UY7KGQ5R.js +0 -192
  261. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  262. package/dist/chunk-VLJJHDAC.js +0 -218
  263. package/dist/chunk-VLJJHDAC.js.map +0 -1
  264. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  265. package/dist/story/index.d.ts +0 -1777
  266. package/dist/story/index.js +0 -273
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Internal storage engine shared by document and image-editor histories.
3
+ * Public APIs keep their domain-specific names and result reasons; this module
4
+ * owns the mechanics that must behave identically across both histories.
5
+ */
6
+
7
+ import type { ContentContainer } from '../storage/ContentContainer.js';
8
+ import { ensureVersionsGitIgnored } from './gitignore.js';
9
+ import { formatVersionTimestamp, parseVersionTimestamp } from './timestamp.js';
10
+ import type { CoalesceOptions, PrunePolicy, Version } from './types.js';
11
+
12
+ const decoder = new TextDecoder();
13
+ const encoder = new TextEncoder();
14
+
15
+ export interface SnapshotPathStrategy {
16
+ readonly prefix: string;
17
+ readonly extension: string;
18
+ }
19
+
20
+ export interface SaveTextSnapshotOptions {
21
+ content: string;
22
+ basename: string;
23
+ mimeType: string;
24
+ now?: Date;
25
+ force?: boolean;
26
+ }
27
+
28
+ export interface SaveTextSnapshotResult {
29
+ saved: boolean;
30
+ version: Version | null;
31
+ reason: 'saved' | 'unchanged';
32
+ }
33
+
34
+ export function buildSnapshotPath(
35
+ strategy: SnapshotPathStrategy,
36
+ basename: string,
37
+ date: Date,
38
+ collision = 0,
39
+ ): string {
40
+ const stamp = formatVersionTimestamp(date);
41
+ const suffix = collision > 0 ? `-${collision + 1}` : '';
42
+ return `${strategy.prefix}${basename}.${stamp}${suffix}.${strategy.extension}`;
43
+ }
44
+
45
+ export function parseSnapshotPath(
46
+ strategy: SnapshotPathStrategy,
47
+ path: string,
48
+ ): { basename: string; timestamp: Date; collision: number } | null {
49
+ if (!path.startsWith(strategy.prefix)) return null;
50
+ const extension = `.${strategy.extension}`;
51
+ const rest = path.slice(strategy.prefix.length);
52
+ if (!rest.endsWith(extension)) return null;
53
+ const stem = rest.slice(0, -extension.length);
54
+ const lastDot = stem.lastIndexOf('.');
55
+ if (lastDot <= 0) return null;
56
+
57
+ const basename = stem.slice(0, lastDot);
58
+ const tail = stem.slice(lastDot + 1);
59
+ const dash = tail.indexOf('-');
60
+ let stamp = tail;
61
+ let collision = 0;
62
+ if (dash >= 0) {
63
+ stamp = tail.slice(0, dash);
64
+ const suffix = Number(tail.slice(dash + 1));
65
+ if (!Number.isInteger(suffix) || suffix < 2) return null;
66
+ collision = suffix - 1;
67
+ }
68
+
69
+ const timestamp = parseVersionTimestamp(stamp);
70
+ return timestamp ? { basename, timestamp, collision } : null;
71
+ }
72
+
73
+ export async function listSnapshots(
74
+ container: ContentContainer,
75
+ strategy: SnapshotPathStrategy,
76
+ basename?: string,
77
+ ): Promise<Version[]> {
78
+ const entries = await container.listFiles(strategy.prefix);
79
+ const versions: Version[] = [];
80
+ for (const entry of entries) {
81
+ const parsed = parseSnapshotPath(strategy, entry.path);
82
+ if (!parsed || (basename !== undefined && parsed.basename !== basename)) continue;
83
+ versions.push({
84
+ path: entry.path,
85
+ basename: parsed.basename,
86
+ timestamp: parsed.timestamp,
87
+ size: entry.size,
88
+ collision: parsed.collision,
89
+ });
90
+ }
91
+
92
+ versions.sort((a, b) => {
93
+ const timestampDelta = b.timestamp.getTime() - a.timestamp.getTime();
94
+ return timestampDelta || b.collision - a.collision;
95
+ });
96
+ return versions;
97
+ }
98
+
99
+ export async function readTextSnapshot(
100
+ container: ContentContainer,
101
+ version: Version | string,
102
+ ): Promise<string | null> {
103
+ const path = typeof version === 'string' ? version : version.path;
104
+ const data = await container.readFile(path);
105
+ return data ? decoder.decode(data) : null;
106
+ }
107
+
108
+ export async function saveTextSnapshot(
109
+ container: ContentContainer,
110
+ strategy: SnapshotPathStrategy,
111
+ options: SaveTextSnapshotOptions,
112
+ ): Promise<SaveTextSnapshotResult> {
113
+ const versions = await listSnapshots(container, strategy, options.basename);
114
+ if (!options.force && versions.length > 0) {
115
+ const existing = await readTextSnapshot(container, versions[0]!);
116
+ if (existing === options.content) {
117
+ // Backfill containers written before history owned its ignore rule.
118
+ await ensureVersionsGitIgnored(container);
119
+ return { saved: false, version: null, reason: 'unchanged' };
120
+ }
121
+ }
122
+
123
+ const now = options.now ?? new Date();
124
+ let collision = 0;
125
+ let path = buildSnapshotPath(strategy, options.basename, now, collision);
126
+ while (await container.exists(path)) {
127
+ collision += 1;
128
+ path = buildSnapshotPath(strategy, options.basename, now, collision);
129
+ }
130
+
131
+ const data = encoder.encode(options.content);
132
+ await ensureVersionsGitIgnored(container);
133
+ await container.writeFile(path, data, options.mimeType);
134
+ return {
135
+ saved: true,
136
+ version: {
137
+ path,
138
+ basename: options.basename,
139
+ timestamp: now,
140
+ size: data.byteLength,
141
+ collision,
142
+ },
143
+ reason: 'saved',
144
+ };
145
+ }
146
+
147
+ export async function pruneSnapshots(
148
+ container: ContentContainer,
149
+ strategy: SnapshotPathStrategy,
150
+ policy: PrunePolicy,
151
+ basename?: string,
152
+ ): Promise<Version[]> {
153
+ const versions = await listSnapshots(container, strategy, basename);
154
+ let toDelete: Version[];
155
+
156
+ if (policy.type === 'keep-last-n') {
157
+ toDelete = versions.slice(Math.max(0, Math.floor(policy.n)));
158
+ } else if (policy.type === 'older-than') {
159
+ const cutoff = policy.date.getTime();
160
+ toDelete = versions.filter((version) => version.timestamp.getTime() < cutoff);
161
+ } else {
162
+ toDelete = versions.filter((version) => !policy.keep(version, versions));
163
+ }
164
+
165
+ await removeSnapshots(container, toDelete);
166
+ return toDelete;
167
+ }
168
+
169
+ export async function coalesceSnapshots(
170
+ container: ContentContainer,
171
+ strategy: SnapshotPathStrategy,
172
+ options: CoalesceOptions = {},
173
+ basename?: string,
174
+ ): Promise<Version[]> {
175
+ const windowMs = options.windowMs ?? 60_000;
176
+ const versions = await listSnapshots(container, strategy, basename);
177
+ const toDelete: Version[] = [];
178
+
179
+ // Compare against the last kept snapshot. Pairwise comparison against a
180
+ // deleted neighbor would chain through a dense run and over-prune it.
181
+ if (versions.length > 0) {
182
+ let anchor = versions[0]!;
183
+ for (let index = 1; index < versions.length; index++) {
184
+ const candidate = versions[index]!;
185
+ if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {
186
+ toDelete.push(candidate);
187
+ } else {
188
+ anchor = candidate;
189
+ }
190
+ }
191
+ }
192
+
193
+ await removeSnapshots(container, toDelete);
194
+ return toDelete;
195
+ }
196
+
197
+ async function removeSnapshots(
198
+ container: ContentContainer,
199
+ versions: readonly Version[],
200
+ ): Promise<void> {
201
+ for (const version of versions) await container.removeFile(version.path);
202
+ }
@@ -5,7 +5,9 @@
5
5
  * the document inside the same {@link ContentContainer}. They live under
6
6
  * `.versions/<docBasename>.<sortableTimestamp>.md`. The container is the
7
7
  * unit of persistence — when the host serializes the container to a ZIP,
8
- * a real folder, or anywhere else, the version snapshots ride along.
8
+ * a real folder, or anywhere else, the version snapshots ride along. When
9
+ * snapshots are retained, a container-root `.gitignore` rule excludes the
10
+ * `.versions/` folder from Git.
9
11
  */
10
12
 
11
13
  /**
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/imageEdit/state.ts","../src/imageEdit/persistence.ts","../src/imageEdit/versionPaths.ts","../src/imageEdit/versions.ts","../src/imageEdit/ImageEditVersionManager.ts","../src/imageEdit/export.ts"],"sourcesContent":["/**\n * Pure helpers for constructing and mutating {@link ImageEditDoc} values.\n *\n * All functions are side-effect free and return new doc objects rather\n * than mutating their inputs, so they're safe to use from a reducer.\n */\n\nimport type { ImageEditDoc, ImageEditLayer } from '../schemas/ImageEditDoc.js';\n\n/** Filename used for the persisted editor state inside the sidecar. */\nexport const IMAGE_EDIT_STATE_FILENAME = 'state.json';\n\n/** Subfolder (inside the sidecar) for embedded asset bytes. */\nexport const IMAGE_EDIT_ASSETS_PREFIX = 'assets/';\n\n/** Generate a short random layer id. */\nfunction nextLayerId(): string {\n return `layer-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create an empty doc with the given canvas size and no layers.\n *\n * `meta.createdAt` and `meta.updatedAt` are set to the current time so\n * the doc has a complete metadata footprint from the start.\n */\nexport function createEmptyImageEditDoc(\n width: number,\n height: number,\n options: { background?: string; sourcePath?: string; now?: Date } = {},\n): ImageEditDoc {\n const now = (options.now ?? new Date()).toISOString();\n return {\n version: 1,\n canvas: {\n width,\n height,\n background: options.background ?? 'transparent',\n },\n layers: [],\n meta: {\n sourcePath: options.sourcePath,\n createdAt: now,\n updatedAt: now,\n },\n };\n}\n\n/** Append a layer (top of stack). Assigns an id when missing. */\nexport function addLayer(doc: ImageEditDoc, layer: ImageEditLayer): ImageEditDoc {\n const withId: ImageEditLayer = layer.id ? layer : { ...layer, id: nextLayerId() };\n return touch({ ...doc, layers: [...doc.layers, withId] });\n}\n\n/** Remove the layer with the given id. No-op if not found. */\nexport function removeLayer(doc: ImageEditDoc, layerId: string): ImageEditDoc {\n const next = doc.layers.filter((l) => l.id !== layerId);\n if (next.length === doc.layers.length) return doc;\n return touch({ ...doc, layers: next });\n}\n\n/**\n * Reorder a layer to a new index in the stack. The index is clamped to\n * the valid range; out-of-range / missing layers are no-ops.\n */\nexport function reorderLayer(doc: ImageEditDoc, layerId: string, toIndex: number): ImageEditDoc {\n const fromIndex = doc.layers.findIndex((l) => l.id === layerId);\n if (fromIndex < 0) return doc;\n const clamped = Math.max(0, Math.min(doc.layers.length - 1, Math.floor(toIndex)));\n if (clamped === fromIndex) return doc;\n const next = doc.layers.slice();\n const [layer] = next.splice(fromIndex, 1);\n next.splice(clamped, 0, layer!);\n return touch({ ...doc, layers: next });\n}\n\n/**\n * Replace the layer with the given id by applying a partial update.\n * The patch is shallow-merged at the top level; callers updating\n * `content`, `position`, etc. must spread those sub-objects themselves.\n */\nexport function updateLayer(\n doc: ImageEditDoc,\n layerId: string,\n patch: Partial<ImageEditLayer>,\n): ImageEditDoc {\n let changed = false;\n const next = doc.layers.map((l) => {\n if (l.id !== layerId) return l;\n changed = true;\n return { ...l, ...patch } as ImageEditLayer;\n });\n if (!changed) return doc;\n return touch({ ...doc, layers: next });\n}\n\n/** Replace the canvas configuration. */\nexport function setCanvas(doc: ImageEditDoc, canvas: ImageEditDoc['canvas']): ImageEditDoc {\n return touch({ ...doc, canvas });\n}\n\n/** Bump `meta.updatedAt`. Pure: returns a new doc. */\nexport function touch(doc: ImageEditDoc, now: Date = new Date()): ImageEditDoc {\n return {\n ...doc,\n meta: {\n ...doc.meta,\n createdAt: doc.meta?.createdAt ?? now.toISOString(),\n updatedAt: now.toISOString(),\n },\n };\n}\n","/**\n * Load and save {@link ImageEditDoc} JSON to a sidecar\n * {@link ContentContainer} (typically a {@link ScopedContentContainer}\n * rooted at `<basename>_files/`).\n */\n\nimport type { ContentContainer } from '../storage/ContentContainer.js';\nimport type { ImageEditDoc } from '../schemas/ImageEditDoc.js';\nimport { IMAGE_EDIT_STATE_FILENAME } from './state.js';\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\n/**\n * Read `state.json` from the sidecar and parse it. Returns `null` when\n * the file does not exist; throws on parse / shape errors.\n */\nexport async function readImageEditDoc(\n container: ContentContainer,\n filename: string = IMAGE_EDIT_STATE_FILENAME,\n): Promise<ImageEditDoc | null> {\n const data = await container.readFile(filename);\n if (!data) return null;\n const text = decoder.decode(data);\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`readImageEditDoc: ${filename} is not valid JSON: ${msg}`);\n }\n assertImageEditDoc(parsed, filename);\n return parsed;\n}\n\n/** Serialize `doc` to JSON and write it to the sidecar. */\nexport async function writeImageEditDoc(\n container: ContentContainer,\n doc: ImageEditDoc,\n filename: string = IMAGE_EDIT_STATE_FILENAME,\n): Promise<void> {\n const text = JSON.stringify(doc, null, 2);\n await container.writeFile(filename, encoder.encode(text), 'application/json');\n}\n\nfunction assertImageEditDoc(value: unknown, filename: string): asserts value is ImageEditDoc {\n if (!value || typeof value !== 'object') {\n throw new Error(`readImageEditDoc: ${filename} root must be an object`);\n }\n const v = value as Partial<ImageEditDoc>;\n if (v.version !== 1) {\n throw new Error(\n `readImageEditDoc: ${filename} has unsupported schema version ${String(v.version)} (expected 1)`,\n );\n }\n if (!v.canvas || typeof v.canvas.width !== 'number' || typeof v.canvas.height !== 'number') {\n throw new Error(`readImageEditDoc: ${filename} canvas.width/height must be numbers`);\n }\n if (!Array.isArray(v.layers)) {\n throw new Error(`readImageEditDoc: ${filename} layers must be an array`);\n }\n v.layers.forEach((layer, i) => assertLayer(layer, i, filename));\n}\n\nconst VALID_LAYER_TYPES = new Set(['image', 'text', 'shape', 'path']);\n\nfunction assertLayer(value: unknown, index: number, filename: string): void {\n const ctx = `${filename} layers[${index}]`;\n if (!value || typeof value !== 'object') {\n throw new Error(`readImageEditDoc: ${ctx} must be an object`);\n }\n const layer = value as Record<string, unknown>;\n\n if (typeof layer['id'] !== 'string') {\n throw new Error(`readImageEditDoc: ${ctx} must have a string id`);\n }\n if (typeof layer['type'] !== 'string' || !VALID_LAYER_TYPES.has(layer['type'])) {\n throw new Error(\n `readImageEditDoc: ${ctx} type must be one of image|text|shape, got ${String(layer['type'])}`,\n );\n }\n\n const pos = layer['position'];\n if (\n !pos ||\n typeof pos !== 'object' ||\n (typeof (pos as Record<string, unknown>)['x'] !== 'number' &&\n typeof (pos as Record<string, unknown>)['x'] !== 'string') ||\n (typeof (pos as Record<string, unknown>)['y'] !== 'number' &&\n typeof (pos as Record<string, unknown>)['y'] !== 'string')\n ) {\n throw new Error(`readImageEditDoc: ${ctx} position must have numeric or string x and y`);\n }\n\n const content = layer['content'];\n if (!content || typeof content !== 'object') {\n throw new Error(`readImageEditDoc: ${ctx} content must be an object`);\n }\n const c = content as Record<string, unknown>;\n\n if (layer['type'] === 'image' && typeof c['src'] !== 'string') {\n throw new Error(`readImageEditDoc: ${ctx} (image) content.src must be a string`);\n }\n if (layer['type'] === 'text' && typeof c['text'] !== 'string') {\n throw new Error(`readImageEditDoc: ${ctx} (text) content.text must be a string`);\n }\n if (layer['type'] === 'shape' && typeof c['shape'] !== 'string') {\n throw new Error(`readImageEditDoc: ${ctx} (shape) content.shape must be a string`);\n }\n if (layer['type'] === 'path' && typeof c['d'] !== 'string') {\n throw new Error(`readImageEditDoc: ${ctx} (path) content.d must be a string`);\n }\n}\n","/**\n * Path conventions for image-editor version snapshots.\n *\n * Snapshots live inside the sidecar at\n * `.versions/<basename>.<timestamp>[<-suffix>].json`. The collision\n * suffix (`-2`, `-3`, ...) is appended only when two snapshots land on\n * the same UTC second.\n *\n * Mirrors `core/src/versions/paths.ts` but keyed on `.json` instead of\n * `.md` so the two histories live side-by-side without colliding.\n */\n\nimport { formatVersionTimestamp, parseVersionTimestamp } from '../versions/timestamp.js';\n\n/** Subfolder (inside the sidecar) that holds image-editor snapshots. */\nexport const IMAGE_EDIT_VERSIONS_PREFIX = '.versions/';\n\n/** Default basename when none is supplied. Matches `IMAGE_EDIT_STATE_FILENAME` stem. */\nexport const IMAGE_EDIT_DEFAULT_BASENAME = 'state';\n\n/** Build a snapshot path. Optionally append a collision suffix. */\nexport function buildImageEditVersionPath(basename: string, date: Date, collision = 0): string {\n const stamp = formatVersionTimestamp(date);\n const suffix = collision > 0 ? `-${collision + 1}` : '';\n return `${IMAGE_EDIT_VERSIONS_PREFIX}${basename}.${stamp}${suffix}.json`;\n}\n\n/**\n * Parse a snapshot path. Returns `null` when the path doesn't match the\n * convention (so unrelated `.versions/*.json` files are ignored).\n */\nexport function parseImageEditVersionPath(\n path: string,\n): { basename: string; timestamp: Date; collision: number } | null {\n if (!path.startsWith(IMAGE_EDIT_VERSIONS_PREFIX)) return null;\n const rest = path.slice(IMAGE_EDIT_VERSIONS_PREFIX.length);\n if (!rest.endsWith('.json')) return null;\n const stem = rest.slice(0, -'.json'.length);\n const lastDot = stem.lastIndexOf('.');\n if (lastDot <= 0) return null;\n const basename = stem.slice(0, lastDot);\n const tail = stem.slice(lastDot + 1);\n const dash = tail.indexOf('-');\n let stamp: string;\n let collision = 0;\n if (dash >= 0) {\n stamp = tail.slice(0, dash);\n const n = Number(tail.slice(dash + 1));\n if (!Number.isInteger(n) || n < 2) return null;\n collision = n - 1;\n } else {\n stamp = tail;\n }\n const timestamp = parseVersionTimestamp(stamp);\n if (!timestamp) return null;\n return { basename, timestamp, collision };\n}\n","/**\n * Image-editor version operations against a sidecar\n * {@link ContentContainer}.\n *\n * Mirrors `core/src/versions/operations.ts` but operates on\n * {@link ImageEditDoc} JSON state at `.versions/<basename>.<timestamp>.json`\n * instead of the markdown document. Reuses {@link PrunePolicy} so hosts\n * can share their pruning configuration between the two histories.\n */\n\nimport type { ContentContainer } from '../storage/ContentContainer.js';\nimport type { ImageEditDoc } from '../schemas/ImageEditDoc.js';\nimport type { CoalesceOptions, PrunePolicy, Version } from '../versions/types.js';\nimport { readImageEditDoc, writeImageEditDoc } from './persistence.js';\nimport { IMAGE_EDIT_STATE_FILENAME } from './state.js';\nimport {\n IMAGE_EDIT_VERSIONS_PREFIX,\n IMAGE_EDIT_DEFAULT_BASENAME,\n buildImageEditVersionPath,\n parseImageEditVersionPath,\n} from './versionPaths.js';\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\n/** Options for {@link saveImageEditVersion}. */\nexport interface SaveImageEditVersionOptions {\n /** Override the doc to snapshot; otherwise `state.json` is read from the sidecar. */\n doc?: ImageEditDoc;\n /** Override the snapshot timestamp. Defaults to `new Date()`. */\n now?: Date;\n /** Skip the diff-vs-latest check and force a write. */\n force?: boolean;\n /** Override the basename used in version filenames. Defaults to `'state'`. */\n basename?: string;\n /** Override the source filename inside the sidecar. Defaults to `state.json`. */\n stateFilename?: string;\n}\n\n/** Result of a {@link saveImageEditVersion} call. */\nexport interface SaveImageEditVersionResult {\n saved: boolean;\n version: Version | null;\n /** `'saved' | 'unchanged' | 'no-state'` — `'no-state'` means there was nothing to snapshot. */\n reason: 'saved' | 'unchanged' | 'no-state';\n}\n\n/** Options for {@link revertToImageEditVersion}. */\nexport interface RevertImageEditOptions {\n /**\n * Whether to snapshot the *current* state before replacing it. Defaults to `true`.\n */\n snapshotCurrent?: boolean;\n /** Override the source filename. Defaults to `state.json`. */\n stateFilename?: string;\n}\n\n/** List image-edit snapshots in the sidecar, newest-first. */\nexport async function listImageEditVersions(\n container: ContentContainer,\n basename?: string,\n): Promise<Version[]> {\n const entries = await container.listFiles(IMAGE_EDIT_VERSIONS_PREFIX);\n const versions: Version[] = [];\n for (const entry of entries) {\n const parsed = parseImageEditVersionPath(entry.path);\n if (!parsed) continue;\n if (basename !== undefined && parsed.basename !== basename) continue;\n versions.push({\n path: entry.path,\n basename: parsed.basename,\n timestamp: parsed.timestamp,\n size: entry.size,\n collision: parsed.collision,\n });\n }\n versions.sort((a, b) => {\n const dt = b.timestamp.getTime() - a.timestamp.getTime();\n if (dt !== 0) return dt;\n return b.collision - a.collision;\n });\n return versions;\n}\n\n/** Read a snapshot's JSON text. Returns `null` if the snapshot is missing. */\nexport async function readImageEditVersionText(\n container: ContentContainer,\n version: Version | string,\n): Promise<string | null> {\n const path = typeof version === 'string' ? version : version.path;\n const data = await container.readFile(path);\n if (!data) return null;\n return decoder.decode(data);\n}\n\n/** Read and parse a snapshot. Returns `null` if missing. */\nexport async function readImageEditVersion(\n container: ContentContainer,\n version: Version | string,\n): Promise<ImageEditDoc | null> {\n const text = await readImageEditVersionText(container, version);\n if (text === null) return null;\n return JSON.parse(text) as ImageEditDoc;\n}\n\n/**\n * Snapshot the current image-editor state if it differs from the latest\n * existing snapshot.\n *\n * Diff is computed on the *serialized* JSON (with stable 2-space\n * indentation) so semantically-equal states with different key order\n * still snapshot — accept the over-snapshot rather than running a deep\n * structural compare.\n */\nexport async function saveImageEditVersion(\n container: ContentContainer,\n options: SaveImageEditVersionOptions = {},\n): Promise<SaveImageEditVersionResult> {\n const stateFilename = options.stateFilename ?? IMAGE_EDIT_STATE_FILENAME;\n const doc = options.doc ?? (await readImageEditDoc(container, stateFilename));\n if (!doc) {\n return { saved: false, version: null, reason: 'no-state' };\n }\n const basename = options.basename ?? IMAGE_EDIT_DEFAULT_BASENAME;\n const serialized = JSON.stringify(doc, null, 2);\n\n const versions = await listImageEditVersions(container, basename);\n if (!options.force && versions.length > 0) {\n const latest = versions[0]!;\n const existing = await readImageEditVersionText(container, latest);\n if (existing === serialized) {\n return { saved: false, version: null, reason: 'unchanged' };\n }\n }\n\n const now = options.now ?? new Date();\n let collision = 0;\n let path = buildImageEditVersionPath(basename, now, collision);\n while (await container.exists(path)) {\n collision += 1;\n path = buildImageEditVersionPath(basename, now, collision);\n }\n\n const data = encoder.encode(serialized);\n await container.writeFile(path, data, 'application/json');\n\n const version: Version = {\n path,\n basename,\n timestamp: now,\n size: data.byteLength,\n collision,\n };\n return { saved: true, version, reason: 'saved' };\n}\n\n/** Revert `state.json` to a prior snapshot. Snapshots the current state first by default. */\nexport async function revertToImageEditVersion(\n container: ContentContainer,\n version: Version | string,\n options: RevertImageEditOptions = {},\n): Promise<{ reverted: boolean; snapshotted: Version | null }> {\n const doc = await readImageEditVersion(container, version);\n if (!doc) {\n return { reverted: false, snapshotted: null };\n }\n\n let snapshotted: Version | null = null;\n if (options.snapshotCurrent !== false) {\n const result = await saveImageEditVersion(container, { stateFilename: options.stateFilename });\n snapshotted = result.version;\n }\n\n await writeImageEditDoc(container, doc, options.stateFilename);\n return { reverted: true, snapshotted };\n}\n\n/** Delete snapshots that don't satisfy the policy. */\nexport async function pruneImageEditVersions(\n container: ContentContainer,\n policy: PrunePolicy,\n basename?: string,\n): Promise<Version[]> {\n const versions = await listImageEditVersions(container, basename);\n const toDelete: Version[] = [];\n\n if (policy.type === 'keep-last-n') {\n const n = Math.max(0, Math.floor(policy.n));\n toDelete.push(...versions.slice(n));\n } else if (policy.type === 'older-than') {\n const cutoff = policy.date.getTime();\n for (const v of versions) {\n if (v.timestamp.getTime() < cutoff) toDelete.push(v);\n }\n } else {\n for (const v of versions) {\n if (!policy.keep(v, versions)) toDelete.push(v);\n }\n }\n\n for (const v of toDelete) {\n await container.removeFile(v.path);\n }\n return toDelete;\n}\n\n/** Collapse adjacent snapshots within `windowMs`, keeping the newer of each pair. */\nexport async function coalesceImageEditVersions(\n container: ContentContainer,\n options: CoalesceOptions = {},\n basename?: string,\n): Promise<Version[]> {\n const windowMs = options.windowMs ?? 60_000;\n const versions = await listImageEditVersions(container, basename);\n const toDelete: Version[] = [];\n if (versions.length > 0) {\n let anchor = versions[0]!;\n for (let i = 1; i < versions.length; i++) {\n const candidate = versions[i]!;\n if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {\n toDelete.push(candidate);\n } else {\n anchor = candidate;\n }\n }\n }\n for (const v of toDelete) {\n await container.removeFile(v.path);\n }\n return toDelete;\n}\n","/**\n * Convenience wrapper around the image-edit version operations that\n * captures a {@link ContentContainer} reference and an optional basename\n * override. Mirrors `DocumentVersionManager` in shape.\n */\n\nimport type { ContentContainer } from '../storage/ContentContainer.js';\nimport type { CoalesceOptions, PrunePolicy, Version } from '../versions/types.js';\nimport {\n coalesceImageEditVersions,\n listImageEditVersions,\n pruneImageEditVersions,\n readImageEditVersion,\n revertToImageEditVersion,\n saveImageEditVersion,\n type RevertImageEditOptions,\n type SaveImageEditVersionOptions,\n type SaveImageEditVersionResult,\n} from './versions.js';\nimport type { ImageEditDoc } from '../schemas/ImageEditDoc.js';\n\nexport interface ImageEditVersionManagerOptions {\n /** Override the basename used in version filenames. Defaults to `'state'`. */\n basename?: string;\n /** Override the source filename inside the sidecar. Defaults to `state.json`. */\n stateFilename?: string;\n}\n\nexport class ImageEditVersionManager {\n private readonly container: ContentContainer;\n private readonly basename: string | undefined;\n private readonly stateFilename: string | undefined;\n\n constructor(container: ContentContainer, options: ImageEditVersionManagerOptions = {}) {\n this.container = container;\n this.basename = options.basename;\n this.stateFilename = options.stateFilename;\n }\n\n saveVersion(options: SaveImageEditVersionOptions = {}): Promise<SaveImageEditVersionResult> {\n return saveImageEditVersion(this.container, {\n basename: this.basename,\n stateFilename: this.stateFilename,\n ...options,\n });\n }\n\n listVersions(): Promise<Version[]> {\n return listImageEditVersions(this.container, this.basename);\n }\n\n readVersion(version: Version | string): Promise<ImageEditDoc | null> {\n return readImageEditVersion(this.container, version);\n }\n\n revertToVersion(\n version: Version | string,\n options: RevertImageEditOptions = {},\n ): Promise<{ reverted: boolean; snapshotted: Version | null }> {\n return revertToImageEditVersion(this.container, version, {\n stateFilename: this.stateFilename,\n ...options,\n });\n }\n\n pruneVersions(policy: PrunePolicy): Promise<Version[]> {\n return pruneImageEditVersions(this.container, policy, this.basename);\n }\n\n coalesceVersions(options: CoalesceOptions = {}): Promise<Version[]> {\n return coalesceImageEditVersions(this.container, options, this.basename);\n }\n}\n","/**\n * Rasterize an {@link ImageEditDoc} to a PNG / JPEG / WebP {@link Blob}.\n *\n * The pipeline:\n * 1. Build an SVG string mirroring the canvas + layers, with image\n * hrefs inlined as `data:` URLs (so the canvas isn't tainted).\n * 2. Wrap the SVG in a `Blob` and load it through an `Image`.\n * 3. Draw it onto a canvas (`OffscreenCanvas` when available,\n * otherwise a detached `HTMLCanvasElement`).\n * 4. Convert the canvas to the requested format.\n *\n * Browser-only — calls `Image`, `URL.createObjectURL`, and canvas APIs.\n * Returns a Promise that rejects on SVG parse / load failures.\n */\n\nimport type { ContentContainer } from '../storage/ContentContainer.js';\nimport type { ImageEditCanvas, ImageEditDoc, ImageEditLayer } from '../schemas/ImageEditDoc.js';\nimport type { MarkerStyle, Position } from '../schemas/Doc.js';\nimport { markerPath, shapePath } from '../doc/utils/shapeGeometry.js';\n\n/** Supported export formats. */\nexport type ImageEditExportFormat = 'png' | 'jpeg' | 'webp';\n\n/** Options for {@link exportImageEditDoc}. */\nexport interface ImageEditExportOptions {\n /** Output format. Defaults to `'png'`. */\n format?: ImageEditExportFormat;\n /** JPEG/WebP quality 0..1. Defaults to `0.92`. Ignored for PNG. */\n quality?: number;\n /** Resolution scale factor. Defaults to `1` (canvas pixels = output pixels). */\n scale?: number;\n}\n\nconst FORMAT_MIME: Record<ImageEditExportFormat, string> = {\n png: 'image/png',\n jpeg: 'image/jpeg',\n webp: 'image/webp',\n};\n\n/**\n * Export the doc to a raster {@link Blob}. Asset bytes referenced by image\n * layers are read from `container` and inlined as data URLs.\n */\nexport async function exportImageEditDoc(\n doc: ImageEditDoc,\n container: ContentContainer,\n options: ImageEditExportOptions = {},\n): Promise<Blob> {\n const format: ImageEditExportFormat = options.format ?? 'png';\n const quality = options.quality ?? 0.92;\n const scale = options.scale ?? 1;\n const mime = FORMAT_MIME[format];\n\n const svg = await buildSvgString(doc, container);\n const outW = Math.max(1, Math.round(doc.canvas.width * scale));\n const outH = Math.max(1, Math.round(doc.canvas.height * scale));\n\n const img = await loadSvgImage(svg);\n const canvas = createRasterCanvas(outW, outH);\n const ctx = canvas.getContext('2d');\n if (!ctx) throw new Error('exportImageEditDoc: 2D canvas context unavailable');\n\n // For JPEG (no alpha), pre-fill with the canvas background or white so\n // transparent regions aren't rendered as black.\n if (format === 'jpeg') {\n const bg =\n doc.canvas.background && doc.canvas.background !== 'transparent'\n ? doc.canvas.background\n : '#ffffff';\n ctx.fillStyle = bg;\n ctx.fillRect(0, 0, outW, outH);\n }\n\n ctx.drawImage(img as CanvasImageSource, 0, 0, outW, outH);\n return canvasToBlob(canvas, mime, quality);\n}\n\n// ============================================\n// SVG construction\n// ============================================\n\n/**\n * Build the standalone SVG string that mirrors the canvas + layers. Exposed\n * for tests (the raster step needs a browser); shape/text/path layers don't\n * touch `container`, only image layers do (to inline asset bytes).\n */\nexport async function buildSvgString(\n doc: ImageEditDoc,\n container: ContentContainer,\n): Promise<string> {\n const { width, height, background } = doc.canvas;\n const bgRect =\n background && background !== 'transparent'\n ? `<rect width=\"${width}\" height=\"${height}\" fill=\"${escapeAttr(background)}\"/>`\n : '';\n\n const layerXml: string[] = [];\n for (const layer of doc.layers) {\n if (layer.visible === false) continue;\n layerXml.push(await renderLayerToSvg(layer, doc.canvas, container));\n }\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>` +\n `<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"` +\n ` width=\"${width}\" height=\"${height}\" viewBox=\"0 0 ${width} ${height}\">` +\n bgRect +\n layerXml.join('') +\n `</svg>`\n );\n}\n\nasync function renderLayerToSvg(\n layer: ImageEditLayer,\n canvas: ImageEditCanvas,\n container: ContentContainer,\n): Promise<string> {\n const opacity = layer.opacity ?? 1;\n const opacityAttr = opacity < 1 ? ` opacity=\"${opacity}\"` : '';\n const inner = await renderLayerInner(layer, canvas, container);\n return `<g data-layer-id=\"${escapeAttr(layer.id)}\"${opacityAttr}>${inner}</g>`;\n}\n\nasync function renderLayerInner(\n layer: ImageEditLayer,\n canvas: ImageEditCanvas,\n container: ContentContainer,\n): Promise<string> {\n const { x, y, w, h } = resolveBox(layer.position, canvas);\n\n if (layer.type === 'image') {\n const dataUrl = await loadAssetAsDataUrl(container, layer.content.src);\n return (\n `<image x=\"${x}\" y=\"${y}\" width=\"${w}\" height=\"${h}\"` +\n ` preserveAspectRatio=\"${preserveAspectRatioFor(layer.content.fit)}\"` +\n ` xlink:href=\"${escapeAttr(dataUrl)}\"/>`\n );\n }\n\n if (layer.type === 'shape') {\n const c = layer.content;\n const fill = c.fill ?? 'none';\n const stroke = c.stroke ? ` stroke=\"${escapeAttr(c.stroke)}\"` : '';\n const strokeWidth = c.strokeWidth ? ` stroke-width=\"${c.strokeWidth}\"` : '';\n if (c.shape === 'rect') {\n const rx = c.borderRadius ? ` rx=\"${c.borderRadius}\" ry=\"${c.borderRadius}\"` : '';\n return `<rect x=\"${x}\" y=\"${y}\" width=\"${w}\" height=\"${h}\" fill=\"${escapeAttr(fill)}\"${stroke}${strokeWidth}${rx}/>`;\n }\n if (c.shape === 'circle') {\n const r = Math.min(w, h) / 2;\n return `<circle cx=\"${x + w / 2}\" cy=\"${y + h / 2}\" r=\"${r}\" fill=\"${escapeAttr(fill)}\"${stroke}${strokeWidth}/>`;\n }\n // line\n return `<line x1=\"${x}\" y1=\"${y}\" x2=\"${x + w}\" y2=\"${y + h}\"${stroke}${strokeWidth}/>`;\n }\n\n if (layer.type === 'path') {\n return renderPathToSvg(layer, x, y, w, h);\n }\n\n // text\n const c = layer.content;\n const style = c.style;\n const lineHeight = style.lineHeight ?? 1.4;\n const lineHeightPx = style.fontSize * lineHeight;\n const lines = (c.text ?? '').split('\\n');\n const tspans = lines\n .map(\n (line, i) => `<tspan x=\"${x}\" dy=\"${i === 0 ? 0 : lineHeightPx}\">${escapeText(line)}</tspan>`,\n )\n .join('');\n const fontFamily = style.fontFamily ?? 'sans-serif';\n const fontWeight = style.fontWeight ?? 'normal';\n const textAnchor =\n style.textAlign === 'center' ? 'middle' : style.textAlign === 'right' ? 'end' : 'start';\n return (\n `<text x=\"${x}\" y=\"${y + style.fontSize}\" font-family=\"${escapeAttr(fontFamily)}\"` +\n ` font-size=\"${style.fontSize}\" font-weight=\"${fontWeight}\"` +\n ` fill=\"${escapeAttr(style.color)}\" text-anchor=\"${textAnchor}\">` +\n tspans +\n `</text>`\n );\n}\n\n/**\n * Serialize a `path` layer to SVG. Mirrors the runtime\n * `react/src/layers/PathLayer.tsx`: when `shapeKind` is set, `d` is\n * re-derived from the position box via `shapePath` (so named shapes scale);\n * otherwise the stored absolute `d` is used. Start/end markers are emitted\n * as `<marker>` defs scoped to the layer id, matching `markerPath` geometry.\n */\nfunction renderPathToSvg(\n layer: ImageEditLayer & { type: 'path' },\n x: number,\n y: number,\n w: number,\n h: number,\n): string {\n const c = layer.content;\n const d = c.shapeKind ? (shapePath(c.shapeKind, x, y, w, h) ?? c.d) : c.d;\n const stroke = c.stroke ?? '#1e293b';\n const strokeWidth = c.strokeWidth ?? 2;\n const fill = c.fill ?? 'none';\n const dash = c.dasharray ? ` stroke-dasharray=\"${escapeAttr(c.dasharray)}\"` : '';\n\n // Markers: explicit start/end win; otherwise derive from the legacy `arrow` flag.\n const startStyle = effectiveExportMarker(c.startMarker, c.arrow, 'start');\n const endStyle = effectiveExportMarker(c.endMarker, c.arrow, 'end');\n const start = markerPath(startStyle, 'start');\n const end = markerPath(endStyle, 'end');\n const startId = `marker-start-${layer.id}`;\n const endId = `marker-end-${layer.id}`;\n const defs =\n start || end\n ? `<defs>${start ? markerDefSvg(startId, 'start', start, stroke) : ''}${\n end ? markerDefSvg(endId, 'end', end, stroke) : ''\n }</defs>`\n : '';\n const markerStart = start ? ` marker-start=\"url(#${startId})\"` : '';\n const markerEnd = end ? ` marker-end=\"url(#${endId})\"` : '';\n\n return (\n defs +\n `<path d=\"${escapeAttr(d)}\" fill=\"${escapeAttr(fill)}\"` +\n ` stroke=\"${escapeAttr(stroke)}\" stroke-width=\"${strokeWidth}\"${dash}${markerStart}${markerEnd}/>`\n );\n}\n\nfunction effectiveExportMarker(\n explicit: MarkerStyle | undefined,\n arrow: (ImageEditLayer & { type: 'path' })['content']['arrow'],\n end: 'start' | 'end',\n): MarkerStyle {\n if (explicit) return explicit;\n return arrow === 'both' || arrow === end ? 'arrow' : 'none';\n}\n\nfunction markerDefSvg(\n id: string,\n dir: 'start' | 'end',\n marker: { d: string; filled: boolean },\n stroke: string,\n): string {\n const refX = dir === 'end' ? 9 : 1;\n const paint = marker.filled\n ? `fill=\"${escapeAttr(stroke)}\"`\n : `fill=\"none\" stroke=\"${escapeAttr(stroke)}\" stroke-width=\"1.5\"`;\n return (\n `<marker id=\"${escapeAttr(id)}\" viewBox=\"0 0 10 10\" refX=\"${refX}\" refY=\"5\"` +\n ` markerWidth=\"4\" markerHeight=\"4\" orient=\"auto-start-reverse\" markerUnits=\"strokeWidth\">` +\n `<path d=\"${escapeAttr(marker.d)}\" ${paint}/></marker>`\n );\n}\n\nfunction preserveAspectRatioFor(fit?: 'cover' | 'contain' | 'fill'): string {\n if (fit === 'cover') return 'xMidYMid slice';\n if (fit === 'fill') return 'none';\n return 'xMidYMid meet'; // contain (default)\n}\n\nfunction resolveBox(\n position: Position,\n canvas: ImageEditCanvas,\n): { x: number; y: number; w: number; h: number } {\n const x = resolveValue(position.x, canvas.width);\n const y = resolveValue(position.y, canvas.height);\n const w =\n position.width !== undefined ? resolveValue(position.width, canvas.width) : canvas.width;\n const h =\n position.height !== undefined ? resolveValue(position.height, canvas.height) : canvas.height;\n const offset = anchorOffset(position.anchor, w, h);\n return { x: x + offset.x, y: y + offset.y, w, h };\n}\n\nfunction resolveValue(v: number | string, base: number): number {\n if (typeof v === 'number') return v;\n if (v.endsWith('%')) return (parseFloat(v) / 100) * base;\n return parseFloat(v) || 0;\n}\n\nfunction anchorOffset(\n anchor: Position['anchor'] | undefined,\n w: number,\n h: number,\n): { x: number; y: number } {\n switch (anchor) {\n case 'center':\n return { x: -w / 2, y: -h / 2 };\n case 'top-right':\n return { x: -w, y: 0 };\n case 'bottom-left':\n return { x: 0, y: -h };\n case 'bottom-right':\n return { x: -w, y: -h };\n case 'top-left':\n default:\n return { x: 0, y: 0 };\n }\n}\n\n// ============================================\n// Asset inlining\n// ============================================\n\nasync function loadAssetAsDataUrl(container: ContentContainer, src: string): Promise<string> {\n // Already a data: or absolute URL — pass through.\n if (/^(data:|https?:|blob:)/.test(src)) return src;\n const data = await container.readFile(src);\n if (!data) throw new Error(`exportImageEditDoc: missing asset \"${src}\"`);\n const list = await container.listFiles(src);\n const mime = list.find((e) => e.path === src)?.mimeType ?? guessMime(src);\n return arrayBufferToDataUrl(data, mime);\n}\n\nfunction arrayBufferToDataUrl(data: ArrayBuffer, mime: string): string {\n const bytes = new Uint8Array(data);\n let binary = '';\n // chunk to avoid call-stack limits with large images\n const CHUNK = 0x8000;\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + CHUNK)));\n }\n // btoa exists in browsers and modern Node (>= 16); fall back to Buffer for older runtimes.\n const b64 = typeof btoa === 'function' ? btoa(binary) : Buffer.from(bytes).toString('base64');\n return `data:${mime};base64,${b64}`;\n}\n\nfunction guessMime(path: string): string {\n const dot = path.lastIndexOf('.');\n const ext = dot >= 0 ? path.slice(dot).toLowerCase() : '';\n switch (ext) {\n case '.png':\n return 'image/png';\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg';\n case '.webp':\n return 'image/webp';\n case '.gif':\n return 'image/gif';\n case '.svg':\n return 'image/svg+xml';\n default:\n return 'application/octet-stream';\n }\n}\n\n// ============================================\n// SVG → Image → Canvas → Blob\n// ============================================\n\nasync function loadSvgImage(svg: string): Promise<HTMLImageElement> {\n const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n try {\n return await new Promise<HTMLImageElement>((resolve, reject) => {\n const img = new Image();\n img.onload = () => resolve(img);\n img.onerror = () => reject(new Error('exportImageEditDoc: SVG image failed to load'));\n img.src = url;\n });\n } finally {\n URL.revokeObjectURL(url);\n }\n}\n\ntype RasterCanvas = HTMLCanvasElement | OffscreenCanvas;\n\nfunction createRasterCanvas(width: number, height: number): RasterCanvas {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n const c = document.createElement('canvas');\n c.width = width;\n c.height = height;\n return c;\n}\n\nasync function canvasToBlob(canvas: RasterCanvas, mime: string, quality: number): Promise<Blob> {\n if ('convertToBlob' in canvas) {\n return canvas.convertToBlob({ type: mime, quality });\n }\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => {\n if (blob) resolve(blob);\n else reject(new Error('exportImageEditDoc: canvas.toBlob returned null'));\n },\n mime,\n quality,\n );\n });\n}\n\n// ============================================\n// XML escaping\n// ============================================\n\nfunction escapeAttr(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n}\n\nfunction escapeText(s: string): string {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n"],"mappings":"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","names":["decoder","encoder","c"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/markdown/stringify.ts","../src/markdown/sanitize.ts"],"sourcesContent":["/**\n * JSON → Markdown Serializer\n *\n * Converts a MarkdownDocument JSON structure back to a markdown string\n * using the unified/remark ecosystem with GFM, math, and directive extensions.\n */\n\nimport { unified } from 'unified';\nimport remarkStringify from 'remark-stringify';\nimport remarkGfm from 'remark-gfm';\nimport remarkMath from 'remark-math';\nimport remarkDirective from 'remark-directive';\nimport type { MarkdownDocument, StringifyOptions } from './types.js';\nimport { toMdast } from './convert.js';\nimport { formatBlockScalar } from './utils.js';\n\n// Cache the default processor (all extensions, default formatting) to avoid rebuilding on every call.\nlet defaultProcessor: any;\n\n// ============================================\n// Annotation-span unescaping\n// ============================================\n\n// Quoted runs as they appear in remark-stringify output. Backslashes inside\n// the span are doubled by remark (a literal `\\` before punctuation becomes\n// `\\\\`), so `\\\\.` covers both authored escapes and remark's own.\nconst DQ_RUN = `\"(?:[^\"\\\\\\\\]|\\\\\\\\.)*\"`;\nconst SQ_RUN = `'(?:[^'\\\\\\\\]|\\\\\\\\.)*'`;\n\n/**\n * A `{[…]}` template-annotation (or inline-icon) span as it appears in\n * remark output: `{\\[inner]}`, where `inner` may contain backslash escapes\n * and quoted runs (which may themselves contain `]`).\n */\nconst ESCAPED_TEMPLATE_SPAN_RE = new RegExp(\n `\\\\{\\\\\\\\\\\\[((?:${DQ_RUN}|${SQ_RUN}|\\\\\\\\.|[^\\\\]\\\\\\\\])+)\\\\]\\\\}`,\n 'g',\n);\n\n/** A heading line (annotations are only emitted trailing on headings). */\nconst HEADING_LINE_RE = /^#{1,6} .*$/gm;\n\n/**\n * The trailing Pandoc `{…}` span on a heading line, possibly followed by an\n * already-unescaped `{[…]}` template span (canonical emit order).\n */\nconst TRAILING_ESCAPED_PANDOC_SPAN_RE = new RegExp(\n `\\\\{(?!\\\\\\\\?\\\\[)((?:${DQ_RUN}|${SQ_RUN}|\\\\\\\\.|[^}\\\\\\\\])*)\\\\}(?=\\\\s*(?:\\\\{\\\\[.*)?$)`,\n);\n\n/**\n * Backslash+punctuation pairs that are safe to unescape inside an\n * annotation span: the bare character re-parses as literal text in\n * trailing-heading position, so removing the backslash is purely\n * cosmetic and round-trips.\n *\n * Deliberately EXCLUDED (left escaped in the emitted markdown): emphasis\n * and other inline-construct openers (`*`, `_`, `` ` ``, `~`, `<`, `>`,\n * `&`, `!`, `|`, `^`). Unescaping those could make remark-parse split the\n * heading's text run into inline nodes on reparse, which would hide the\n * annotation from the extractor. remark-parse resolves the surviving\n * `\\X` escapes back to bare characters, so values still round-trip —\n * the escapes are a cosmetic cost only.\n *\n * Pairs are consumed left to right, so an authored `\\\"` that remark\n * doubled to `\\\\\"` correctly comes back as `\\\"`.\n */\nconst UNESCAPE_PUNCT_RE = /\\\\([\\\\[\\]:#.,+=/?;@%$(){}'\"-])/g;\n\nfunction unescapeMarkdownPunct(text: string): string {\n return text.replace(UNESCAPE_PUNCT_RE, '$1');\n}\n\n/**\n * Serialize a MarkdownDocument back to a markdown string.\n *\n * All extensions (GFM, math, directives) are enabled by default.\n * Use the `options` parameter to control formatting and disable extensions.\n *\n * @param doc - The MarkdownDocument to serialize\n * @param options - Serialization options (formatting, extensions)\n * @returns A markdown string\n *\n * @example\n * ```ts\n * const doc: MarkdownDocument = {\n * type: 'document',\n * children: [\n * { type: 'heading', depth: 1, children: [{ type: 'text', value: 'Hello' }] },\n * { type: 'paragraph', children: [{ type: 'text', value: 'World' }] },\n * ],\n * };\n * const md = stringifyMarkdown(doc);\n * // '# Hello\\n\\nWorld\\n'\n * ```\n */\nexport function stringifyMarkdown(doc: MarkdownDocument, options?: StringifyOptions): string {\n // Convert MarkdownDocument → mdast tree\n const mdastTree = toMdast(doc);\n\n // Use cached default processor when all extensions and default formatting are used.\n const useDefaults =\n !options ||\n (options.gfm !== false &&\n options.math !== false &&\n options.directive !== false &&\n !options.bullet &&\n !options.bulletOrdered &&\n !options.emphasis &&\n !options.strong &&\n !options.rule &&\n !options.fence &&\n options.setext == null);\n\n let processor: any;\n\n if (useDefaults) {\n if (!defaultProcessor) {\n defaultProcessor = unified()\n .use(remarkGfm)\n .use(remarkMath)\n .use(remarkDirective)\n .use(remarkStringify, {\n bullet: '-',\n bulletOrdered: '.',\n emphasis: '*',\n strong: '*',\n rule: '-',\n fence: '`',\n setext: false,\n });\n }\n processor = defaultProcessor;\n } else {\n // Build a custom processor with requested options.\n // unified's .use() chaining changes the generic signature each time,\n // making strict typing impractical — use a widened Processor type.\n processor = unified();\n\n if (options?.gfm !== false) {\n processor = processor.use(remarkGfm);\n }\n if (options?.math !== false) {\n processor = processor.use(remarkMath);\n }\n if (options?.directive !== false) {\n processor = processor.use(remarkDirective);\n }\n\n processor = processor.use(remarkStringify, {\n bullet: options?.bullet ?? '-',\n bulletOrdered: options?.bulletOrdered ?? '.',\n emphasis: options?.emphasis ?? '*',\n strong: options?.strong ?? '*',\n rule: options?.rule ?? '-',\n fence: options?.fence ?? '`',\n setext: options?.setext ?? false,\n });\n }\n\n // Stringify mdast → markdown string\n const result = processor.stringify(mdastTree) as string;\n\n // remark-stringify backslash-escapes markdown-significant characters in\n // text nodes (`[` to prevent link syntax, `:` for directives, `*`/`_` for\n // emphasis, a literal `\\` before punctuation, …). Our annotations are\n // injected as plain text, so their interiors come back escaped. Undo that\n // inside `{[…]}` template-annotation spans: removing one backslash from\n // each backslash+punctuation pair (left to right) exactly inverts\n // remark's escaping — including restoring an authored `\\\"` that remark\n // doubled to `\\\\\"`. The span regex understands quoted values, so a\n // quoted `]` doesn't terminate the span.\n let cleaned = result.replace(\n ESCAPED_TEMPLATE_SPAN_RE,\n (_m, inner: string) => `{[${unescapeMarkdownPunct(inner)}]}`,\n );\n\n // Same treatment for the trailing Pandoc `{…}` attribute block on heading\n // lines (canonical emit order is Pandoc block then template annotation,\n // so the span sits at end-of-line or just before a `{[…]}` span).\n cleaned = cleaned.replace(HEADING_LINE_RE, (line) =>\n line.replace(\n TRAILING_ESCAPED_PANDOC_SPAN_RE,\n (_m, inner: string) => `{${unescapeMarkdownPunct(inner)}}`,\n ),\n );\n\n // remark-directive escapes `:` in text (since `:name` would round-trip as\n // a text directive). Inside any other Pandoc-style `{…}` brace span, `:`\n // is purely a value separator (e.g. `connectsTo=foo:flow`), so unescape\n // it to keep round-trips lossless. The negative lookahead skips `{[…]}`\n // template annotations — those are handled above.\n cleaned = cleaned.replace(/\\{(?!\\\\?\\[)[^}]*\\}/g, (match) => match.replace(/\\\\:/g, ':'));\n\n // Prepend YAML frontmatter if present\n if (doc.frontmatter && Object.keys(doc.frontmatter).length > 0) {\n const yamlLines = Object.entries(doc.frontmatter).map(([k, v]) => {\n if (typeof v === 'string') {\n // Multi-line strings emit as a YAML literal block scalar, matching\n // `formatFrontmatterValue` / `setFrontmatterValues` so both paths\n // agree; single-line strings stay verbatim as before.\n return /[\\r\\n]/.test(v)\n ? `${k}: ${formatBlockScalar(v.replace(/\\r\\n?/g, '\\n'))}`\n : `${k}: ${v}`;\n }\n return `${k}: ${JSON.stringify(v)}`;\n });\n return `---\\n${yamlLines.join('\\n')}\\n---\\n\\n${cleaned}`;\n }\n\n return cleaned;\n}\n","import type { HtmlElement, HtmlNode } from './types.js';\n\nexport type HtmlPolicy = 'strip' | 'sanitize' | 'trusted';\n\nexport type UrlKind = 'link' | 'media';\n\nconst SAFE_LINK_SCHEMES = new Set(['http', 'https', 'mailto', 'tel']);\nconst SAFE_MEDIA_SCHEMES = new Set(['http', 'https', 'blob']);\n\n/** Schemes that can execute script — never allowed, even via extraLinkSchemes. */\nconst NEVER_ALLOWED_SCHEMES = new Set(['javascript', 'vbscript', 'data']);\n\nexport interface SanitizeUrlOptions {\n /**\n * Extra lowercase schemes allowed for `kind: 'link'` — the host-app escape\n * hatch for custom link protocols it intercepts itself (e.g. an app-internal\n * navigation scheme). Executable schemes are refused regardless.\n */\n extraLinkSchemes?: readonly string[];\n}\n\nconst SAFE_DATA_MEDIA_RE =\n /^data:(?:image\\/(?!svg\\+xml)[a-z0-9.+-]+|audio\\/[a-z0-9.+-]+|video\\/[a-z0-9.+-]+);/i;\n\nconst SAFE_TAGS = new Set([\n 'a',\n 'abbr',\n 'b',\n 'blockquote',\n 'br',\n 'caption',\n 'cite',\n 'code',\n 'col',\n 'colgroup',\n 'data',\n 'dd',\n 'del',\n 'details',\n 'dfn',\n 'div',\n 'dl',\n 'dt',\n 'em',\n 'figcaption',\n 'figure',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'kbd',\n 'li',\n 'mark',\n 'ol',\n 'p',\n 'pre',\n 'q',\n 's',\n 'samp',\n 'small',\n 'source',\n 'span',\n 'strong',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'track',\n 'u',\n 'ul',\n 'var',\n 'video',\n 'audio',\n]);\n\nconst DROP_WITH_CONTENT_TAGS = new Set([\n 'base',\n 'button',\n 'embed',\n 'form',\n 'iframe',\n 'input',\n 'link',\n 'meta',\n 'object',\n 'option',\n 'script',\n 'select',\n 'style',\n 'svg',\n 'template',\n 'textarea',\n]);\n\nconst GLOBAL_ATTRS = new Set(['class', 'id', 'title', 'role']);\n\nconst TAG_ATTRS: Record<string, Set<string>> = {\n a: new Set(['href', 'target', 'rel']),\n audio: new Set(['src', 'controls', 'preload', 'muted', 'loop']),\n col: new Set(['span', 'width']),\n colgroup: new Set(['span']),\n data: new Set(['value']),\n img: new Set(['src', 'alt', 'width', 'height', 'loading', 'decoding']),\n li: new Set(['value']),\n ol: new Set(['start', 'type']),\n q: new Set(['cite']),\n source: new Set(['src', 'type', 'media']),\n td: new Set(['colspan', 'rowspan', 'align']),\n th: new Set(['colspan', 'rowspan', 'align', 'scope']),\n time: new Set(['datetime']),\n track: new Set(['src', 'kind', 'label', 'srclang', 'default']),\n video: new Set([\n 'src',\n 'poster',\n 'width',\n 'height',\n 'controls',\n 'preload',\n 'muted',\n 'loop',\n 'playsinline',\n ]),\n};\n\nconst SAFE_ATTR_NAME_RE = /^[a-z][a-z0-9:_.-]*$/;\nconst SAFE_OL_TYPES = new Set(['1', 'a', 'A', 'i', 'I']);\nconst SAFE_PRELOAD_VALUES = new Set(['none', 'metadata', 'auto']);\nconst SAFE_TRACK_KINDS = new Set(['subtitles', 'captions', 'descriptions', 'chapters', 'metadata']);\n\n/**\n * Sanitize a URL for a rendered link or media attribute.\n *\n * Relative URLs are allowed. Absolute URLs are restricted to schemes that\n * cannot execute script in a document context. Media URLs additionally allow\n * `blob:` and image/audio/video `data:` URLs because Squisq uses both for\n * browser-local media previews and self-contained exports.\n */\nexport function sanitizeUrl(\n url: string | null | undefined,\n kind: UrlKind = 'link',\n options?: SanitizeUrlOptions,\n): string | null {\n if (typeof url !== 'string') return null;\n\n const trimmed = url.trim();\n if (!trimmed) return null;\n\n const compact = stripUrlSchemeNoise(trimmed);\n const colon = compact.indexOf(':');\n const firstPathChar = firstIndexOfAny(compact, ['/', '?', '#']);\n const hasScheme = colon >= 0 && (firstPathChar < 0 || colon < firstPathChar);\n\n if (!hasScheme) return trimmed;\n\n const scheme = compact.slice(0, colon).toLowerCase();\n if (kind === 'media') {\n if (scheme === 'data') return SAFE_DATA_MEDIA_RE.test(compact) ? trimmed : null;\n return SAFE_MEDIA_SCHEMES.has(scheme) ? trimmed : null;\n }\n\n if (SAFE_LINK_SCHEMES.has(scheme)) return trimmed;\n if (options?.extraLinkSchemes?.includes(scheme) && !NEVER_ALLOWED_SCHEMES.has(scheme)) {\n return trimmed;\n }\n return null;\n}\n\n/**\n * Return a sanitized copy of an HtmlNode tree. The original parsed tree is left\n * untouched so markdown stringify can still preserve source fidelity.\n */\nexport function sanitizeHtmlNodes(nodes: HtmlNode[]): HtmlNode[] {\n const out: HtmlNode[] = [];\n for (const node of nodes) {\n out.push(...sanitizeHtmlNode(node));\n }\n return out;\n}\n\nfunction sanitizeHtmlNode(node: HtmlNode): HtmlNode[] {\n switch (node.type) {\n case 'htmlText':\n return [node];\n case 'htmlComment':\n return [];\n case 'htmlElement':\n return sanitizeHtmlElement(node);\n }\n}\n\nfunction sanitizeHtmlElement(node: HtmlElement): HtmlNode[] {\n const tag = node.tagName.toLowerCase();\n\n if (DROP_WITH_CONTENT_TAGS.has(tag)) return [];\n\n const children = sanitizeHtmlNodes(node.children);\n if (!SAFE_TAGS.has(tag)) return children;\n\n return [\n {\n type: 'htmlElement',\n tagName: tag,\n attributes: sanitizeAttrs(tag, node.attributes),\n children,\n selfClosing: node.selfClosing,\n },\n ];\n}\n\nfunction sanitizeAttrs(tag: string, attrs: Record<string, string>): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [rawName, rawValue] of Object.entries(attrs)) {\n const name = rawName.toLowerCase();\n if (!isAllowedAttr(tag, name)) continue;\n\n const value = String(rawValue);\n const safeValue = sanitizeAttrValue(tag, name, value);\n if (safeValue === null) continue;\n out[name] = safeValue;\n }\n if (tag === 'a' && out.target === '_blank') {\n out.rel = ensureRelTokens(out.rel, ['noopener', 'noreferrer']);\n }\n return out;\n}\n\nfunction isAllowedAttr(tag: string, name: string): boolean {\n if (!SAFE_ATTR_NAME_RE.test(name)) return false;\n if (name.startsWith('on')) return false;\n if (name.startsWith('aria-') || name.startsWith('data-')) return true;\n if (GLOBAL_ATTRS.has(name)) return true;\n return TAG_ATTRS[tag]?.has(name) ?? false;\n}\n\nfunction sanitizeAttrValue(tag: string, name: string, value: string): string | null {\n if (name === 'href') return sanitizeUrl(value, 'link');\n if (name === 'src' || name === 'poster') return sanitizeUrl(value, 'media');\n if (\n (name === 'width' ||\n name === 'height' ||\n name === 'span' ||\n name === 'colspan' ||\n name === 'rowspan' ||\n name === 'value') &&\n !isNonNegativeInteger(value)\n ) {\n return null;\n }\n if (name === 'start' && !isInteger(value)) return null;\n if (name === 'type' && tag === 'ol' && !SAFE_OL_TYPES.has(value)) return null;\n if (name === 'preload' && !SAFE_PRELOAD_VALUES.has(value)) return null;\n if (name === 'kind' && tag === 'track' && !SAFE_TRACK_KINDS.has(value)) return null;\n if (name === 'align' && value !== 'left' && value !== 'right' && value !== 'center') return null;\n if (name === 'target')\n return value === '_blank' || value === '_self' || value === '_parent' || value === '_top'\n ? value\n : null;\n if (name === 'rel') return sanitizeRel(value);\n return value;\n}\n\nfunction sanitizeRel(value: string): string {\n return relTokens(value).join(' ');\n}\n\nfunction ensureRelTokens(value: string | undefined, required: string[]): string {\n const tokens = new Set(value ? relTokens(value) : []);\n for (const token of required) tokens.add(token);\n return Array.from(tokens).join(' ');\n}\n\nfunction relTokens(value: string): string[] {\n const tokens = value\n .split(/\\s+/)\n .map((token) => token.trim().toLowerCase())\n .filter(Boolean)\n .filter((token) => /^[a-z0-9_-]+$/.test(token));\n return Array.from(new Set(tokens));\n}\n\nfunction isInteger(value: string): boolean {\n return /^-?\\d+$/.test(value.trim());\n}\n\nfunction isNonNegativeInteger(value: string): boolean {\n return /^\\d+$/.test(value.trim());\n}\n\nfunction firstIndexOfAny(value: string, needles: string[]): number {\n let result = -1;\n for (const needle of needles) {\n const index = value.indexOf(needle);\n if (index >= 0 && (result < 0 || index < result)) result = index;\n }\n return result;\n}\n\nfunction stripUrlSchemeNoise(value: string): string {\n let out = '';\n for (const char of value) {\n const code = char.charCodeAt(0);\n if (code <= 0x20 || code === 0x7f || char.trim() === '') continue;\n out += char;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/schemas/Doc.ts","../src/schemas/Media.ts","../src/schemas/themeCompile.ts"],"sourcesContent":["/**\n * Doc Schema\n *\n * Defines the JSON format for AI-scriptable visual docs that accompany\n * audio narration. A Doc describes a sequence of animated blocks\n * with text overlays, images, and transitions synchronized to audio.\n *\n * The format is designed to be:\n * 1. AI-generatable - Claude can create scripts from article content\n * 2. Browser-renderable - SVG + CSS animations (ES2015 compatible)\n * 3. Video-exportable - Frame-by-frame capture via Playwright\n *\n * Timing: Blocks are timed to match narration audio segments. Each article\n * has 3-6 MP3 files (intro + sections), and blocks are grouped by segment.\n */\n\nimport type { Transition } from './Transitions.js';\nexport type { Transition, TransitionType, TransitionDirection } from './Transitions.js';\n\n// ============================================\n// Core Types\n// ============================================\n\n/**\n * A directed connection from one block to another.\n *\n * Populated from a heading's Pandoc-style `{connectsTo=…}` attribute\n * (e.g. `## Step 1 {#step1 connectsTo=foo:flow,bar}`). Each item is\n * `target` or `target:type`. Used by diagram-style layouts that draw\n * edges between blocks.\n */\nexport interface BlockConnection {\n /** Target block id (matches another block's `id`). */\n target: string;\n /** Optional connection type/label (e.g., \"flow\", \"requires\"). */\n type?: string;\n}\n\n/**\n * Configuration for the Start/resting block shown before playback begins.\n */\nexport interface StartBlockConfig {\n /** Hero image path (relative to article media). When omitted, a theme-driven background is used. */\n heroSrc?: string;\n /** Alt text for the hero image */\n heroAlt?: string;\n /** Title to display over the hero */\n title: string;\n /** Optional subtitle */\n subtitle?: string;\n /** Ambient motion for the hero image */\n ambientMotion?: 'zoomIn' | 'zoomOut' | 'panLeft' | 'panRight';\n /** Photo credit / artist name for the hero image */\n heroCredit?: string;\n /** License identifier for the hero image */\n heroLicense?: string;\n}\n\n/**\n * A structural problem detected while converting or validating a document.\n *\n * Diagnostics ride on `Doc.diagnostics` so any consumer — the editor, the\n * CLI `validate` command, or an agent re-parsing its own output — gets the\n * same feedback without watching the console. Conversion never throws for\n * content problems; it degrades gracefully and records a diagnostic.\n */\nexport interface DocDiagnostic {\n /** `error` = the author's intent could not be honored (e.g. unparseable\n * data fence); `warning` = something looks wrong but rendering proceeds\n * with a fallback (e.g. unknown template name); `info` = nothing is\n * broken, but the author may want to know (e.g. a redundant annotation). */\n severity: 'error' | 'warning' | 'info';\n /** Stable machine-readable code (e.g. `unknown-template`, `duplicate-id`,\n * `unresolved-connection`, `data-fence-parse`, `missing-asset`). */\n code: string;\n /** Human-readable description, including a suggestion when one exists. */\n message: string;\n /** Id of the block the problem belongs to, when attributable. */\n blockId?: string;\n /** 1-based line number in the markdown source, when known. */\n line?: number;\n}\n\n/**\n * A complete visual doc for an article.\n */\nexport interface Doc {\n /** Article ID this doc belongs to */\n articleId: string;\n\n /** Total duration in seconds (sum of all audio segments) */\n duration: number;\n\n /** Ordered list of blocks */\n blocks: Block[];\n\n /** Audio track configuration */\n audio: AudioTrack;\n\n /** Caption track for closed captions (generated by postprocess-doc) */\n captions?: CaptionTrack;\n\n /**\n * Start/resting block shown before playback begins.\n * Displays hero image with title - acts as a \"poster\" for the doc.\n * The player shows this until the user starts playback.\n */\n startBlock?: StartBlockConfig;\n\n /**\n * Persistent layers rendered on every block (behind and/or on top of content).\n * Used for consistent branding overlays, background gradients, etc.\n */\n persistentLayers?: import('./BlockTemplates.js').PersistentLayerConfig;\n\n /**\n * Optional theme identifier. Resolved at render time via `resolveTheme()`.\n * When omitted, the renderer uses the default theme ('documentary').\n */\n themeId?: string;\n\n /** Optional metadata */\n meta?: {\n generatedAt?: string;\n generatedBy?: string;\n version?: number;\n };\n\n /**\n * YAML frontmatter from the source markdown.\n * Carries rendering hints like `document-render-as` and custom metadata.\n */\n frontmatter?: Record<string, unknown>;\n\n /**\n * User-defined block templates inlined into this doc.\n *\n * Populated from the markdown frontmatter key\n * `squisq-custom-templates`. The template-expansion pipeline\n * (`expandDocBlocks` in `doc/templates/index.ts`) merges these into\n * the registry before walking blocks, so a heading annotated\n * `{[myhero]}` resolves against a doc-defined template named `myhero`.\n *\n * Library templates (stored in localStorage on the editor side) are\n * NOT auto-loaded here — applying a library template to a block\n * copies its definition into this list so the doc remains\n * self-sufficient for SSR and export.\n */\n customTemplates?: import('./CustomTemplates.js').CustomTemplateDefinition[];\n\n /**\n * User-defined themes inlined into this doc.\n *\n * The theme analog of {@link Doc.customTemplates}. Populated from the\n * markdown frontmatter key `squisq-custom-themes`. Exactly one is active\n * at a time — selected by id via `squisq-theme` / {@link Doc.themeId},\n * the doc-level counterpart of a block's `{[name]}` template annotation —\n * but the payload is a list so a doc can carry a small catalog and stay\n * self-sufficient for SSR / export. `resolveThemeForDoc(doc, id)` resolves\n * against this list first (pure, doc-scoped) before built-ins, mirroring\n * how `buildRegistry` resolves custom templates.\n */\n customThemes?: import('./Theme.js').Theme[];\n\n /**\n * Structural problems found while building this doc (unknown templates,\n * unparseable data fences, duplicate ids, unresolved connections, …).\n * Populated by `markdownToDoc()`; extended by `validateMarkdownDoc()`.\n * Absent when the document is clean.\n */\n diagnostics?: DocDiagnostic[];\n\n /**\n * Document-spanning timed media (e.g. a full-length narration MP3/MP4),\n * authored as a media annotation in the preamble (before the first\n * heading) with `anchor=document`. Each clip is timed from the document\n * start and may play across every block. See `resolveMediaSchedule`.\n */\n documentMedia?: import('./Media.js').MediaClip[];\n}\n\n/**\n * A single block in the doc.\n *\n * Blocks can be flat (audio-synced timeline) or nested (markdown-driven hierarchy).\n * When derived from markdown, a block corresponds to a heading section:\n * - `sourceHeading` references the original MarkdownHeading node\n * - `contents` holds the body markdown between this heading and the next\n * - `children` holds sub-heading blocks (e.g., H2s under an H1)\n *\n * Four adjacent fields carry template information — do not confuse them:\n * - `template` — the *resolved* template id this block renders with.\n * - `templateOverrides` — raw `{[…]}` inline params (always strings).\n * - `templateData` — structured inputs from a data fence / GFM table\n * (parsed types, not strings).\n * - `sourceAnnotation.template` — the raw *requested* template name for a\n * standalone-annotation block (before alias resolution).\n * Merge order at render time: template defaults → `templateData` →\n * `templateOverrides`.\n */\nexport interface Block {\n /** Unique identifier for this block.\n * Defaults to a slug derived from the heading text. May be overridden by\n * a Pandoc-style `{#custom-id}` attribute on the heading. */\n id: string;\n\n /** When this block appears (seconds from start).\n * Overridden by a heading attribute `startTime=…` when present. */\n startTime: number;\n\n /** How long this block is visible (seconds).\n * Overridden by a heading attribute `duration=…` when present. */\n duration: number;\n\n /** Which audio segment this block belongs to (0-indexed) */\n audioSegment: number;\n\n /**\n * Pre-computed visual layers, rendered back-to-front.\n *\n * Optional — template-derived blocks typically omit this and compute\n * layers on demand via `getLayers()` from `@bendyline/squisq/doc`.\n * Raw blocks (e.g., hand-crafted by AI) may provide layers directly.\n */\n layers?: Layer[];\n\n /** Entry transition from previous block */\n transition?: Transition;\n\n /** Resolved template id this block renders with (see the quartet note above). */\n template?: string;\n\n /**\n * True when `template` was chosen by content-aware auto-picking\n * (`markdownToDoc`'s `autoTemplates`) rather than authored. Ephemeral:\n * `docToMarkdown` does not materialize auto-picked templates as heading\n * annotations, keeping the markdown round-trip lossless.\n */\n autoTemplate?: boolean;\n\n /**\n * True when this block was produced from a *standalone* `{[templateName …]}`\n * annotation paragraph in another block's body (rather than from a heading).\n * Such blocks have no `sourceHeading`; `docToMarkdown` re-emits their\n * annotation as a synthesized paragraph before their `contents` so the\n * markdown round-trips. See `annotationBlocks.ts`.\n *\n * Presence-marker: only ever `true` (never `false`). Test with\n * `block.standaloneAnnotation` presence, not as a boolean toggle.\n */\n standaloneAnnotation?: true;\n\n /**\n * The annotation a standalone block was built from — the raw (un-resolved)\n * template name and its params, in author order. Round-tripped by\n * `docToMarkdown` (via `serializeAnnotation`) and read by validation as the\n * raw requested template name (symmetric with\n * `sourceHeading.templateAnnotation`). Present only when\n * `standaloneAnnotation` is true.\n */\n sourceAnnotation?: { template?: string; params?: Record<string, string> };\n\n /**\n * Display title for template rendering.\n * Extracted from the sourceHeading when the block is created by markdownToDoc().\n * Templates like sectionHeader and titleBlock read this for their title layer.\n */\n title?: string;\n\n // ── Markdown-driven hierarchy (optional) ──\n\n /**\n * Nested sub-blocks (heading hierarchy).\n * When a doc is derived from markdown, sub-headings within this\n * heading's section become child blocks. For example, H2 blocks\n * under an H1 block appear here.\n */\n children?: Block[];\n\n /**\n * Markdown body content for this block's section.\n * Contains the block-level markdown nodes (paragraphs, lists, code,\n * blockquotes, etc.) that appear between this heading and the next\n * heading or sub-heading. Empty for pure structural blocks.\n */\n contents?: import('../markdown/types.js').MarkdownBlockNode[];\n\n /**\n * Reference to the MarkdownHeading node that created this block.\n * Present when the block was derived from markdown via markdownToDoc().\n * Enables round-tripping back to markdown and provides heading depth.\n * Absent for the preamble block (content before the first heading).\n */\n sourceHeading?: import('../markdown/types.js').MarkdownHeading;\n\n /**\n * Template overrides extracted from a heading's `{[templateName key=value]}`\n * annotation. Only carries template-specific params (e.g. `colorScheme=blue`).\n * Block-level metadata lives in `x` / `y` / `connectsTo` / `metadata` and is\n * sourced from the Pandoc-style `{#id .class key=value}` attribute block instead.\n */\n templateOverrides?: Record<string, string>;\n\n /**\n * Structured template inputs sourced from the block's body content:\n * a ```json data / ```yaml data fence under the heading, or — for the\n * `dataTable` template — the first GFM table in the section. Unlike\n * `templateOverrides` (always strings), values here keep their parsed\n * types (arrays, numbers, nested objects). Merge order at render time:\n * template defaults → `templateData` → `templateOverrides`.\n */\n templateData?: Record<string, unknown>;\n\n // ── Block-level metadata from Pandoc-style `{#id .class key=value}` ──\n\n /**\n * X coordinate for diagram-style positioning (in author-defined units).\n * Sourced from a heading attribute `x=…`.\n */\n x?: number;\n\n /**\n * Y coordinate for diagram-style positioning (in author-defined units).\n * Sourced from a heading attribute `y=…`.\n */\n y?: number;\n\n /**\n * Outgoing connections to other blocks, by id. Sourced from a heading\n * attribute `connectsTo=target1,target2:type,target3`.\n */\n connectsTo?: BlockConnection[];\n\n /**\n * CSS-style class tokens from a heading's Pandoc attribute (e.g. `{.important .v2}`).\n * Parsed and stored; downstream consumers may use them for styling or filtering.\n */\n classes?: string[];\n\n /**\n * Free-form metadata for heading attribute keys outside the typed registry\n * (anything that isn't `x` / `y` / `connectsTo` / `startTime` / `duration`).\n * Values are raw strings exactly as authored.\n */\n metadata?: Record<string, string>;\n\n /**\n * Timed media clips attached to this block, authored as body-level\n * `{[audio …]}` / `{[video …]}` annotations. Each clip is timed relative\n * to this block's `startTime` (via `startAt`) and optionally spills past\n * the block's end. See `MediaClip` / `resolveMediaSchedule`.\n */\n media?: import('./Media.js').MediaClip[];\n}\n\n// ============================================\n// Layer Types\n// ============================================\n\n/**\n * A visual element within a block.\n * Layers are composited back-to-front (first layer is background).\n */\nexport type Layer =\n | ImageLayer\n | TextLayer\n | ShapeLayer\n | PathLayer\n | MapLayer\n | VideoLayer\n | TableLayer;\n\ninterface BaseLayer {\n /** Unique identifier for this layer */\n id: string;\n\n /** Position and size */\n position: Position;\n\n /** Animation to apply */\n animation?: Animation;\n}\n\n/**\n * Photographic treatment applied to an image layer — a theme-level \"grade\"\n * so the same photo looks native in every theme. Rendered as CSS filter\n * functions, which behave identically in the browser player and headless\n * frame capture (video/still export).\n */\nexport interface ImageTreatment {\n /**\n * mono: desaturate toward archival black & white.\n * duotone: single-hue tint (hue taken from `color`).\n * warm / cool: gentle temperature grade.\n */\n type: 'none' | 'mono' | 'duotone' | 'warm' | 'cool';\n /** Blend strength 0..1. Default 0.6. */\n strength?: number;\n /** Duotone tint color (themes default this to their primary). */\n color?: string;\n}\n\n/**\n * Image layer - displays an image with optional Ken Burns effect.\n */\nexport interface ImageLayer extends BaseLayer {\n type: 'image';\n content: {\n /** Path to image file (relative to article media dir) */\n src: string;\n /** Alt text for accessibility */\n alt: string;\n /** How to fit image in bounds */\n fit?: 'cover' | 'contain' | 'fill';\n /** Photo credit / artist name */\n credit?: string;\n /** License identifier (e.g., 'CC BY-SA 4.0') */\n license?: string;\n /** Theme-derived photographic grade (see ImageTreatment). */\n treatment?: ImageTreatment;\n /** Gaussian blur radius in px (background/atmosphere imagery). */\n blur?: number;\n };\n}\n\n/**\n * Text layer - displays text with styling.\n */\nexport interface TextLayer extends BaseLayer {\n type: 'text';\n content: {\n /**\n * Plain text (supports \\n for line breaks). Source of truth for plain\n * consumers — PDF/markdown export, search, accessibility — and the SVG\n * `<text>` fallback. When `html` is set, this is its plain-text\n * projection and must be kept in sync.\n */\n text: string;\n /**\n * Optional sanitized **inline** HTML for rich formatting (bold/italic/\n * links, and — for layout textboxes — headings/lists). When present the\n * renderer draws it via `<foreignObject>` instead of SVG `<text>`. Treated\n * as untrusted and re-sanitized at render time. See `RichTextLayer` in\n * `@bendyline/squisq-react`.\n */\n html?: string;\n /** Text styling */\n style: TextStyle;\n };\n}\n\n/**\n * A two-stop linear gradient fill, shared by shape/path fills and text\n * backgrounds. When present it overrides the solid `fill` color.\n *\n * `angle` is in degrees: 0 = top→bottom, 90 = left→right, increasing\n * clockwise. Defaults to 0. Rendered as an SVG `<linearGradient>` in\n * objectBoundingBox units so it scales with the layer's box.\n */\nexport interface LinearGradient {\n from: string;\n to: string;\n angle?: number;\n}\n\n/** Border line style, mapped to an SVG stroke-dasharray by the renderer. */\nexport type BorderStyle = 'solid' | 'dashed' | 'dotted';\n\n/**\n * Shape layer - simple geometric shapes for visual accents.\n */\n/**\n * Repeating SVG pattern fill for a shape (dots, grid, diagonal lines).\n * Rendered as a native `<pattern>` def — fully vector and export-safe.\n */\nexport interface ShapePattern {\n kind: 'dots' | 'grid' | 'diagonal';\n /** Pattern ink color. */\n color: string;\n /** Tile size in px (default 24). */\n size?: number;\n /** Pattern opacity 0–1 (default 1; tint via a translucent color or this). */\n opacity?: number;\n}\n\n/**\n * Procedural filter applied to a shape. `noise` renders static film grain\n * via SVG feTurbulence — deliberately not animated so frame capture and\n * the live player agree.\n */\nexport interface ShapeFilter {\n type: 'noise';\n /** feTurbulence base frequency (default 0.8 — fine grain). */\n baseFrequency?: number;\n /** Grain opacity 0–1 (default 0.05). */\n opacity?: number;\n}\n\nexport interface ShapeLayer extends BaseLayer {\n type: 'shape';\n content: {\n /** Shape type */\n shape: 'rect' | 'circle' | 'line';\n /** Fill color (CSS color or 'none') */\n fill?: string;\n /** Fill opacity 0–1 (applies to solid and gradient fills). */\n fillOpacity?: number;\n /** Gradient fill; overrides `fill` when set. */\n gradient?: LinearGradient;\n /** Repeating pattern fill; overrides `fill`/`gradient` when set. */\n pattern?: ShapePattern;\n /** Procedural filter (film grain). */\n filter?: ShapeFilter;\n /** Stroke color */\n stroke?: string;\n /** Stroke width in pixels */\n strokeWidth?: number;\n /** Border line style (solid/dashed/dotted). Default solid. */\n borderStyle?: BorderStyle;\n /** Corner radius for rect */\n borderRadius?: number;\n };\n}\n\n/**\n * End-of-line marker style for a path/connector endpoint.\n * `arrow` is a filled triangle (the classic arrowhead); `open` is a stroked\n * V; `diamond`/`circle`/`square` are filled glyphs; `none` draws nothing.\n */\nexport type MarkerStyle = 'none' | 'arrow' | 'open' | 'diamond' | 'circle' | 'square';\n\n/**\n * Path layer - renders an SVG `<path>` for arbitrary curves, connectors,\n * arrows, or filled regions. Used by the diagram template for edges\n * between nodes, the drawing template for non-rect/circle/line shapes, and\n * available to any template that needs a custom geometry.\n *\n * The `position` field carries the layer's bounding box (so animations\n * and clipping work the same as for other layers), but the actual\n * geometry is encoded in the `d` attribute using absolute SVG path\n * coordinates relative to the block viewport.\n */\nexport interface PathLayer extends BaseLayer {\n type: 'path';\n content: {\n /** SVG path `d` attribute (e.g. \"M 10 10 L 90 90\"). Absolute viewBox coords. */\n d: string;\n /**\n * Named shape kind (e.g. `'diamond'`, `'star'`, `'arrow-right'`) for\n * path layers that represent one of Squisq's standard shapes. When\n * set, the renderer re-derives `d` from the layer's `position` box\n * (resolved against the viewport) instead of using the stored `d` —\n * so the shape moves, resizes, and adapts to any aspect ratio like\n * the native rect/circle/line layers do. Plain paths (connectors,\n * freehand) leave this unset and keep their absolute `d`.\n */\n shapeKind?: string;\n /** Stroke color (default: theme text color). */\n stroke?: string;\n /** Stroke width in pixels (default: 2). */\n strokeWidth?: number;\n /** Optional fill color (default: 'none' — pure connectors). */\n fill?: string;\n /** Fill opacity 0–1 (applies to solid and gradient fills). */\n fillOpacity?: number;\n /** Gradient fill; overrides `fill` when set. */\n gradient?: LinearGradient;\n /**\n * Border line style (solid/dashed/dotted). A convenience over\n * `dasharray` for named shapes; when set the renderer derives the\n * dash pattern from it (scaled by stroke width).\n */\n borderStyle?: BorderStyle;\n /** Optional stroke dash pattern (SVG `stroke-dasharray` syntax). */\n dasharray?: string;\n /**\n * Legacy arrowhead flag. Prefer `startMarker`/`endMarker`. When those are\n * unset, `'end'`/`'start'`/`'both'` render a filled-triangle arrowhead.\n */\n arrow?: 'none' | 'end' | 'start' | 'both';\n /** Marker at the path start (overrides `arrow`). Default: derived from `arrow`. */\n startMarker?: MarkerStyle;\n /** Marker at the path end (overrides `arrow`). Default: derived from `arrow`. */\n endMarker?: MarkerStyle;\n };\n}\n\n/**\n * Map layer - displays a geographic map with optional markers.\n *\n * Maps are rendered as composite tile images from free tile providers.\n * Supported styles use open-source/free tiles requiring attribution.\n */\nexport interface MapLayer extends BaseLayer {\n type: 'map';\n content: {\n /** Center coordinates for the map */\n center: {\n lat: number;\n lng: number;\n };\n /** Zoom level (0-18, typically 8-14 for doc blocks) */\n zoom: number;\n /** Map tile style */\n style: MapTileStyle;\n /** Optional markers to display on the map */\n markers?: MapMarker[];\n /** Pre-rendered static image path (for video export reliability) */\n staticSrc?: string;\n /** Show attribution text (default: true) */\n showAttribution?: boolean;\n };\n}\n\n/**\n * Video layer - displays a short video clip, always muted (narration provides audio).\n *\n * Rendered via an HTML5 <video> element inside <foreignObject>, similar to how\n * ImageLayer uses <img> for Ken Burns effects. clipStart/clipEnd define the window\n * within the source video to play. During Playwright frame capture (seekTo mode),\n * the video is seeked programmatically rather than playing in real time.\n */\nexport interface VideoLayer extends BaseLayer {\n type: 'video';\n content: {\n /** Path to video file (relative to article media dir) */\n src: string;\n /** Path to poster frame (shown before video loads / during seek) */\n posterSrc?: string;\n /** Alt text for accessibility */\n alt: string;\n /** How to fit video in bounds */\n fit?: 'cover' | 'contain' | 'fill';\n /** Start time within the source video (seconds) */\n clipStart: number;\n /** End time within the source video (seconds) */\n clipEnd: number;\n /** Total source video duration (for validation) */\n sourceDuration?: number;\n /**\n * Seconds into the owning block before this video begins playing\n * (the block-relative `startAt`). Default 0 — plays from block start.\n */\n startAt?: number;\n /**\n * When true, the video keeps playing past the block's end (the\n * playback scheduler re-homes it to the player level). Default false.\n */\n spillover?: boolean;\n /** Video credit / artist name */\n credit?: string;\n /** License identifier (e.g., 'CC BY-SA 4.0') */\n license?: string;\n };\n}\n\n/**\n * Table layer - renders a data table with themed styling.\n * Uses foreignObject inside SVG for HTML-based table layout.\n */\nexport interface TableLayer extends BaseLayer {\n type: 'table';\n content: {\n /** Header cell values */\n headers: string[];\n /** Data rows (array of cell value arrays) */\n rows: string[][];\n /** Per-column alignment */\n align?: (('left' | 'right' | 'center') | null)[];\n /** Visual styling */\n style: TableLayerStyle;\n };\n}\n\n/**\n * Styling options for a TableLayer.\n */\nexport interface TableLayerStyle {\n /** Header row background color */\n headerBackground: string;\n /** Header row text color */\n headerColor: string;\n /** Body cell background color */\n cellBackground: string;\n /** Body cell text color */\n cellColor: string;\n /** Border/divider color */\n borderColor: string;\n /** Font size in pixels */\n fontSize: number;\n /** Font family */\n fontFamily?: string;\n /** Header font family (falls back to fontFamily) */\n headerFontFamily?: string;\n /** Corner radius for the table container */\n borderRadius?: number;\n}\n\n/**\n * Available map tile styles from free/open-source providers.\n */\nexport type MapTileStyle =\n | 'terrain' // OpenTopoMap - topographic/terrain (default)\n | 'satellite' // ESRI World Imagery\n | 'road' // OpenStreetMap standard\n | 'toner' // Stadia Toner (high contrast B&W)\n | 'watercolor'; // Stadia Watercolor (artistic)\n\n/**\n * A marker to display on the map.\n */\nexport interface MapMarker {\n /** Marker latitude */\n lat: number;\n /** Marker longitude */\n lng: number;\n /** Optional label text */\n label?: string;\n /** Marker color (CSS color, default: #ef4444) */\n color?: string;\n /** Marker icon type */\n icon?: 'pin' | 'circle' | 'star';\n}\n\n// ============================================\n// Position & Styling\n// ============================================\n\n/**\n * Position and dimensions for a layer.\n * Values can be pixels (number) or percentages (string like \"50%\").\n */\nexport interface Position {\n /** X position (pixels or percentage) */\n x: number | string;\n /** Y position (pixels or percentage) */\n y: number | string;\n /** Width (pixels or percentage) */\n width?: number | string;\n /** Height (pixels or percentage) */\n height?: number | string;\n /** Anchor point for positioning */\n anchor?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n}\n\n/**\n * Text styling options.\n */\nexport interface TextStyle {\n /** Font size in pixels */\n fontSize: number;\n /** Font family (defaults to system sans-serif) */\n fontFamily?: string;\n /** Font weight */\n fontWeight?: 'normal' | 'bold';\n /** Font style. Also the inherited baseline for rich `content.html`. */\n fontStyle?: 'normal' | 'italic';\n /** Text color (CSS color) */\n color: string;\n /** Horizontal text alignment within the layer's position box. */\n textAlign?: 'left' | 'center' | 'right';\n /**\n * Vertical alignment of the text within the layer's position box.\n * Requires a `position.height` to have an effect. When omitted, the\n * vertical baseline is derived from `position.anchor` (legacy behavior).\n */\n verticalAlign?: 'top' | 'middle' | 'bottom';\n /** Line height multiplier */\n lineHeight?: number;\n /** Add drop shadow for readability over images */\n shadow?: boolean;\n /** Background color for text box */\n background?: string;\n /** Background opacity 0–1 (applies to solid and gradient backgrounds). */\n backgroundOpacity?: number;\n /** Gradient background; overrides `background` when set. */\n backgroundGradient?: LinearGradient;\n /** Border (stroke) color for the text box. */\n borderColor?: string;\n /** Border width in pixels. A border renders when > 0 and a color is set. */\n borderWidth?: number;\n /** Border line style (solid/dashed/dotted). Default solid. */\n borderStyle?: BorderStyle;\n /** Padding around text (pixels) */\n padding?: number;\n /** Maximum number of lines before truncation (adds \"...\" to last line) */\n maxLines?: number;\n}\n\n// ============================================\n// Animation & Transitions\n// ============================================\n\n/**\n * Animation to apply to a layer.\n */\nexport interface Animation {\n /** Animation type */\n type: AnimationType;\n /** Duration in seconds (defaults to layer's block duration) */\n duration?: number;\n /** Delay before animation starts (seconds) */\n delay?: number;\n /** CSS easing function */\n easing?: string;\n /** For Ken Burns: zoom direction */\n direction?: 'in' | 'out';\n /** For pan animations: pan direction */\n panDirection?: 'left' | 'right' | 'up' | 'down';\n}\n\n/**\n * Available animation types.\n */\nexport type AnimationType =\n | 'none'\n | 'fadeIn'\n | 'fadeOut'\n | 'slowZoom' // Slow zoom with optional pan\n | 'zoomIn'\n | 'zoomOut'\n | 'panLeft'\n | 'panRight'\n | 'typewriter'; // Text appears letter by letter\n\n// ============================================\n// Audio Configuration\n// ============================================\n\n/**\n * Audio track configuration.\n * Articles have multiple MP3 segments (intro + sections).\n */\nexport interface AudioTrack {\n /** Audio segments in playback order */\n segments: AudioSegment[];\n}\n\n// ============================================\n// Captions\n// ============================================\n\n/**\n * A single word with precise timing, used for word-level highlighting\n * in social-style captions. Populated from TTS timing data when available.\n */\nexport interface CaptionWord {\n /** The word text */\n text: string;\n /** Start time in seconds (relative to doc start) */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n}\n\n/**\n * A single caption phrase to display during playback.\n */\nexport interface CaptionPhrase {\n /** The text to display */\n text: string;\n /** Start time in seconds (relative to doc start) */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n /** Which audio segment this caption belongs to (0-indexed) */\n audioSegment: number;\n /**\n * Optional per-word timing from TTS timing data.\n * When present, enables precise word-level highlighting in social\n * caption style. When absent, word timing is interpolated evenly\n * across the phrase duration.\n */\n words?: CaptionWord[];\n}\n\n/**\n * Caption track for a doc.\n */\nexport interface CaptionTrack {\n /** Caption phrases in chronological order */\n phrases: CaptionPhrase[];\n /** When the captions were generated. Optional so that conversion stays\n * deterministic — `markdownToDoc()` only sets it when the caller\n * supplies a timestamp via `captionsGeneratedAt`. */\n generatedAt?: string;\n /** Algorithm version for regeneration detection */\n version: number;\n}\n\n/**\n * A single audio segment (one MP3 file).\n */\nexport interface AudioSegment {\n /** Path to MP3 file (relative to article media dir) */\n src: string;\n /** Segment name (e.g., \"intro\", \"history\") */\n name: string;\n /** Duration in seconds */\n duration: number;\n /** Start time in overall timeline (calculated) */\n startTime: number;\n}\n\n// ============================================\n// Audio Timing Data (from TTS)\n// ============================================\n\n/**\n * A single word-level timing bookmark from TTS synthesis.\n * Used for precise audio-to-content mapping and word-level caption sync.\n */\nexport interface AudioBookmark {\n /** Unique identifier (e.g., \"word-0\", \"word-1\") */\n id: string;\n /** Timestamp in seconds from audio start */\n time: number;\n /** Character offset in the source text */\n charOffset: number;\n /** The word text at this position */\n textFragment?: string;\n}\n\n/**\n * Timing data for an audio segment, typically from a `.timing.json` file\n * generated alongside TTS audio. Contains the source text and per-word\n * timing bookmarks for precise content matching and caption sync.\n */\nexport interface AudioTimingData {\n /** The normalized plain text that was synthesized */\n sourceText: string;\n /** Word-level timing bookmarks */\n bookmarks: AudioBookmark[];\n /** Total audio duration in seconds */\n duration: number;\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\n/**\n * Calculate total duration from audio segments.\n */\nexport function calculateDuration(audio: AudioTrack): number {\n return audio.segments.reduce((sum, seg) => sum + seg.duration, 0);\n}\n\n/**\n * Find which audio segment is playing at a given time.\n */\nexport function getSegmentAtTime(audio: AudioTrack, time: number): number {\n let elapsed = 0;\n for (let i = 0; i < audio.segments.length; i++) {\n elapsed += audio.segments[i].duration;\n if (time < elapsed) return i;\n }\n return audio.segments.length - 1;\n}\n\n/**\n * Find which block should be visible at a given time.\n */\nexport function getBlockAtTime(blocks: Block[], time: number): Block | null {\n for (let i = blocks.length - 1; i >= 0; i--) {\n const block = blocks[i];\n if (time >= block.startTime && time < block.startTime + block.duration) {\n return block;\n }\n }\n return blocks[0] || null;\n}\n\n/**\n * Find the caption phrase that should be displayed at a given time.\n */\nexport function getCaptionAtTime(\n captions: CaptionTrack | undefined,\n time: number,\n): CaptionPhrase | null {\n if (!captions || !captions.phrases.length) return null;\n\n for (const phrase of captions.phrases) {\n if (time >= phrase.startTime && time < phrase.endTime) {\n return phrase;\n }\n }\n return null;\n}\n","/**\n * Media-clip timing model.\n *\n * An additive layer on top of the legacy sequential `Doc.audio.segments[]`\n * narration track. A {@link MediaClip} is a piece of audio or video whose\n * timing is expressed *relative to its parent block* (or the whole document),\n * with an optional `startAt` offset and an optional `spillover` past the\n * block's end. {@link resolveMediaSchedule} flattens these into absolute,\n * doc-timeline `ScheduledClip`s that playback and export consume.\n *\n * Authoring: a body-level `{[audio …]}` / `{[video …]}` annotation inside a\n * block becomes a `block.media` clip; the same annotation in the preamble\n * (before the first heading) with `anchor=document` becomes a\n * `doc.documentMedia` clip that spans the whole timeline.\n */\n\nimport type { Block, Doc } from './Doc.js';\n\n/**\n * A timed piece of media attached to a block (or the document).\n */\nexport interface MediaClip {\n /** Stable id (for React keys and timeline selection). */\n id: string;\n /** Source path (mp3/mp4/…), relative to the article media dir. */\n src: string;\n kind: 'audio' | 'video';\n /**\n * Seconds from the parent block's start when this clip begins. For an\n * `anchor='document'` clip, seconds from the document start. Default 0.\n */\n startAt: number;\n /** Source in-point within the file (seconds). Default 0. */\n clipStart?: number;\n /** Source out-point within the file (seconds). Default: file/block end. */\n clipEnd?: number;\n /**\n * When false (default) the clip stops at its block's end. When true it\n * keeps playing past the block boundary until the clip itself finishes.\n * Ignored for `anchor='document'` clips (they already span the timeline).\n */\n spillover?: boolean;\n /**\n * `'block'` (default) — timed relative to the parent block. `'document'` —\n * timed relative to the document start and able to span every block.\n */\n anchor: 'block' | 'document';\n /**\n * 1-based source line of the authoring annotation, when derived from\n * markdown. Enables round-tripping edits (e.g. the timeline editor rewriting\n * `startAt`) back to the exact line. Absent for programmatically built clips.\n */\n sourceLine?: number;\n}\n\n/**\n * A {@link MediaClip} resolved to absolute document-timeline coordinates.\n * Produced by {@link resolveMediaSchedule}; consumed by the playback\n * scheduler and the MP4 export.\n */\nexport interface ScheduledClip {\n id: string;\n src: string;\n kind: 'audio' | 'video';\n /** Absolute doc-timeline second the clip starts. */\n absoluteStart: number;\n /** Absolute doc-timeline second the clip ends (exclusive). */\n absoluteEnd: number;\n /** Source in-point to seek to at `absoluteStart`. */\n sourceIn: number;\n anchor: 'block' | 'document';\n /** Owning block id for block-anchored clips (for video re-homing/export). */\n blockId?: string;\n /** 1-based source line of the authoring annotation, when known. */\n sourceLine?: number;\n}\n\n/** Depth-first flatten of the block tree (local copy to avoid a doc-layer dep). */\nfunction flatten(blocks: Block[], out: Block[] = []): Block[] {\n for (const b of blocks) {\n out.push(b);\n if (b.children && b.children.length > 0) flatten(b.children, out);\n }\n return out;\n}\n\n/** Played length of a clip when known from its in/out points, else null. */\nfunction clipLength(clip: MediaClip): number | null {\n if (clip.clipEnd == null) return null;\n return Math.max(0, clip.clipEnd - (clip.clipStart ?? 0));\n}\n\n/**\n * Base playback span ignoring media — the larger of the audio-segment sum\n * and the last block's end. Document clips that don't pin a length run to\n * this point.\n */\nfunction baseTimelineEnd(doc: Doc): number {\n const blockEnd = flatten(doc.blocks).reduce(\n (max, b) => Math.max(max, b.startTime + b.duration),\n 0,\n );\n return Math.max(doc.duration ?? 0, blockEnd);\n}\n\n/**\n * Flatten every block clip and document clip into absolute-timed\n * `ScheduledClip`s. Pure — depends only on `doc`.\n */\nexport function resolveMediaSchedule(doc: Doc): ScheduledClip[] {\n const out: ScheduledClip[] = [];\n const docEnd = baseTimelineEnd(doc);\n\n for (const block of flatten(doc.blocks)) {\n const blockEnd = block.startTime + block.duration;\n for (const clip of block.media ?? []) {\n const start = block.startTime + clip.startAt;\n const len = clipLength(clip);\n let end: number;\n if (clip.spillover) {\n // Continue past the block; fall back to the block end when the\n // played length is unknown (no clipEnd authored).\n end = len != null ? start + len : blockEnd;\n } else {\n end = len != null ? Math.min(start + len, blockEnd) : blockEnd;\n }\n out.push({\n id: clip.id,\n src: clip.src,\n kind: clip.kind,\n absoluteStart: start,\n absoluteEnd: Math.max(start, end),\n sourceIn: clip.clipStart ?? 0,\n anchor: 'block',\n blockId: block.id,\n ...(clip.sourceLine != null ? { sourceLine: clip.sourceLine } : {}),\n });\n }\n }\n\n for (const clip of doc.documentMedia ?? []) {\n const start = clip.startAt;\n const len = clipLength(clip);\n const end = len != null ? start + len : docEnd;\n out.push({\n id: clip.id,\n src: clip.src,\n kind: clip.kind,\n absoluteStart: start,\n absoluteEnd: Math.max(start, end),\n sourceIn: clip.clipStart ?? 0,\n anchor: 'document',\n ...(clip.sourceLine != null ? { sourceLine: clip.sourceLine } : {}),\n });\n }\n\n return out;\n}\n\n/**\n * Total playback duration including any media that spills past the last\n * block (or audio segment). Export uses this for the frame count and the\n * player for its effective timeline length.\n */\nexport function getDocPlaybackDuration(doc: Doc): number {\n const base = baseTimelineEnd(doc);\n const mediaEnd = resolveMediaSchedule(doc).reduce((max, c) => Math.max(max, c.absoluteEnd), 0);\n return Math.max(base, mediaEnd);\n}\n","/**\n * Theme Compilation\n *\n * Turns a partial Theme (typically authored by the customizer panel from\n * a few seed colors and preset choices) into a complete, validated Theme\n * by:\n *\n * 1. Filling unspecified fields from a hardcoded `STARTER_THEME`.\n * 2. Deriving missing color slots from `seedColors` via OKLCh math.\n * 3. Validating the result.\n *\n * Built-in themes ship as fully-specified Theme JSON and skip this step.\n */\n\nimport type {\n Theme,\n DeepPartial,\n ThemeColorPalette,\n ThemeColorScheme,\n ThemeSeedColors,\n FontFamily,\n} from './Theme.js';\nimport { THEME_SCHEMA_VERSION, createTheme } from './Theme.js';\nimport { assertTheme } from './themeValidator.js';\nimport {\n oklchLighten,\n oklchDarken,\n oklchSetChroma,\n pickContrastingText,\n relativeLuminance,\n} from './colorUtils.js';\n\n/** Internal default theme used to fill in fields the customizer doesn't expose. */\nconst STARTER_BODY_FONT: FontFamily = { stackId: 'system-sans' };\nconst STARTER_TITLE_FONT: FontFamily = { stackId: 'system-serif' };\nconst STARTER_MONO_FONT: FontFamily = { stackId: 'system-mono' };\n\nconst STARTER_COLOR_SCHEMES: Record<string, ThemeColorScheme> = {\n blue: { bg: '#1a365d', text: '#63b3ed', accent: '#90cdf4' },\n green: { bg: '#22543d', text: '#9ae6b4', accent: '#68d391' },\n purple: { bg: '#44337a', text: '#d6bcfa', accent: '#b794f4' },\n red: { bg: '#742a2a', text: '#fc8181', accent: '#feb2b2' },\n orange: { bg: '#744210', text: '#fbd38d', accent: '#f6ad55' },\n teal: { bg: '#234e52', text: '#81e6d9', accent: '#4fd1c5' },\n};\n\nexport const STARTER_THEME: Theme = {\n schemaVersion: THEME_SCHEMA_VERSION,\n id: 'custom',\n name: 'Custom Theme',\n description: 'Customizer starter — gets overridden by user choices.',\n colors: {\n primary: '#3182ce',\n secondary: '#4a5568',\n background: '#1a202c',\n backgroundLight: '#2d3748',\n text: '#f7fafc',\n textMuted: '#a0aec0',\n highlight: '#4299e1',\n warning: '#fc8181',\n },\n typography: {\n bodyFont: STARTER_BODY_FONT,\n titleFont: STARTER_TITLE_FONT,\n monoFont: STARTER_MONO_FONT,\n titleWeight: 'bold',\n },\n style: {\n textShadow: true,\n overlayOpacity: 0.45,\n animationSpeed: 1.0,\n borderRadius: 6,\n },\n renderStyle: {\n name: 'standard',\n defaultTextAnimation: 'fadeIn',\n defaultImageAnimation: 'slowZoom',\n ambientMotion: true,\n defaultTransition: { type: 'fade', duration: 0.7 },\n },\n colorSchemes: STARTER_COLOR_SCHEMES,\n};\n\n/**\n * Derive a full `ThemeColorPalette` from `seedColors`, with any explicit\n * `partialColors` taking precedence over derived values. Background and\n * text are guessed from luminance when the user gave only a primary.\n */\nexport function deriveColorPalette(\n seeds: ThemeSeedColors,\n partialColors: Partial<ThemeColorPalette> = {},\n opts: { contrast?: 'subtle' | 'balanced' | 'high' } = {},\n): ThemeColorPalette {\n const spread = opts.contrast === 'high' ? 0.22 : opts.contrast === 'subtle' ? 0.08 : 0.15;\n\n const primary = seeds.primary;\n const secondary = seeds.secondary ?? oklchSetChroma(oklchLighten(primary, 0.05), 0.5);\n const accent = seeds.accent ?? oklchLighten(primary, spread);\n\n // Decide light vs dark surface from the explicit background, the seed background,\n // or default to dark.\n const bgSeed = partialColors.background ?? seeds.background;\n let background: string;\n if (bgSeed) {\n background = bgSeed;\n } else {\n // Pick a dark or light background that contrasts with primary\n background = relativeLuminance(primary) > 0.5 ? '#0a0a0a' : '#1a202c';\n }\n const isLightSurface = relativeLuminance(background) > 0.5;\n\n const backgroundLight =\n partialColors.backgroundLight ??\n (isLightSurface ? oklchDarken(background, 0.04) : oklchLighten(background, 0.04));\n\n const text =\n partialColors.text ?? seeds.text ?? pickContrastingText(background, '#f7fafc', '#1a202c');\n const textMuted =\n partialColors.textMuted ??\n (isLightSurface ? oklchLighten(text, 0.25) : oklchDarken(text, 0.25));\n\n const highlight = partialColors.highlight ?? accent;\n const warning = partialColors.warning ?? '#fc8181';\n\n return {\n primary: partialColors.primary ?? primary,\n secondary: partialColors.secondary ?? secondary,\n background,\n backgroundLight,\n text,\n textMuted,\n highlight,\n warning,\n };\n}\n\n/** Map a contrast preset to numeric spread used by `deriveColorPalette`. */\nexport type ContrastPreset = 'subtle' | 'balanced' | 'high';\n\nexport interface CompileOptions {\n /** Contrast level for OKLCh derivation (default 'balanced'). */\n contrast?: ContrastPreset;\n /**\n * Base theme to inherit from. When provided, the partial is deep-merged\n * over this theme instead of the neutral `STARTER_THEME`, so the compiled\n * theme inherits the base's render style, color schemes, typography, and\n * persistent layers — and only the fields the user changed are overridden.\n * The base's id is recorded on the result as `basedOn`.\n */\n base?: Theme;\n}\n\n/**\n * Compile a partial Theme into a complete one. Fills missing fields from\n * the base (`opts.base` when given, else `STARTER_THEME`), derives missing\n * color slots from `seedColors` (when present), and validates the result.\n */\nexport function compileTheme(partial: DeepPartial<Theme>, opts: CompileOptions = {}): Theme {\n // Step 1: deep-merge over the base (a chosen theme, or the neutral starter)\n const base = opts.base ?? STARTER_THEME;\n const merged = createTheme(base, partial);\n merged.schemaVersion = THEME_SCHEMA_VERSION;\n // Record which theme this was derived from so a customizer can re-inherit.\n if (opts.base && !merged.basedOn) merged.basedOn = opts.base.id;\n\n // Step 1b: typography fonts are discriminated unions ({stackId} | {custom}),\n // so deep-merge would leave stale keys from the starter when the partial\n // switches form. Replace each font wholesale when explicitly provided.\n const partialTypography = partial.typography;\n if (partialTypography) {\n if (partialTypography.titleFont !== undefined) {\n merged.typography.titleFont = partialTypography.titleFont as FontFamily;\n }\n if (partialTypography.bodyFont !== undefined) {\n merged.typography.bodyFont = partialTypography.bodyFont as FontFamily;\n }\n if (partialTypography.monoFont !== undefined) {\n merged.typography.monoFont = partialTypography.monoFont as FontFamily;\n }\n }\n\n // Step 1c: colorSchemes is a map the user edits wholesale (add / remove\n // accents). Deep-merge can't delete keys, so replace it outright when the\n // partial provides it — mirroring the font replacement above. This lets a\n // removed accent actually disappear rather than lingering from the base.\n if (partial.colorSchemes !== undefined) {\n merged.colorSchemes = partial.colorSchemes as Record<string, ThemeColorScheme>;\n }\n\n // Step 2: derive missing color slots from seeds, if present\n if (merged.seedColors) {\n const partialColors = (partial.colors ?? {}) as Partial<ThemeColorPalette>;\n merged.colors = deriveColorPalette(merged.seedColors, partialColors, {\n contrast: opts.contrast,\n });\n }\n\n // Step 3: validate\n return assertTheme(merged, `compiled theme \"${merged.id}\"`);\n}\n\n/**\n * Parse a JSON string into a validated Theme. Throws on invalid input.\n */\nexport function parseTheme(json: string): Theme {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Invalid theme JSON: ${msg}`);\n }\n return assertTheme(parsed, 'parsed theme');\n}\n\n/**\n * Serialize a Theme to a stable, pretty-printed JSON string. Round-trips\n * cleanly through `parseTheme`.\n */\nexport function serializeTheme(theme: Theme): string {\n return JSON.stringify(theme, null, 2);\n}\n"],"mappings":"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","names":[]}