@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,41 @@
1
+ /**
2
+ * ASCII tree renderer: nested nodes → clean tree art.
3
+ *
4
+ * A deterministic depth-first walk emitting rail/branch prefixes. The
5
+ * fixpoint is structural: `parseTree(renderTree(t))` preserves the node
6
+ * hierarchy + labels + comments, and `renderTree(parseTree(renderTree(t)))`
7
+ * is byte-identical (idempotent after one normalization cycle). Unlike the
8
+ * diagram renderer there is no spatial layout, so jitter is impossible.
9
+ */
10
+
11
+ import { ASCII_TREE_VOCAB, UNICODE_TREE_VOCAB } from './chars.js';
12
+ import type { Tree, TreeNode } from './types.js';
13
+
14
+ export interface RenderTreeOptions {
15
+ /** Character vocabulary; defaults to the tree's own detected style. */
16
+ style?: 'unicode' | 'ascii';
17
+ }
18
+
19
+ export function renderTree(tree: Tree, options: RenderTreeOptions = {}): string {
20
+ const vocab = (options.style ?? tree.style) === 'ascii' ? ASCII_TREE_VOCAB : UNICODE_TREE_VOCAB;
21
+ const lines: string[] = [];
22
+
23
+ const label = (node: TreeNode): string =>
24
+ node.comment ? `${node.label} # ${node.comment}` : node.label;
25
+
26
+ const walk = (nodes: readonly TreeNode[], prefix: string): void => {
27
+ nodes.forEach((node, i) => {
28
+ const last = i === nodes.length - 1;
29
+ lines.push(prefix + (last ? vocab.elbow : vocab.tee) + label(node));
30
+ walk(node.children, prefix + (last ? vocab.gap : vocab.rail));
31
+ });
32
+ };
33
+
34
+ // Roots sit at column 0 with no connector; their children get branches.
35
+ for (const root of tree.roots) {
36
+ lines.push(label(root));
37
+ walk(root.children, '');
38
+ }
39
+
40
+ return lines.join('\n');
41
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Public types for the ASCII treeview codec.
3
+ *
4
+ * A tree is a hierarchy of labelled nodes (file trees, dependency trees,
5
+ * category outlines). Unlike the diagram codec's spatial grid model, the
6
+ * tree model is a plain nested structure — rendering is a deterministic
7
+ * tree-walk, so the parse↔render fixpoint is structural and trivially
8
+ * byte-stable.
9
+ */
10
+
11
+ export interface TreeNode {
12
+ /**
13
+ * Stable id: slug of the label, deduplicated with `-2`/`-3` suffixes in
14
+ * document (depth-first) order. Identical art re-parses to identical ids
15
+ * (the outline widget's selection resolves by id).
16
+ */
17
+ id: string;
18
+ /** Display label, verbatim (may keep a trailing `/`). */
19
+ label: string;
20
+ /** Child nodes (empty for leaves). */
21
+ children: TreeNode[];
22
+ /** True when this node reads as a container (trailing `/` or has children). */
23
+ isDir?: boolean;
24
+ /** Trailing `# …` / `<-- …` comment, preserved across round-trips. */
25
+ comment?: string;
26
+ }
27
+
28
+ export interface Tree {
29
+ /** Top-level nodes (a forest — usually one root for a file tree). */
30
+ roots: TreeNode[];
31
+ /** Character vocabulary detected in the source; render emits the same. */
32
+ style: 'unicode' | 'ascii';
33
+ /** Non-fatal parse notes (ragged dedents, dropped lines, …). */
34
+ warnings: string[];
35
+ }
36
+
37
+ export interface TreeDetection {
38
+ isTree: boolean;
39
+ /** Present when `isTree` — avoids a second parse in callers. */
40
+ tree?: Tree;
41
+ /** Machine-greppable accept/reject reasons. */
42
+ reasons: string[];
43
+ }
@@ -50,16 +50,9 @@ function isFullBleedCoverMedia(layer: Layer): boolean {
50
50
  return fit === undefined || fit === 'cover';
51
51
  }
52
52
 
53
- /**
54
- * Whether the theme asks for ambient Ken Burns on otherwise-static imagery.
55
- * `defaultImageAnimation: 'slowZoom'` is treated as an alias for the boolean
56
- * (that's what themes authoring it were reaching for).
57
- */
53
+ /** Whether the theme asks for ambient Ken Burns on otherwise-static imagery. */
58
54
  export function themeWantsAmbientMotion(theme: Theme): boolean {
59
- return (
60
- theme.renderStyle.ambientMotion === true ||
61
- theme.renderStyle.defaultImageAnimation === 'slowZoom'
62
- );
55
+ return theme.renderStyle.ambientMotion === true;
63
56
  }
64
57
 
65
58
  /**
@@ -0,0 +1,78 @@
1
+ /** Text fitting shared by the live diagram canvas and template renderer. */
2
+
3
+ export const DIAGRAM_LABEL_LINE_HEIGHT = 1.25;
4
+ export const DIAGRAM_LABEL_HORIZONTAL_PADDING = 24;
5
+ export const DIAGRAM_LABEL_VERTICAL_PADDING = 16;
6
+ export const DIAGRAM_LABEL_MIN_FONT_SIZE = 10;
7
+
8
+ export interface DiagramLabelFit {
9
+ fontSize: number;
10
+ lineCount: number;
11
+ /** Upward shift that centers the full line group around the node midpoint. */
12
+ firstLineOffset: number;
13
+ textWidth: number;
14
+ }
15
+
16
+ /**
17
+ * Fit a plain diagram label inside a node card using the same character-width
18
+ * estimate and word wrapping as React's `TextLayer`.
19
+ */
20
+ export function fitDiagramLabel(
21
+ text: string,
22
+ boxWidth: number,
23
+ boxHeight: number,
24
+ preferredFontSize: number,
25
+ ): DiagramLabelFit {
26
+ const textWidth = Math.max(1, boxWidth - DIAGRAM_LABEL_HORIZONTAL_PADDING);
27
+ const textHeight = Math.max(1, boxHeight - DIAGRAM_LABEL_VERTICAL_PADDING);
28
+ const preferred = Math.max(DIAGRAM_LABEL_MIN_FONT_SIZE, Math.floor(preferredFontSize));
29
+
30
+ let fontSize = preferred;
31
+ let lineCount = countWrappedLines(text, fontSize, textWidth);
32
+ while (
33
+ fontSize > DIAGRAM_LABEL_MIN_FONT_SIZE &&
34
+ lineCount * fontSize * DIAGRAM_LABEL_LINE_HEIGHT > textHeight
35
+ ) {
36
+ fontSize--;
37
+ lineCount = countWrappedLines(text, fontSize, textWidth);
38
+ }
39
+
40
+ return {
41
+ fontSize,
42
+ lineCount,
43
+ firstLineOffset:
44
+ lineCount === 1 ? 0 : -((lineCount - 1) * fontSize * DIAGRAM_LABEL_LINE_HEIGHT) / 2,
45
+ textWidth,
46
+ };
47
+ }
48
+
49
+ function countWrappedLines(text: string, fontSize: number, maxWidth: number): number {
50
+ return text
51
+ .split('\n')
52
+ .reduce((count, line) => count + wrappedLineCount(line, fontSize, maxWidth), 0);
53
+ }
54
+
55
+ function wrappedLineCount(text: string, fontSize: number, maxWidth: number): number {
56
+ if (!text.trim()) return 1;
57
+ const charsPerLine = Math.floor(maxWidth / (fontSize * 0.5));
58
+ if (charsPerLine <= 0) return 1;
59
+
60
+ let lines = 0;
61
+ let currentLength = 0;
62
+ for (const word of text.split(/\s+/)) {
63
+ const testLength = currentLength > 0 ? currentLength + 1 + word.length : word.length;
64
+ if (testLength <= charsPerLine) {
65
+ currentLength = testLength;
66
+ continue;
67
+ }
68
+ if (currentLength > 0) lines++;
69
+ if (word.length > charsPerLine) {
70
+ lines += Math.floor((word.length - 1) / charsPerLine);
71
+ currentLength = word.length % charsPerLine || charsPerLine;
72
+ } else {
73
+ currentLength = word.length;
74
+ }
75
+ }
76
+ if (currentLength > 0) lines++;
77
+ return Math.max(1, lines);
78
+ }
@@ -9,9 +9,11 @@
9
9
  * - `markerPath(style, dir)` → the `<marker>` path for a line end-style, in the
10
10
  * standard 0–10 marker viewBox (shared by the SSR `PathLayer` renderer and the
11
11
  * editor's edge renderer).
12
- * - `connectorPath(routing, start, end)` + `clipEndpoints(a, b)` → the line/edge
12
+ * - `connectorPath(routing, start, end)` + `snapEndpoints(a, b)` → the line/edge
13
13
  * geometry between two shapes (straight / orthogonal / curved), lifted from the
14
- * diagram template so drawings and diagrams draw identical connectors.
14
+ * diagram template so drawings and diagrams draw identical connectors. The older
15
+ * `clipEndpoints(a, b)` helper remains for free-angle clipping, but rendered
16
+ * connectors use snapped ports so authored lines land on stable attachment points.
15
17
  */
16
18
 
17
19
  import type { MarkerStyle } from '../../schemas/Doc.js';
@@ -411,6 +413,27 @@ export interface ClipBox {
411
413
  ry: number;
412
414
  }
413
415
 
416
+ export type ConnectorPort =
417
+ | 'top'
418
+ | 'right'
419
+ | 'bottom'
420
+ | 'left'
421
+ | 'top-left'
422
+ | 'top-right'
423
+ | 'bottom-right'
424
+ | 'bottom-left';
425
+
426
+ export interface ConnectorSnapPoint {
427
+ port: ConnectorPort;
428
+ x: number;
429
+ y: number;
430
+ }
431
+
432
+ export interface ConnectorAnchor {
433
+ side: 'top' | 'right' | 'bottom' | 'left';
434
+ offset: number;
435
+ }
436
+
414
437
  /** Intersection of the center-to-center line with `from`'s bounding box. */
415
438
  export function clipPoint(from: ClipBox, to: ClipBox): { x: number; y: number } {
416
439
  const dx = to.cx - from.cx;
@@ -430,23 +453,150 @@ export function clipEndpoints(
430
453
  return { start: clipPoint(a, b), end: clipPoint(b, a) };
431
454
  }
432
455
 
456
+ /** The stable connector ports exposed around a shape/card box. */
457
+ export function snapPoints(box: ClipBox): ConnectorSnapPoint[] {
458
+ const left = box.cx - box.rx;
459
+ const right = box.cx + box.rx;
460
+ const top = box.cy - box.ry;
461
+ const bottom = box.cy + box.ry;
462
+ return [
463
+ { port: 'top', x: box.cx, y: top },
464
+ { port: 'right', x: right, y: box.cy },
465
+ { port: 'bottom', x: box.cx, y: bottom },
466
+ { port: 'left', x: left, y: box.cy },
467
+ { port: 'top-left', x: left, y: top },
468
+ { port: 'top-right', x: right, y: top },
469
+ { port: 'bottom-right', x: right, y: bottom },
470
+ { port: 'bottom-left', x: left, y: bottom },
471
+ ];
472
+ }
473
+
474
+ /** Resolve a normalized authored anchor to a concrete point on a box. */
475
+ export function anchorPoint(box: ClipBox, anchor: ConnectorAnchor): ConnectorSnapPoint {
476
+ const offset = Math.max(0, Math.min(1, anchor.offset));
477
+ const left = box.cx - box.rx;
478
+ const right = box.cx + box.rx;
479
+ const top = box.cy - box.ry;
480
+ const bottom = box.cy + box.ry;
481
+ switch (anchor.side) {
482
+ case 'top':
483
+ return { port: 'top', x: left + (right - left) * offset, y: top };
484
+ case 'right':
485
+ return { port: 'right', x: right, y: top + (bottom - top) * offset };
486
+ case 'bottom':
487
+ return { port: 'bottom', x: left + (right - left) * offset, y: bottom };
488
+ case 'left':
489
+ return { port: 'left', x: left, y: top + (bottom - top) * offset };
490
+ }
491
+ }
492
+
493
+ /** Nearest stable connector port on `box` to an arbitrary point. */
494
+ export function nearestSnapPoint(
495
+ box: ClipBox,
496
+ point: { x: number; y: number },
497
+ ): ConnectorSnapPoint {
498
+ let best = snapPoints(box)[0];
499
+ let bestScore = Number.POSITIVE_INFINITY;
500
+ for (const candidate of snapPoints(box)) {
501
+ const score = distanceSq(candidate, point) + portTieBreak(candidate.port);
502
+ if (score < bestScore) {
503
+ best = candidate;
504
+ bestScore = score;
505
+ }
506
+ }
507
+ return best;
508
+ }
509
+
510
+ /**
511
+ * Pick the pair of connector ports, one on each box, with the shortest
512
+ * distance between them. This gives stable attachments to side/corner handles
513
+ * instead of arbitrary center-ray intersections.
514
+ */
515
+ export function snapEndpoints(
516
+ a: ClipBox,
517
+ b: ClipBox,
518
+ ): { start: ConnectorSnapPoint; end: ConnectorSnapPoint } {
519
+ let bestStart = snapPoints(a)[0];
520
+ let bestEnd = snapPoints(b)[0];
521
+ let bestScore = Number.POSITIVE_INFINITY;
522
+ for (const start of snapPoints(a)) {
523
+ for (const end of snapPoints(b)) {
524
+ const score = distanceSq(start, end) + portTieBreak(start.port) + portTieBreak(end.port);
525
+ if (score < bestScore) {
526
+ bestStart = start;
527
+ bestEnd = end;
528
+ bestScore = score;
529
+ }
530
+ }
531
+ }
532
+ return { start: bestStart, end: bestEnd };
533
+ }
534
+
535
+ function distanceSq(a: { x: number; y: number }, b: { x: number; y: number }): number {
536
+ const dx = a.x - b.x;
537
+ const dy = a.y - b.y;
538
+ return dx * dx + dy * dy;
539
+ }
540
+
541
+ function portTieBreak(port: ConnectorPort): number {
542
+ return port.includes('-') ? 0.02 : 0;
543
+ }
544
+
433
545
  /** Build the SVG `d` for a connector between two clipped endpoints. */
434
546
  export function connectorPath(
435
547
  routing: ConnectorRouting,
436
- start: { x: number; y: number },
437
- end: { x: number; y: number },
548
+ start: { x: number; y: number; port?: ConnectorPort },
549
+ end: { x: number; y: number; port?: ConnectorPort },
438
550
  ): string {
439
551
  if (routing === 'straight') {
440
552
  return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
441
553
  }
442
554
  if (routing === 'orthogonal') {
555
+ const startAxis = portAxis(start.port);
556
+ const endAxis = portAxis(end.port);
557
+ if (startAxis === 'vertical' && endAxis === 'vertical') {
558
+ const midY = (start.y + end.y) / 2;
559
+ return `M ${r(start.x)} ${r(start.y)} L ${r(start.x)} ${r(midY)} L ${r(end.x)} ${r(midY)} L ${r(end.x)} ${r(end.y)}`;
560
+ }
561
+ if (startAxis === 'vertical' && endAxis === 'horizontal') {
562
+ return `M ${r(start.x)} ${r(start.y)} L ${r(start.x)} ${r(end.y)} L ${r(end.x)} ${r(end.y)}`;
563
+ }
564
+ if (startAxis === 'horizontal' && endAxis === 'vertical') {
565
+ return `M ${r(start.x)} ${r(start.y)} L ${r(end.x)} ${r(start.y)} L ${r(end.x)} ${r(end.y)}`;
566
+ }
443
567
  const midX = (start.x + end.x) / 2;
444
568
  return `M ${r(start.x)} ${r(start.y)} L ${r(midX)} ${r(start.y)} L ${r(midX)} ${r(end.y)} L ${r(end.x)} ${r(end.y)}`;
445
569
  }
446
570
  // curved
447
- const dx = Math.abs(end.x - start.x);
448
- const cp = Math.max(40, dx / 2);
449
- return `M ${r(start.x)} ${r(start.y)} C ${r(start.x + cp)} ${r(start.y)}, ${r(end.x - cp)} ${r(end.y)}, ${r(end.x)} ${r(end.y)}`;
571
+ const distance = Math.hypot(end.x - start.x, end.y - start.y);
572
+ const cp = Math.max(40, distance / 2);
573
+ const startVector = portVector(start.port, { x: 1, y: 0 });
574
+ const endVector = portVector(end.port, { x: -1, y: 0 });
575
+ return `M ${r(start.x)} ${r(start.y)} C ${r(start.x + startVector.x * cp)} ${r(start.y + startVector.y * cp)}, ${r(end.x + endVector.x * cp)} ${r(end.y + endVector.y * cp)}, ${r(end.x)} ${r(end.y)}`;
576
+ }
577
+
578
+ function portAxis(port: ConnectorPort | undefined): 'horizontal' | 'vertical' | null {
579
+ if (port === 'top' || port === 'bottom') return 'vertical';
580
+ if (port === 'left' || port === 'right') return 'horizontal';
581
+ return null;
582
+ }
583
+
584
+ function portVector(
585
+ port: ConnectorPort | undefined,
586
+ fallback: { x: number; y: number },
587
+ ): { x: number; y: number } {
588
+ switch (port) {
589
+ case 'top':
590
+ return { x: 0, y: -1 };
591
+ case 'right':
592
+ return { x: 1, y: 0 };
593
+ case 'bottom':
594
+ return { x: 0, y: 1 };
595
+ case 'left':
596
+ return { x: -1, y: 0 };
597
+ default:
598
+ return fallback;
599
+ }
450
600
  }
451
601
 
452
602
  /** Map a `lineStyle` (solid|dashed|dotted) to an SVG `stroke-dasharray`. */
@@ -36,12 +36,12 @@ import { deriveTemplateInputs } from './templateInputs.js';
36
36
  import { lintTemplateParams } from './templates/inputDescriptors.js';
37
37
  import {
38
38
  templateRegistry,
39
- TEMPLATE_ALIASES,
40
39
  resolveTemplateName,
41
40
  isShapeName,
42
41
  normalizeShapeKind,
43
42
  SHAPE_NAMES,
44
43
  } from './templates/index.js';
44
+ import { TEMPLATE_ALIASES } from './templates/templateNames.js';
45
45
  import { nearestName } from './utils/nearest.js';
46
46
 
47
47
  // ============================================
@@ -38,8 +38,13 @@ export interface MapOptions {
38
38
  audioSegment: number;
39
39
  colorScheme?: ColorScheme;
40
40
  accentImage?: AccentImage;
41
+ /** Doc-timeline SECONDS when the extracted content is spoken. */
41
42
  sourceStartTime?: number;
42
43
  sourceDuration?: number;
44
+ /** Provenance: the source block the element was extracted from. */
45
+ sourceBlockId?: string;
46
+ /** Provenance: char offset of the extraction in the source plain text. */
47
+ sourceCharOffset?: number;
43
48
  }
44
49
 
45
50
  /**
@@ -52,6 +57,8 @@ export function mapElementToBlock(element: ExtractedElement, options: MapOptions
52
57
  audioSegment: options.audioSegment,
53
58
  sourceStartTime: options.sourceStartTime,
54
59
  sourceDuration: options.sourceDuration,
60
+ sourceBlockId: options.sourceBlockId,
61
+ sourceCharOffset: options.sourceCharOffset,
55
62
  };
56
63
 
57
64
  switch (element.data.type) {
@@ -203,9 +203,10 @@ function renderPathToSvg(
203
203
  const fill = c.fill ?? 'none';
204
204
  const dash = c.dasharray ? ` stroke-dasharray="${escapeAttr(c.dasharray)}"` : '';
205
205
 
206
- // Markers: explicit start/end win; otherwise derive from the legacy `arrow` flag.
207
- const startStyle = effectiveExportMarker(c.startMarker, c.arrow, 'start');
208
- const endStyle = effectiveExportMarker(c.endMarker, c.arrow, 'end');
206
+ // Keep old serialized documents readable without retaining `arrow` in the public type.
207
+ const legacyArrow = (c as typeof c & { arrow?: 'none' | 'end' | 'start' | 'both' }).arrow;
208
+ const startStyle = effectiveExportMarker(c.startMarker, legacyArrow, 'start');
209
+ const endStyle = effectiveExportMarker(c.endMarker, legacyArrow, 'end');
209
210
  const start = markerPath(startStyle, 'start');
210
211
  const end = markerPath(endStyle, 'end');
211
212
  const startId = `marker-start-${layer.id}`;
@@ -228,11 +229,11 @@ function renderPathToSvg(
228
229
 
229
230
  function effectiveExportMarker(
230
231
  explicit: MarkerStyle | undefined,
231
- arrow: (ImageEditLayer & { type: 'path' })['content']['arrow'],
232
+ legacyArrow: 'none' | 'end' | 'start' | 'both' | undefined,
232
233
  end: 'start' | 'end',
233
234
  ): MarkerStyle {
234
235
  if (explicit) return explicit;
235
- return arrow === 'both' || arrow === end ? 'arrow' : 'none';
236
+ return legacyArrow === 'both' || legacyArrow === end ? 'arrow' : 'none';
236
237
  }
237
238
 
238
239
  function markerDefSvg(
@@ -10,7 +10,11 @@
10
10
  * `.md` so the two histories live side-by-side without colliding.
11
11
  */
12
12
 
13
- import { formatVersionTimestamp, parseVersionTimestamp } from '../versions/timestamp.js';
13
+ import {
14
+ buildSnapshotPath,
15
+ parseSnapshotPath,
16
+ type SnapshotPathStrategy,
17
+ } from '../versions/snapshotStore.js';
14
18
 
15
19
  /** Subfolder (inside the sidecar) that holds image-editor snapshots. */
16
20
  export const IMAGE_EDIT_VERSIONS_PREFIX = '.versions/';
@@ -18,11 +22,15 @@ export const IMAGE_EDIT_VERSIONS_PREFIX = '.versions/';
18
22
  /** Default basename when none is supplied. Matches `IMAGE_EDIT_STATE_FILENAME` stem. */
19
23
  export const IMAGE_EDIT_DEFAULT_BASENAME = 'state';
20
24
 
25
+ /** Internal strategy shared with the generic snapshot store. */
26
+ export const IMAGE_EDIT_VERSION_PATHS: SnapshotPathStrategy = Object.freeze({
27
+ prefix: IMAGE_EDIT_VERSIONS_PREFIX,
28
+ extension: 'json',
29
+ });
30
+
21
31
  /** Build a snapshot path. Optionally append a collision suffix. */
22
32
  export function buildImageEditVersionPath(basename: string, date: Date, collision = 0): string {
23
- const stamp = formatVersionTimestamp(date);
24
- const suffix = collision > 0 ? `-${collision + 1}` : '';
25
- return `${IMAGE_EDIT_VERSIONS_PREFIX}${basename}.${stamp}${suffix}.json`;
33
+ return buildSnapshotPath(IMAGE_EDIT_VERSION_PATHS, basename, date, collision);
26
34
  }
27
35
 
28
36
  /**
@@ -32,26 +40,5 @@ export function buildImageEditVersionPath(basename: string, date: Date, collisio
32
40
  export function parseImageEditVersionPath(
33
41
  path: string,
34
42
  ): { basename: string; timestamp: Date; collision: number } | null {
35
- if (!path.startsWith(IMAGE_EDIT_VERSIONS_PREFIX)) return null;
36
- const rest = path.slice(IMAGE_EDIT_VERSIONS_PREFIX.length);
37
- if (!rest.endsWith('.json')) return null;
38
- const stem = rest.slice(0, -'.json'.length);
39
- const lastDot = stem.lastIndexOf('.');
40
- if (lastDot <= 0) return null;
41
- const basename = stem.slice(0, lastDot);
42
- const tail = stem.slice(lastDot + 1);
43
- const dash = tail.indexOf('-');
44
- let stamp: string;
45
- let collision = 0;
46
- if (dash >= 0) {
47
- stamp = tail.slice(0, dash);
48
- const n = Number(tail.slice(dash + 1));
49
- if (!Number.isInteger(n) || n < 2) return null;
50
- collision = n - 1;
51
- } else {
52
- stamp = tail;
53
- }
54
- const timestamp = parseVersionTimestamp(stamp);
55
- if (!timestamp) return null;
56
- return { basename, timestamp, collision };
43
+ return parseSnapshotPath(IMAGE_EDIT_VERSION_PATHS, path);
57
44
  }
@@ -5,23 +5,24 @@
5
5
  * Mirrors `core/src/versions/operations.ts` but operates on
6
6
  * {@link ImageEditDoc} JSON state at `.versions/<basename>.<timestamp>.json`
7
7
  * instead of the markdown document. Reuses {@link PrunePolicy} so hosts
8
- * can share their pruning configuration between the two histories.
8
+ * can share their pruning configuration between the two histories. The
9
+ * sidecar root gets a `.gitignore` rule for `.versions/` when snapshots
10
+ * are retained.
9
11
  */
10
12
 
11
13
  import type { ContentContainer } from '../storage/ContentContainer.js';
12
14
  import type { ImageEditDoc } from '../schemas/ImageEditDoc.js';
13
15
  import type { CoalesceOptions, PrunePolicy, Version } from '../versions/types.js';
16
+ import {
17
+ coalesceSnapshots,
18
+ listSnapshots,
19
+ pruneSnapshots,
20
+ readTextSnapshot,
21
+ saveTextSnapshot,
22
+ } from '../versions/snapshotStore.js';
14
23
  import { readImageEditDoc, writeImageEditDoc } from './persistence.js';
15
24
  import { IMAGE_EDIT_STATE_FILENAME } from './state.js';
16
- import {
17
- IMAGE_EDIT_VERSIONS_PREFIX,
18
- IMAGE_EDIT_DEFAULT_BASENAME,
19
- buildImageEditVersionPath,
20
- parseImageEditVersionPath,
21
- } from './versionPaths.js';
22
-
23
- const decoder = new TextDecoder();
24
- const encoder = new TextEncoder();
25
+ import { IMAGE_EDIT_DEFAULT_BASENAME, IMAGE_EDIT_VERSION_PATHS } from './versionPaths.js';
25
26
 
26
27
  /** Options for {@link saveImageEditVersion}. */
27
28
  export interface SaveImageEditVersionOptions {
@@ -60,26 +61,7 @@ export async function listImageEditVersions(
60
61
  container: ContentContainer,
61
62
  basename?: string,
62
63
  ): Promise<Version[]> {
63
- const entries = await container.listFiles(IMAGE_EDIT_VERSIONS_PREFIX);
64
- const versions: Version[] = [];
65
- for (const entry of entries) {
66
- const parsed = parseImageEditVersionPath(entry.path);
67
- if (!parsed) continue;
68
- if (basename !== undefined && parsed.basename !== basename) continue;
69
- versions.push({
70
- path: entry.path,
71
- basename: parsed.basename,
72
- timestamp: parsed.timestamp,
73
- size: entry.size,
74
- collision: parsed.collision,
75
- });
76
- }
77
- versions.sort((a, b) => {
78
- const dt = b.timestamp.getTime() - a.timestamp.getTime();
79
- if (dt !== 0) return dt;
80
- return b.collision - a.collision;
81
- });
82
- return versions;
64
+ return listSnapshots(container, IMAGE_EDIT_VERSION_PATHS, basename);
83
65
  }
84
66
 
85
67
  /** Read a snapshot's JSON text. Returns `null` if the snapshot is missing. */
@@ -87,10 +69,7 @@ export async function readImageEditVersionText(
87
69
  container: ContentContainer,
88
70
  version: Version | string,
89
71
  ): Promise<string | null> {
90
- const path = typeof version === 'string' ? version : version.path;
91
- const data = await container.readFile(path);
92
- if (!data) return null;
93
- return decoder.decode(data);
72
+ return readTextSnapshot(container, version);
94
73
  }
95
74
 
96
75
  /** Read and parse a snapshot. Returns `null` if missing. */
@@ -124,34 +103,13 @@ export async function saveImageEditVersion(
124
103
  const basename = options.basename ?? IMAGE_EDIT_DEFAULT_BASENAME;
125
104
  const serialized = JSON.stringify(doc, null, 2);
126
105
 
127
- const versions = await listImageEditVersions(container, basename);
128
- if (!options.force && versions.length > 0) {
129
- const latest = versions[0]!;
130
- const existing = await readImageEditVersionText(container, latest);
131
- if (existing === serialized) {
132
- return { saved: false, version: null, reason: 'unchanged' };
133
- }
134
- }
135
-
136
- const now = options.now ?? new Date();
137
- let collision = 0;
138
- let path = buildImageEditVersionPath(basename, now, collision);
139
- while (await container.exists(path)) {
140
- collision += 1;
141
- path = buildImageEditVersionPath(basename, now, collision);
142
- }
143
-
144
- const data = encoder.encode(serialized);
145
- await container.writeFile(path, data, 'application/json');
146
-
147
- const version: Version = {
148
- path,
106
+ return saveTextSnapshot(container, IMAGE_EDIT_VERSION_PATHS, {
107
+ content: serialized,
149
108
  basename,
150
- timestamp: now,
151
- size: data.byteLength,
152
- collision,
153
- };
154
- return { saved: true, version, reason: 'saved' };
109
+ mimeType: 'application/json',
110
+ now: options.now,
111
+ force: options.force,
112
+ });
155
113
  }
156
114
 
157
115
  /** Revert `state.json` to a prior snapshot. Snapshots the current state first by default. */
@@ -181,27 +139,7 @@ export async function pruneImageEditVersions(
181
139
  policy: PrunePolicy,
182
140
  basename?: string,
183
141
  ): Promise<Version[]> {
184
- const versions = await listImageEditVersions(container, basename);
185
- const toDelete: Version[] = [];
186
-
187
- if (policy.type === 'keep-last-n') {
188
- const n = Math.max(0, Math.floor(policy.n));
189
- toDelete.push(...versions.slice(n));
190
- } else if (policy.type === 'older-than') {
191
- const cutoff = policy.date.getTime();
192
- for (const v of versions) {
193
- if (v.timestamp.getTime() < cutoff) toDelete.push(v);
194
- }
195
- } else {
196
- for (const v of versions) {
197
- if (!policy.keep(v, versions)) toDelete.push(v);
198
- }
199
- }
200
-
201
- for (const v of toDelete) {
202
- await container.removeFile(v.path);
203
- }
204
- return toDelete;
142
+ return pruneSnapshots(container, IMAGE_EDIT_VERSION_PATHS, policy, basename);
205
143
  }
206
144
 
207
145
  /** Collapse adjacent snapshots within `windowMs`, keeping the newer of each pair. */
@@ -210,22 +148,5 @@ export async function coalesceImageEditVersions(
210
148
  options: CoalesceOptions = {},
211
149
  basename?: string,
212
150
  ): Promise<Version[]> {
213
- const windowMs = options.windowMs ?? 60_000;
214
- const versions = await listImageEditVersions(container, basename);
215
- const toDelete: Version[] = [];
216
- if (versions.length > 0) {
217
- let anchor = versions[0]!;
218
- for (let i = 1; i < versions.length; i++) {
219
- const candidate = versions[i]!;
220
- if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {
221
- toDelete.push(candidate);
222
- } else {
223
- anchor = candidate;
224
- }
225
- }
226
- }
227
- for (const v of toDelete) {
228
- await container.removeFile(v.path);
229
- }
230
- return toDelete;
151
+ return coalesceSnapshots(container, IMAGE_EDIT_VERSION_PATHS, options, basename);
231
152
  }
package/src/index.ts CHANGED
@@ -12,3 +12,4 @@ export * from './jsonForm/index.js';
12
12
  export * from './imageEdit/index.js';
13
13
  export * from './icons/index.js';
14
14
  export * from './recommend/index.js';
15
+ export * from './narration/index.js';