@bendyline/squisq 2.1.0 → 2.3.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 (442) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +24 -0
  3. package/README.md +8 -8
  4. package/dist/{ContentContainer-BXUlIf18.d.ts → ContentContainer-B2w9sUoL.d.ts} +15 -0
  5. package/dist/{Doc-DynJleo-.d.ts → Doc-COe-rPQB.d.ts} +7 -5
  6. package/dist/{ImageEditDoc-Ca4Tr9uN.d.ts → ImageEditDoc-dtKahVyD.d.ts} +1 -1
  7. package/dist/{annotationCoercion-Dr_DY9Hx.d.ts → annotationCoercion-PtRQhk5V.d.ts} +1 -1
  8. package/dist/{chunk-VWCJ2WXC.js → chunk-2ZWIXGAC.js} +0 -1
  9. package/dist/{chunk-VYFRI7R6.js → chunk-32R2RZAO.js} +72 -3
  10. package/dist/chunk-3IGPAPQ7.js +645 -0
  11. package/dist/{chunk-QE7TWGVT.js → chunk-4VOD55SX.js} +0 -1
  12. package/dist/{chunk-3E5F2XMR.js → chunk-67L6WRJN.js} +0 -1
  13. package/dist/{chunk-5DKBFXPY.js → chunk-6ECMNUIP.js} +26 -18
  14. package/dist/{chunk-DBS6RIFL.js → chunk-7N4G32LG.js} +0 -1
  15. package/dist/{chunk-JOQYPRDZ.js → chunk-7YEZFHLS.js} +10 -8
  16. package/dist/{chunk-2UQOHYE2.js → chunk-7ZAAICW4.js} +0 -1
  17. package/dist/{chunk-66QO7XFB.js → chunk-ACZOVWX3.js} +117 -7
  18. package/dist/{chunk-5J4HKRJF.js → chunk-BAOV476U.js} +0 -1
  19. package/dist/chunk-BCCXTMN5.js +699 -0
  20. package/dist/{chunk-JHZWYRSW.js → chunk-C33GTPUZ.js} +11 -4
  21. package/dist/{chunk-XNLB57UD.js → chunk-EK446BUE.js} +18 -2
  22. package/dist/{chunk-VR7UPJ7C.js → chunk-FCG3BIZN.js} +12 -1
  23. package/dist/{chunk-JI77MGAK.js → chunk-GIIOY6XR.js} +2178 -1291
  24. package/dist/{chunk-Q7LROV2H.js → chunk-NKNMGIIN.js} +0 -1
  25. package/dist/chunk-OPO5K4DZ.js +241 -0
  26. package/dist/{chunk-TUWMK62B.js → chunk-PUS54YU6.js} +45 -2
  27. package/dist/{chunk-OTKVFVL6.js → chunk-QNTQIEIV.js} +130 -47
  28. package/dist/{chunk-5UYI3RJB.js → chunk-QWCFK5FN.js} +55 -40
  29. package/dist/{chunk-GZY66SDN.js → chunk-R3KTTOMS.js} +0 -1
  30. package/dist/{chunk-ILWDXLUK.js → chunk-RS5AP3J4.js} +49 -10
  31. package/dist/{chunk-QUUPYOSP.js → chunk-SPTY4C6F.js} +136 -5
  32. package/dist/{chunk-4X3JQXNM.js → chunk-XOFT65EX.js} +1 -2
  33. package/dist/{chunk-FVTQSNR7.js → chunk-XRW6P7IF.js} +13 -5
  34. package/dist/{chunk-R2YZTCLO.js → chunk-ZQKZSJAX.js} +1 -2
  35. package/dist/doc/index.d.ts +311 -13
  36. package/dist/doc/index.js +25 -17
  37. package/dist/generate/index.d.ts +2 -2
  38. package/dist/generate/index.js +1 -3
  39. package/dist/icons/index.js +2 -4
  40. package/dist/icons/inlineIconMarker.js +1 -2
  41. package/dist/imageEdit/index.d.ts +48 -17
  42. package/dist/imageEdit/index.js +3 -4
  43. package/dist/index.d.ts +13 -13
  44. package/dist/index.js +67 -33
  45. package/dist/jsonForm/index.d.ts +10 -3
  46. package/dist/jsonForm/index.js +8 -7
  47. package/dist/markdown/index.d.ts +75 -11
  48. package/dist/markdown/index.js +32 -7
  49. package/dist/{materializePageSection-DDzubQXo.d.ts → materializePageSection-BSMOocve.d.ts} +2 -2
  50. package/dist/narration/index.d.ts +16 -3
  51. package/dist/narration/index.js +12 -14
  52. package/dist/random/index.d.ts +11 -2
  53. package/dist/random/index.js +1 -3
  54. package/dist/recommend/index.d.ts +1 -1
  55. package/dist/recommend/index.js +2 -4
  56. package/dist/schemas/index.d.ts +18 -6
  57. package/dist/schemas/index.js +7 -6
  58. package/dist/spatial/index.d.ts +2 -1
  59. package/dist/spatial/index.js +1 -2
  60. package/dist/storage/index.d.ts +4 -2
  61. package/dist/storage/index.js +1 -2
  62. package/dist/{themeLibrary-B72m0W1y.d.ts → themeLibrary-BUTWjufL.d.ts} +1 -1
  63. package/dist/timing/index.js +2 -4
  64. package/dist/transform/index.d.ts +3 -3
  65. package/dist/transform/index.js +6 -7
  66. package/dist/{types-C4kiRn_k.d.ts → types-8QjefM9J.d.ts} +38 -2
  67. package/dist/{types-BwMPl2CB.d.ts → types-CUNc9biN.d.ts} +24 -1
  68. package/dist/versions/index.d.ts +14 -11
  69. package/dist/versions/index.js +2 -3
  70. package/package.json +8 -3
  71. package/dist/chunk-2UQOHYE2.js.map +0 -1
  72. package/dist/chunk-3E5F2XMR.js.map +0 -1
  73. package/dist/chunk-4X3JQXNM.js.map +0 -1
  74. package/dist/chunk-5DKBFXPY.js.map +0 -1
  75. package/dist/chunk-5J4HKRJF.js.map +0 -1
  76. package/dist/chunk-5UYI3RJB.js.map +0 -1
  77. package/dist/chunk-66QO7XFB.js.map +0 -1
  78. package/dist/chunk-AMIR72JP.js +0 -380
  79. package/dist/chunk-AMIR72JP.js.map +0 -1
  80. package/dist/chunk-DBS6RIFL.js.map +0 -1
  81. package/dist/chunk-DNJKBAI7.js +0 -1
  82. package/dist/chunk-DNJKBAI7.js.map +0 -1
  83. package/dist/chunk-ET53IIEP.js +0 -1
  84. package/dist/chunk-ET53IIEP.js.map +0 -1
  85. package/dist/chunk-FVTQSNR7.js.map +0 -1
  86. package/dist/chunk-GZY66SDN.js.map +0 -1
  87. package/dist/chunk-ILWDXLUK.js.map +0 -1
  88. package/dist/chunk-JHZWYRSW.js.map +0 -1
  89. package/dist/chunk-JI77MGAK.js.map +0 -1
  90. package/dist/chunk-JOQYPRDZ.js.map +0 -1
  91. package/dist/chunk-OTKVFVL6.js.map +0 -1
  92. package/dist/chunk-PGGGKYWG.js +0 -1
  93. package/dist/chunk-PGGGKYWG.js.map +0 -1
  94. package/dist/chunk-Q7LROV2H.js.map +0 -1
  95. package/dist/chunk-QE7TWGVT.js.map +0 -1
  96. package/dist/chunk-QUUPYOSP.js.map +0 -1
  97. package/dist/chunk-R2GC6RLD.js +0 -344
  98. package/dist/chunk-R2GC6RLD.js.map +0 -1
  99. package/dist/chunk-R2YZTCLO.js.map +0 -1
  100. package/dist/chunk-S7A5D7ZJ.js +0 -174
  101. package/dist/chunk-S7A5D7ZJ.js.map +0 -1
  102. package/dist/chunk-TUWMK62B.js.map +0 -1
  103. package/dist/chunk-V543AI2F.js +0 -1
  104. package/dist/chunk-V543AI2F.js.map +0 -1
  105. package/dist/chunk-VR7UPJ7C.js.map +0 -1
  106. package/dist/chunk-VWCJ2WXC.js.map +0 -1
  107. package/dist/chunk-VYFRI7R6.js.map +0 -1
  108. package/dist/chunk-XNLB57UD.js.map +0 -1
  109. package/dist/chunk-ZLQKNOZQ.js +0 -1
  110. package/dist/chunk-ZLQKNOZQ.js.map +0 -1
  111. package/dist/doc/index.js.map +0 -1
  112. package/dist/generate/index.js.map +0 -1
  113. package/dist/icons/index.js.map +0 -1
  114. package/dist/icons/inlineIconMarker.js.map +0 -1
  115. package/dist/imageEdit/index.js.map +0 -1
  116. package/dist/index.js.map +0 -1
  117. package/dist/jsonForm/index.js.map +0 -1
  118. package/dist/markdown/index.js.map +0 -1
  119. package/dist/narration/index.js.map +0 -1
  120. package/dist/random/index.js.map +0 -1
  121. package/dist/recommend/index.js.map +0 -1
  122. package/dist/schemas/index.js.map +0 -1
  123. package/dist/spatial/index.js.map +0 -1
  124. package/dist/storage/index.js.map +0 -1
  125. package/dist/timing/index.js.map +0 -1
  126. package/dist/transform/index.js.map +0 -1
  127. package/dist/versions/index.js.map +0 -1
  128. package/src/__tests__/animationUtils.test.ts +0 -117
  129. package/src/__tests__/annotationCoercion.test.ts +0 -243
  130. package/src/__tests__/applyNarrationTiming.test.ts +0 -190
  131. package/src/__tests__/applyRenderStyle.test.ts +0 -146
  132. package/src/__tests__/asciiDiagramBattle.test.ts +0 -246
  133. package/src/__tests__/asciiDiagramDetect.test.ts +0 -162
  134. package/src/__tests__/asciiDiagramMapping.test.ts +0 -38
  135. package/src/__tests__/asciiDiagramParse.test.ts +0 -233
  136. package/src/__tests__/asciiDiagramPipeline.test.ts +0 -273
  137. package/src/__tests__/asciiDiagramRender.test.ts +0 -162
  138. package/src/__tests__/asciiDiagramRepair.test.ts +0 -84
  139. package/src/__tests__/asciiTimelineDetect.test.ts +0 -157
  140. package/src/__tests__/asciiTimelineParse.test.ts +0 -205
  141. package/src/__tests__/asciiTimelinePipeline.test.ts +0 -349
  142. package/src/__tests__/asciiTimelineRender.test.ts +0 -376
  143. package/src/__tests__/atmosphereLayers.test.ts +0 -108
  144. package/src/__tests__/attrTokens.test.ts +0 -167
  145. package/src/__tests__/colorUtils.test.ts +0 -112
  146. package/src/__tests__/contentContainer.test.ts +0 -325
  147. package/src/__tests__/contentExtractor.test.ts +0 -243
  148. package/src/__tests__/coverBlock.test.ts +0 -310
  149. package/src/__tests__/customTemplateSample.test.ts +0 -144
  150. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -221
  151. package/src/__tests__/customThemesFrontmatter.test.ts +0 -146
  152. package/src/__tests__/dataTable.test.ts +0 -145
  153. package/src/__tests__/diagramBlock.test.ts +0 -122
  154. package/src/__tests__/diagramLayout.test.ts +0 -134
  155. package/src/__tests__/diagramText.test.ts +0 -26
  156. package/src/__tests__/docToMarkdownMedia.test.ts +0 -83
  157. package/src/__tests__/drawing.test.ts +0 -109
  158. package/src/__tests__/drawingBlock.test.ts +0 -124
  159. package/src/__tests__/drawingLayout.test.ts +0 -171
  160. package/src/__tests__/fixtures/asciiDiagramBattle.ts +0 -512
  161. package/src/__tests__/fixtures/asciiDiagrams.ts +0 -287
  162. package/src/__tests__/fixtures/asciiTimelines.ts +0 -35
  163. package/src/__tests__/fixtures/treeviewFixtures.ts +0 -243
  164. package/src/__tests__/geohash.test.ts +0 -110
  165. package/src/__tests__/getLayers.test.ts +0 -243
  166. package/src/__tests__/getLayersFallback.test.ts +0 -103
  167. package/src/__tests__/haversine.test.ts +0 -63
  168. package/src/__tests__/iconResolve.test.ts +0 -122
  169. package/src/__tests__/imageEditExportSvg.test.ts +0 -70
  170. package/src/__tests__/imageEditState.test.ts +0 -74
  171. package/src/__tests__/imageEditVersions.test.ts +0 -243
  172. package/src/__tests__/inferDocumentTitle.test.ts +0 -50
  173. package/src/__tests__/inlineIconMarker.test.ts +0 -36
  174. package/src/__tests__/inputDescriptors.test.ts +0 -162
  175. package/src/__tests__/jsonForm.chooseControl.test.ts +0 -86
  176. package/src/__tests__/jsonForm.evaluateWhen.test.ts +0 -59
  177. package/src/__tests__/jsonForm.inferSchema.test.ts +0 -25
  178. package/src/__tests__/jsonForm.pointer.test.ts +0 -149
  179. package/src/__tests__/jsonForm.tokens.test.ts +0 -84
  180. package/src/__tests__/layoutBlock.test.ts +0 -83
  181. package/src/__tests__/layoutLayout.test.ts +0 -169
  182. package/src/__tests__/localForageAdapter.test.ts +0 -155
  183. package/src/__tests__/majorApiTypes.test.ts +0 -18
  184. package/src/__tests__/markdown.test.ts +0 -1009
  185. package/src/__tests__/markdownIcons.test.ts +0 -104
  186. package/src/__tests__/markdownSanitize.test.ts +0 -81
  187. package/src/__tests__/markdownToDoc.test.ts +0 -957
  188. package/src/__tests__/matchFontFamily.test.ts +0 -114
  189. package/src/__tests__/materializeBlockLayers.test.ts +0 -167
  190. package/src/__tests__/materializePageSections.test.ts +0 -404
  191. package/src/__tests__/materializeTestUtils.ts +0 -17
  192. package/src/__tests__/mediaAnnotations.test.ts +0 -91
  193. package/src/__tests__/mediaSchedule.test.ts +0 -118
  194. package/src/__tests__/narrationAlign.test.ts +0 -150
  195. package/src/__tests__/narrationFeatures.test.ts +0 -71
  196. package/src/__tests__/narrationNuclei.test.ts +0 -45
  197. package/src/__tests__/narrationPacing.test.ts +0 -189
  198. package/src/__tests__/narrationScript.test.ts +0 -149
  199. package/src/__tests__/narrationSessionRepro.test.ts +0 -62
  200. package/src/__tests__/narrationSidecar.test.ts +0 -88
  201. package/src/__tests__/narrationSyllables.test.ts +0 -65
  202. package/src/__tests__/narrationTestSignals.ts +0 -167
  203. package/src/__tests__/narrationVad.test.ts +0 -66
  204. package/src/__tests__/pageStyle.test.ts +0 -258
  205. package/src/__tests__/pandocAttrParse.test.ts +0 -187
  206. package/src/__tests__/recommendTemplates.test.ts +0 -183
  207. package/src/__tests__/resolveDocTheme.test.ts +0 -69
  208. package/src/__tests__/rootExports.test.ts +0 -61
  209. package/src/__tests__/scopedContentContainer.test.ts +0 -109
  210. package/src/__tests__/seededRandom.test.ts +0 -142
  211. package/src/__tests__/shapeGeometry.test.ts +0 -138
  212. package/src/__tests__/shapeVocabulary.test.ts +0 -133
  213. package/src/__tests__/storage.test.ts +0 -60
  214. package/src/__tests__/structuredData.test.ts +0 -249
  215. package/src/__tests__/surface.test.ts +0 -68
  216. package/src/__tests__/templateAnnotationParse.test.ts +0 -173
  217. package/src/__tests__/templateInputs.test.ts +0 -69
  218. package/src/__tests__/templateMetadata.test.ts +0 -32
  219. package/src/__tests__/templates.test.ts +0 -710
  220. package/src/__tests__/themeCompile.test.ts +0 -225
  221. package/src/__tests__/themeValidator.test.ts +0 -131
  222. package/src/__tests__/timing.test.ts +0 -167
  223. package/src/__tests__/transformProvenance.test.ts +0 -160
  224. package/src/__tests__/transformV2.test.ts +0 -163
  225. package/src/__tests__/transitionNormalize.test.ts +0 -61
  226. package/src/__tests__/treeviewBattle.test.ts +0 -76
  227. package/src/__tests__/treeviewDetect.test.ts +0 -168
  228. package/src/__tests__/treeviewParse.test.ts +0 -118
  229. package/src/__tests__/treeviewPipeline.test.ts +0 -183
  230. package/src/__tests__/treeviewRender.test.ts +0 -91
  231. package/src/__tests__/validateDoc.test.ts +0 -260
  232. package/src/__tests__/versions.test.ts +0 -459
  233. package/src/doc/annotationBlocks.ts +0 -117
  234. package/src/doc/applyNarrationTiming.ts +0 -244
  235. package/src/doc/asciiDiagram/chars.ts +0 -210
  236. package/src/doc/asciiDiagram/detect.ts +0 -175
  237. package/src/doc/asciiDiagram/index.ts +0 -38
  238. package/src/doc/asciiDiagram/mapping.ts +0 -225
  239. package/src/doc/asciiDiagram/parse.ts +0 -1095
  240. package/src/doc/asciiDiagram/render.ts +0 -963
  241. package/src/doc/asciiDiagram/repair.ts +0 -400
  242. package/src/doc/asciiDiagram/types.ts +0 -65
  243. package/src/doc/asciiTimeline/detect.ts +0 -138
  244. package/src/doc/asciiTimeline/index.ts +0 -26
  245. package/src/doc/asciiTimeline/mapping.ts +0 -43
  246. package/src/doc/asciiTimeline/parse.ts +0 -870
  247. package/src/doc/asciiTimeline/render.ts +0 -427
  248. package/src/doc/asciiTimeline/types.ts +0 -73
  249. package/src/doc/asciiTimeline/wrappedFlow.ts +0 -427
  250. package/src/doc/audioMapping.ts +0 -442
  251. package/src/doc/customTemplatesFrontmatter.ts +0 -273
  252. package/src/doc/customThemesFrontmatter.ts +0 -95
  253. package/src/doc/docToMarkdown.ts +0 -284
  254. package/src/doc/index.ts +0 -161
  255. package/src/doc/markdownToDoc.ts +0 -1054
  256. package/src/doc/materializeBlockLayers.ts +0 -465
  257. package/src/doc/mediaAnnotations.ts +0 -120
  258. package/src/doc/page/PageSection.ts +0 -174
  259. package/src/doc/page/materializePageSection.ts +0 -616
  260. package/src/doc/page/resolvePageBlock.ts +0 -108
  261. package/src/doc/page/sectionExtractors.ts +0 -466
  262. package/src/doc/pageCss.ts +0 -628
  263. package/src/doc/resolveDocTheme.ts +0 -56
  264. package/src/doc/standaloneAnnotation.ts +0 -58
  265. package/src/doc/structuredData.ts +0 -301
  266. package/src/doc/templateInputs.ts +0 -424
  267. package/src/doc/templates/__tests__/customTemplate.test.ts +0 -170
  268. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +0 -124
  269. package/src/doc/templates/__tests__/listBlock.test.ts +0 -64
  270. package/src/doc/templates/accentImage.ts +0 -362
  271. package/src/doc/templates/captionUtils.ts +0 -56
  272. package/src/doc/templates/comparisonBar.ts +0 -173
  273. package/src/doc/templates/coverBlock.ts +0 -255
  274. package/src/doc/templates/customTemplate.ts +0 -51
  275. package/src/doc/templates/dataTable.ts +0 -102
  276. package/src/doc/templates/dateEvent.ts +0 -147
  277. package/src/doc/templates/definitionCard.ts +0 -140
  278. package/src/doc/templates/diagramBlock.ts +0 -403
  279. package/src/doc/templates/diagramLayout.ts +0 -159
  280. package/src/doc/templates/drawingBlock.ts +0 -325
  281. package/src/doc/templates/drawingLayout.ts +0 -539
  282. package/src/doc/templates/factCard.ts +0 -146
  283. package/src/doc/templates/fallbackBlock.ts +0 -123
  284. package/src/doc/templates/featureBlock.ts +0 -209
  285. package/src/doc/templates/fullBleedQuote.ts +0 -83
  286. package/src/doc/templates/imageWithCaption.ts +0 -186
  287. package/src/doc/templates/index.ts +0 -642
  288. package/src/doc/templates/inputDescriptors.ts +0 -451
  289. package/src/doc/templates/layoutBlock.ts +0 -43
  290. package/src/doc/templates/layoutLayout.ts +0 -243
  291. package/src/doc/templates/listBlock.ts +0 -207
  292. package/src/doc/templates/mapBlock.ts +0 -138
  293. package/src/doc/templates/metadata.ts +0 -130
  294. package/src/doc/templates/persistentLayers.ts +0 -526
  295. package/src/doc/templates/photoGrid.ts +0 -190
  296. package/src/doc/templates/pullQuote.ts +0 -141
  297. package/src/doc/templates/quoteBlock.ts +0 -141
  298. package/src/doc/templates/registry.ts +0 -88
  299. package/src/doc/templates/sectionHeader.ts +0 -120
  300. package/src/doc/templates/statHighlight.ts +0 -146
  301. package/src/doc/templates/templateNames.ts +0 -22
  302. package/src/doc/templates/timelineBlock.ts +0 -613
  303. package/src/doc/templates/titleBlock.ts +0 -150
  304. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +0 -487
  305. package/src/doc/templates/tokens/resolveTokens.ts +0 -453
  306. package/src/doc/templates/treeBlock.ts +0 -131
  307. package/src/doc/templates/twoColumn.ts +0 -263
  308. package/src/doc/templates/videoPullQuote.ts +0 -142
  309. package/src/doc/templates/videoWithCaption.ts +0 -135
  310. package/src/doc/treeview/chars.ts +0 -72
  311. package/src/doc/treeview/detect.ts +0 -145
  312. package/src/doc/treeview/index.ts +0 -30
  313. package/src/doc/treeview/mapping.ts +0 -95
  314. package/src/doc/treeview/parse.ts +0 -201
  315. package/src/doc/treeview/render.ts +0 -41
  316. package/src/doc/treeview/types.ts +0 -43
  317. package/src/doc/utils/animationUtils.ts +0 -399
  318. package/src/doc/utils/applyRenderStyle.ts +0 -106
  319. package/src/doc/utils/diagramText.ts +0 -78
  320. package/src/doc/utils/imageTreatment.ts +0 -70
  321. package/src/doc/utils/nearest.ts +0 -59
  322. package/src/doc/utils/shapeGeometry.ts +0 -614
  323. package/src/doc/utils/themeUtils.ts +0 -243
  324. package/src/doc/validate.ts +0 -575
  325. package/src/generate/contentExtractor.ts +0 -921
  326. package/src/generate/index.ts +0 -18
  327. package/src/generate/templateMapper.ts +0 -177
  328. package/src/icons/iconData.ts +0 -31
  329. package/src/icons/index.ts +0 -26
  330. package/src/icons/inlineIconMarker.ts +0 -73
  331. package/src/icons/resolve.ts +0 -139
  332. package/src/imageEdit/ImageEditVersionManager.ts +0 -73
  333. package/src/imageEdit/export.ts +0 -410
  334. package/src/imageEdit/index.ts +0 -72
  335. package/src/imageEdit/persistence.ts +0 -113
  336. package/src/imageEdit/state.ts +0 -112
  337. package/src/imageEdit/versionPaths.ts +0 -44
  338. package/src/imageEdit/versions.ts +0 -152
  339. package/src/index.ts +0 -15
  340. package/src/internal/immutable.ts +0 -58
  341. package/src/jsonForm/chooseControl.ts +0 -88
  342. package/src/jsonForm/evaluateWhen.ts +0 -59
  343. package/src/jsonForm/index.ts +0 -32
  344. package/src/jsonForm/inferSchema.ts +0 -25
  345. package/src/jsonForm/pointer.ts +0 -154
  346. package/src/jsonForm/tokens.ts +0 -78
  347. package/src/jsonForm/types.ts +0 -167
  348. package/src/markdown/annotationCoercion.ts +0 -295
  349. package/src/markdown/attrTokens.ts +0 -243
  350. package/src/markdown/convert.ts +0 -1353
  351. package/src/markdown/htmlParse.ts +0 -212
  352. package/src/markdown/index.ts +0 -152
  353. package/src/markdown/parse.ts +0 -103
  354. package/src/markdown/sanitize.ts +0 -318
  355. package/src/markdown/stringify.ts +0 -207
  356. package/src/markdown/types.ts +0 -710
  357. package/src/markdown/utils.ts +0 -459
  358. package/src/narration/align.ts +0 -514
  359. package/src/narration/features.ts +0 -231
  360. package/src/narration/index.ts +0 -86
  361. package/src/narration/nuclei.ts +0 -118
  362. package/src/narration/pacing.ts +0 -211
  363. package/src/narration/script.ts +0 -239
  364. package/src/narration/session.ts +0 -100
  365. package/src/narration/sidecar.ts +0 -187
  366. package/src/narration/syllables.ts +0 -85
  367. package/src/narration/trace.ts +0 -50
  368. package/src/narration/types.ts +0 -299
  369. package/src/narration/vad.ts +0 -131
  370. package/src/ngeohash.d.ts +0 -11
  371. package/src/random/SeededRandom.ts +0 -127
  372. package/src/random/index.ts +0 -1
  373. package/src/recommend/index.ts +0 -16
  374. package/src/recommend/numberHighlight.ts +0 -25
  375. package/src/recommend/templates.ts +0 -393
  376. package/src/schemas/BlockTemplates.ts +0 -1046
  377. package/src/schemas/CustomTemplates.ts +0 -126
  378. package/src/schemas/Doc.ts +0 -1073
  379. package/src/schemas/ImageEditDoc.ts +0 -102
  380. package/src/schemas/LayoutStrategy.ts +0 -178
  381. package/src/schemas/Media.ts +0 -179
  382. package/src/schemas/MediaProvider.ts +0 -70
  383. package/src/schemas/PageStyle.ts +0 -213
  384. package/src/schemas/Theme.ts +0 -413
  385. package/src/schemas/Transitions.ts +0 -224
  386. package/src/schemas/Types.ts +0 -15
  387. package/src/schemas/Viewport.ts +0 -91
  388. package/src/schemas/colorUtils.ts +0 -219
  389. package/src/schemas/fontStacks.ts +0 -339
  390. package/src/schemas/index.ts +0 -31
  391. package/src/schemas/pageStyleDefaults.ts +0 -204
  392. package/src/schemas/themeCompile.ts +0 -244
  393. package/src/schemas/themeConstants.ts +0 -2
  394. package/src/schemas/themeLibrary.ts +0 -60
  395. package/src/schemas/themeValidator.ts +0 -609
  396. package/src/schemas/themes/bold.json +0 -77
  397. package/src/schemas/themes/cinematic.json +0 -143
  398. package/src/schemas/themes/documentary.json +0 -125
  399. package/src/schemas/themes/gezellig.json +0 -123
  400. package/src/schemas/themes/index.ts +0 -53
  401. package/src/schemas/themes/magazine.json +0 -126
  402. package/src/schemas/themes/minimalist.json +0 -74
  403. package/src/schemas/themes/morning-light.json +0 -117
  404. package/src/schemas/themes/standard-dark.json +0 -109
  405. package/src/schemas/themes/standard.json +0 -98
  406. package/src/schemas/themes/tech-dark.json +0 -143
  407. package/src/schemas/themes/warm-earth.json +0 -74
  408. package/src/spatial/Geohash.ts +0 -196
  409. package/src/spatial/Haversine.ts +0 -59
  410. package/src/spatial/index.ts +0 -2
  411. package/src/storage/ContentContainer.ts +0 -220
  412. package/src/storage/LocalForageAdapter.ts +0 -103
  413. package/src/storage/LocalStorageAdapter.ts +0 -57
  414. package/src/storage/MediaProviderFromContainer.ts +0 -92
  415. package/src/storage/MemoryStorageAdapter.ts +0 -37
  416. package/src/storage/ScopedContentContainer.ts +0 -143
  417. package/src/storage/Storage.ts +0 -28
  418. package/src/storage/index.ts +0 -9
  419. package/src/timing/index.ts +0 -17
  420. package/src/timing/narrationTiming.ts +0 -220
  421. package/src/timing/readingTime.ts +0 -94
  422. package/src/transform/applyTransform.ts +0 -122
  423. package/src/transform/blockAnalyzer.ts +0 -181
  424. package/src/transform/index.ts +0 -23
  425. package/src/transform/registry.ts +0 -217
  426. package/src/transform/styles/dataDriven.ts +0 -18
  427. package/src/transform/styles/documentary.ts +0 -18
  428. package/src/transform/styles/index.ts +0 -5
  429. package/src/transform/styles/magazine.ts +0 -19
  430. package/src/transform/styles/minimal.ts +0 -18
  431. package/src/transform/styles/narrative.ts +0 -19
  432. package/src/transform/templateSelector.ts +0 -389
  433. package/src/transform/timingAllocator.ts +0 -257
  434. package/src/transform/types.ts +0 -158
  435. package/src/versions/DocumentVersionManager.ts +0 -70
  436. package/src/versions/gitignore.ts +0 -34
  437. package/src/versions/index.ts +0 -56
  438. package/src/versions/operations.ts +0 -144
  439. package/src/versions/paths.ts +0 -51
  440. package/src/versions/snapshotStore.ts +0 -202
  441. package/src/versions/timestamp.ts +0 -48
  442. package/src/versions/types.ts +0 -93
