@bendyline/squisq 1.5.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
  6. package/dist/chunk-2PWOO4ZE.js.map +1 -0
  7. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  8. package/dist/chunk-3E5F2XMR.js.map +1 -0
  9. package/dist/chunk-4X3JQXNM.js +35 -0
  10. package/dist/chunk-4X3JQXNM.js.map +1 -0
  11. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  12. package/dist/chunk-5DKBFXPY.js.map +1 -0
  13. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  14. package/dist/chunk-5J4HKRJF.js.map +1 -0
  15. package/dist/chunk-5UYI3RJB.js +2503 -0
  16. package/dist/chunk-5UYI3RJB.js.map +1 -0
  17. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  18. package/dist/chunk-66QO7XFB.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-CY2R2VPR.js +884 -0
  22. package/dist/chunk-CY2R2VPR.js.map +1 -0
  23. package/dist/chunk-FVJUUTEM.js +74 -0
  24. package/dist/chunk-FVJUUTEM.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-MMPBOUK7.js} +10 -73
  30. package/dist/chunk-MMPBOUK7.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-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  36. package/dist/chunk-R2GC6RLD.js.map +1 -0
  37. package/dist/chunk-R2YZTCLO.js +292 -0
  38. package/dist/chunk-R2YZTCLO.js.map +1 -0
  39. package/dist/chunk-S7A5D7ZJ.js +174 -0
  40. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  41. package/dist/chunk-TFJVXUHH.js +2358 -0
  42. package/dist/chunk-TFJVXUHH.js.map +1 -0
  43. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  44. package/dist/chunk-TUWMK62B.js.map +1 -0
  45. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  46. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  47. package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
  48. package/dist/chunk-W4O2TX5I.js.map +1 -0
  49. package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
  50. package/dist/chunk-X6DUONGT.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +127 -33
  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 +246 -80
  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__/customTemplatesFrontmatter.test.ts +0 -15
  97. package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
  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 +54 -4
  114. package/src/__tests__/markdownSanitize.test.ts +7 -5
  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 +12 -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 +70 -2
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +138 -2
  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/__tests__/listBlock.test.ts +64 -0
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/fallbackBlock.ts +1 -1
  174. package/src/doc/templates/index.ts +134 -279
  175. package/src/doc/templates/inputDescriptors.ts +20 -1
  176. package/src/doc/templates/listBlock.ts +88 -30
  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 +67 -5
  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/index.ts +1 -0
  204. package/src/markdown/stringify.ts +4 -9
  205. package/src/markdown/utils.ts +36 -14
  206. package/src/narration/align.ts +514 -0
  207. package/src/narration/features.ts +231 -0
  208. package/src/narration/index.ts +86 -0
  209. package/src/narration/nuclei.ts +118 -0
  210. package/src/narration/pacing.ts +211 -0
  211. package/src/narration/script.ts +239 -0
  212. package/src/narration/session.ts +100 -0
  213. package/src/narration/sidecar.ts +187 -0
  214. package/src/narration/syllables.ts +85 -0
  215. package/src/narration/trace.ts +50 -0
  216. package/src/narration/types.ts +299 -0
  217. package/src/narration/vad.ts +131 -0
  218. package/src/recommend/numberHighlight.ts +25 -0
  219. package/src/recommend/templates.ts +108 -12
  220. package/src/schemas/BlockTemplates.ts +156 -7
  221. package/src/schemas/Doc.ts +72 -9
  222. package/src/schemas/Media.ts +12 -2
  223. package/src/schemas/Theme.ts +45 -25
  224. package/src/schemas/Viewport.ts +2 -2
  225. package/src/schemas/fontStacks.ts +79 -0
  226. package/src/schemas/themeCompile.ts +15 -0
  227. package/src/schemas/themeConstants.ts +2 -0
  228. package/src/schemas/themeLibrary.ts +25 -19
  229. package/src/schemas/themeValidator.ts +1 -1
  230. package/src/schemas/themes/gezellig.json +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-5TYS5V3G.js.map +0 -1
  247. package/dist/chunk-A4O7GIWE.js.map +0 -1
  248. package/dist/chunk-BEQODJWV.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-GPLTCSXO.js.map +0 -1
  251. package/dist/chunk-HWVFJAAH.js.map +0 -1
  252. package/dist/chunk-IMSCRLLV.js.map +0 -1
  253. package/dist/chunk-JELIRVDP.js +0 -24
  254. package/dist/chunk-JELIRVDP.js.map +0 -1
  255. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  256. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  257. package/dist/chunk-LTN6I7QW.js.map +0 -1
  258. package/dist/chunk-P3K7NLHF.js.map +0 -1
  259. package/dist/chunk-T5UK6YOB.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 -1801
  266. package/dist/story/index.js +0 -279
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,292 @@
1
+ import {
2
+ TRANSITION_DIRECTIONS,
3
+ TRANSITION_TYPES,
4
+ normalizeTransitionDirection,
5
+ normalizeTransitionType
6
+ } from "./chunk-QE7TWGVT.js";
7
+
8
+ // src/markdown/annotationCoercion.ts
9
+ var KNOWN_BLOCK_META_KEYS = {
10
+ x: "number",
11
+ y: "number",
12
+ startTime: "time",
13
+ duration: "time",
14
+ connectsTo: "connectionList",
15
+ transition: "transition",
16
+ transitionDuration: "time",
17
+ transitionDirection: "transitionDirection"
18
+ };
19
+ var BLOCK_META_KEY_DESCRIPTORS = [
20
+ {
21
+ key: "transition",
22
+ description: "Block-to-block transition effect",
23
+ values: TRANSITION_TYPES
24
+ },
25
+ {
26
+ key: "transitionDuration",
27
+ description: "How long the transition lasts",
28
+ valueHint: "seconds \u2014 e.g. 0.7, 700ms"
29
+ },
30
+ {
31
+ key: "transitionDirection",
32
+ description: "Directional variant for the transition",
33
+ values: TRANSITION_DIRECTIONS
34
+ },
35
+ {
36
+ key: "startTime",
37
+ description: "Timeline start time of this block",
38
+ valueHint: "mm:ss or seconds \u2014 e.g. 01:30, 5, 1500ms"
39
+ },
40
+ {
41
+ key: "duration",
42
+ description: "How long this block lasts",
43
+ valueHint: "mm:ss or seconds \u2014 e.g. 45, 1500ms"
44
+ },
45
+ {
46
+ key: "x",
47
+ description: "Horizontal position on the diagram canvas",
48
+ valueHint: "number \u2014 e.g. 600"
49
+ },
50
+ {
51
+ key: "y",
52
+ description: "Vertical position on the diagram canvas",
53
+ valueHint: "number \u2014 e.g. 300"
54
+ },
55
+ {
56
+ key: "connectsTo",
57
+ description: "Diagram connections to other blocks",
58
+ valueHint: "comma-separated target or target:type \u2014 e.g. foo,bar:flow"
59
+ }
60
+ ];
61
+ function coerceAnnotationValues(params) {
62
+ const blockMeta = {};
63
+ const metadata = {};
64
+ const warnings = [];
65
+ let transitionType = null;
66
+ let transitionDuration;
67
+ let transitionDirection;
68
+ for (const [key, raw] of Object.entries(params)) {
69
+ const kind = KNOWN_BLOCK_META_KEYS[key];
70
+ if (!kind) {
71
+ metadata[key] = raw;
72
+ continue;
73
+ }
74
+ if (kind === "number") {
75
+ const n = parseNumber(raw);
76
+ if (n == null) {
77
+ warnings.push(`Invalid number for "${key}": ${JSON.stringify(raw)}`);
78
+ } else {
79
+ blockMeta[key] = n;
80
+ }
81
+ } else if (kind === "time") {
82
+ const s = parseTimeSeconds(raw);
83
+ if (s == null) {
84
+ warnings.push(`Invalid time for "${key}": ${JSON.stringify(raw)}`);
85
+ } else if (key === "transitionDuration") {
86
+ transitionDuration = s;
87
+ } else {
88
+ blockMeta[key] = s;
89
+ }
90
+ } else if (kind === "connectionList") {
91
+ const { list, warning } = parseConnectionList(raw);
92
+ if (warning) warnings.push(`"${key}": ${warning}`);
93
+ blockMeta.connectsTo = list;
94
+ } else if (kind === "transition") {
95
+ const transition = normalizeTransitionType(raw);
96
+ if (transition == null) {
97
+ warnings.push(`Invalid transition for "${key}": ${JSON.stringify(raw)}`);
98
+ } else {
99
+ transitionType = transition;
100
+ }
101
+ } else if (kind === "transitionDirection") {
102
+ const direction = normalizeTransitionDirection(raw);
103
+ if (direction == null) {
104
+ warnings.push(`Invalid transition direction for "${key}": ${JSON.stringify(raw)}`);
105
+ } else {
106
+ transitionDirection = direction;
107
+ }
108
+ }
109
+ }
110
+ if (transitionType) {
111
+ blockMeta.transition = {
112
+ type: transitionType,
113
+ ...transitionDuration !== void 0 ? { duration: transitionDuration } : {},
114
+ ...transitionDirection !== void 0 ? { direction: transitionDirection } : {}
115
+ };
116
+ }
117
+ return { blockMeta, metadata, warnings };
118
+ }
119
+ function parseNumber(raw) {
120
+ if (raw.trim() === "") return null;
121
+ const n = Number(raw);
122
+ return Number.isFinite(n) ? n : null;
123
+ }
124
+ var TIME_BARE_RE = /^\d+(?:\.\d+)?$/;
125
+ var TIME_MS_RE = /^(\d+(?:\.\d+)?)ms$/;
126
+ var TIME_MMSS_RE = /^(\d+):(\d{1,2})(?:\.(\d+))?$/;
127
+ function parseTimeSeconds(raw) {
128
+ const trimmed = raw.trim();
129
+ if (!trimmed) return null;
130
+ if (TIME_BARE_RE.test(trimmed)) {
131
+ const n = Number(trimmed);
132
+ return Number.isFinite(n) ? n : null;
133
+ }
134
+ const msMatch = trimmed.match(TIME_MS_RE);
135
+ if (msMatch) {
136
+ const n = Number(msMatch[1]);
137
+ return Number.isFinite(n) ? n / 1e3 : null;
138
+ }
139
+ const mmssMatch = trimmed.match(TIME_MMSS_RE);
140
+ if (mmssMatch) {
141
+ const mins = Number(mmssMatch[1]);
142
+ const secs = Number(mmssMatch[2]);
143
+ const frac = mmssMatch[3] ? Number(`0.${mmssMatch[3]}`) : 0;
144
+ if (!Number.isFinite(mins) || !Number.isFinite(secs) || !Number.isFinite(frac)) {
145
+ return null;
146
+ }
147
+ if (secs >= 60) return null;
148
+ return mins * 60 + secs + frac;
149
+ }
150
+ return null;
151
+ }
152
+ function parseConnectionList(raw) {
153
+ const trimmed = raw.trim();
154
+ if (!trimmed) return { list: [], warning: null };
155
+ const list = [];
156
+ let droppedEmpty = false;
157
+ for (const part of trimmed.split(",")) {
158
+ const entry = part.trim();
159
+ if (!entry) {
160
+ droppedEmpty = true;
161
+ continue;
162
+ }
163
+ const colonIdx = entry.indexOf(":");
164
+ if (colonIdx < 0) {
165
+ list.push({ target: entry });
166
+ } else {
167
+ const target = entry.slice(0, colonIdx).trim();
168
+ const type = entry.slice(colonIdx + 1).trim();
169
+ if (!target) {
170
+ droppedEmpty = true;
171
+ continue;
172
+ }
173
+ list.push(type ? { target, type } : { target });
174
+ }
175
+ }
176
+ return {
177
+ list,
178
+ warning: droppedEmpty ? "dropped empty connection entries" : null
179
+ };
180
+ }
181
+
182
+ // src/markdown/attrTokens.ts
183
+ var DQ_RUN = `"(?:[^"\\\\]|\\\\.)*"`;
184
+ var SQ_RUN = `'(?:[^'\\\\]|\\\\.)*'`;
185
+ var TEMPLATE_STRICT_RE = new RegExp(
186
+ `\\s*\\{\\[((?:[^\\]"']|${DQ_RUN}|${SQ_RUN})*)\\]\\}[\\s\\]}]*$`
187
+ );
188
+ var TEMPLATE_LEGACY_RE = /\s*\{\[([^\]]*)\]\}[\s\]}]*$/;
189
+ var PANDOC_STRICT_RE = new RegExp(`\\s*\\{(?!\\[)((?:[^}"']|${DQ_RUN}|${SQ_RUN})*)\\}\\s*$`);
190
+ var PANDOC_LEGACY_RE = /\s*\{(?!\[)([^}]*)\}\s*$/;
191
+ function matchTrailing(text, strict, legacy) {
192
+ const m = strict.exec(text) ?? legacy.exec(text);
193
+ return m ? { inner: m[1], index: m.index } : null;
194
+ }
195
+ function matchTrailingTemplateAnnotation(text) {
196
+ return matchTrailing(text, TEMPLATE_STRICT_RE, TEMPLATE_LEGACY_RE);
197
+ }
198
+ function matchTrailingPandocAttr(text) {
199
+ return matchTrailing(text, PANDOC_STRICT_RE, PANDOC_LEGACY_RE);
200
+ }
201
+ var WHITESPACE_RE = /\s/;
202
+ function tokenizeAttrTokens(input) {
203
+ const tokens = [];
204
+ let i = 0;
205
+ const n = input.length;
206
+ while (i < n) {
207
+ while (i < n && WHITESPACE_RE.test(input[i])) i++;
208
+ if (i >= n) break;
209
+ let token = "";
210
+ while (i < n && !WHITESPACE_RE.test(input[i])) {
211
+ const ch = input[i];
212
+ const atValueStart = token === "" || token.endsWith("=");
213
+ if ((ch === '"' || ch === "'") && atValueStart) {
214
+ token += ch;
215
+ i++;
216
+ while (i < n && input[i] !== ch) {
217
+ if (input[i] === "\\" && i + 1 < n && (input[i + 1] === ch || input[i + 1] === "\\")) {
218
+ token += input[i] + input[i + 1];
219
+ i += 2;
220
+ } else {
221
+ token += input[i];
222
+ i++;
223
+ }
224
+ }
225
+ if (i < n && input[i] === ch) {
226
+ token += ch;
227
+ i++;
228
+ }
229
+ } else {
230
+ token += ch;
231
+ i++;
232
+ }
233
+ }
234
+ if (token) tokens.push(token);
235
+ }
236
+ return tokens;
237
+ }
238
+ var DQ_UNESCAPE_RE = /\\([\\"])/g;
239
+ var SQ_UNESCAPE_RE = /\\([\\'])/g;
240
+ function unquoteAttrValue(raw) {
241
+ if (raw.length >= 2) {
242
+ const q = raw[0];
243
+ if ((q === '"' || q === "'") && raw.endsWith(q)) {
244
+ const body = raw.slice(1, -1);
245
+ return body.replace(q === '"' ? DQ_UNESCAPE_RE : SQ_UNESCAPE_RE, "$1");
246
+ }
247
+ }
248
+ return raw;
249
+ }
250
+ function splitKeyValueToken(token) {
251
+ const eqIdx = token.indexOf("=");
252
+ if (eqIdx <= 0) return null;
253
+ return { key: token.slice(0, eqIdx), value: unquoteAttrValue(token.slice(eqIdx + 1)) };
254
+ }
255
+ var NEEDS_QUOTING_RE = /[\s"'[\]{}]/;
256
+ var DQ_ESCAPE_RE = /[\\"]/g;
257
+ var SQ_ESCAPE_RE = /[\\']/g;
258
+ function needsQuoting(value) {
259
+ return value === "" || NEEDS_QUOTING_RE.test(value);
260
+ }
261
+ function quoteAttrValue(value) {
262
+ if (!needsQuoting(value)) return value;
263
+ if (value.includes('"') && !value.includes("'")) {
264
+ return `'${value.replace(SQ_ESCAPE_RE, (ch) => `\\${ch}`)}'`;
265
+ }
266
+ return `"${value.replace(DQ_ESCAPE_RE, (ch) => `\\${ch}`)}"`;
267
+ }
268
+ function serializeAnnotation(template, params) {
269
+ const parts = [];
270
+ if (template) parts.push(template);
271
+ for (const [key, value] of Object.entries(params ?? {})) {
272
+ parts.push(`${key}=${quoteAttrValue(value)}`);
273
+ }
274
+ return `{[${parts.join(" ")}]}`;
275
+ }
276
+
277
+ export {
278
+ KNOWN_BLOCK_META_KEYS,
279
+ BLOCK_META_KEY_DESCRIPTORS,
280
+ coerceAnnotationValues,
281
+ parseNumber,
282
+ parseTimeSeconds,
283
+ matchTrailingTemplateAnnotation,
284
+ matchTrailingPandocAttr,
285
+ tokenizeAttrTokens,
286
+ unquoteAttrValue,
287
+ splitKeyValueToken,
288
+ needsQuoting,
289
+ quoteAttrValue,
290
+ serializeAnnotation
291
+ };
292
+ //# sourceMappingURL=chunk-R2YZTCLO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/markdown/annotationCoercion.ts","../src/markdown/attrTokens.ts"],"sourcesContent":["/**\n * Heading-attribute value coercion.\n *\n * The Pandoc-style heading attribute block `{#id .class key=value}` parses\n * to a flat `Record<string, string>` of `key → raw value`. This module\n * coerces a known subset of keys to typed values (numbers, time-in-seconds,\n * `BlockConnection[]`) so downstream consumers don't have to re-parse them.\n *\n * Unknown keys are returned in `metadata` as raw strings. Malformed known-key\n * values are dropped from the typed result and reported via `warnings` (the\n * raw string is preserved in `params` upstream for lossless round-tripping —\n * coercion failures do not corrupt the source).\n */\n\nimport type { BlockConnection } from './types.js';\nimport type { TransitionDirection, TransitionType } from '../schemas/Transitions.js';\nimport {\n normalizeTransitionDirection,\n normalizeTransitionType,\n TRANSITION_TYPES,\n TRANSITION_DIRECTIONS,\n} from '../schemas/Transitions.js';\n\n/**\n * Registry of known block-meta keys and their coercion strategies.\n *\n * Adding a new typed key: add an entry here, add the typed field to\n * `HeadingAttributes.blockMeta` in `types.ts`, and apply the coerced value\n * in `markdownToDoc.ts`.\n */\nexport const KNOWN_BLOCK_META_KEYS = {\n x: 'number',\n y: 'number',\n startTime: 'time',\n duration: 'time',\n connectsTo: 'connectionList',\n transition: 'transition',\n transitionDuration: 'time',\n transitionDirection: 'transitionDirection',\n} as const;\n\nexport type KnownBlockMetaKey = keyof typeof KNOWN_BLOCK_META_KEYS;\n\n/**\n * Editor-facing description of a known block-meta key: what it does, and\n * either its closed set of valid values or a free-form format hint. This is\n * the single source the markdown editor's `{[name key=value]}` attribute\n * autocomplete reads from, so the suggestion list stays in lockstep with\n * `KNOWN_BLOCK_META_KEYS` and the transition vocabulary rather than drifting\n * in a hand-maintained copy. `values`/`valueHint` are mutually exclusive: a\n * key has a closed enum (`values`) or a format hint (`valueHint`), not both.\n */\nexport interface BlockMetaKeyDescriptor {\n key: KnownBlockMetaKey;\n /** One-line description, shown as the completion item's detail. */\n description: string;\n /** Closed set of valid values, for keys backed by an enum. */\n values?: readonly string[];\n /** Format hint shown when the value is free-form (no closed set). */\n valueHint?: string;\n}\n\n/**\n * Ordered descriptors for every key in `KNOWN_BLOCK_META_KEYS`. Order is the\n * suggestion display order: the transition family first (the most common\n * authoring case), then timeline timing, then diagram geometry/links.\n */\nexport const BLOCK_META_KEY_DESCRIPTORS: readonly BlockMetaKeyDescriptor[] = [\n {\n key: 'transition',\n description: 'Block-to-block transition effect',\n values: TRANSITION_TYPES,\n },\n {\n key: 'transitionDuration',\n description: 'How long the transition lasts',\n valueHint: 'seconds — e.g. 0.7, 700ms',\n },\n {\n key: 'transitionDirection',\n description: 'Directional variant for the transition',\n values: TRANSITION_DIRECTIONS,\n },\n {\n key: 'startTime',\n description: 'Timeline start time of this block',\n valueHint: 'mm:ss or seconds — e.g. 01:30, 5, 1500ms',\n },\n {\n key: 'duration',\n description: 'How long this block lasts',\n valueHint: 'mm:ss or seconds — e.g. 45, 1500ms',\n },\n {\n key: 'x',\n description: 'Horizontal position on the diagram canvas',\n valueHint: 'number — e.g. 600',\n },\n {\n key: 'y',\n description: 'Vertical position on the diagram canvas',\n valueHint: 'number — e.g. 300',\n },\n {\n key: 'connectsTo',\n description: 'Diagram connections to other blocks',\n valueHint: 'comma-separated target or target:type — e.g. foo,bar:flow',\n },\n];\n\n/**\n * The shape of the typed values produced by coercion. Mirrors\n * `HeadingAttributes.blockMeta` in `types.ts`.\n */\nexport interface CoercedBlockMeta {\n x?: number;\n y?: number;\n startTime?: number;\n duration?: number;\n connectsTo?: BlockConnection[];\n transition?: {\n type: TransitionType;\n duration?: number;\n direction?: TransitionDirection;\n };\n}\n\nexport interface CoerceResult {\n /** Typed values for known keys whose coercion succeeded. */\n blockMeta: CoercedBlockMeta;\n /** Raw string values for keys not in the known-key registry. */\n metadata: Record<string, string>;\n /** Human-readable diagnostics — currently emitted for coercion failures. */\n warnings: string[];\n}\n\n/**\n * Coerce a flat `key → raw string` map into typed block-meta + an\n * untyped metadata bag, per the `KNOWN_BLOCK_META_KEYS` registry.\n */\nexport function coerceAnnotationValues(params: Record<string, string>): CoerceResult {\n const blockMeta: CoercedBlockMeta = {};\n const metadata: Record<string, string> = {};\n const warnings: string[] = [];\n let transitionType: TransitionType | null = null;\n let transitionDuration: number | undefined;\n let transitionDirection: TransitionDirection | undefined;\n\n for (const [key, raw] of Object.entries(params)) {\n const kind = (KNOWN_BLOCK_META_KEYS as Record<string, string>)[key];\n if (!kind) {\n metadata[key] = raw;\n continue;\n }\n if (kind === 'number') {\n const n = parseNumber(raw);\n if (n == null) {\n warnings.push(`Invalid number for \"${key}\": ${JSON.stringify(raw)}`);\n } else {\n (blockMeta as Record<string, unknown>)[key] = n;\n }\n } else if (kind === 'time') {\n const s = parseTimeSeconds(raw);\n if (s == null) {\n warnings.push(`Invalid time for \"${key}\": ${JSON.stringify(raw)}`);\n } else if (key === 'transitionDuration') {\n transitionDuration = s;\n } else {\n (blockMeta as Record<string, unknown>)[key] = s;\n }\n } else if (kind === 'connectionList') {\n const { list, warning } = parseConnectionList(raw);\n if (warning) warnings.push(`\"${key}\": ${warning}`);\n blockMeta.connectsTo = list;\n } else if (kind === 'transition') {\n const transition = normalizeTransitionType(raw);\n if (transition == null) {\n warnings.push(`Invalid transition for \"${key}\": ${JSON.stringify(raw)}`);\n } else {\n transitionType = transition;\n }\n } else if (kind === 'transitionDirection') {\n const direction = normalizeTransitionDirection(raw);\n if (direction == null) {\n warnings.push(`Invalid transition direction for \"${key}\": ${JSON.stringify(raw)}`);\n } else {\n transitionDirection = direction;\n }\n }\n }\n\n if (transitionType) {\n blockMeta.transition = {\n type: transitionType,\n ...(transitionDuration !== undefined ? { duration: transitionDuration } : {}),\n ...(transitionDirection !== undefined ? { direction: transitionDirection } : {}),\n };\n }\n\n return { blockMeta, metadata, warnings };\n}\n\n// ============================================\n// Coercers\n// ============================================\n\n/** Parse a finite number from a string. Returns null on failure. */\nexport function parseNumber(raw: string): number | null {\n if (raw.trim() === '') return null;\n const n = Number(raw);\n return Number.isFinite(n) ? n : null;\n}\n\nconst TIME_BARE_RE = /^\\d+(?:\\.\\d+)?$/;\nconst TIME_MS_RE = /^(\\d+(?:\\.\\d+)?)ms$/;\nconst TIME_MMSS_RE = /^(\\d+):(\\d{1,2})(?:\\.(\\d+))?$/;\n\n/**\n * Parse a time value to seconds. Accepts:\n * - bare number: `5` → 5, `5.5` → 5.5\n * - `mm:ss`: `01:30` → 90\n * - `mm:ss.ms`: `01:30.500` → 90.5\n * - `1500ms` → 1.5\n *\n * Returns null on malformed input.\n */\nexport function parseTimeSeconds(raw: string): number | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n\n if (TIME_BARE_RE.test(trimmed)) {\n const n = Number(trimmed);\n return Number.isFinite(n) ? n : null;\n }\n\n const msMatch = trimmed.match(TIME_MS_RE);\n if (msMatch) {\n const n = Number(msMatch[1]);\n return Number.isFinite(n) ? n / 1000 : null;\n }\n\n const mmssMatch = trimmed.match(TIME_MMSS_RE);\n if (mmssMatch) {\n const mins = Number(mmssMatch[1]);\n const secs = Number(mmssMatch[2]);\n const frac = mmssMatch[3] ? Number(`0.${mmssMatch[3]}`) : 0;\n if (!Number.isFinite(mins) || !Number.isFinite(secs) || !Number.isFinite(frac)) {\n return null;\n }\n if (secs >= 60) return null;\n return mins * 60 + secs + frac;\n }\n\n return null;\n}\n\n/**\n * Parse a comma-separated connection list. Each entry is `target` or\n * `target:type`. Empty entries (`a,,b`) are filtered with a warning; an\n * entirely empty input yields an empty list with no warning.\n */\nexport function parseConnectionList(raw: string): {\n list: BlockConnection[];\n warning: string | null;\n} {\n const trimmed = raw.trim();\n if (!trimmed) return { list: [], warning: null };\n\n const list: BlockConnection[] = [];\n let droppedEmpty = false;\n for (const part of trimmed.split(',')) {\n const entry = part.trim();\n if (!entry) {\n droppedEmpty = true;\n continue;\n }\n const colonIdx = entry.indexOf(':');\n if (colonIdx < 0) {\n list.push({ target: entry });\n } else {\n const target = entry.slice(0, colonIdx).trim();\n const type = entry.slice(colonIdx + 1).trim();\n if (!target) {\n droppedEmpty = true;\n continue;\n }\n list.push(type ? { target, type } : { target });\n }\n }\n\n return {\n list,\n warning: droppedEmpty ? 'dropped empty connection entries' : null,\n };\n}\n","/**\n * Shared key=value attribute machinery for the two heading annotation forms:\n * the squisq-native `{[templateName key=value …]}` template annotation and\n * the Pandoc-style `{#id .class key=value}` attribute block.\n *\n * Both forms share one interior grammar — whitespace-separated tokens whose\n * values may be quoted (double or single) to include spaces, brackets, or\n * quote characters. This module is the single implementation of that\n * grammar: one tokenizer, one value un/escaper, one quoting rule. The\n * trailing-annotation recognizers live here too so the editor bridge\n * (packages/editor-react/src/tiptapBridge.ts) stays in sync by importing\n * them rather than copying regexes.\n *\n * Value syntax:\n * - Unquoted: no whitespace, quotes, or `[ ] { }` (those would collide with\n * the surrounding annotation delimiters), e.g. `columns=3`, `name=O'Brien`\n * (a quote *inside* an unquoted value is literal).\n * - Quoted: `key=\"value with spaces\"` or `key='…'`. Inside a quoted run,\n * `\\` escapes the active quote character and itself; any other `\\` is\n * literal. Quoted values may contain `]` and `}`.\n * - Serialization emits double quotes, switching to single quotes when the\n * value contains a double quote (markdown character-escape resolution\n * makes backslash-escaped quotes fragile in `.md` source, so picking the\n * other quote character is preferred over escaping). A value containing\n * BOTH quote characters falls back to double quotes with `\\\"` escapes —\n * that survives the API path but is best avoided in markdown source.\n */\n\n// ============================================\n// Trailing-annotation recognizers\n// ============================================\n\n/** Regex source for a double-quoted run with `\\\"` / `\\\\` escapes. */\nconst DQ_RUN = `\"(?:[^\"\\\\\\\\]|\\\\\\\\.)*\"`;\n/** Regex source for a single-quoted run with `\\'` / `\\\\` escapes. */\nconst SQ_RUN = `'(?:[^'\\\\\\\\]|\\\\\\\\.)*'`;\n\n/**\n * Quote-aware match for a trailing `{[…]}` annotation: the interior is any\n * mix of quoted runs and characters other than `]` and quotes, so quoted\n * values may contain `]`. An empty interior is valid so `{[]}` can remain\n * lossless metadata instead of visible heading text. The trailing\n * `[\\s\\]}]*` tolerates an accidental doubled `]}` that users sometimes type\n * when learning the syntax.\n */\nconst TEMPLATE_STRICT_RE = new RegExp(\n `\\\\s*\\\\{\\\\[((?:[^\\\\]\"']|${DQ_RUN}|${SQ_RUN})*)\\\\]\\\\}[\\\\s\\\\]}]*$`,\n);\n\n/**\n * Legacy fallback matching the pre-quote-aware grammar (`[^\\]]*` interior).\n * Tried after the strict form so inputs with unbalanced quotes keep parsing\n * exactly as they did before quote support landed.\n */\nconst TEMPLATE_LEGACY_RE = /\\s*\\{\\[([^\\]]*)\\]\\}[\\s\\]}]*$/;\n\n/**\n * Quote-aware match for a trailing Pandoc `{…}` block. The negative\n * lookahead `(?!\\[)` keeps it from colliding with the `{[…]}` template\n * form. Quoted values may contain `}`.\n */\nconst PANDOC_STRICT_RE = new RegExp(`\\\\s*\\\\{(?!\\\\[)((?:[^}\"']|${DQ_RUN}|${SQ_RUN})*)\\\\}\\\\s*$`);\n\n/** Legacy fallback for the Pandoc block (`[^}]*` interior). */\nconst PANDOC_LEGACY_RE = /\\s*\\{(?!\\[)([^}]*)\\}\\s*$/;\n\n/** Result of matching a trailing annotation against a text run. */\nexport interface TrailingAnnotationMatch {\n /** Interior of the brace block (between `{[`/`{` and `]}`/`}`), untrimmed. */\n inner: string;\n /** Offset of the match start (including leading whitespace) in the input. */\n index: number;\n}\n\nfunction matchTrailing(\n text: string,\n strict: RegExp,\n legacy: RegExp,\n): TrailingAnnotationMatch | null {\n const m = strict.exec(text) ?? legacy.exec(text);\n return m ? { inner: m[1], index: m.index } : null;\n}\n\n/**\n * Match a trailing `{[templateName key=value …]}` annotation. Quote-aware\n * (values may contain `]` when quoted), with a legacy fallback so inputs\n * the old `[^\\]]+` grammar accepted still match.\n */\nexport function matchTrailingTemplateAnnotation(text: string): TrailingAnnotationMatch | null {\n return matchTrailing(text, TEMPLATE_STRICT_RE, TEMPLATE_LEGACY_RE);\n}\n\n/**\n * Match a trailing Pandoc `{#id .class key=value}` block. Quote-aware\n * (values may contain `}` when quoted), with a legacy fallback.\n */\nexport function matchTrailingPandocAttr(text: string): TrailingAnnotationMatch | null {\n return matchTrailing(text, PANDOC_STRICT_RE, PANDOC_LEGACY_RE);\n}\n\n// ============================================\n// Tokenizer\n// ============================================\n\nconst WHITESPACE_RE = /\\s/;\n\n/**\n * Quote-aware whitespace tokenizer for the interior of either annotation\n * form.\n *\n * A quote run starts only at the beginning of a token or immediately after\n * `=` — a quote elsewhere (e.g. `name=O'Brien`) is a literal character.\n * Inside a run, `\\` escapes the active quote character and itself; the\n * escape pair is preserved in the returned token (callers strip and\n * unescape via {@link splitKeyValueToken} / {@link unquoteAttrValue}).\n * An unbalanced opening quote is tolerated: the run extends to the end of\n * the input.\n */\nexport function tokenizeAttrTokens(input: string): string[] {\n const tokens: string[] = [];\n let i = 0;\n const n = input.length;\n while (i < n) {\n while (i < n && WHITESPACE_RE.test(input[i])) i++;\n if (i >= n) break;\n\n let token = '';\n while (i < n && !WHITESPACE_RE.test(input[i])) {\n const ch = input[i];\n const atValueStart = token === '' || token.endsWith('=');\n if ((ch === '\"' || ch === \"'\") && atValueStart) {\n token += ch;\n i++;\n while (i < n && input[i] !== ch) {\n if (input[i] === '\\\\' && i + 1 < n && (input[i + 1] === ch || input[i + 1] === '\\\\')) {\n token += input[i] + input[i + 1];\n i += 2;\n } else {\n token += input[i];\n i++;\n }\n }\n if (i < n && input[i] === ch) {\n token += ch;\n i++;\n }\n } else {\n token += ch;\n i++;\n }\n }\n if (token) tokens.push(token);\n }\n return tokens;\n}\n\n// ============================================\n// Value un/escaping\n// ============================================\n\nconst DQ_UNESCAPE_RE = /\\\\([\\\\\"])/g;\nconst SQ_UNESCAPE_RE = /\\\\([\\\\'])/g;\n\n/**\n * Strip surrounding quotes from a raw token value and unescape the active\n * quote character and backslash. Values not wrapped in matching quotes are\n * returned verbatim (including any interior quotes or backslashes).\n */\nexport function unquoteAttrValue(raw: string): string {\n if (raw.length >= 2) {\n const q = raw[0];\n if ((q === '\"' || q === \"'\") && raw.endsWith(q)) {\n const body = raw.slice(1, -1);\n return body.replace(q === '\"' ? DQ_UNESCAPE_RE : SQ_UNESCAPE_RE, '$1');\n }\n }\n return raw;\n}\n\n/**\n * Split a `key=value` token into its parts, unquoting/unescaping the value.\n * Returns null for tokens with no `=` past the first character (bare words,\n * `#id`, `.class`, or a leading `=`).\n */\nexport function splitKeyValueToken(token: string): { key: string; value: string } | null {\n const eqIdx = token.indexOf('=');\n if (eqIdx <= 0) return null;\n return { key: token.slice(0, eqIdx), value: unquoteAttrValue(token.slice(eqIdx + 1)) };\n}\n\n// ============================================\n// Serialization\n// ============================================\n\nconst NEEDS_QUOTING_RE = /[\\s\"'[\\]{}]/;\nconst DQ_ESCAPE_RE = /[\\\\\"]/g;\nconst SQ_ESCAPE_RE = /[\\\\']/g;\n\n/**\n * Whether a value must be quoted to survive tokenization and the outer\n * annotation regexes: any whitespace, quote character, bracket, or brace —\n * or an empty value, which would otherwise serialize as a bare `key=`.\n */\nexport function needsQuoting(value: string): boolean {\n return value === '' || NEEDS_QUOTING_RE.test(value);\n}\n\n/**\n * Serialize an attribute value, quoting when {@link needsQuoting} says the\n * bare form would not round-trip.\n *\n * Double quotes are canonical; a value containing a double quote (but no\n * single quote) is single-quoted instead, because markdown resolves\n * backslash escapes in text runs before our parser sees them — switching\n * the quote character round-trips through `.md` source where `\\\"` would\n * not. Backslashes and the active quote character are escaped.\n */\nexport function quoteAttrValue(value: string): string {\n if (!needsQuoting(value)) return value;\n if (value.includes('\"') && !value.includes(\"'\")) {\n return `'${value.replace(SQ_ESCAPE_RE, (ch) => `\\\\${ch}`)}'`;\n }\n return `\"${value.replace(DQ_ESCAPE_RE, (ch) => `\\\\${ch}`)}\"`;\n}\n\n/**\n * Serialize a `{[templateName key=value …]}` template annotation. The inverse\n * of {@link parseStandaloneAnnotation} / the tokenizer: emits the template\n * name (when present) followed by each `key=value` pair in insertion order,\n * quoting values via {@link quoteAttrValue}. Used to re-emit heading-less\n * standalone annotation blocks in `docToMarkdown`.\n */\nexport function serializeAnnotation(\n template: string | undefined,\n params?: Record<string, string>,\n): string {\n const parts: string[] = [];\n if (template) parts.push(template);\n for (const [key, value] of Object.entries(params ?? {})) {\n parts.push(`${key}=${quoteAttrValue(value)}`);\n }\n return `{[${parts.join(' ')}]}`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,174 @@
1
+ // src/versions/timestamp.ts
2
+ var TIMESTAMP_RE = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/;
3
+ function pad2(n) {
4
+ return n < 10 ? `0${n}` : String(n);
5
+ }
6
+ function formatVersionTimestamp(date) {
7
+ return String(date.getUTCFullYear()).padStart(4, "0") + pad2(date.getUTCMonth() + 1) + pad2(date.getUTCDate()) + "T" + pad2(date.getUTCHours()) + pad2(date.getUTCMinutes()) + pad2(date.getUTCSeconds()) + "Z";
8
+ }
9
+ function parseVersionTimestamp(stamp) {
10
+ const m = TIMESTAMP_RE.exec(stamp);
11
+ if (!m) return null;
12
+ const [, y, mo, d, h, mi, s] = m;
13
+ const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s));
14
+ if (Number.isNaN(ms)) return null;
15
+ return new Date(ms);
16
+ }
17
+ var VERSION_TIMESTAMP_PATTERN = TIMESTAMP_RE;
18
+
19
+ // src/versions/gitignore.ts
20
+ var decoder = new TextDecoder();
21
+ var encoder = new TextEncoder();
22
+ var GITIGNORE_PATH = ".gitignore";
23
+ var VERSIONS_IGNORE_RULE = ".versions/";
24
+ var EQUIVALENT_VERSIONS_RULE = /^(!?)\/?\.versions\/?$/;
25
+ async function ensureVersionsGitIgnored(container) {
26
+ const data = await container.readFile(GITIGNORE_PATH);
27
+ const existing = data ? decoder.decode(data) : "";
28
+ let alreadyIgnored = false;
29
+ for (const line of existing.split(/\r?\n/)) {
30
+ const match = EQUIVALENT_VERSIONS_RULE.exec(line.trim());
31
+ if (match) alreadyIgnored = match[1] !== "!";
32
+ }
33
+ if (alreadyIgnored) return;
34
+ const newline = existing.includes("\r\n") ? "\r\n" : "\n";
35
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? newline : "";
36
+ const next = `${existing}${separator}${VERSIONS_IGNORE_RULE}${newline}`;
37
+ await container.writeFile(GITIGNORE_PATH, encoder.encode(next), "text/plain");
38
+ }
39
+
40
+ // src/versions/snapshotStore.ts
41
+ var decoder2 = new TextDecoder();
42
+ var encoder2 = new TextEncoder();
43
+ function buildSnapshotPath(strategy, basename, date, collision = 0) {
44
+ const stamp = formatVersionTimestamp(date);
45
+ const suffix = collision > 0 ? `-${collision + 1}` : "";
46
+ return `${strategy.prefix}${basename}.${stamp}${suffix}.${strategy.extension}`;
47
+ }
48
+ function parseSnapshotPath(strategy, path) {
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
+ const basename = stem.slice(0, lastDot);
57
+ const tail = stem.slice(lastDot + 1);
58
+ const dash = tail.indexOf("-");
59
+ let stamp = tail;
60
+ let collision = 0;
61
+ if (dash >= 0) {
62
+ stamp = tail.slice(0, dash);
63
+ const suffix = Number(tail.slice(dash + 1));
64
+ if (!Number.isInteger(suffix) || suffix < 2) return null;
65
+ collision = suffix - 1;
66
+ }
67
+ const timestamp = parseVersionTimestamp(stamp);
68
+ return timestamp ? { basename, timestamp, collision } : null;
69
+ }
70
+ async function listSnapshots(container, strategy, basename) {
71
+ const entries = await container.listFiles(strategy.prefix);
72
+ const versions = [];
73
+ for (const entry of entries) {
74
+ const parsed = parseSnapshotPath(strategy, entry.path);
75
+ if (!parsed || basename !== void 0 && parsed.basename !== basename) continue;
76
+ versions.push({
77
+ path: entry.path,
78
+ basename: parsed.basename,
79
+ timestamp: parsed.timestamp,
80
+ size: entry.size,
81
+ collision: parsed.collision
82
+ });
83
+ }
84
+ versions.sort((a, b) => {
85
+ const timestampDelta = b.timestamp.getTime() - a.timestamp.getTime();
86
+ return timestampDelta || b.collision - a.collision;
87
+ });
88
+ return versions;
89
+ }
90
+ async function readTextSnapshot(container, version) {
91
+ const path = typeof version === "string" ? version : version.path;
92
+ const data = await container.readFile(path);
93
+ return data ? decoder2.decode(data) : null;
94
+ }
95
+ async function saveTextSnapshot(container, strategy, options) {
96
+ const versions = await listSnapshots(container, strategy, options.basename);
97
+ if (!options.force && versions.length > 0) {
98
+ const existing = await readTextSnapshot(container, versions[0]);
99
+ if (existing === options.content) {
100
+ await ensureVersionsGitIgnored(container);
101
+ return { saved: false, version: null, reason: "unchanged" };
102
+ }
103
+ }
104
+ const now = options.now ?? /* @__PURE__ */ new Date();
105
+ let collision = 0;
106
+ let path = buildSnapshotPath(strategy, options.basename, now, collision);
107
+ while (await container.exists(path)) {
108
+ collision += 1;
109
+ path = buildSnapshotPath(strategy, options.basename, now, collision);
110
+ }
111
+ const data = encoder2.encode(options.content);
112
+ await ensureVersionsGitIgnored(container);
113
+ await container.writeFile(path, data, options.mimeType);
114
+ return {
115
+ saved: true,
116
+ version: {
117
+ path,
118
+ basename: options.basename,
119
+ timestamp: now,
120
+ size: data.byteLength,
121
+ collision
122
+ },
123
+ reason: "saved"
124
+ };
125
+ }
126
+ async function pruneSnapshots(container, strategy, policy, basename) {
127
+ const versions = await listSnapshots(container, strategy, basename);
128
+ let toDelete;
129
+ if (policy.type === "keep-last-n") {
130
+ toDelete = versions.slice(Math.max(0, Math.floor(policy.n)));
131
+ } else if (policy.type === "older-than") {
132
+ const cutoff = policy.date.getTime();
133
+ toDelete = versions.filter((version) => version.timestamp.getTime() < cutoff);
134
+ } else {
135
+ toDelete = versions.filter((version) => !policy.keep(version, versions));
136
+ }
137
+ await removeSnapshots(container, toDelete);
138
+ return toDelete;
139
+ }
140
+ async function coalesceSnapshots(container, strategy, options = {}, basename) {
141
+ const windowMs = options.windowMs ?? 6e4;
142
+ const versions = await listSnapshots(container, strategy, basename);
143
+ const toDelete = [];
144
+ if (versions.length > 0) {
145
+ let anchor = versions[0];
146
+ for (let index = 1; index < versions.length; index++) {
147
+ const candidate = versions[index];
148
+ if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {
149
+ toDelete.push(candidate);
150
+ } else {
151
+ anchor = candidate;
152
+ }
153
+ }
154
+ }
155
+ await removeSnapshots(container, toDelete);
156
+ return toDelete;
157
+ }
158
+ async function removeSnapshots(container, versions) {
159
+ for (const version of versions) await container.removeFile(version.path);
160
+ }
161
+
162
+ export {
163
+ formatVersionTimestamp,
164
+ parseVersionTimestamp,
165
+ VERSION_TIMESTAMP_PATTERN,
166
+ buildSnapshotPath,
167
+ parseSnapshotPath,
168
+ listSnapshots,
169
+ readTextSnapshot,
170
+ saveTextSnapshot,
171
+ pruneSnapshots,
172
+ coalesceSnapshots
173
+ };
174
+ //# sourceMappingURL=chunk-S7A5D7ZJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/versions/timestamp.ts","../src/versions/gitignore.ts","../src/versions/snapshotStore.ts"],"sourcesContent":["/**\n * Sortable, human-readable timestamps for version filenames.\n *\n * Format: ISO 8601 basic, UTC, second precision, `Z` suffix —\n * e.g. `20260430T152030Z`. Lexicographic order matches chronological order,\n * so `listFiles` returns versions in time order without parsing.\n */\n\nconst TIMESTAMP_RE = /^(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})Z$/;\n\nfunction pad2(n: number): string {\n return n < 10 ? `0${n}` : String(n);\n}\n\n/**\n * Format a Date as a sortable, filename-safe UTC timestamp string.\n */\nexport function formatVersionTimestamp(date: Date): string {\n return (\n String(date.getUTCFullYear()).padStart(4, '0') +\n pad2(date.getUTCMonth() + 1) +\n pad2(date.getUTCDate()) +\n 'T' +\n pad2(date.getUTCHours()) +\n pad2(date.getUTCMinutes()) +\n pad2(date.getUTCSeconds()) +\n 'Z'\n );\n}\n\n/**\n * Parse a version timestamp string back to a Date. Returns null if the input\n * doesn't match the expected format.\n */\nexport function parseVersionTimestamp(stamp: string): Date | null {\n const m = TIMESTAMP_RE.exec(stamp);\n if (!m) return null;\n const [, y, mo, d, h, mi, s] = m;\n const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s));\n if (Number.isNaN(ms)) return null;\n return new Date(ms);\n}\n\n/**\n * The pattern a valid version timestamp matches. Exported so other modules\n * can validate filenames without re-deriving the regex.\n */\nexport const VERSION_TIMESTAMP_PATTERN = TIMESTAMP_RE;\n","/**\n * Keep version snapshots out of Git when a ContentContainer is persisted\n * as a real folder (typically a `<basename>_files/` sidecar).\n */\n\nimport type { ContentContainer } from '../storage/ContentContainer.js';\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\nconst GITIGNORE_PATH = '.gitignore';\nconst VERSIONS_IGNORE_RULE = '.versions/';\nconst EQUIVALENT_VERSIONS_RULE = /^(!?)\\/?\\.versions\\/?$/;\n\n/**\n * Ensure the container-root `.gitignore` ignores its `.versions/` folder.\n * Existing rules and newline style are preserved.\n */\nexport async function ensureVersionsGitIgnored(container: ContentContainer): Promise<void> {\n const data = await container.readFile(GITIGNORE_PATH);\n const existing = data ? decoder.decode(data) : '';\n\n let alreadyIgnored = false;\n for (const line of existing.split(/\\r?\\n/)) {\n const match = EQUIVALENT_VERSIONS_RULE.exec(line.trim());\n if (match) alreadyIgnored = match[1] !== '!';\n }\n if (alreadyIgnored) return;\n\n const newline = existing.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const separator = existing.length > 0 && !existing.endsWith('\\n') ? newline : '';\n const next = `${existing}${separator}${VERSIONS_IGNORE_RULE}${newline}`;\n await container.writeFile(GITIGNORE_PATH, encoder.encode(next), 'text/plain');\n}\n","/**\n * Internal storage engine shared by document and image-editor histories.\n * Public APIs keep their domain-specific names and result reasons; this module\n * owns the mechanics that must behave identically across both histories.\n */\n\nimport type { ContentContainer } from '../storage/ContentContainer.js';\nimport { ensureVersionsGitIgnored } from './gitignore.js';\nimport { formatVersionTimestamp, parseVersionTimestamp } from './timestamp.js';\nimport type { CoalesceOptions, PrunePolicy, Version } from './types.js';\n\nconst decoder = new TextDecoder();\nconst encoder = new TextEncoder();\n\nexport interface SnapshotPathStrategy {\n readonly prefix: string;\n readonly extension: string;\n}\n\nexport interface SaveTextSnapshotOptions {\n content: string;\n basename: string;\n mimeType: string;\n now?: Date;\n force?: boolean;\n}\n\nexport interface SaveTextSnapshotResult {\n saved: boolean;\n version: Version | null;\n reason: 'saved' | 'unchanged';\n}\n\nexport function buildSnapshotPath(\n strategy: SnapshotPathStrategy,\n basename: string,\n date: Date,\n collision = 0,\n): string {\n const stamp = formatVersionTimestamp(date);\n const suffix = collision > 0 ? `-${collision + 1}` : '';\n return `${strategy.prefix}${basename}.${stamp}${suffix}.${strategy.extension}`;\n}\n\nexport function parseSnapshotPath(\n strategy: SnapshotPathStrategy,\n path: string,\n): { basename: string; timestamp: Date; collision: number } | null {\n if (!path.startsWith(strategy.prefix)) return null;\n const extension = `.${strategy.extension}`;\n const rest = path.slice(strategy.prefix.length);\n if (!rest.endsWith(extension)) return null;\n const stem = rest.slice(0, -extension.length);\n const lastDot = stem.lastIndexOf('.');\n if (lastDot <= 0) return null;\n\n const basename = stem.slice(0, lastDot);\n const tail = stem.slice(lastDot + 1);\n const dash = tail.indexOf('-');\n let stamp = tail;\n let collision = 0;\n if (dash >= 0) {\n stamp = tail.slice(0, dash);\n const suffix = Number(tail.slice(dash + 1));\n if (!Number.isInteger(suffix) || suffix < 2) return null;\n collision = suffix - 1;\n }\n\n const timestamp = parseVersionTimestamp(stamp);\n return timestamp ? { basename, timestamp, collision } : null;\n}\n\nexport async function listSnapshots(\n container: ContentContainer,\n strategy: SnapshotPathStrategy,\n basename?: string,\n): Promise<Version[]> {\n const entries = await container.listFiles(strategy.prefix);\n const versions: Version[] = [];\n for (const entry of entries) {\n const parsed = parseSnapshotPath(strategy, entry.path);\n if (!parsed || (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\n versions.sort((a, b) => {\n const timestampDelta = b.timestamp.getTime() - a.timestamp.getTime();\n return timestampDelta || b.collision - a.collision;\n });\n return versions;\n}\n\nexport async function readTextSnapshot(\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 return data ? decoder.decode(data) : null;\n}\n\nexport async function saveTextSnapshot(\n container: ContentContainer,\n strategy: SnapshotPathStrategy,\n options: SaveTextSnapshotOptions,\n): Promise<SaveTextSnapshotResult> {\n const versions = await listSnapshots(container, strategy, options.basename);\n if (!options.force && versions.length > 0) {\n const existing = await readTextSnapshot(container, versions[0]!);\n if (existing === options.content) {\n // Backfill containers written before history owned its ignore rule.\n await ensureVersionsGitIgnored(container);\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 = buildSnapshotPath(strategy, options.basename, now, collision);\n while (await container.exists(path)) {\n collision += 1;\n path = buildSnapshotPath(strategy, options.basename, now, collision);\n }\n\n const data = encoder.encode(options.content);\n await ensureVersionsGitIgnored(container);\n await container.writeFile(path, data, options.mimeType);\n return {\n saved: true,\n version: {\n path,\n basename: options.basename,\n timestamp: now,\n size: data.byteLength,\n collision,\n },\n reason: 'saved',\n };\n}\n\nexport async function pruneSnapshots(\n container: ContentContainer,\n strategy: SnapshotPathStrategy,\n policy: PrunePolicy,\n basename?: string,\n): Promise<Version[]> {\n const versions = await listSnapshots(container, strategy, basename);\n let toDelete: Version[];\n\n if (policy.type === 'keep-last-n') {\n toDelete = versions.slice(Math.max(0, Math.floor(policy.n)));\n } else if (policy.type === 'older-than') {\n const cutoff = policy.date.getTime();\n toDelete = versions.filter((version) => version.timestamp.getTime() < cutoff);\n } else {\n toDelete = versions.filter((version) => !policy.keep(version, versions));\n }\n\n await removeSnapshots(container, toDelete);\n return toDelete;\n}\n\nexport async function coalesceSnapshots(\n container: ContentContainer,\n strategy: SnapshotPathStrategy,\n options: CoalesceOptions = {},\n basename?: string,\n): Promise<Version[]> {\n const windowMs = options.windowMs ?? 60_000;\n const versions = await listSnapshots(container, strategy, basename);\n const toDelete: Version[] = [];\n\n // Compare against the last kept snapshot. Pairwise comparison against a\n // deleted neighbor would chain through a dense run and over-prune it.\n if (versions.length > 0) {\n let anchor = versions[0]!;\n for (let index = 1; index < versions.length; index++) {\n const candidate = versions[index]!;\n if (anchor.timestamp.getTime() - candidate.timestamp.getTime() <= windowMs) {\n toDelete.push(candidate);\n } else {\n anchor = candidate;\n }\n }\n }\n\n await removeSnapshots(container, toDelete);\n return toDelete;\n}\n\nasync function removeSnapshots(\n container: ContentContainer,\n versions: readonly Version[],\n): Promise<void> {\n for (const version of versions) await container.removeFile(version.path);\n}\n"],"mappings":"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","names":["decoder","encoder"]}