@bendyline/squisq 1.3.0 → 1.4.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 (419) hide show
  1. package/dist/{Doc-Ch4tc3vv.d.ts → Doc-BeSeH2iX.d.ts} +118 -41
  2. package/dist/ImageEditDoc-BTIvw0oq.d.ts +84 -0
  3. package/dist/__tests__/colorUtils.test.d.ts +2 -0
  4. package/dist/__tests__/colorUtils.test.d.ts.map +1 -0
  5. package/dist/__tests__/colorUtils.test.js +94 -0
  6. package/dist/__tests__/colorUtils.test.js.map +1 -0
  7. package/dist/__tests__/getLayers.test.js +31 -7
  8. package/dist/__tests__/getLayers.test.js.map +1 -1
  9. package/dist/__tests__/iconResolve.test.d.ts +2 -0
  10. package/dist/__tests__/iconResolve.test.d.ts.map +1 -0
  11. package/dist/__tests__/iconResolve.test.js +99 -0
  12. package/dist/__tests__/iconResolve.test.js.map +1 -0
  13. package/dist/__tests__/imageEditState.test.d.ts +2 -0
  14. package/dist/__tests__/imageEditState.test.d.ts.map +1 -0
  15. package/dist/__tests__/imageEditState.test.js +61 -0
  16. package/dist/__tests__/imageEditState.test.js.map +1 -0
  17. package/dist/__tests__/imageEditVersions.test.d.ts +2 -0
  18. package/dist/__tests__/imageEditVersions.test.d.ts.map +1 -0
  19. package/dist/__tests__/imageEditVersions.test.js +141 -0
  20. package/dist/__tests__/imageEditVersions.test.js.map +1 -0
  21. package/dist/__tests__/inferDocumentTitle.test.d.ts +2 -0
  22. package/dist/__tests__/inferDocumentTitle.test.d.ts.map +1 -0
  23. package/dist/__tests__/inferDocumentTitle.test.js +42 -0
  24. package/dist/__tests__/inferDocumentTitle.test.js.map +1 -0
  25. package/dist/__tests__/jsonForm.chooseControl.test.d.ts +2 -0
  26. package/dist/__tests__/jsonForm.chooseControl.test.d.ts.map +1 -0
  27. package/dist/__tests__/jsonForm.chooseControl.test.js +74 -0
  28. package/dist/__tests__/jsonForm.chooseControl.test.js.map +1 -0
  29. package/dist/__tests__/jsonForm.evaluateWhen.test.d.ts +2 -0
  30. package/dist/__tests__/jsonForm.evaluateWhen.test.d.ts.map +1 -0
  31. package/dist/__tests__/jsonForm.evaluateWhen.test.js +51 -0
  32. package/dist/__tests__/jsonForm.evaluateWhen.test.js.map +1 -0
  33. package/dist/__tests__/jsonForm.inferSchema.test.d.ts +2 -0
  34. package/dist/__tests__/jsonForm.inferSchema.test.d.ts.map +1 -0
  35. package/dist/__tests__/jsonForm.inferSchema.test.js +23 -0
  36. package/dist/__tests__/jsonForm.inferSchema.test.js.map +1 -0
  37. package/dist/__tests__/jsonForm.pointer.test.d.ts +2 -0
  38. package/dist/__tests__/jsonForm.pointer.test.d.ts.map +1 -0
  39. package/dist/__tests__/jsonForm.pointer.test.js +94 -0
  40. package/dist/__tests__/jsonForm.pointer.test.js.map +1 -0
  41. package/dist/__tests__/markdownIcons.test.d.ts +2 -0
  42. package/dist/__tests__/markdownIcons.test.d.ts.map +1 -0
  43. package/dist/__tests__/markdownIcons.test.js +93 -0
  44. package/dist/__tests__/markdownIcons.test.js.map +1 -0
  45. package/dist/__tests__/markdownToDoc.test.js +15 -4
  46. package/dist/__tests__/markdownToDoc.test.js.map +1 -1
  47. package/dist/__tests__/recommendTemplates.test.d.ts +2 -0
  48. package/dist/__tests__/recommendTemplates.test.d.ts.map +1 -0
  49. package/dist/__tests__/recommendTemplates.test.js +158 -0
  50. package/dist/__tests__/recommendTemplates.test.js.map +1 -0
  51. package/dist/__tests__/scopedContentContainer.test.d.ts +2 -0
  52. package/dist/__tests__/scopedContentContainer.test.d.ts.map +1 -0
  53. package/dist/__tests__/scopedContentContainer.test.js +74 -0
  54. package/dist/__tests__/scopedContentContainer.test.js.map +1 -0
  55. package/dist/__tests__/slideshowGenerator.test.js +1 -1
  56. package/dist/__tests__/slideshowGenerator.test.js.map +1 -1
  57. package/dist/__tests__/templateAnnotationParse.test.d.ts +2 -0
  58. package/dist/__tests__/templateAnnotationParse.test.d.ts.map +1 -0
  59. package/dist/__tests__/templateAnnotationParse.test.js +42 -0
  60. package/dist/__tests__/templateAnnotationParse.test.js.map +1 -0
  61. package/dist/__tests__/templates.test.js +90 -9
  62. package/dist/__tests__/templates.test.js.map +1 -1
  63. package/dist/__tests__/themeCompile.test.d.ts +2 -0
  64. package/dist/__tests__/themeCompile.test.d.ts.map +1 -0
  65. package/dist/__tests__/themeCompile.test.js +134 -0
  66. package/dist/__tests__/themeCompile.test.js.map +1 -0
  67. package/dist/__tests__/themeValidator.test.d.ts +2 -0
  68. package/dist/__tests__/themeValidator.test.d.ts.map +1 -0
  69. package/dist/__tests__/themeValidator.test.js +107 -0
  70. package/dist/__tests__/themeValidator.test.js.map +1 -0
  71. package/dist/__tests__/versions.test.d.ts +2 -0
  72. package/dist/__tests__/versions.test.d.ts.map +1 -0
  73. package/dist/__tests__/versions.test.js +339 -0
  74. package/dist/__tests__/versions.test.js.map +1 -0
  75. package/dist/{chunk-CYQHG27J.js → chunk-2TMAWB4Y.js} +74 -1
  76. package/dist/chunk-2TMAWB4Y.js.map +1 -0
  77. package/dist/{chunk-OASJKWO5.js → chunk-3K5OG6KI.js} +92 -7
  78. package/dist/chunk-3K5OG6KI.js.map +1 -0
  79. package/dist/chunk-57CVQCFX.js +193 -0
  80. package/dist/chunk-57CVQCFX.js.map +1 -0
  81. package/dist/{chunk-QM5PKNPW.js → chunk-5WFRKURX.js} +1 -1
  82. package/dist/chunk-5WFRKURX.js.map +1 -0
  83. package/dist/chunk-B4EEON3N.js +1317 -0
  84. package/dist/chunk-B4EEON3N.js.map +1 -0
  85. package/dist/chunk-DBS6RIFL.js +81 -0
  86. package/dist/chunk-DBS6RIFL.js.map +1 -0
  87. package/dist/{chunk-MIBRIRGJ.js → chunk-FR2RBTKO.js} +3 -3
  88. package/dist/{chunk-MIBRIRGJ.js.map → chunk-FR2RBTKO.js.map} +1 -1
  89. package/dist/chunk-H3AXU4MJ.js +179 -0
  90. package/dist/chunk-H3AXU4MJ.js.map +1 -0
  91. package/dist/chunk-JELIRVDP.js +24 -0
  92. package/dist/chunk-JELIRVDP.js.map +1 -0
  93. package/dist/chunk-K32VJONL.js +193 -0
  94. package/dist/chunk-K32VJONL.js.map +1 -0
  95. package/dist/{chunk-6BAVXFYC.js → chunk-KSWJR66U.js} +218 -42
  96. package/dist/chunk-KSWJR66U.js.map +1 -0
  97. package/dist/{chunk-TSG2OSE6.js → chunk-MYIH7FWD.js} +3 -3
  98. package/dist/chunk-MYIH7FWD.js.map +1 -0
  99. package/dist/chunk-VLJJHDAC.js +218 -0
  100. package/dist/chunk-VLJJHDAC.js.map +1 -0
  101. package/dist/chunk-WMOI7FN3.js +182 -0
  102. package/dist/chunk-WMOI7FN3.js.map +1 -0
  103. package/dist/chunk-ZTX4PFFT.js +520 -0
  104. package/dist/chunk-ZTX4PFFT.js.map +1 -0
  105. package/dist/doc/audioMapping.d.ts.map +1 -1
  106. package/dist/doc/audioMapping.js +42 -7
  107. package/dist/doc/audioMapping.js.map +1 -1
  108. package/dist/doc/getLayers.d.ts.map +1 -1
  109. package/dist/doc/getLayers.js +29 -18
  110. package/dist/doc/getLayers.js.map +1 -1
  111. package/dist/doc/markdownToDoc.d.ts.map +1 -1
  112. package/dist/doc/markdownToDoc.js +40 -6
  113. package/dist/doc/markdownToDoc.js.map +1 -1
  114. package/dist/doc/templates/featureBlock.d.ts +18 -0
  115. package/dist/doc/templates/featureBlock.d.ts.map +1 -0
  116. package/dist/doc/templates/featureBlock.js +180 -0
  117. package/dist/doc/templates/featureBlock.js.map +1 -0
  118. package/dist/doc/templates/fullBleedQuote.d.ts +12 -0
  119. package/dist/doc/templates/fullBleedQuote.d.ts.map +1 -1
  120. package/dist/doc/templates/fullBleedQuote.js +12 -0
  121. package/dist/doc/templates/fullBleedQuote.js.map +1 -1
  122. package/dist/doc/templates/index.d.ts +28 -4
  123. package/dist/doc/templates/index.d.ts.map +1 -1
  124. package/dist/doc/templates/index.js +45 -12
  125. package/dist/doc/templates/index.js.map +1 -1
  126. package/dist/doc/templates/listBlock.d.ts.map +1 -1
  127. package/dist/doc/templates/listBlock.js +22 -5
  128. package/dist/doc/templates/listBlock.js.map +1 -1
  129. package/dist/doc/templates/statHighlight.d.ts +12 -0
  130. package/dist/doc/templates/statHighlight.d.ts.map +1 -1
  131. package/dist/doc/templates/statHighlight.js +12 -0
  132. package/dist/doc/templates/statHighlight.js.map +1 -1
  133. package/dist/doc/templates/titleBlock.d.ts +11 -0
  134. package/dist/doc/templates/titleBlock.d.ts.map +1 -1
  135. package/dist/doc/templates/titleBlock.js +8 -0
  136. package/dist/doc/templates/titleBlock.js.map +1 -1
  137. package/dist/doc/utils/themeUtils.d.ts +4 -4
  138. package/dist/doc/utils/themeUtils.d.ts.map +1 -1
  139. package/dist/doc/utils/themeUtils.js +8 -7
  140. package/dist/doc/utils/themeUtils.js.map +1 -1
  141. package/dist/generate/slideshowGenerator.js +1 -1
  142. package/dist/generate/slideshowGenerator.js.map +1 -1
  143. package/dist/generate/templateMapper.js +2 -2
  144. package/dist/generate/templateMapper.js.map +1 -1
  145. package/dist/icons/iconData.d.ts +26 -0
  146. package/dist/icons/iconData.d.ts.map +1 -0
  147. package/dist/icons/iconData.js +12 -0
  148. package/dist/icons/iconData.js.map +1 -0
  149. package/dist/icons/index.d.ts +12 -0
  150. package/dist/icons/index.d.ts.map +1 -0
  151. package/dist/icons/index.js +10 -0
  152. package/dist/icons/index.js.map +1 -0
  153. package/dist/icons/resolve.d.ts +61 -0
  154. package/dist/icons/resolve.d.ts.map +1 -0
  155. package/dist/icons/resolve.js +130 -0
  156. package/dist/icons/resolve.js.map +1 -0
  157. package/dist/imageEdit/ImageEditVersionManager.d.ts +31 -0
  158. package/dist/imageEdit/ImageEditVersionManager.d.ts.map +1 -0
  159. package/dist/imageEdit/ImageEditVersionManager.js +39 -0
  160. package/dist/imageEdit/ImageEditVersionManager.js.map +1 -0
  161. package/dist/imageEdit/export.d.ts +33 -0
  162. package/dist/imageEdit/export.d.ts.map +1 -0
  163. package/dist/imageEdit/export.js +253 -0
  164. package/dist/imageEdit/export.js.map +1 -0
  165. package/dist/imageEdit/index.d.ts +14 -0
  166. package/dist/imageEdit/index.d.ts.map +1 -0
  167. package/dist/imageEdit/index.js +22 -0
  168. package/dist/imageEdit/index.js.map +1 -0
  169. package/dist/imageEdit/persistence.d.ts +15 -0
  170. package/dist/imageEdit/persistence.d.ts.map +1 -0
  171. package/dist/imageEdit/persistence.js +49 -0
  172. package/dist/imageEdit/persistence.js.map +1 -0
  173. package/dist/imageEdit/state.d.ts +42 -0
  174. package/dist/imageEdit/state.d.ts.map +1 -0
  175. package/dist/imageEdit/state.js +98 -0
  176. package/dist/imageEdit/state.js.map +1 -0
  177. package/dist/imageEdit/versionPaths.d.ts +27 -0
  178. package/dist/imageEdit/versionPaths.d.ts.map +1 -0
  179. package/dist/imageEdit/versionPaths.js +57 -0
  180. package/dist/imageEdit/versionPaths.js.map +1 -0
  181. package/dist/imageEdit/versions.d.ts +67 -0
  182. package/dist/imageEdit/versions.d.ts.map +1 -0
  183. package/dist/imageEdit/versions.js +161 -0
  184. package/dist/imageEdit/versions.js.map +1 -0
  185. package/dist/index.d.ts +4 -0
  186. package/dist/index.d.ts.map +1 -1
  187. package/dist/index.js +4 -0
  188. package/dist/index.js.map +1 -1
  189. package/dist/jsonForm/chooseControl.d.ts +20 -0
  190. package/dist/jsonForm/chooseControl.d.ts.map +1 -0
  191. package/dist/jsonForm/chooseControl.js +82 -0
  192. package/dist/jsonForm/chooseControl.js.map +1 -0
  193. package/dist/jsonForm/evaluateWhen.d.ts +12 -0
  194. package/dist/jsonForm/evaluateWhen.d.ts.map +1 -0
  195. package/dist/jsonForm/evaluateWhen.js +65 -0
  196. package/dist/jsonForm/evaluateWhen.js.map +1 -0
  197. package/dist/jsonForm/index.d.ts +14 -0
  198. package/dist/jsonForm/index.d.ts.map +1 -0
  199. package/dist/jsonForm/index.js +12 -0
  200. package/dist/jsonForm/index.js.map +1 -0
  201. package/dist/jsonForm/inferSchema.d.ts +12 -0
  202. package/dist/jsonForm/inferSchema.d.ts.map +1 -0
  203. package/dist/jsonForm/inferSchema.js +15 -0
  204. package/dist/jsonForm/inferSchema.js.map +1 -0
  205. package/dist/jsonForm/pointer.d.ts +26 -0
  206. package/dist/jsonForm/pointer.d.ts.map +1 -0
  207. package/dist/jsonForm/pointer.js +102 -0
  208. package/dist/jsonForm/pointer.js.map +1 -0
  209. package/dist/jsonForm/types.d.ts +120 -0
  210. package/dist/jsonForm/types.d.ts.map +1 -0
  211. package/dist/jsonForm/types.js +10 -0
  212. package/dist/jsonForm/types.js.map +1 -0
  213. package/dist/markdown/convert.d.ts +11 -1
  214. package/dist/markdown/convert.d.ts.map +1 -1
  215. package/dist/markdown/convert.js +135 -5
  216. package/dist/markdown/convert.js.map +1 -1
  217. package/dist/markdown/index.d.ts +1 -1
  218. package/dist/markdown/index.d.ts.map +1 -1
  219. package/dist/markdown/index.js +1 -1
  220. package/dist/markdown/index.js.map +1 -1
  221. package/dist/markdown/types.d.ts +17 -2
  222. package/dist/markdown/types.d.ts.map +1 -1
  223. package/dist/markdown/utils.d.ts +40 -0
  224. package/dist/markdown/utils.d.ts.map +1 -1
  225. package/dist/markdown/utils.js +155 -0
  226. package/dist/markdown/utils.js.map +1 -1
  227. package/dist/recommend/index.d.ts +16 -0
  228. package/dist/recommend/index.d.ts.map +1 -0
  229. package/dist/recommend/index.js +15 -0
  230. package/dist/recommend/index.js.map +1 -0
  231. package/dist/recommend/templates.d.ts +39 -0
  232. package/dist/recommend/templates.d.ts.map +1 -0
  233. package/dist/recommend/templates.js +235 -0
  234. package/dist/recommend/templates.js.map +1 -0
  235. package/dist/schemas/BlockTemplates.d.ts +46 -8
  236. package/dist/schemas/BlockTemplates.d.ts.map +1 -1
  237. package/dist/schemas/BlockTemplates.js +3 -3
  238. package/dist/schemas/BlockTemplates.js.map +1 -1
  239. package/dist/schemas/ImageEditDoc.d.ts +81 -0
  240. package/dist/schemas/ImageEditDoc.d.ts.map +1 -0
  241. package/dist/schemas/ImageEditDoc.js +20 -0
  242. package/dist/schemas/ImageEditDoc.js.map +1 -0
  243. package/dist/schemas/Theme.d.ts +70 -31
  244. package/dist/schemas/Theme.d.ts.map +1 -1
  245. package/dist/schemas/Theme.js +48 -16
  246. package/dist/schemas/Theme.js.map +1 -1
  247. package/dist/schemas/colorUtils.d.ts +51 -0
  248. package/dist/schemas/colorUtils.d.ts.map +1 -0
  249. package/dist/schemas/colorUtils.js +154 -0
  250. package/dist/schemas/colorUtils.js.map +1 -0
  251. package/dist/schemas/fontStacks.d.ts +57 -0
  252. package/dist/schemas/fontStacks.d.ts.map +1 -0
  253. package/dist/schemas/fontStacks.js +227 -0
  254. package/dist/schemas/fontStacks.js.map +1 -0
  255. package/dist/schemas/index.d.ts +5 -0
  256. package/dist/schemas/index.d.ts.map +1 -1
  257. package/dist/schemas/index.js +4 -0
  258. package/dist/schemas/index.js.map +1 -1
  259. package/dist/schemas/themeCompile.d.ts +45 -0
  260. package/dist/schemas/themeCompile.d.ts.map +1 -0
  261. package/dist/schemas/themeCompile.js +160 -0
  262. package/dist/schemas/themeCompile.js.map +1 -0
  263. package/dist/schemas/themeLibrary.d.ts +11 -15
  264. package/dist/schemas/themeLibrary.d.ts.map +1 -1
  265. package/dist/schemas/themeLibrary.js +18 -566
  266. package/dist/schemas/themeLibrary.js.map +1 -1
  267. package/dist/schemas/themeValidator.d.ts +26 -0
  268. package/dist/schemas/themeValidator.d.ts.map +1 -0
  269. package/dist/schemas/themeValidator.js +352 -0
  270. package/dist/schemas/themeValidator.js.map +1 -0
  271. package/dist/schemas/themes/bold.json +48 -0
  272. package/dist/schemas/themes/cinematic.json +54 -0
  273. package/dist/schemas/themes/documentary.json +49 -0
  274. package/dist/schemas/themes/gezellig.json +44 -0
  275. package/dist/schemas/themes/index.d.ts +26 -0
  276. package/dist/schemas/themes/index.d.ts.map +1 -0
  277. package/dist/schemas/themes/index.js +49 -0
  278. package/dist/schemas/themes/index.js.map +1 -0
  279. package/dist/schemas/themes/magazine.json +44 -0
  280. package/dist/schemas/themes/minimalist.json +45 -0
  281. package/dist/schemas/themes/morning-light.json +42 -0
  282. package/dist/schemas/themes/standard-dark.json +50 -0
  283. package/dist/schemas/themes/standard.json +42 -0
  284. package/dist/schemas/themes/tech-dark.json +48 -0
  285. package/dist/schemas/themes/warm-earth.json +50 -0
  286. package/dist/storage/ScopedContentContainer.d.ts +44 -0
  287. package/dist/storage/ScopedContentContainer.d.ts.map +1 -0
  288. package/dist/storage/ScopedContentContainer.js +94 -0
  289. package/dist/storage/ScopedContentContainer.js.map +1 -0
  290. package/dist/storage/index.d.ts +1 -0
  291. package/dist/storage/index.d.ts.map +1 -1
  292. package/dist/storage/index.js +1 -0
  293. package/dist/storage/index.js.map +1 -1
  294. package/dist/story/index.d.ts +53 -13
  295. package/dist/story/index.js +12 -4
  296. package/dist/themeLibrary-BHjJ0Qx6.d.ts +40 -0
  297. package/dist/{types-CkNttVSF.d.ts → types-BdZkdJ3z.d.ts} +17 -2
  298. package/dist/types-D98li1gV.d.ts +93 -0
  299. package/dist/versions/DocumentVersionManager.d.ts +31 -0
  300. package/dist/versions/DocumentVersionManager.d.ts.map +1 -0
  301. package/dist/versions/DocumentVersionManager.js +33 -0
  302. package/dist/versions/DocumentVersionManager.js.map +1 -0
  303. package/dist/versions/index.d.ts +29 -0
  304. package/dist/versions/index.d.ts.map +1 -0
  305. package/dist/versions/index.js +28 -0
  306. package/dist/versions/index.js.map +1 -0
  307. package/dist/versions/operations.d.ts +44 -0
  308. package/dist/versions/operations.d.ts.map +1 -0
  309. package/dist/versions/operations.js +190 -0
  310. package/dist/versions/operations.js.map +1 -0
  311. package/dist/versions/paths.d.ts +29 -0
  312. package/dist/versions/paths.d.ts.map +1 -0
  313. package/dist/versions/paths.js +66 -0
  314. package/dist/versions/paths.js.map +1 -0
  315. package/dist/versions/timestamp.d.ts +22 -0
  316. package/dist/versions/timestamp.d.ts.map +1 -0
  317. package/dist/versions/timestamp.js +44 -0
  318. package/dist/versions/timestamp.js.map +1 -0
  319. package/dist/versions/types.d.ts +92 -0
  320. package/dist/versions/types.d.ts.map +1 -0
  321. package/dist/versions/types.js +11 -0
  322. package/dist/versions/types.js.map +1 -0
  323. package/package.json +28 -1
  324. package/src/__tests__/colorUtils.test.ts +112 -0
  325. package/src/__tests__/getLayers.test.ts +32 -7
  326. package/src/__tests__/iconResolve.test.ts +122 -0
  327. package/src/__tests__/imageEditState.test.ts +74 -0
  328. package/src/__tests__/imageEditVersions.test.ts +169 -0
  329. package/src/__tests__/inferDocumentTitle.test.ts +50 -0
  330. package/src/__tests__/jsonForm.chooseControl.test.ts +86 -0
  331. package/src/__tests__/jsonForm.evaluateWhen.test.ts +59 -0
  332. package/src/__tests__/jsonForm.inferSchema.test.ts +25 -0
  333. package/src/__tests__/jsonForm.pointer.test.ts +103 -0
  334. package/src/__tests__/markdownIcons.test.ts +104 -0
  335. package/src/__tests__/markdownToDoc.test.ts +19 -4
  336. package/src/__tests__/recommendTemplates.test.ts +183 -0
  337. package/src/__tests__/scopedContentContainer.test.ts +89 -0
  338. package/src/__tests__/slideshowGenerator.test.ts +1 -1
  339. package/src/__tests__/templateAnnotationParse.test.ts +42 -0
  340. package/src/__tests__/templates.test.ts +93 -9
  341. package/src/__tests__/themeCompile.test.ts +156 -0
  342. package/src/__tests__/themeValidator.test.ts +125 -0
  343. package/src/__tests__/versions.test.ts +415 -0
  344. package/src/doc/audioMapping.ts +55 -11
  345. package/src/doc/getLayers.ts +32 -21
  346. package/src/doc/markdownToDoc.ts +45 -7
  347. package/src/doc/templates/featureBlock.ts +202 -0
  348. package/src/doc/templates/fullBleedQuote.ts +13 -0
  349. package/src/doc/templates/index.ts +50 -16
  350. package/src/doc/templates/listBlock.ts +24 -5
  351. package/src/doc/templates/statHighlight.ts +13 -0
  352. package/src/doc/templates/titleBlock.ts +9 -0
  353. package/src/doc/utils/themeUtils.ts +8 -7
  354. package/src/generate/slideshowGenerator.ts +1 -1
  355. package/src/generate/templateMapper.ts +2 -2
  356. package/src/icons/iconData.ts +31 -0
  357. package/src/icons/index.ts +18 -0
  358. package/src/icons/resolve.ts +139 -0
  359. package/src/imageEdit/ImageEditVersionManager.ts +73 -0
  360. package/src/imageEdit/export.ts +326 -0
  361. package/src/imageEdit/index.ts +71 -0
  362. package/src/imageEdit/persistence.ts +62 -0
  363. package/src/imageEdit/state.ts +112 -0
  364. package/src/imageEdit/versionPaths.ts +57 -0
  365. package/src/imageEdit/versions.ts +231 -0
  366. package/src/index.ts +4 -0
  367. package/src/jsonForm/chooseControl.ts +88 -0
  368. package/src/jsonForm/evaluateWhen.ts +59 -0
  369. package/src/jsonForm/index.ts +22 -0
  370. package/src/jsonForm/inferSchema.ts +25 -0
  371. package/src/jsonForm/pointer.ts +109 -0
  372. package/src/jsonForm/types.ts +167 -0
  373. package/src/markdown/convert.ts +145 -5
  374. package/src/markdown/index.ts +3 -0
  375. package/src/markdown/types.ts +19 -2
  376. package/src/markdown/utils.ts +159 -0
  377. package/src/recommend/index.ts +16 -0
  378. package/src/recommend/templates.ts +273 -0
  379. package/src/schemas/BlockTemplates.ts +50 -7
  380. package/src/schemas/ImageEditDoc.ts +95 -0
  381. package/src/schemas/Theme.ts +98 -31
  382. package/src/schemas/colorUtils.ts +186 -0
  383. package/src/schemas/fontStacks.ts +257 -0
  384. package/src/schemas/index.ts +12 -0
  385. package/src/schemas/themeCompile.ts +203 -0
  386. package/src/schemas/themeLibrary.ts +18 -597
  387. package/src/schemas/themeValidator.ts +388 -0
  388. package/src/schemas/themes/bold.json +48 -0
  389. package/src/schemas/themes/cinematic.json +54 -0
  390. package/src/schemas/themes/documentary.json +49 -0
  391. package/src/schemas/themes/gezellig.json +44 -0
  392. package/src/schemas/themes/index.ts +53 -0
  393. package/src/schemas/themes/magazine.json +44 -0
  394. package/src/schemas/themes/minimalist.json +45 -0
  395. package/src/schemas/themes/morning-light.json +42 -0
  396. package/src/schemas/themes/standard-dark.json +50 -0
  397. package/src/schemas/themes/standard.json +42 -0
  398. package/src/schemas/themes/tech-dark.json +48 -0
  399. package/src/schemas/themes/warm-earth.json +50 -0
  400. package/src/storage/ScopedContentContainer.ts +113 -0
  401. package/src/storage/index.ts +1 -0
  402. package/src/versions/DocumentVersionManager.ts +70 -0
  403. package/src/versions/index.ts +54 -0
  404. package/src/versions/operations.ts +230 -0
  405. package/src/versions/paths.ts +66 -0
  406. package/src/versions/timestamp.ts +48 -0
  407. package/src/versions/types.ts +91 -0
  408. package/dist/chunk-6BAVXFYC.js.map +0 -1
  409. package/dist/chunk-7GF6RDED.js +0 -82
  410. package/dist/chunk-7GF6RDED.js.map +0 -1
  411. package/dist/chunk-CYQHG27J.js.map +0 -1
  412. package/dist/chunk-F22AE6BY.js +0 -121
  413. package/dist/chunk-F22AE6BY.js.map +0 -1
  414. package/dist/chunk-FIQR4AJG.js +0 -517
  415. package/dist/chunk-FIQR4AJG.js.map +0 -1
  416. package/dist/chunk-OASJKWO5.js.map +0 -1
  417. package/dist/chunk-QM5PKNPW.js.map +0 -1
  418. package/dist/chunk-TSG2OSE6.js.map +0 -1
  419. package/dist/themeLibrary-CEo2D4b6.d.ts +0 -44