@@ -1,1095 +0,0 @@
1
- /**
2
- * ASCII diagram parser: box-and-line art → semantic nodes/edges.
3
- *
4
- * Pure and deterministic; never throws. Unparseable input degrades to an
5
- * empty diagram (plus warnings), never an exception — callers decide what
6
- * to do with sparse results (the detector requires ≥2 nodes, for example).
7
- *
8
- * Pipeline: normalize → char grid → box tracing (corner walks; `+` is
9
- * resolved smallest-rectangle-first) → containment tree → labels/ids →
10
- * edge extraction (mask box perimeters + leaf interiors, then connected
11
- * components of line/arrow cells with label "bridges") → loose text.
12
- */
13
-
14
- import {
15
- arrowDirection,
16
- asciiArrowDirection,
17
- isAsciiStructural,
18
- isBlCorner,
19
- isBrCorner,
20
- isFlatBorderChar,
21
- isHoriz,
22
- isJunction,
23
- isLineChar,
24
- isPlus,
25
- isSideBorderChar,
26
- isTlCorner,
27
- isTrCorner,
28
- isUnicodeStructural,
29
- isVert,
30
- type ArrowDirection,
31
- } from './chars.js';
32
- import type { AsciiDiagram, AsciiDiagramEdge, AsciiDiagramNode } from './types.js';
33
-
34
- /** Internal stats consumed by the detector (loose-content ratio etc.). */
35
- export interface AsciiParseStats {
36
- totalNonSpaceCells: number;
37
- looseNonSpaceCells: number;
38
- hasWideChars: boolean;
39
- }
40
-
41
- export interface Rect {
42
- r0: number;
43
- c0: number;
44
- r1: number;
45
- c1: number;
46
- }
47
-
48
- interface ParsedBox extends Rect {
49
- /** Title embedded in the top border (`┌── Title ──┐`), if any. */
50
- borderTitle?: string;
51
- parentIdx: number;
52
- labelLines: string[];
53
- id: string;
54
- }
55
-
56
- const MAX_TITLE_RUN = 60;
57
- const MAX_BRIDGE_GAP = 40;
58
- const MAX_LOOSE_WARNINGS = 10;
59
- /**
60
- * Consecutive non-border cells tolerated while walking a box's side wall.
61
- * Hand-drawn art frequently has ragged side borders — a label line that
62
- * overflowed its box (e.g. a rename widened the text) leaves a space where
63
- * the `│` should be. The four corners plus the solid top/bottom borders are
64
- * the real box anchors, so a short run of gaps in a side wall shouldn't stop
65
- * the wall from reaching its corner. Kept small so two unrelated horizontal
66
- * rules can't span into a phantom box.
67
- */
68
- const MAX_WALL_GAP = 3;
69
- /** Larger side-wall gap budget for the opt-in repair pass (badly-drawn art). */
70
- const REPAIR_WALL_GAP = 10;
71
-
72
- /** True crossings: vertical and horizontal runs pass through independently. */
73
- const CROSS_CHARS = new Set(['┼', '╋', '╬', '╫', '╪']);
74
-
75
- export function parseAsciiDiagram(text: string): AsciiDiagram {
76
- return parseAsciiDiagramWithStats(text).diagram;
77
- }
78
-
79
- export function parseAsciiDiagramWithStats(text: string): {
80
- diagram: AsciiDiagram;
81
- stats: AsciiParseStats;
82
- } {
83
- const warnings: string[] = [];
84
- const grid = buildGrid(text, warnings);
85
- const H = grid.length;
86
- const W = H > 0 ? grid[0].length : 0;
87
-
88
- let totalNonSpaceCells = 0;
89
- let hasWideChars = false;
90
- for (let r = 0; r < H; r++) {
91
- for (let c = 0; c < W; c++) {
92
- const ch = grid[r][c];
93
- if (ch !== ' ') totalNonSpaceCells++;
94
- if (!hasWideChars && isWideChar(ch)) hasWideChars = true;
95
- }
96
- }
97
- if (hasWideChars) warnings.push('wide-chars');
98
-
99
- const style = detectStyle(grid);
100
-
101
- // ---- Boxes -------------------------------------------------------------
102
- const rects = traceBoxes(grid);
103
- const boxes = buildBoxes(grid, rects, warnings);
104
-
105
- // ---- Mask: box perimeters, leaf interiors, container label text --------
106
- const mask = new Uint8Array(W * H);
107
- const perimeterOwners = new Map<number, number[]>(); // cell index → box indices
108
- for (let b = 0; b < boxes.length; b++) {
109
- const box = boxes[b];
110
- for (let c = box.c0; c <= box.c1; c++) {
111
- for (const r of [box.r0, box.r1]) {
112
- mask[r * W + c] = 1;
113
- const owners = perimeterOwners.get(r * W + c) ?? [];
114
- owners.push(b);
115
- perimeterOwners.set(r * W + c, owners);
116
- }
117
- }
118
- for (let r = box.r0 + 1; r <= box.r1 - 1; r++) {
119
- for (const c of [box.c0, box.c1]) {
120
- mask[r * W + c] = 1;
121
- const owners = perimeterOwners.get(r * W + c) ?? [];
122
- owners.push(b);
123
- perimeterOwners.set(r * W + c, owners);
124
- }
125
- }
126
- }
127
- const isContainer = boxes.map((_, i) => boxes.some((b) => b.parentIdx === i));
128
- for (let b = 0; b < boxes.length; b++) {
129
- const box = boxes[b];
130
- if (!isContainer[b]) {
131
- // Leaf: whole interior is label space.
132
- for (let r = box.r0 + 1; r <= box.r1 - 1; r++) {
133
- for (let c = box.c0 + 1; c <= box.c1 - 1; c++) mask[r * W + c] = 1;
134
- }
135
- } else {
136
- // Container: only the label text cells above the topmost child.
137
- const minChildR0 = Math.min(
138
- ...boxes.filter((child) => child.parentIdx === b).map((child) => child.r0),
139
- );
140
- for (let r = box.r0 + 1; r < minChildR0; r++) {
141
- for (let c = box.c0 + 1; c <= box.c1 - 1; c++) {
142
- const ch = grid[r][c];
143
- if (ch !== ' ' && !isLineChar(ch) && arrowDirection(ch) === null) mask[r * W + c] = 1;
144
- }
145
- }
146
- }
147
- }
148
-
149
- // ---- Edges ---------------------------------------------------------------
150
- const { edges, edgeCells } = extractEdges(grid, mask, perimeterOwners, boxes, warnings);
151
-
152
- // ---- Loose text ---------------------------------------------------------
153
- let looseNonSpaceCells = 0;
154
- const looseRuns: string[] = [];
155
- for (let r = 0; r < H; r++) {
156
- let run = '';
157
- const flush = () => {
158
- if (run.trim().length > 0) looseRuns.push(`@${r}: ${truncate(run.trim(), 30)}`);
159
- run = '';
160
- };
161
- const isLooseCell = (c: number): boolean => {
162
- if (c < 0 || c >= W) return false;
163
- const idx = r * W + c;
164
- return grid[r][c] !== ' ' && !mask[idx] && !edgeCells.has(idx);
165
- };
166
- for (let c = 0; c < W; c++) {
167
- if (isLooseCell(c)) {
168
- run += grid[r][c];
169
- looseNonSpaceCells++;
170
- } else if (run !== '' && grid[r][c] === ' ' && isLooseCell(c + 1)) {
171
- run += ' '; // single interior space keeps phrases together
172
- } else {
173
- flush();
174
- }
175
- }
176
- flush();
177
- }
178
- for (const runText of looseRuns.slice(0, MAX_LOOSE_WARNINGS)) {
179
- warnings.push(`loose-text ${runText}`);
180
- }
181
- if (looseRuns.length > MAX_LOOSE_WARNINGS) {
182
- warnings.push(`loose-text (+${looseRuns.length - MAX_LOOSE_WARNINGS} more)`);
183
- }
184
-
185
- // ---- Assemble -----------------------------------------------------------
186
- const nodes: AsciiDiagramNode[] = boxes.map((box) => ({
187
- id: box.id,
188
- label: box.labelLines.join('\n'),
189
- col: box.c0,
190
- row: box.r0,
191
- wCols: box.c1 - box.c0 + 1,
192
- hRows: box.r1 - box.r0 + 1,
193
- ...(box.parentIdx >= 0 ? { containerId: boxes[box.parentIdx].id } : {}),
194
- }));
195
-
196
- return {
197
- diagram: { nodes, edges, style, warnings },
198
- stats: { totalNonSpaceCells, looseNonSpaceCells, hasWideChars },
199
- };
200
- }
201
-
202
- // ---------------------------------------------------------------------------
203
- // Grid
204
- // ---------------------------------------------------------------------------
205
-
206
- function buildGrid(text: string, warnings: string[]): string[][] {
207
- const rawLines = text.replace(/\r\n?/g, '\n').split('\n');
208
- let hadTab = false;
209
- const rows: string[][] = rawLines.map((line) => {
210
- const cells: string[] = [];
211
- for (const ch of line) {
212
- if (ch === '\t') {
213
- hadTab = true;
214
- do {
215
- cells.push(' ');
216
- } while (cells.length % 4 !== 0);
217
- } else {
218
- cells.push(ch);
219
- }
220
- }
221
- return cells;
222
- });
223
- if (hadTab) warnings.push('tabs-expanded');
224
- let width = 0;
225
- for (const row of rows) width = Math.max(width, row.length);
226
- for (const row of rows) {
227
- while (row.length < width) row.push(' ');
228
- }
229
- return rows;
230
- }
231
-
232
- function isWideChar(ch: string): boolean {
233
- const c = ch.codePointAt(0) ?? 0;
234
- return (
235
- (c >= 0x1100 && c <= 0x115f) ||
236
- (c >= 0x2e80 && c <= 0x303e) ||
237
- (c >= 0x3041 && c <= 0x33ff) ||
238
- (c >= 0x3400 && c <= 0x4dbf) ||
239
- (c >= 0x4e00 && c <= 0x9fff) ||
240
- (c >= 0xac00 && c <= 0xd7a3) ||
241
- (c >= 0xf900 && c <= 0xfaff) ||
242
- (c >= 0xfe30 && c <= 0xfe4f) ||
243
- (c >= 0xff00 && c <= 0xff60) ||
244
- (c >= 0x1f300 && c <= 0x1faff)
245
- );
246
- }
247
-
248
- function detectStyle(grid: string[][]): 'unicode' | 'ascii' {
249
- let unicode = 0;
250
- let ascii = 0;
251
- for (const row of grid) {
252
- for (const ch of row) {
253
- if (isUnicodeStructural(ch)) unicode++;
254
- else if (isAsciiStructural(ch)) ascii++;
255
- }
256
- }
257
- return ascii > unicode ? 'ascii' : 'unicode';
258
- }
259
-
260
- // ---------------------------------------------------------------------------
261
- // Box tracing
262
- // ---------------------------------------------------------------------------
263
-
264
- export interface TracedRect extends Rect {
265
- borderTitle?: string;
266
- }
267
-
268
- /**
269
- * Trace every box rectangle in the grid. `repair` mode is the aggressive
270
- * variant used only by the opt-in `repairAsciiDiagram` pass: it lets a side
271
- * wall cross edge-routing chars (a hand-drawn box whose label overflowed can
272
- * push arrows/lines through where its `│` should be), so more boxes of
273
- * genuinely-broken art are recovered. Conservative auto-detection never uses
274
- * it (a phantom box there would hide real code).
275
- */
276
- export function traceBoxes(grid: string[][], repair = false): TracedRect[] {
277
- const H = grid.length;
278
- const W = H > 0 ? grid[0].length : 0;
279
- const found: TracedRect[] = [];
280
- for (let r = 0; r < H; r++) {
281
- for (let c = 0; c < W; c++) {
282
- if (!isTlCandidate(grid, r, c)) continue;
283
- const rect = traceBoxFrom(grid, r, c, repair);
284
- if (rect) found.push(rect);
285
- }
286
- }
287
- return found;
288
- }
289
-
290
- /** Build the code-point grid for a fence's text (CRLF/tab-normalized, padded). */
291
- export function toGrid(text: string): string[][] {
292
- return buildGrid(text, []);
293
- }
294
-
295
- function isTlCandidate(grid: string[][], r: number, c: number): boolean {
296
- const ch = grid[r][c];
297
- if (isTlCorner(ch)) return true;
298
- // `+` (ASCII) and junction chars (Unicode `├ ┬ ┼` on shared borders /
299
- // sectioned cards) can open a box when a border continues right + down.
300
- if (!isPlus(ch) && !isJunction(ch)) return false;
301
- const right = grid[r][c + 1] ?? ' ';
302
- const below = grid[r + 1]?.[c] ?? ' ';
303
- const rightOk = isFlatBorderChar(right) || isTrCorner(right);
304
- const belowOk = isSideBorderChar(below) || isBlCorner(below);
305
- return rightOk && belowOk;
306
- }
307
-
308
- /**
309
- * Attempt to confirm a full rectangle starting at a top-left corner.
310
- * Smallest-rectangle-first: the first (nearest) TR/bottom-row combination
311
- * that closes wins, so `+` lattices decompose into cells and stacked
312
- * shared-border boxes parse as siblings.
313
- */
314
- function traceBoxFrom(grid: string[][], r0: number, c0: number, repair = false): TracedRect | null {
315
- const H = grid.length;
316
- const W = grid[0]?.length ?? 0;
317
-
318
- // -- Top border walk: collect TR candidates; allow one embedded title run.
319
- const trCandidates: number[] = [];
320
- let titleRun: { from: number; to: number } | null = null;
321
- let c = c0 + 1;
322
- while (c < W) {
323
- const ch = grid[r0][c];
324
- if (isTrCorner(ch)) {
325
- trCandidates.push(c);
326
- break; // a Unicode TR corner definitively ends the border
327
- }
328
- if (isPlus(ch) || isJunction(ch)) {
329
- trCandidates.push(c); // ambiguous: candidate AND possible continuation
330
- c++;
331
- continue;
332
- }
333
- if (isFlatBorderChar(ch)) {
334
- c++;
335
- continue;
336
- }
337
- // An arrowhead embedded in the border (`┌───▼──┐`, `┌─▼──▼──▼─┐`) is a
338
- // stem where an edge attaches, not a title — walk through it so the box
339
- // still closes (and so a border can hold several arrival points). Edge
340
- // extraction connects the line above/below to this box.
341
- if (arrowDirection(ch) !== null) {
342
- c++;
343
- continue;
344
- }
345
- // Possible embedded title (`┌── Title ──┐`): a single non-border run
346
- // bounded by border chars on both sides.
347
- if (titleRun) break;
348
- let end = c;
349
- let sawText = false;
350
- while (end < W && end - c <= MAX_TITLE_RUN) {
351
- const tch = grid[r0][end];
352
- if (isFlatBorderChar(tch) || isTrCorner(tch) || arrowDirection(tch) !== null) break;
353
- if (tch !== ' ') sawText = true;
354
- end++;
355
- }
356
- if (end >= W || end - c > MAX_TITLE_RUN || !sawText || end === c) break;
357
- titleRun = { from: c, to: end - 1 };
358
- c = end;
359
- }
360
- if (trCandidates.length === 0) return null;
361
-
362
- // -- Left wall (same for every TR candidate).
363
- const leftCandidates = walkSideWall(grid, c0, r0, H, isBlCorner, repair);
364
- if (leftCandidates.length === 0) return null;
365
- const leftSet = new Set(leftCandidates);
366
-
367
- for (const c1 of trCandidates) {
368
- if (c1 < c0 + 2) continue; // minimum width 3
369
-
370
- // -- Right wall for this candidate.
371
- const rightCandidates = walkSideWall(grid, c1, r0, H, isBrCorner, repair);
372
-
373
- for (const r1 of rightCandidates) {
374
- if (r1 < r0 + 2) continue; // minimum height 3
375
- if (!leftSet.has(r1)) continue;
376
- // -- Bottom border must be solid.
377
- let ok = true;
378
- for (let bc = c0 + 1; bc <= c1 - 1; bc++) {
379
- if (!isFlatBorderChar(grid[r1][bc])) {
380
- ok = false;
381
- break;
382
- }
383
- }
384
- if (!ok) continue;
385
- // -- Anti-phantom guard for the gap tolerance: a real box has an actual
386
- // side-border char on EACH wall's interior. A rectangle bridged purely
387
- // through gap cells (e.g. the `┌──┴──┐`/`└──┬──┘` routing corners of a
388
- // fan-out/fan-in, whose sides are empty) has none, and must not become a
389
- // box. Clean boxes always pass — their interior rows carry `│`/`├`/`┤`.
390
- // Repair mode widens the check to a ±1 column window so a uniformly
391
- // SHIFTED side border (corners at c1 but the `│` a column over — common
392
- // in hand-drawn art) still counts as evidence; a phantom's empty sides
393
- // stay empty in the window too, so it's still rejected.
394
- const win = repair ? 1 : 0;
395
- let leftBorders = 0;
396
- let rightBorders = 0;
397
- for (let ir = r0 + 1; ir <= r1 - 1; ir++) {
398
- if (hasSideBorderNear(grid[ir], c0, win)) leftBorders++;
399
- if (hasSideBorderNear(grid[ir], c1, win)) rightBorders++;
400
- }
401
- if (leftBorders === 0 || rightBorders === 0) continue;
402
- const title =
403
- titleRun && titleRun.to < c1
404
- ? grid[r0]
405
- .slice(titleRun.from, titleRun.to + 1)
406
- .join('')
407
- .trim()
408
- : undefined;
409
- return { r0, c0, r1, c1, ...(title ? { borderTitle: title } : {}) };
410
- }
411
- }
412
- return null;
413
- }
414
-
415
- /**
416
- * Walk one side wall of a box down from just below its top corner, collecting
417
- * corner-candidate rows (and junction rows, which double as shared-border
418
- * candidates). Tolerates up to {@link MAX_WALL_GAP} consecutive non-border
419
- * cells so a ragged / overflowed side border (hand-drawn art, or a rename
420
- * that widened a label past its box) still reaches the bottom corner. The
421
- * caller still requires the intersecting BL/BR corners and a solid bottom
422
- * border, so the gap tolerance can't invent a box out of empty columns.
423
- */
424
- /** True when a side-border char sits within `win` columns of `col` on `row`. */
425
- function hasSideBorderNear(row: string[], col: number, win: number): boolean {
426
- for (let c = col - win; c <= col + win; c++) {
427
- const ch = row[c];
428
- if (ch !== undefined && isSideBorderChar(ch)) return true;
429
- }
430
- return false;
431
- }
432
-
433
- function walkSideWall(
434
- grid: string[][],
435
- col: number,
436
- r0: number,
437
- H: number,
438
- isCorner: (ch: string) => boolean,
439
- repair = false,
440
- ): number[] {
441
- const candidates: number[] = [];
442
- const maxGap = repair ? REPAIR_WALL_GAP : MAX_WALL_GAP;
443
- let gap = 0;
444
- for (let r = r0 + 1; r < H; r++) {
445
- const ch = grid[r][col];
446
- if (isCorner(ch)) {
447
- candidates.push(r);
448
- break;
449
- }
450
- if (isPlus(ch) || isJunction(ch)) {
451
- candidates.push(r);
452
- gap = 0;
453
- continue;
454
- }
455
- if (isSideBorderChar(ch)) {
456
- gap = 0;
457
- continue;
458
- }
459
- // Only spaces / overflow label text are tolerable ragged-wall gaps. A
460
- // line or arrowhead sitting in the wall path is edge routing crossing
461
- // here (e.g. a fan-in `▼` between a source box and the collector), not a
462
- // box wall — stop, so routing can't bridge into a phantom box. In the
463
- // opt-in repair pass we tolerate even that (badly-drawn boxes push routing
464
- // through their walls); the side-border-evidence guard still applies.
465
- if (!repair && (isLineChar(ch) || arrowDirection(ch) !== null)) break;
466
- if (++gap > maxGap) break;
467
- }
468
- return candidates;
469
- }
470
-
471
- // ---------------------------------------------------------------------------
472
- // Containment, labels, ids
473
- // ---------------------------------------------------------------------------
474
-
475
- function strictlyContains(parent: Rect, child: Rect): boolean {
476
- return (
477
- parent.r0 < child.r0 && parent.c0 < child.c0 && parent.r1 > child.r1 && parent.c1 > child.c1
478
- );
479
- }
480
-
481
- /**
482
- * True when the boxes' INTERIORS overlap. Sharing a border line (stacked
483
- * `+--+ / +--+` cards, lattices) is legal and must not warn.
484
- */
485
- function interiorsOverlap(a: Rect, b: Rect): boolean {
486
- return (
487
- a.r0 + 1 <= b.r1 - 1 && b.r0 + 1 <= a.r1 - 1 && a.c0 + 1 <= b.c1 - 1 && b.c0 + 1 <= a.c1 - 1
488
- );
489
- }
490
-
491
- function area(rect: Rect): number {
492
- return (rect.r1 - rect.r0 + 1) * (rect.c1 - rect.c0 + 1);
493
- }
494
-
495
- function buildBoxes(grid: string[][], rects: TracedRect[], warnings: string[]): ParsedBox[] {
496
- // Reading order: row, then col.
497
- const sorted = [...rects].sort((a, b) => a.r0 - b.r0 || a.c0 - b.c0);
498
-
499
- // Parents: smallest strictly-containing box; ties → first in reading order.
500
- const parentIdx = sorted.map((child, i) => {
501
- let best = -1;
502
- let bestArea = Infinity;
503
- for (let j = 0; j < sorted.length; j++) {
504
- if (j === i) continue;
505
- if (!strictlyContains(sorted[j], child)) continue;
506
- const a = area(sorted[j]);
507
- if (a < bestArea) {
508
- bestArea = a;
509
- best = j;
510
- }
511
- }
512
- return best;
513
- });
514
-
515
- // Overlap (interiors intersect, neither contains) → keep both + warning.
516
- let warnedOverlap = false;
517
- for (let i = 0; i < sorted.length && !warnedOverlap; i++) {
518
- for (let j = i + 1; j < sorted.length; j++) {
519
- if (
520
- interiorsOverlap(sorted[i], sorted[j]) &&
521
- !strictlyContains(sorted[i], sorted[j]) &&
522
- !strictlyContains(sorted[j], sorted[i])
523
- ) {
524
- warnings.push('overlapping-boxes');
525
- warnedOverlap = true;
526
- break;
527
- }
528
- }
529
- }
530
-
531
- const hasChildren = sorted.map((_, i) => parentIdx.includes(i));
532
-
533
- const usedIds = new Map<string, number>();
534
- const boxes: ParsedBox[] = sorted.map((rect, i) => {
535
- const labelLines: string[] = [];
536
- if (rect.borderTitle) labelLines.push(rect.borderTitle);
537
- if (!hasChildren[i]) {
538
- for (let r = rect.r0 + 1; r <= rect.r1 - 1; r++) {
539
- const rowText = grid[r].slice(rect.c0 + 1, rect.c1).join('');
540
- if (isDividerRow(rowText)) continue;
541
- const trimmed = rowText.trim();
542
- if (trimmed.length > 0) labelLines.push(trimmed);
543
- }
544
- } else {
545
- const minChildR0 = Math.min(
546
- ...sorted.filter((_, j) => parentIdx[j] === i).map((child) => child.r0),
547
- );
548
- for (let r = rect.r0 + 1; r < minChildR0; r++) {
549
- const rowText = grid[r]
550
- .slice(rect.c0 + 1, rect.c1)
551
- .map((ch) => (isLineChar(ch) || arrowDirection(ch) !== null ? ' ' : ch))
552
- .join('')
553
- .trim();
554
- if (rowText.length > 0) labelLines.push(rowText);
555
- }
556
- }
557
- const base = slugify(labelLines[0] ?? '');
558
- const count = usedIds.get(base) ?? 0;
559
- usedIds.set(base, count + 1);
560
- const id = count === 0 ? base : `${base}-${count + 1}`;
561
- return { ...rect, parentIdx: parentIdx[i], labelLines, id };
562
- });
563
- return boxes;
564
- }
565
-
566
- /** A leaf interior row that is purely a horizontal divider (`├────┤` style). */
567
- function isDividerRow(rowText: string): boolean {
568
- let sawLine = false;
569
- for (const ch of rowText) {
570
- if (ch === ' ') continue;
571
- if (isHoriz(ch) || isJunction(ch) || isPlus(ch)) {
572
- sawLine = true;
573
- continue;
574
- }
575
- return false;
576
- }
577
- return sawLine;
578
- }
579
-
580
- function slugify(s: string): string {
581
- const slug = s
582
- .toLowerCase()
583
- .replace(/[^a-z0-9]+/g, '-')
584
- .replace(/^-+|-+$/g, '');
585
- return slug || 'node';
586
- }
587
-
588
- // ---------------------------------------------------------------------------
589
- // Edge extraction
590
- // ---------------------------------------------------------------------------
591
-
592
- interface EdgeCellInfo {
593
- arrow: ArrowDirection | null;
594
- bridgeLabel?: string;
595
- }
596
-
597
- function extractEdges(
598
- grid: string[][],
599
- mask: Uint8Array,
600
- perimeterOwners: Map<number, number[]>,
601
- boxes: ParsedBox[],
602
- warnings: string[],
603
- ): { edges: AsciiDiagramEdge[]; edgeCells: Set<number> } {
604
- const H = grid.length;
605
- const W = H > 0 ? grid[0].length : 0;
606
-
607
- // -- Eligible cells: line/arrow chars outside the mask.
608
- const cells = new Map<number, EdgeCellInfo>();
609
- for (let r = 0; r < H; r++) {
610
- for (let c = 0; c < W; c++) {
611
- const idx = r * W + c;
612
- if (mask[idx]) continue;
613
- const ch = grid[r][c];
614
- const uni = arrowDirection(ch);
615
- if (uni) {
616
- cells.set(idx, { arrow: uni });
617
- continue;
618
- }
619
- const asc = asciiArrowDirection(ch);
620
- if (asc && asciiArrowGate(grid, r, c, asc)) {
621
- cells.set(idx, { arrow: asc });
622
- continue;
623
- }
624
- if (isLineChar(ch)) cells.set(idx, { arrow: null });
625
- }
626
- }
627
-
628
- // -- Bridges: `──label──` glue (label + interior spaces become cells).
629
- for (let r = 0; r < H; r++) {
630
- let c = 0;
631
- while (c < W) {
632
- const idx = r * W + c;
633
- if (!cells.has(idx) || !isHoriz(grid[r][c])) {
634
- c++;
635
- continue;
636
- }
637
- // End of this horizontal run.
638
- let runEnd = c;
639
- while (runEnd + 1 < W && cells.has(r * W + runEnd + 1) && isHoriz(grid[r][runEnd + 1])) {
640
- runEnd++;
641
- }
642
- // Candidate gap after the run.
643
- let g = runEnd + 1;
644
- let sawText = false;
645
- let failed = false;
646
- while (g < W && g - runEnd - 1 <= MAX_BRIDGE_GAP) {
647
- const gch = grid[r][g];
648
- const gidx = r * W + g;
649
- if (mask[gidx] || isVert(gch)) {
650
- failed = true;
651
- break;
652
- }
653
- if (isHoriz(gch)) break;
654
- if (isLineChar(gch) || arrowDirection(gch) !== null) {
655
- failed = true;
656
- break;
657
- }
658
- const asc = asciiArrowDirection(gch);
659
- if (asc && asciiArrowGate(grid, r, g, asc)) {
660
- failed = true;
661
- break;
662
- }
663
- if (gch !== ' ') sawText = true;
664
- g++;
665
- }
666
- if (
667
- !failed &&
668
- g < W &&
669
- g > runEnd + 1 &&
670
- sawText &&
671
- isHoriz(grid[r][g]) &&
672
- !mask[r * W + g]
673
- ) {
674
- const label = grid[r]
675
- .slice(runEnd + 1, g)
676
- .join('')
677
- .trim();
678
- for (let bc = runEnd + 1; bc < g; bc++) {
679
- cells.set(r * W + bc, { arrow: null, bridgeLabel: label });
680
- }
681
- c = g;
682
- } else {
683
- c = runEnd + 1;
684
- }
685
- }
686
- }
687
-
688
- // -- Connected components (4-connectivity), with cross cells (`┼`)
689
- // acting as PASS-THROUGH: at a cross, the vertical and horizontal runs
690
- // connect independently, so two edges crossing each other stay two
691
- // edges instead of merging into one four-armed blob. (ASCII `+` cannot
692
- // be disambiguated from a turn, so it stays fully connective.)
693
- const isCrossCell = (idx: number): boolean => CROSS_CHARS.has(grid[Math.floor(idx / W)][idx % W]);
694
- // State key = idx * 2 + channel (0 = horizontal, 1 = vertical); non-cross
695
- // cells always live on channel 0.
696
- const stateKey = (idx: number, channel: 0 | 1): number => idx * 2 + channel;
697
- const visited = new Set<number>();
698
- const components: number[][] = [];
699
- const seeds: Array<[number, 0 | 1]> = [];
700
- for (const idx of cells.keys()) {
701
- seeds.push([idx, 0]);
702
- if (isCrossCell(idx)) seeds.push([idx, 1]);
703
- }
704
- for (const [seedIdx, seedChannel] of seeds) {
705
- if (visited.has(stateKey(seedIdx, seedChannel))) continue;
706
- const componentCells = new Set<number>();
707
- const queue: Array<[number, 0 | 1]> = [[seedIdx, seedChannel]];
708
- visited.add(stateKey(seedIdx, seedChannel));
709
- while (queue.length > 0) {
710
- const [idx, channel] = queue.pop() as [number, 0 | 1];
711
- componentCells.add(idx);
712
- const r = Math.floor(idx / W);
713
- const c = idx % W;
714
- const neighbors: Array<[number, number]> =
715
- isCrossCell(idx) && channel === 1
716
- ? [
717
- [r - 1, c],
718
- [r + 1, c],
719
- ]
720
- : isCrossCell(idx)
721
- ? [
722
- [r, c - 1],
723
- [r, c + 1],
724
- ]
725
- : [
726
- [r - 1, c],
727
- [r + 1, c],
728
- [r, c - 1],
729
- [r, c + 1],
730
- ];
731
- for (const [nr, nc] of neighbors) {
732
- if (nr < 0 || nr >= H || nc < 0 || nc >= W) continue;
733
- const nidx = nr * W + nc;
734
- if (!cells.has(nidx)) continue;
735
- const nChannel: 0 | 1 = isCrossCell(nidx) ? (nr !== r ? 1 : 0) : 0;
736
- const key = stateKey(nidx, nChannel);
737
- if (!visited.has(key)) {
738
- visited.add(key);
739
- queue.push([nidx, nChannel]);
740
- }
741
- }
742
- }
743
- components.push([...componentCells].sort((a, b) => a - b));
744
- }
745
- components.sort((a, b) => a[0] - b[0]);
746
-
747
- // -- Component → edges.
748
- const edges: AsciiDiagramEdge[] = [];
749
- const seen = new Set<string>();
750
- let mergedParallel = false;
751
- const looseComponents = new Set<number>();
752
- const sideLabelCells = new Set<number>();
753
-
754
- for (const component of components) {
755
- const inComponent = new Set(component);
756
- // Endpoints: ≤1 neighbor within the component, plus every arrow cell.
757
- const endpoints: number[] = [];
758
- for (const idx of component) {
759
- const r = Math.floor(idx / W);
760
- const c = idx % W;
761
- let neighbors = 0;
762
- for (const [nr, nc] of [
763
- [r - 1, c],
764
- [r + 1, c],
765
- [r, c - 1],
766
- [r, c + 1],
767
- ]) {
768
- if (inComponent.has(nr * W + nc)) neighbors++;
769
- }
770
- const info = cells.get(idx) as EdgeCellInfo;
771
- if (neighbors <= 1 || info.arrow !== null) endpoints.push(idx);
772
- }
773
-
774
- // Attachments: endpoint adjacent (Chebyshev ≤ 1) to a box perimeter.
775
- interface Attachment {
776
- boxIdx: number;
777
- isArrowEnd: boolean;
778
- }
779
- const attachmentsByBox = new Map<number, Attachment>();
780
- let dangling = false;
781
- for (const idx of endpoints) {
782
- const r = Math.floor(idx / W);
783
- const c = idx % W;
784
- const info = cells.get(idx) as EdgeCellInfo;
785
- const boxIdx = snapEndpoint(r, c, info.arrow, perimeterOwners, boxes, W, H);
786
- if (boxIdx === null) {
787
- dangling = true;
788
- continue;
789
- }
790
- const existing = attachmentsByBox.get(boxIdx);
791
- if (existing) {
792
- existing.isArrowEnd = existing.isArrowEnd || info.arrow !== null;
793
- } else {
794
- attachmentsByBox.set(boxIdx, { boxIdx, isArrowEnd: info.arrow !== null });
795
- }
796
- }
797
- const nearestOwner = (r: number, c: number): number | null => {
798
- const owners = perimeterOwners.get(r * W + c) ?? [];
799
- if (owners.length === 0) return null;
800
- return [...owners].sort(
801
- (a, b) => boxes[a].r0 - boxes[b].r0 || boxes[a].c0 - boxes[b].c0 || a - b,
802
- )[0];
803
- };
804
-
805
- // Junction cells touching a perimeter are attachments too — the
806
- // `┌──┴──┐` bus-split shape connects to the box it branched from even
807
- // though the junction cell has 2+ neighbors and no arrowhead.
808
- for (const idx of component) {
809
- const r = Math.floor(idx / W);
810
- const c = idx % W;
811
- const ch = grid[r][c];
812
- if (!isJunction(ch) && !isPlus(ch)) continue;
813
- for (const [dr, dc] of [
814
- [-1, 0],
815
- [1, 0],
816
- [0, -1],
817
- [0, 1],
818
- ]) {
819
- const boxIdx = nearestOwner(r + dr, c + dc);
820
- if (boxIdx !== null && !attachmentsByBox.has(boxIdx)) {
821
- attachmentsByBox.set(boxIdx, { boxIdx, isArrowEnd: false });
822
- }
823
- }
824
- }
825
-
826
- // Arrow cells attach to the box on the side OPPOSITE the arrowhead:
827
- // a tight vertical stack (`└─┬─┘` / `▼` / `┌───┐`, boxes one row apart)
828
- // is a single arrow cell whose only endpoint snaps to the pointed box —
829
- // without this the source box is missed and the edge drops. Very common
830
- // in AI output.
831
- for (const idx of component) {
832
- const r = Math.floor(idx / W);
833
- const c = idx % W;
834
- const info = cells.get(idx) as EdgeCellInfo;
835
- if (info.arrow === null) continue;
836
- const [dr, dc] =
837
- info.arrow === 'down'
838
- ? [-1, 0]
839
- : info.arrow === 'up'
840
- ? [1, 0]
841
- : info.arrow === 'right'
842
- ? [0, -1]
843
- : [0, 1];
844
- const boxIdx = nearestOwner(r + dr, c + dc);
845
- if (boxIdx !== null && !attachmentsByBox.has(boxIdx)) {
846
- attachmentsByBox.set(boxIdx, { boxIdx, isArrowEnd: false });
847
- }
848
- }
849
-
850
- const attachments = [...attachmentsByBox.values()].sort(
851
- (a, b) => boxes[a.boxIdx].r0 - boxes[b.boxIdx].r0 || boxes[a.boxIdx].c0 - boxes[b.boxIdx].c0,
852
- );
853
-
854
- if (attachments.length === 0) {
855
- for (const idx of component) looseComponents.add(idx);
856
- warnings.push('dangling-edge');
857
- continue;
858
- }
859
- if (attachments.length === 1) {
860
- warnings.push('dangling-edge');
861
- continue;
862
- }
863
- if (dangling) warnings.push('dangling-edge');
864
-
865
- // Label: first bridge in the component, else a side label — a text run
866
- // sitting one space to the right of one of the component's vertical
867
- // cells (`│ born`), the way labels attach to vertical drops.
868
- const bridgeLabels: string[] = [];
869
- for (const idx of component) {
870
- const info = cells.get(idx) as EdgeCellInfo;
871
- if (info.bridgeLabel && !bridgeLabels.includes(info.bridgeLabel)) {
872
- bridgeLabels.push(info.bridgeLabel);
873
- }
874
- }
875
- // A label run stops at a box border, an arrow, or a structural line —
876
- // but an ASCII hyphen (`-`) inside a word (`on-fail`, `read-only`) is
877
- // label text, not a line, so it's allowed. The leading-space gate keeps
878
- // real lines (which abut the `│` directly) from being read as labels.
879
- const isLabelStop = (ch: string): boolean =>
880
- (isLineChar(ch) && ch !== '-') || arrowDirection(ch) !== null;
881
- const hasAlnum = (s: string): boolean => /[a-z0-9]/i.test(s);
882
-
883
- let label = bridgeLabels[0];
884
- if (!label) {
885
- // Try the right side (`│ born`) then the left side (`born │`) of
886
- // each vertical cell, topmost-first.
887
- outer: for (const idx of component) {
888
- const r = Math.floor(idx / W);
889
- const c = idx % W;
890
- if (!isVert(grid[r][c])) continue;
891
- if ((grid[r][c + 1] ?? '') === ' ') {
892
- let end = c + 2;
893
- let run = '';
894
- while (end < W) {
895
- const ch = grid[r][end];
896
- if (mask[r * W + end] || isLabelStop(ch)) break;
897
- if (ch === ' ' && (grid[r][end + 1] ?? ' ') === ' ') break;
898
- run += ch;
899
- end++;
900
- }
901
- const clean = run.trim().replace(/^-+|-+$/g, '');
902
- if (clean.length > 0 && clean.length <= MAX_BRIDGE_GAP && hasAlnum(clean)) {
903
- label = clean;
904
- for (let lc = c + 2; lc < end; lc++) sideLabelCells.add(r * W + lc);
905
- break outer;
906
- }
907
- }
908
- if ((grid[r][c - 1] ?? '') === ' ') {
909
- let start = c - 2;
910
- let run = '';
911
- while (start >= 0) {
912
- const ch = grid[r][start];
913
- if (mask[r * W + start] || isLabelStop(ch)) break;
914
- if (ch === ' ' && (grid[r][start - 1] ?? ' ') === ' ') break;
915
- run = ch + run;
916
- start--;
917
- }
918
- const clean = run.trim().replace(/^-+|-+$/g, '');
919
- if (clean.length > 0 && clean.length <= MAX_BRIDGE_GAP && hasAlnum(clean)) {
920
- label = clean;
921
- for (let lc = start + 1; lc <= c - 2; lc++) sideLabelCells.add(r * W + lc);
922
- break outer;
923
- }
924
- }
925
- }
926
- }
927
- if (bridgeLabels.length > 1) warnings.push('multiple-edge-labels');
928
-
929
- const pushEdge = (sourceIdx: number, targetIdx: number, directed: boolean) => {
930
- const edge: AsciiDiagramEdge = {
931
- source: boxes[sourceIdx].id,
932
- target: boxes[targetIdx].id,
933
- ...(label ? { label } : {}),
934
- directed,
935
- };
936
- const key = `${edge.source}|${edge.target}|${edge.directed}|${edge.label ?? ''}`;
937
- if (seen.has(key)) {
938
- mergedParallel = true;
939
- return;
940
- }
941
- seen.add(key);
942
- edges.push(edge);
943
- };
944
-
945
- const arrowEnds = attachments.filter((a) => a.isArrowEnd);
946
- const plainEnds = attachments.filter((a) => !a.isArrowEnd);
947
-
948
- if (attachments.length === 2) {
949
- const [a, b] = attachments;
950
- if (arrowEnds.length === 2) {
951
- pushEdge(a.boxIdx, b.boxIdx, true);
952
- pushEdge(b.boxIdx, a.boxIdx, true);
953
- } else if (arrowEnds.length === 1) {
954
- const target = arrowEnds[0];
955
- const source = a === target ? b : a;
956
- pushEdge(source.boxIdx, target.boxIdx, true);
957
- } else {
958
- pushEdge(a.boxIdx, b.boxIdx, false);
959
- }
960
- } else if (plainEnds.length === 1 && arrowEnds.length >= 1) {
961
- for (const target of arrowEnds) pushEdge(plainEnds[0].boxIdx, target.boxIdx, true); // fan-out
962
- } else if (arrowEnds.length === 1 && plainEnds.length >= 1) {
963
- for (const source of plainEnds) pushEdge(source.boxIdx, arrowEnds[0].boxIdx, true); // fan-in
964
- } else if (arrowEnds.length === 0) {
965
- warnings.push('ambiguous-bus');
966
- for (let i = 1; i < attachments.length; i++) {
967
- pushEdge(attachments[0].boxIdx, attachments[i].boxIdx, false);
968
- }
969
- } else {
970
- warnings.push('ambiguous-bus');
971
- for (const target of arrowEnds) {
972
- if (plainEnds.length > 0) pushEdge(plainEnds[0].boxIdx, target.boxIdx, true);
973
- }
974
- }
975
- }
976
-
977
- if (mergedParallel) warnings.push('parallel-edges-merged');
978
-
979
- edges.sort(
980
- (a, b) =>
981
- a.source.localeCompare(b.source) ||
982
- a.target.localeCompare(b.target) ||
983
- (a.label ?? '').localeCompare(b.label ?? ''),
984
- );
985
-
986
- // Edge cells for loose-text accounting: everything in a component that
987
- // produced (or tried to produce) an edge, minus fully-detached art,
988
- // plus consumed side-label runs.
989
- const edgeCells = new Set<number>();
990
- for (const component of components) {
991
- for (const idx of component) {
992
- if (!looseComponents.has(idx)) edgeCells.add(idx);
993
- }
994
- }
995
- for (const idx of sideLabelCells) edgeCells.add(idx);
996
- return { edges, edgeCells };
997
- }
998
-
999
- /** ASCII arrow chars count only when a matching line char sits behind them. */
1000
- function asciiArrowGate(grid: string[][], r: number, c: number, dir: ArrowDirection): boolean {
1001
- const behind =
1002
- dir === 'down'
1003
- ? grid[r - 1]?.[c]
1004
- : dir === 'up'
1005
- ? grid[r + 1]?.[c]
1006
- : dir === 'right'
1007
- ? grid[r]?.[c - 1]
1008
- : grid[r]?.[c + 1];
1009
- if (behind === undefined) return false;
1010
- return dir === 'down' || dir === 'up' ? isVert(behind) : isHoriz(behind);
1011
- }
1012
-
1013
- /**
1014
- * Snap an endpoint cell to a box: prefer a perimeter cell in the arrow's
1015
- * pointing direction, else the nearest (Chebyshev ≤ 1) perimeter owner,
1016
- * ties broken by reading order.
1017
- */
1018
- function snapEndpoint(
1019
- r: number,
1020
- c: number,
1021
- arrow: ArrowDirection | null,
1022
- perimeterOwners: Map<number, number[]>,
1023
- boxes: ParsedBox[],
1024
- W: number,
1025
- H: number,
1026
- ): number | null {
1027
- const ownersAt = (rr: number, cc: number): number[] => {
1028
- if (rr < 0 || rr >= H || cc < 0 || cc >= W) return [];
1029
- return perimeterOwners.get(rr * W + cc) ?? [];
1030
- };
1031
- if (arrow) {
1032
- const [dr, dc] =
1033
- arrow === 'down' ? [1, 0] : arrow === 'up' ? [-1, 0] : arrow === 'right' ? [0, 1] : [0, -1];
1034
- // Pointed cell at distance 1, then 2 (`--> ` with a space before the box).
1035
- for (const dist of [1, 2]) {
1036
- const pointed = ownersAt(r + dr * dist, c + dc * dist);
1037
- if (pointed.length > 0) {
1038
- // The pointed-at box: the one whose border faces the arrow.
1039
- const sorted = [...pointed].sort((a, b) => {
1040
- const da = directionDepth(boxes[a], r + dr * dist, c + dc * dist, arrow);
1041
- const db = directionDepth(boxes[b], r + dr * dist, c + dc * dist, arrow);
1042
- return db - da || a - b;
1043
- });
1044
- return sorted[0];
1045
- }
1046
- }
1047
- }
1048
- const candidates = new Map<number, number>(); // boxIdx → priority (0 = orthogonal, 1 = diagonal, 2 = one space away)
1049
- for (const [dr, dc, priority] of [
1050
- [-1, 0, 0],
1051
- [1, 0, 0],
1052
- [0, -1, 0],
1053
- [0, 1, 0],
1054
- [-1, -1, 1],
1055
- [-1, 1, 1],
1056
- [1, -1, 1],
1057
- [1, 1, 1],
1058
- [-2, 0, 2],
1059
- [2, 0, 2],
1060
- [0, -2, 2],
1061
- [0, 2, 2],
1062
- ]) {
1063
- for (const owner of ownersAt(r + dr, c + dc)) {
1064
- const existing = candidates.get(owner);
1065
- if (existing === undefined || priority < existing) candidates.set(owner, priority);
1066
- }
1067
- }
1068
- if (candidates.size === 0) return null;
1069
- const ranked = [...candidates.entries()].sort(
1070
- (a, b) =>
1071
- a[1] - b[1] ||
1072
- boxes[a[0]].r0 - boxes[b[0]].r0 ||
1073
- boxes[a[0]].c0 - boxes[b[0]].c0 ||
1074
- a[0] - b[0],
1075
- );
1076
- return ranked[0][0];
1077
- }
1078
-
1079
- /** How deep the cell sits toward a box's interior along the arrow direction (higher = more pointed-at). */
1080
- function directionDepth(box: Rect, r: number, c: number, arrow: ArrowDirection): number {
1081
- switch (arrow) {
1082
- case 'down':
1083
- return box.r0 === r ? 1 : 0; // arrow points down at the box's TOP border
1084
- case 'up':
1085
- return box.r1 === r ? 1 : 0;
1086
- case 'right':
1087
- return box.c0 === c ? 1 : 0;
1088
- case 'left':
1089
- return box.c1 === c ? 1 : 0;
1090
- }
1091
- }
1092
-
1093
- function truncate(s: string, max: number): string {
1094
- return s.length > max ? `${s.slice(0, max - 1)}…` : s;
1095
- }