@@ -0,0 +1,415 @@
1
+ import { describe, expect, it, beforeEach } from 'vitest';
2
+ import { MemoryContentContainer } from '../storage/ContentContainer';
3
+ import {
4
+ DocumentVersionManager,
5
+ VERSIONS_PREFIX,
6
+ buildVersionPath,
7
+ coalesceVersions,
8
+ formatVersionTimestamp,
9
+ getDocBasename,
10
+ listVersions,
11
+ parseVersionPath,
12
+ parseVersionTimestamp,
13
+ pruneVersions,
14
+ readVersion,
15
+ revertToVersion,
16
+ saveVersion,
17
+ } from '../versions';
18
+
19
+ const decoder = new TextDecoder();
20
+
21
+ async function readUtf8(c: MemoryContentContainer, path: string): Promise<string | null> {
22
+ const data = await c.readFile(path);
23
+ return data ? decoder.decode(data) : null;
24
+ }
25
+
26
+ describe('formatVersionTimestamp / parseVersionTimestamp', () => {
27
+ it('round-trips to second precision', () => {
28
+ const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
29
+ const stamp = formatVersionTimestamp(d);
30
+ expect(stamp).toBe('20260430T152030Z');
31
+ const parsed = parseVersionTimestamp(stamp);
32
+ expect(parsed?.getTime()).toBe(d.getTime());
33
+ });
34
+
35
+ it('returns null for invalid input', () => {
36
+ expect(parseVersionTimestamp('not-a-stamp')).toBeNull();
37
+ expect(parseVersionTimestamp('20260430T152030')).toBeNull(); // missing Z
38
+ expect(parseVersionTimestamp('2026-04-30T15:20:30Z')).toBeNull(); // wrong format
39
+ });
40
+
41
+ it('orders chronologically when sorted lexicographically', () => {
42
+ const a = formatVersionTimestamp(new Date(Date.UTC(2026, 0, 1, 0, 0, 0)));
43
+ const b = formatVersionTimestamp(new Date(Date.UTC(2026, 0, 1, 0, 0, 1)));
44
+ const c = formatVersionTimestamp(new Date(Date.UTC(2026, 5, 15, 12, 0, 0)));
45
+ expect([c, a, b].sort()).toEqual([a, b, c]);
46
+ });
47
+ });
48
+
49
+ describe('paths', () => {
50
+ it('getDocBasename strips folder + extension', () => {
51
+ expect(getDocBasename('index.md')).toBe('index');
52
+ expect(getDocBasename('subdir/foo.md')).toBe('foo');
53
+ expect(getDocBasename('a/b/c/longname.md')).toBe('longname');
54
+ expect(getDocBasename('noext')).toBe('noext');
55
+ });
56
+
57
+ it('buildVersionPath round-trips through parseVersionPath', () => {
58
+ const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
59
+ const p = buildVersionPath('index', d);
60
+ expect(p).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
61
+ const parsed = parseVersionPath(p);
62
+ expect(parsed).not.toBeNull();
63
+ expect(parsed!.basename).toBe('index');
64
+ expect(parsed!.timestamp.getTime()).toBe(d.getTime());
65
+ expect(parsed!.collision).toBe(0);
66
+ });
67
+
68
+ it('buildVersionPath / parseVersionPath handle collision suffix', () => {
69
+ const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
70
+ const p1 = buildVersionPath('index', d, 1);
71
+ expect(p1).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-2.md`);
72
+ const parsed = parseVersionPath(p1);
73
+ expect(parsed?.collision).toBe(1);
74
+ });
75
+
76
+ it('parseVersionPath returns null for non-version files', () => {
77
+ expect(parseVersionPath('index.md')).toBeNull();
78
+ expect(parseVersionPath('.versions/notes.txt')).toBeNull();
79
+ expect(parseVersionPath('.versions/index.invalidstamp.md')).toBeNull();
80
+ expect(parseVersionPath('.versions/index.20260430T152030Z-1.md')).toBeNull(); // suffix must be >= 2
81
+ });
82
+ });
83
+
84
+ describe('saveVersion', () => {
85
+ let container: MemoryContentContainer;
86
+
87
+ beforeEach(() => {
88
+ container = new MemoryContentContainer();
89
+ });
90
+
91
+ it('returns no-document when container has no markdown', async () => {
92
+ const result = await saveVersion(container);
93
+ expect(result.saved).toBe(false);
94
+ expect(result.reason).toBe('no-document');
95
+ });
96
+
97
+ it('returns empty when document content is empty', async () => {
98
+ await container.writeDocument('', 'index.md');
99
+ const result = await saveVersion(container);
100
+ expect(result.saved).toBe(false);
101
+ expect(result.reason).toBe('empty');
102
+ });
103
+
104
+ it('writes a snapshot to .versions/ on first save', async () => {
105
+ await container.writeDocument('# hello', 'index.md');
106
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
107
+ const result = await saveVersion(container, { now });
108
+
109
+ expect(result.saved).toBe(true);
110
+ expect(result.reason).toBe('saved');
111
+ expect(result.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
112
+ expect(result.version?.basename).toBe('index');
113
+ expect(await readUtf8(container, result.version!.path)).toBe('# hello');
114
+ });
115
+
116
+ it('returns unchanged on identical second save', async () => {
117
+ await container.writeDocument('# hello', 'index.md');
118
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
119
+ const result = await saveVersion(container, {
120
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)),
121
+ });
122
+ expect(result.saved).toBe(false);
123
+ expect(result.reason).toBe('unchanged');
124
+ const all = await listVersions(container);
125
+ expect(all).toHaveLength(1);
126
+ });
127
+
128
+ it('writes again when content changes', async () => {
129
+ await container.writeDocument('# hello', 'index.md');
130
+ const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
131
+ await container.writeDocument('# hello world', 'index.md');
132
+ const r2 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)) });
133
+
134
+ expect(r1.saved).toBe(true);
135
+ expect(r2.saved).toBe(true);
136
+ expect(r1.version?.path).not.toBe(r2.version?.path);
137
+ expect(await listVersions(container)).toHaveLength(2);
138
+ });
139
+
140
+ it('force: true writes despite equality', async () => {
141
+ await container.writeDocument('# hello', 'index.md');
142
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
143
+ const r = await saveVersion(container, {
144
+ force: true,
145
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)),
146
+ });
147
+ expect(r.saved).toBe(true);
148
+ expect(await listVersions(container)).toHaveLength(2);
149
+ });
150
+
151
+ it('appends a collision suffix when two saves land on the same UTC second', async () => {
152
+ await container.writeDocument('# hello', 'index.md');
153
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
154
+ const r1 = await saveVersion(container, { now });
155
+ // Mutate doc and force-save in the same second.
156
+ await container.writeDocument('# hello v2', 'index.md');
157
+ const r2 = await saveVersion(container, { now });
158
+
159
+ expect(r1.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
160
+ expect(r2.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-2.md`);
161
+ });
162
+
163
+ it('content option overrides container.readDocument()', async () => {
164
+ await container.writeDocument('# stored', 'index.md');
165
+ const r = await saveVersion(container, {
166
+ content: '# override',
167
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)),
168
+ });
169
+ expect(r.saved).toBe(true);
170
+ expect(await readUtf8(container, r.version!.path)).toBe('# override');
171
+ });
172
+
173
+ it('basename option overrides the derived basename', async () => {
174
+ await container.writeDocument('# hi', 'index.md');
175
+ const r = await saveVersion(container, {
176
+ basename: 'custom',
177
+ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)),
178
+ });
179
+ expect(r.version?.path).toBe(`${VERSIONS_PREFIX}custom.20260430T152030Z.md`);
180
+ });
181
+ });
182
+
183
+ describe('listVersions', () => {
184
+ let container: MemoryContentContainer;
185
+ beforeEach(() => {
186
+ container = new MemoryContentContainer();
187
+ });
188
+
189
+ it('ignores non-matching filenames inside .versions/', async () => {
190
+ await container.writeFile(`${VERSIONS_PREFIX}README.txt`, new TextEncoder().encode('skip me'));
191
+ await container.writeFile(
192
+ `${VERSIONS_PREFIX}index.invalidstamp.md`,
193
+ new TextEncoder().encode('also skip'),
194
+ );
195
+ expect(await listVersions(container)).toEqual([]);
196
+ });
197
+
198
+ it('returns newest-first', async () => {
199
+ await container.writeDocument('a', 'index.md');
200
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
201
+ await container.writeDocument('b', 'index.md');
202
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 11, 0, 0)) });
203
+ await container.writeDocument('c', 'index.md');
204
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 12, 0, 0)) });
205
+
206
+ const versions = await listVersions(container);
207
+ expect(versions.map((v) => v.timestamp.getUTCHours())).toEqual([12, 11, 10]);
208
+ });
209
+
210
+ it('basename filter excludes other docs', async () => {
211
+ await container.writeFile(
212
+ `${VERSIONS_PREFIX}foo.20260430T100000Z.md`,
213
+ new TextEncoder().encode('foo'),
214
+ );
215
+ await container.writeFile(
216
+ `${VERSIONS_PREFIX}bar.20260430T100000Z.md`,
217
+ new TextEncoder().encode('bar'),
218
+ );
219
+ expect((await listVersions(container, 'foo')).map((v) => v.basename)).toEqual(['foo']);
220
+ });
221
+
222
+ it('orders collision-suffix snapshots newest-first within the same UTC second', async () => {
223
+ // Three saves at the same UTC second produce paths with collisions 0, 1, 2.
224
+ // The lex order of the suffix bytes (`-` < `.`) does not match the write
225
+ // order, so the comparator must use the parsed collision number.
226
+ await container.writeDocument('a', 'index.md');
227
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
228
+ await saveVersion(container, { now });
229
+ await container.writeDocument('b', 'index.md');
230
+ await saveVersion(container, { now });
231
+ await container.writeDocument('c', 'index.md');
232
+ await saveVersion(container, { now });
233
+
234
+ const versions = await listVersions(container);
235
+ expect(versions.map((v) => v.collision)).toEqual([2, 1, 0]);
236
+ expect(versions[0]!.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-3.md`);
237
+ expect(versions[2]!.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
238
+ });
239
+
240
+ it('saveVersion dedup compares against the actual newest snapshot after a collision', async () => {
241
+ // Regression: when a collision suffix exists, dedup must read the
242
+ // collision file (the real "latest"), not the earlier no-suffix file.
243
+ await container.writeDocument('first', 'index.md');
244
+ const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
245
+ await saveVersion(container, { now });
246
+ await container.writeDocument('second', 'index.md');
247
+ await saveVersion(container, { now });
248
+
249
+ // Latest content on disk is "second"; saving it again should be a no-op.
250
+ const r = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)) });
251
+ expect(r.saved).toBe(false);
252
+ expect(r.reason).toBe('unchanged');
253
+ });
254
+ });
255
+
256
+ describe('readVersion', () => {
257
+ it('accepts both Version object and string path', async () => {
258
+ const container = new MemoryContentContainer();
259
+ await container.writeDocument('# content', 'index.md');
260
+ const r = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
261
+ expect(await readVersion(container, r.version!)).toBe('# content');
262
+ expect(await readVersion(container, r.version!.path)).toBe('# content');
263
+ });
264
+
265
+ it('returns null for missing snapshot', async () => {
266
+ const container = new MemoryContentContainer();
267
+ expect(await readVersion(container, '.versions/missing.20260430T100000Z.md')).toBeNull();
268
+ });
269
+ });
270
+
271
+ describe('revertToVersion', () => {
272
+ let container: MemoryContentContainer;
273
+ beforeEach(() => {
274
+ container = new MemoryContentContainer();
275
+ });
276
+
277
+ it('writes a snapshot of the current doc before reverting (default)', async () => {
278
+ await container.writeDocument('# v1', 'index.md');
279
+ const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
280
+ await container.writeDocument('# v2 in flight', 'index.md');
281
+
282
+ const result = await revertToVersion(container, r1.version!);
283
+ expect(result.reverted).toBe(true);
284
+ expect(result.snapshotted).not.toBeNull();
285
+ expect(await container.readDocument()).toBe('# v1');
286
+ // We should now have two snapshots: the original + the just-snapshotted v2.
287
+ expect(await listVersions(container)).toHaveLength(2);
288
+ });
289
+
290
+ it('snapshotCurrent: false skips the safety snapshot', async () => {
291
+ await container.writeDocument('# v1', 'index.md');
292
+ const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
293
+ await container.writeDocument('# v2 in flight', 'index.md');
294
+
295
+ const result = await revertToVersion(container, r1.version!, { snapshotCurrent: false });
296
+ expect(result.snapshotted).toBeNull();
297
+ expect(await container.readDocument()).toBe('# v1');
298
+ expect(await listVersions(container)).toHaveLength(1);
299
+ });
300
+
301
+ it('returns reverted: false for missing snapshot', async () => {
302
+ const result = await revertToVersion(container, '.versions/missing.20260430T100000Z.md');
303
+ expect(result.reverted).toBe(false);
304
+ });
305
+ });
306
+
307
+ describe('pruneVersions', () => {
308
+ let container: MemoryContentContainer;
309
+
310
+ async function seed(times: number): Promise<void> {
311
+ for (let i = 0; i < times; i++) {
312
+ await container.writeDocument(`# v${i}`, 'index.md');
313
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10 + i, 0, 0)) });
314
+ }
315
+ }
316
+
317
+ beforeEach(() => {
318
+ container = new MemoryContentContainer();
319
+ });
320
+
321
+ it('keep-last-n keeps the n newest', async () => {
322
+ await seed(5);
323
+ const deleted = await pruneVersions(container, { type: 'keep-last-n', n: 3 });
324
+ expect(deleted).toHaveLength(2);
325
+ const remaining = await listVersions(container);
326
+ expect(remaining).toHaveLength(3);
327
+ expect(remaining.map((v) => v.timestamp.getUTCHours())).toEqual([14, 13, 12]);
328
+ });
329
+
330
+ it('keep-last-n with n: 0 deletes everything', async () => {
331
+ await seed(3);
332
+ await pruneVersions(container, { type: 'keep-last-n', n: 0 });
333
+ expect(await listVersions(container)).toHaveLength(0);
334
+ });
335
+
336
+ it('older-than deletes only items older than the cutoff', async () => {
337
+ await seed(5);
338
+ const cutoff = new Date(Date.UTC(2026, 3, 30, 12, 0, 0));
339
+ const deleted = await pruneVersions(container, { type: 'older-than', date: cutoff });
340
+ // hours 10 and 11 are older than 12 → deleted.
341
+ expect(deleted).toHaveLength(2);
342
+ const remaining = await listVersions(container);
343
+ expect(remaining.map((v) => v.timestamp.getUTCHours())).toEqual([14, 13, 12]);
344
+ });
345
+
346
+ it('predicate keeps items the predicate accepts', async () => {
347
+ await seed(4);
348
+ const deleted = await pruneVersions(container, {
349
+ type: 'predicate',
350
+ keep: (v) => v.timestamp.getUTCHours() % 2 === 0,
351
+ });
352
+ // 10 and 12 are even (kept); 11 and 13 are odd (deleted).
353
+ expect(deleted).toHaveLength(2);
354
+ expect((await listVersions(container)).map((v) => v.timestamp.getUTCHours())).toEqual([12, 10]);
355
+ });
356
+ });
357
+
358
+ describe('coalesceVersions', () => {
359
+ it('collapses snapshots within the window', async () => {
360
+ const container = new MemoryContentContainer();
361
+ // Three saves: t=0, t=30s (within window), t=2min (outside window).
362
+ await container.writeDocument('a', 'index.md');
363
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
364
+ await container.writeDocument('b', 'index.md');
365
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 30)) });
366
+ await container.writeDocument('c', 'index.md');
367
+ await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 2, 0)) });
368
+
369
+ const deleted = await coalesceVersions(container, { windowMs: 60_000 });
370
+ // Walking newest-first: the (10:00:30, 10:00:00) pair is 30s apart
371
+ // (within the window), so 10:00:00 — the older of the two — is dropped.
372
+ // The (10:02:00, 10:00:30) pair is 90s apart, so 10:00:30 survives.
373
+ expect(deleted).toHaveLength(1);
374
+ expect(deleted[0]!.timestamp.getTime()).toBe(Date.UTC(2026, 3, 30, 10, 0, 0));
375
+ });
376
+
377
+ it('anchors to the last kept snapshot instead of chaining adjacent pairs', async () => {
378
+ // Regression: four snapshots 30s apart with windowMs: 60_000 should keep
379
+ // the newest plus anything beyond 60s of the anchor (here, t=0). Chaining
380
+ // through deleted entries would erase everything but the newest.
381
+ const container = new MemoryContentContainer();
382
+ const base = Date.UTC(2026, 3, 30, 10, 0, 0);
383
+ await container.writeDocument('a', 'index.md');
384
+ await saveVersion(container, { now: new Date(base) });
385
+ await container.writeDocument('b', 'index.md');
386
+ await saveVersion(container, { now: new Date(base + 30_000) });
387
+ await container.writeDocument('c', 'index.md');
388
+ await saveVersion(container, { now: new Date(base + 60_000) });
389
+ await container.writeDocument('d', 'index.md');
390
+ await saveVersion(container, { now: new Date(base + 90_000) });
391
+
392
+ const deleted = await coalesceVersions(container, { windowMs: 60_000 });
393
+ // Anchor=t+90 keeps t+30 dropped (60s ≤ window), keeps t=0 (90s > window
394
+ // from anchor → new anchor). t+60 is also within 60s of t+90 → dropped.
395
+ expect(deleted).toHaveLength(2);
396
+ const remaining = await listVersions(container);
397
+ expect(remaining.map((v) => v.timestamp.getTime() - base)).toEqual([90_000, 0]);
398
+ });
399
+ });
400
+
401
+ describe('DocumentVersionManager', () => {
402
+ it('forwards calls and applies the basename override', async () => {
403
+ const container = new MemoryContentContainer();
404
+ await container.writeDocument('# hi', 'index.md');
405
+ const manager = new DocumentVersionManager(container, { basename: 'custom' });
406
+ const r = await manager.saveVersion({ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
407
+ expect(r.version?.path).toBe(`${VERSIONS_PREFIX}custom.20260430T152030Z.md`);
408
+
409
+ const list = await manager.listVersions();
410
+ expect(list).toHaveLength(1);
411
+
412
+ const content = await manager.readVersion(list[0]!);
413
+ expect(content).toBe('# hi');
414
+ });
415
+ });
@@ -112,16 +112,55 @@ function flattenForAudio(blocks: Block[]): Block[] {
112
112
  return result;
113
113
  }
114
114
 
115
- /** Info about an MP3 file found in the container. */
115
+ /**
116
+ * Info about a narration audio file found in the container.
117
+ *
118
+ * Originally limited to MP3, this now also covers WebM/MP4/Ogg/M4A
119
+ * audio files that the browser recorder writes into `audio/`. The type
120
+ * name is retained for backwards compatibility with the surrounding
121
+ * matching logic; the data shape is identical regardless of codec.
122
+ */
116
123
  interface Mp3Info {
117
- /** Path in the container (e.g., "images/c23n-issaquah-highlands-intro.mp3") */
124
+ /** Path in the container (e.g., "audio/narration-2026.webm"). */
118
125
  path: string;
119
- /** Filename without path (e.g., "c23n-issaquah-highlands-intro.mp3") */
126
+ /** Filename without path (e.g., "narration-2026.webm"). */
120
127
  filename: string;
121
- /** Parsed timing data (if .timing.json exists) */
128
+ /** Parsed timing data (if .timing.json exists). */
122
129
  timing: AudioTimingData | null;
123
130
  }
124
131
 
132
+ /**
133
+ * File extensions that unambiguously denote audio anywhere in the
134
+ * container. These predate the recorder feature.
135
+ */
136
+ const AUDIO_ONLY_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a'];
137
+
138
+ /**
139
+ * Extensions that could be audio or video — `.webm` and `.mp4` are the
140
+ * containers `MediaRecorder` produces for browser-side narration, but
141
+ * they're also used for video clips. Only treat them as narration when
142
+ * they live under the `audio/` directory the recorder writes to.
143
+ */
144
+ const AMBIGUOUS_EXTENSIONS = ['.webm', '.mp4'];
145
+
146
+ function isNarrationAudioPath(path: string): boolean {
147
+ const lower = path.toLowerCase();
148
+ if (AUDIO_ONLY_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
149
+ if (AMBIGUOUS_EXTENSIONS.some((ext) => lower.endsWith(ext))) {
150
+ return lower.startsWith('audio/');
151
+ }
152
+ return false;
153
+ }
154
+
155
+ /** Strip a known narration audio extension from a filename. */
156
+ function stripAudioExtension(filename: string): string {
157
+ const lower = filename.toLowerCase();
158
+ for (const ext of [...AUDIO_ONLY_EXTENSIONS, ...AMBIGUOUS_EXTENSIONS]) {
159
+ if (lower.endsWith(ext)) return filename.slice(0, -ext.length);
160
+ }
161
+ return filename;
162
+ }
163
+
125
164
  // ── Core Functions ───────────────────────────────────────────────────
126
165
 
127
166
  /**
@@ -142,12 +181,17 @@ function parseConsolidatedTiming(data: ArrayBuffer): Record<string, AudioTimingD
142
181
  }
143
182
 
144
183
  /**
145
- * Discover all MP3 files in a container and load their timing data.
146
- * Checks for a consolidated timing.json first, then falls back to per-file .timing.json.
184
+ * Discover all narration audio files in a container and load their
185
+ * timing data. Covers historical `.mp3`/`.wav`/`.ogg`/`.m4a` files
186
+ * anywhere in the container plus `.webm`/`.mp4` files under `audio/`
187
+ * (the destination the browser recorder writes to).
188
+ *
189
+ * Checks for a consolidated timing.json first, then falls back to
190
+ * per-file `.timing.json` sidecars.
147
191
  */
148
192
  async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
149
193
  const files = await container.listFiles();
150
- const mp3Files = files.filter((f) => f.path.endsWith('.mp3'));
194
+ const audioFiles = files.filter((f) => isNarrationAudioPath(f.path));
151
195
  const results: Mp3Info[] = [];
152
196
 
153
197
  // Try to load consolidated timing.json
@@ -157,15 +201,15 @@ async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
157
201
  consolidatedSections = parseConsolidatedTiming(consolidatedData);
158
202
  }
159
203
 
160
- for (const file of mp3Files) {
204
+ for (const file of audioFiles) {
161
205
  const filename = file.path.split('/').pop() ?? file.path;
162
206
  let timing: AudioTimingData | null = null;
163
207
 
164
208
  // Try consolidated timing first: match section name from filename
165
209
  if (consolidatedSections) {
166
- const mp3Base = filename.replace(/\.mp3$/, '');
210
+ const audioBase = stripAudioExtension(filename);
167
211
  for (const [sectionName, sectionTiming] of Object.entries(consolidatedSections)) {
168
- if (mp3Base.endsWith(`-${sectionName}`)) {
212
+ if (audioBase.endsWith(`-${sectionName}`)) {
169
213
  timing = sectionTiming;
170
214
  break;
171
215
  }
@@ -259,7 +303,7 @@ function autoMatchBlocks(
259
303
  // Fallback: filename-based matching
260
304
  if (score < MIN_CONTENT_SIMILARITY && block.title) {
261
305
  const titleSlug = slugify(block.title);
262
- const fileSlug = slugify(mp3.filename.replace(/\.mp3$/, ''));
306
+ const fileSlug = slugify(stripAudioExtension(mp3.filename));
263
307
  // Check if the title slug appears in the filename slug
264
308
  if (fileSlug.includes(titleSlug) && titleSlug.length >= 3) {
265
309
  score = Math.max(score, MIN_FILENAME_SIMILARITY + 0.1);
@@ -34,7 +34,7 @@ import type { ViewportConfig } from '../schemas/Viewport.js';
34
34
  import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
35
35
  import { createTemplateContext, isTemplateBlock } from '../schemas/BlockTemplates.js';
36
36
  import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
37
- import { templateRegistry } from './templates/index.js';
37
+ import { templateRegistry, resolveTemplateName } from './templates/index.js';
38
38
  import { expandPersistentLayers } from './templates/persistentLayers.js';
39
39
 
40
40
  // ============================================
@@ -98,29 +98,40 @@ export function getLayers(block: DocBlock, context: RenderContext = {}): Layer[]
98
98
  return injectPersistentLayers(existingLayers, block, context);
99
99
  }
100
100
 
101
- // 2. Template path: look up and call the template function
102
- if (isTemplateBlock(block) && block.template in templateRegistry) {
103
- const templateName = block.template as keyof typeof templateRegistry;
104
- const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
105
- let layers: Layer[];
106
- try {
107
- // Each registry entry accepts its specific TemplateBlock variant; the
108
- // discriminated union guarantees the shapes match at runtime.
109
- const templateFn = templateRegistry[templateName] as (
110
- input: TemplateBlock,
111
- ctx: TemplateContext,
112
- ) => Layer[];
113
- layers = templateFn(block, templateCtx);
114
- if (!Array.isArray(layers)) {
115
- console.error(`Template ${templateName} did not return an array, got:`, typeof layers);
101
+ // 2. Template path: look up and call the template function.
102
+ //
103
+ // Resolve through TEMPLATE_ALIASES so legacy ids (`titleBlock`,
104
+ // `quoteBlock`, `mapBlock`, `listBlock`) hit the registry by their
105
+ // canonical short names. Without this the block-section path renders
106
+ // an empty SVG card: `hasTemplate()` accepts the alias (so
107
+ // `isAnnotatedBlock` returns true and the card wrapper renders), but
108
+ // a raw `block.template in templateRegistry` check below misses it
109
+ // and the layer list comes back empty.
110
+ if (isTemplateBlock(block)) {
111
+ const resolved = resolveTemplateName(block.template);
112
+ if (resolved in templateRegistry) {
113
+ const templateName = resolved as keyof typeof templateRegistry;
114
+ const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
115
+ let layers: Layer[];
116
+ try {
117
+ // Each registry entry accepts its specific TemplateBlock variant; the
118
+ // discriminated union guarantees the shapes match at runtime.
119
+ const templateFn = templateRegistry[templateName] as (
120
+ input: TemplateBlock,
121
+ ctx: TemplateContext,
122
+ ) => Layer[];
123
+ layers = templateFn(block, templateCtx);
124
+ if (!Array.isArray(layers)) {
125
+ console.error(`Template ${templateName} did not return an array, got:`, typeof layers);
126
+ layers = [];
127
+ }
128
+ } catch (err: unknown) {
129
+ console.error(`Error expanding template ${templateName}:`, err);
116
130
  layers = [];
117
131
  }
118
- } catch (err: unknown) {
119
- console.error(`Error expanding template ${templateName}:`, err);
120
- layers = [];
121
- }
122
132
 
123
- return injectPersistentLayers(layers, block, context);
133
+ return injectPersistentLayers(layers, block, context);
134
+ }
124
135
  }
125
136
 
126
137
  // 3. Fallback: no layers and no known template
@@ -28,10 +28,11 @@ import type {
28
28
  MarkdownBlockNode,
29
29
  MarkdownHeading,
30
30
  MarkdownNode,
31
- MarkdownImage,
31
+ HtmlNode,
32
32
  } from '../markdown/types.js';
33
33
  import { extractPlainText } from '../markdown/utils.js';
34
34
  import { estimateReadingTime } from '../timing/readingTime.js';
35
+ import { resolveTemplateName } from './templates/index.js';
35
36
 
36
37
  // ============================================
37
38
  // Options
@@ -152,9 +153,13 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
152
153
  function makeBlock(heading: MarkdownHeading | null): Block {
153
154
  const id = heading ? generateId(heading, headingIndex++) : 'preamble';
154
155
 
155
- // Use template from annotation if present, otherwise fall back to default
156
+ // Use template from annotation if present, otherwise fall back to default.
157
+ // Legacy template ids (e.g. `titleBlock`) are normalized to their canonical
158
+ // short form (`title`) so downstream comparisons can rely on the new names
159
+ // without sprinkling alias resolution everywhere.
156
160
  const annotation = heading?.templateAnnotation;
157
- const template = annotation?.template ?? (heading ? defaultTemplate : undefined);
161
+ const rawTemplate = annotation?.template ?? (heading ? defaultTemplate : undefined);
162
+ const template = rawTemplate != null ? resolveTemplateName(rawTemplate) : undefined;
158
163
 
159
164
  // Extract heading text so templates (e.g. sectionHeader) that expect a
160
165
  // `title` property receive it without having to reach into sourceHeading.
@@ -434,12 +439,45 @@ function splitIntoSentences(text: string): string[] {
434
439
  return merged;
435
440
  }
436
441
 
442
+ interface ImageRef {
443
+ url: string;
444
+ alt?: string;
445
+ }
446
+
437
447
  /**
438
- * Walk a MarkdownNode tree depth-first to find the first image node.
439
- * Returns the MarkdownImage or undefined if none found.
448
+ * Walk a parsed HTML sub-tree depth-first for an `<img>` with a `src`.
449
+ * Used to surface images that the WYSIWYG editor serialized as raw HTML
450
+ * (e.g. when an image was resized — markdown's `![alt](url)` shorthand
451
+ * has no width syntax, so resized images round-trip as `<img src width>`).
440
452
  */
441
- function findFirstImage(node: MarkdownNode): MarkdownImage | undefined {
442
- if (node.type === 'image') return node as MarkdownImage;
453
+ function findFirstHtmlImage(nodes: HtmlNode[]): ImageRef | undefined {
454
+ for (const node of nodes) {
455
+ if (node.type !== 'htmlElement') continue;
456
+ if (node.tagName === 'img' && node.attributes.src) {
457
+ return { url: node.attributes.src, alt: node.attributes.alt };
458
+ }
459
+ const nested = findFirstHtmlImage(node.children);
460
+ if (nested) return nested;
461
+ }
462
+ return undefined;
463
+ }
464
+
465
+ /**
466
+ * Walk a MarkdownNode tree depth-first to find the first image reference,
467
+ * whether expressed as a markdown image node or a raw HTML `<img>` tag.
468
+ */
469
+ function findFirstImage(node: MarkdownNode): ImageRef | undefined {
470
+ if (node.type === 'image') {
471
+ const img = node as { url: string; alt?: string };
472
+ return { url: img.url, alt: img.alt };
473
+ }
474
+ if (node.type === 'htmlBlock' || node.type === 'htmlInline') {
475
+ const html = node as { htmlChildren?: HtmlNode[] };
476
+ if (html.htmlChildren) {
477
+ const found = findFirstHtmlImage(html.htmlChildren);
478
+ if (found) return found;
479
+ }
480
+ }
443
481
  if ('children' in node && Array.isArray((node as { children?: unknown[] }).children)) {
444
482
  for (const child of (node as { children: MarkdownNode[] }).children) {
445
483
  const found = findFirstImage(child);