@bendyline/squisq 2.0.1 → 2.2.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 (438) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +24 -0
  3. package/dist/{ContentContainer-BXUlIf18.d.ts → ContentContainer-B2w9sUoL.d.ts} +15 -0
  4. package/dist/{Doc-ylqWKDc_.d.ts → Doc-BubIpWsp.d.ts} +160 -3
  5. package/dist/{ImageEditDoc-D828IXJ1.d.ts → ImageEditDoc-CGxk5CHc.d.ts} +1 -1
  6. package/dist/{annotationCoercion-Dr_DY9Hx.d.ts → annotationCoercion-PtRQhk5V.d.ts} +1 -1
  7. package/dist/chunk-2L55443L.js +0 -0
  8. package/dist/chunk-2ZWIXGAC.js +214 -0
  9. package/dist/{chunk-2PWOO4ZE.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-X6DUONGT.js → chunk-7ARJZYQY.js} +280 -128
  15. package/dist/{chunk-DBS6RIFL.js → chunk-7N4G32LG.js} +0 -1
  16. package/dist/{chunk-JOQYPRDZ.js → chunk-7YEZFHLS.js} +10 -8
  17. package/dist/{chunk-2UQOHYE2.js → chunk-7ZAAICW4.js} +0 -1
  18. package/dist/{chunk-66QO7XFB.js → chunk-ACZOVWX3.js} +117 -7
  19. package/dist/{chunk-5J4HKRJF.js → chunk-BAOV476U.js} +0 -1
  20. package/dist/chunk-BCCXTMN5.js +699 -0
  21. package/dist/{chunk-W4O2TX5I.js → chunk-C33GTPUZ.js} +579 -4
  22. package/dist/{chunk-MMPBOUK7.js → chunk-CB45YGAM.js} +7 -2
  23. package/dist/{chunk-CY2R2VPR.js → chunk-DYPMOPIW.js} +18 -2
  24. package/dist/{chunk-VR7UPJ7C.js → chunk-FCG3BIZN.js} +12 -1
  25. package/dist/{chunk-5UYI3RJB.js → chunk-FDYDYL77.js} +54 -40
  26. package/dist/{chunk-TFJVXUHH.js → chunk-I2O6W7DX.js} +1513 -79
  27. package/dist/{chunk-Q7LROV2H.js → chunk-NKNMGIIN.js} +0 -1
  28. package/dist/chunk-OPO5K4DZ.js +241 -0
  29. package/dist/{chunk-TUWMK62B.js → chunk-PUS54YU6.js} +45 -2
  30. package/dist/chunk-QKPXHDLR.js +0 -0
  31. package/dist/{chunk-GZY66SDN.js → chunk-R3KTTOMS.js} +0 -1
  32. package/dist/{chunk-PXLVXTLW.js → chunk-RS5AP3J4.js} +72 -15
  33. package/dist/chunk-WV5ARNDA.js +0 -0
  34. package/dist/{chunk-4X3JQXNM.js → chunk-XOFT65EX.js} +1 -2
  35. package/dist/{chunk-FVTQSNR7.js → chunk-XRW6P7IF.js} +13 -5
  36. package/dist/chunk-XWNNA3KE.js +0 -0
  37. package/dist/chunk-YZNGMV3G.js +0 -0
  38. package/dist/{chunk-R2YZTCLO.js → chunk-ZQKZSJAX.js} +1 -2
  39. package/dist/doc/index.d.ts +166 -12
  40. package/dist/doc/index.js +36 -17
  41. package/dist/generate/index.d.ts +2 -2
  42. package/dist/generate/index.js +2 -3
  43. package/dist/icons/index.js +3 -4
  44. package/dist/icons/inlineIconMarker.js +1 -2
  45. package/dist/imageEdit/index.d.ts +48 -17
  46. package/dist/imageEdit/index.js +3 -4
  47. package/dist/index.d.ts +13 -12
  48. package/dist/index.js +118 -33
  49. package/dist/jsonForm/index.d.ts +10 -3
  50. package/dist/jsonForm/index.js +8 -7
  51. package/dist/markdown/index.d.ts +75 -11
  52. package/dist/markdown/index.js +32 -7
  53. package/dist/materializePageSection-wSLA2A1g.d.ts +223 -0
  54. package/dist/narration/index.d.ts +16 -3
  55. package/dist/narration/index.js +13 -14
  56. package/dist/random/index.d.ts +11 -2
  57. package/dist/random/index.js +2 -3
  58. package/dist/recommend/index.d.ts +1 -1
  59. package/dist/recommend/index.js +3 -4
  60. package/dist/schemas/index.d.ts +31 -6
  61. package/dist/schemas/index.js +43 -6
  62. package/dist/spatial/index.d.ts +2 -1
  63. package/dist/spatial/index.js +1 -2
  64. package/dist/storage/index.d.ts +4 -2
  65. package/dist/storage/index.js +1 -2
  66. package/dist/{themeLibrary-RjVgLpUi.d.ts → themeLibrary-DU9mUmVl.d.ts} +1 -1
  67. package/dist/timing/index.js +3 -4
  68. package/dist/transform/index.d.ts +9 -10
  69. package/dist/transform/index.js +6 -7
  70. package/dist/{types-C4kiRn_k.d.ts → types-8QjefM9J.d.ts} +38 -2
  71. package/dist/{types-BwMPl2CB.d.ts → types-CUNc9biN.d.ts} +24 -1
  72. package/dist/versions/index.d.ts +14 -11
  73. package/dist/versions/index.js +2 -3
  74. package/package.json +4 -2
  75. package/dist/chunk-2PWOO4ZE.js.map +0 -1
  76. package/dist/chunk-2UQOHYE2.js.map +0 -1
  77. package/dist/chunk-3E5F2XMR.js.map +0 -1
  78. package/dist/chunk-4X3JQXNM.js.map +0 -1
  79. package/dist/chunk-5DKBFXPY.js.map +0 -1
  80. package/dist/chunk-5J4HKRJF.js.map +0 -1
  81. package/dist/chunk-5UYI3RJB.js.map +0 -1
  82. package/dist/chunk-66QO7XFB.js.map +0 -1
  83. package/dist/chunk-AMIR72JP.js +0 -380
  84. package/dist/chunk-AMIR72JP.js.map +0 -1
  85. package/dist/chunk-CY2R2VPR.js.map +0 -1
  86. package/dist/chunk-DBS6RIFL.js.map +0 -1
  87. package/dist/chunk-DNJKBAI7.js +0 -1
  88. package/dist/chunk-DNJKBAI7.js.map +0 -1
  89. package/dist/chunk-ET53IIEP.js +0 -1
  90. package/dist/chunk-ET53IIEP.js.map +0 -1
  91. package/dist/chunk-FVJUUTEM.js +0 -74
  92. package/dist/chunk-FVJUUTEM.js.map +0 -1
  93. package/dist/chunk-FVTQSNR7.js.map +0 -1
  94. package/dist/chunk-GZY66SDN.js.map +0 -1
  95. package/dist/chunk-JOQYPRDZ.js.map +0 -1
  96. package/dist/chunk-MMPBOUK7.js.map +0 -1
  97. package/dist/chunk-PGGGKYWG.js +0 -1
  98. package/dist/chunk-PGGGKYWG.js.map +0 -1
  99. package/dist/chunk-PXLVXTLW.js.map +0 -1
  100. package/dist/chunk-Q7LROV2H.js.map +0 -1
  101. package/dist/chunk-QE7TWGVT.js.map +0 -1
  102. package/dist/chunk-R2GC6RLD.js +0 -344
  103. package/dist/chunk-R2GC6RLD.js.map +0 -1
  104. package/dist/chunk-R2YZTCLO.js.map +0 -1
  105. package/dist/chunk-S7A5D7ZJ.js +0 -174
  106. package/dist/chunk-S7A5D7ZJ.js.map +0 -1
  107. package/dist/chunk-TFJVXUHH.js.map +0 -1
  108. package/dist/chunk-TUWMK62B.js.map +0 -1
  109. package/dist/chunk-V543AI2F.js +0 -1
  110. package/dist/chunk-V543AI2F.js.map +0 -1
  111. package/dist/chunk-VR7UPJ7C.js.map +0 -1
  112. package/dist/chunk-W4O2TX5I.js.map +0 -1
  113. package/dist/chunk-X6DUONGT.js.map +0 -1
  114. package/dist/chunk-ZLQKNOZQ.js +0 -1
  115. package/dist/chunk-ZLQKNOZQ.js.map +0 -1
  116. package/dist/doc/index.js.map +0 -1
  117. package/dist/generate/index.js.map +0 -1
  118. package/dist/icons/index.js.map +0 -1
  119. package/dist/icons/inlineIconMarker.js.map +0 -1
  120. package/dist/imageEdit/index.js.map +0 -1
  121. package/dist/index.js.map +0 -1
  122. package/dist/jsonForm/index.js.map +0 -1
  123. package/dist/markdown/index.js.map +0 -1
  124. package/dist/narration/index.js.map +0 -1
  125. package/dist/random/index.js.map +0 -1
  126. package/dist/recommend/index.js.map +0 -1
  127. package/dist/schemas/index.js.map +0 -1
  128. package/dist/spatial/index.js.map +0 -1
  129. package/dist/storage/index.js.map +0 -1
  130. package/dist/timing/index.js.map +0 -1
  131. package/dist/transform/index.js.map +0 -1
  132. package/dist/versions/index.js.map +0 -1
  133. package/src/__tests__/animationUtils.test.ts +0 -117
  134. package/src/__tests__/annotationCoercion.test.ts +0 -243
  135. package/src/__tests__/applyNarrationTiming.test.ts +0 -190
  136. package/src/__tests__/applyRenderStyle.test.ts +0 -146
  137. package/src/__tests__/asciiDiagramBattle.test.ts +0 -246
  138. package/src/__tests__/asciiDiagramDetect.test.ts +0 -162
  139. package/src/__tests__/asciiDiagramMapping.test.ts +0 -38
  140. package/src/__tests__/asciiDiagramParse.test.ts +0 -233
  141. package/src/__tests__/asciiDiagramPipeline.test.ts +0 -273
  142. package/src/__tests__/asciiDiagramRender.test.ts +0 -162
  143. package/src/__tests__/asciiDiagramRepair.test.ts +0 -84
  144. package/src/__tests__/asciiTimelineDetect.test.ts +0 -157
  145. package/src/__tests__/asciiTimelineParse.test.ts +0 -205
  146. package/src/__tests__/asciiTimelinePipeline.test.ts +0 -349
  147. package/src/__tests__/asciiTimelineRender.test.ts +0 -376
  148. package/src/__tests__/atmosphereLayers.test.ts +0 -108
  149. package/src/__tests__/attrTokens.test.ts +0 -167
  150. package/src/__tests__/colorUtils.test.ts +0 -112
  151. package/src/__tests__/contentContainer.test.ts +0 -325
  152. package/src/__tests__/contentExtractor.test.ts +0 -243
  153. package/src/__tests__/coverBlock.test.ts +0 -310
  154. package/src/__tests__/customTemplateSample.test.ts +0 -144
  155. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -221
  156. package/src/__tests__/customThemesFrontmatter.test.ts +0 -146
  157. package/src/__tests__/dataTable.test.ts +0 -145
  158. package/src/__tests__/diagramBlock.test.ts +0 -122
  159. package/src/__tests__/diagramLayout.test.ts +0 -134
  160. package/src/__tests__/diagramText.test.ts +0 -26
  161. package/src/__tests__/docToMarkdownMedia.test.ts +0 -83
  162. package/src/__tests__/drawing.test.ts +0 -109
  163. package/src/__tests__/drawingBlock.test.ts +0 -124
  164. package/src/__tests__/drawingLayout.test.ts +0 -171
  165. package/src/__tests__/fixtures/asciiDiagramBattle.ts +0 -512
  166. package/src/__tests__/fixtures/asciiDiagrams.ts +0 -287
  167. package/src/__tests__/fixtures/asciiTimelines.ts +0 -35
  168. package/src/__tests__/fixtures/treeviewFixtures.ts +0 -243
  169. package/src/__tests__/geohash.test.ts +0 -110
  170. package/src/__tests__/getLayers.test.ts +0 -243
  171. package/src/__tests__/getLayersFallback.test.ts +0 -103
  172. package/src/__tests__/haversine.test.ts +0 -63
  173. package/src/__tests__/iconResolve.test.ts +0 -122
  174. package/src/__tests__/imageEditExportSvg.test.ts +0 -70
  175. package/src/__tests__/imageEditState.test.ts +0 -74
  176. package/src/__tests__/imageEditVersions.test.ts +0 -243
  177. package/src/__tests__/inferDocumentTitle.test.ts +0 -50
  178. package/src/__tests__/inlineIconMarker.test.ts +0 -36
  179. package/src/__tests__/inputDescriptors.test.ts +0 -162
  180. package/src/__tests__/jsonForm.chooseControl.test.ts +0 -86
  181. package/src/__tests__/jsonForm.evaluateWhen.test.ts +0 -59
  182. package/src/__tests__/jsonForm.inferSchema.test.ts +0 -25
  183. package/src/__tests__/jsonForm.pointer.test.ts +0 -149
  184. package/src/__tests__/jsonForm.tokens.test.ts +0 -84
  185. package/src/__tests__/layoutBlock.test.ts +0 -83
  186. package/src/__tests__/layoutLayout.test.ts +0 -169
  187. package/src/__tests__/localForageAdapter.test.ts +0 -155
  188. package/src/__tests__/majorApiTypes.test.ts +0 -18
  189. package/src/__tests__/markdown.test.ts +0 -1009
  190. package/src/__tests__/markdownIcons.test.ts +0 -104
  191. package/src/__tests__/markdownSanitize.test.ts +0 -81
  192. package/src/__tests__/markdownToDoc.test.ts +0 -957
  193. package/src/__tests__/matchFontFamily.test.ts +0 -114
  194. package/src/__tests__/materializeBlockLayers.test.ts +0 -135
  195. package/src/__tests__/materializeTestUtils.ts +0 -17
  196. package/src/__tests__/mediaAnnotations.test.ts +0 -91
  197. package/src/__tests__/mediaSchedule.test.ts +0 -118
  198. package/src/__tests__/narrationAlign.test.ts +0 -150
  199. package/src/__tests__/narrationFeatures.test.ts +0 -71
  200. package/src/__tests__/narrationNuclei.test.ts +0 -45
  201. package/src/__tests__/narrationPacing.test.ts +0 -189
  202. package/src/__tests__/narrationScript.test.ts +0 -149
  203. package/src/__tests__/narrationSessionRepro.test.ts +0 -62
  204. package/src/__tests__/narrationSidecar.test.ts +0 -88
  205. package/src/__tests__/narrationSyllables.test.ts +0 -65
  206. package/src/__tests__/narrationTestSignals.ts +0 -167
  207. package/src/__tests__/narrationVad.test.ts +0 -66
  208. package/src/__tests__/pandocAttrParse.test.ts +0 -187
  209. package/src/__tests__/recommendTemplates.test.ts +0 -183
  210. package/src/__tests__/resolveDocTheme.test.ts +0 -69
  211. package/src/__tests__/rootExports.test.ts +0 -61
  212. package/src/__tests__/scopedContentContainer.test.ts +0 -109
  213. package/src/__tests__/seededRandom.test.ts +0 -142
  214. package/src/__tests__/shapeGeometry.test.ts +0 -138
  215. package/src/__tests__/shapeVocabulary.test.ts +0 -133
  216. package/src/__tests__/storage.test.ts +0 -60
  217. package/src/__tests__/structuredData.test.ts +0 -249
  218. package/src/__tests__/surface.test.ts +0 -68
  219. package/src/__tests__/templateAnnotationParse.test.ts +0 -160
  220. package/src/__tests__/templateInputs.test.ts +0 -69
  221. package/src/__tests__/templateMetadata.test.ts +0 -32
  222. package/src/__tests__/templates.test.ts +0 -710
  223. package/src/__tests__/themeCompile.test.ts +0 -225
  224. package/src/__tests__/themeValidator.test.ts +0 -131
  225. package/src/__tests__/timing.test.ts +0 -167
  226. package/src/__tests__/transformProvenance.test.ts +0 -160
  227. package/src/__tests__/transformV2.test.ts +0 -163
  228. package/src/__tests__/transitionNormalize.test.ts +0 -61
  229. package/src/__tests__/treeviewBattle.test.ts +0 -76
  230. package/src/__tests__/treeviewDetect.test.ts +0 -168
  231. package/src/__tests__/treeviewParse.test.ts +0 -118
  232. package/src/__tests__/treeviewPipeline.test.ts +0 -183
  233. package/src/__tests__/treeviewRender.test.ts +0 -91
  234. package/src/__tests__/validateDoc.test.ts +0 -260
  235. package/src/__tests__/versions.test.ts +0 -459
  236. package/src/doc/annotationBlocks.ts +0 -117
  237. package/src/doc/applyNarrationTiming.ts +0 -244
  238. package/src/doc/asciiDiagram/chars.ts +0 -210
  239. package/src/doc/asciiDiagram/detect.ts +0 -175
  240. package/src/doc/asciiDiagram/index.ts +0 -38
  241. package/src/doc/asciiDiagram/mapping.ts +0 -225
  242. package/src/doc/asciiDiagram/parse.ts +0 -1095
  243. package/src/doc/asciiDiagram/render.ts +0 -963
  244. package/src/doc/asciiDiagram/repair.ts +0 -400
  245. package/src/doc/asciiDiagram/types.ts +0 -65
  246. package/src/doc/asciiTimeline/detect.ts +0 -138
  247. package/src/doc/asciiTimeline/index.ts +0 -26
  248. package/src/doc/asciiTimeline/mapping.ts +0 -43
  249. package/src/doc/asciiTimeline/parse.ts +0 -870
  250. package/src/doc/asciiTimeline/render.ts +0 -427
  251. package/src/doc/asciiTimeline/types.ts +0 -73
  252. package/src/doc/asciiTimeline/wrappedFlow.ts +0 -427
  253. package/src/doc/audioMapping.ts +0 -442
  254. package/src/doc/customTemplatesFrontmatter.ts +0 -273
  255. package/src/doc/customThemesFrontmatter.ts +0 -95
  256. package/src/doc/docToMarkdown.ts +0 -284
  257. package/src/doc/index.ts +0 -127
  258. package/src/doc/markdownToDoc.ts +0 -1054
  259. package/src/doc/materializeBlockLayers.ts +0 -304
  260. package/src/doc/mediaAnnotations.ts +0 -120
  261. package/src/doc/resolveDocTheme.ts +0 -56
  262. package/src/doc/standaloneAnnotation.ts +0 -58
  263. package/src/doc/structuredData.ts +0 -301
  264. package/src/doc/templateInputs.ts +0 -423
  265. package/src/doc/templates/__tests__/customTemplate.test.ts +0 -170
  266. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +0 -124
  267. package/src/doc/templates/__tests__/listBlock.test.ts +0 -64
  268. package/src/doc/templates/accentImage.ts +0 -362
  269. package/src/doc/templates/captionUtils.ts +0 -56
  270. package/src/doc/templates/comparisonBar.ts +0 -173
  271. package/src/doc/templates/coverBlock.ts +0 -255
  272. package/src/doc/templates/customTemplate.ts +0 -51
  273. package/src/doc/templates/dataTable.ts +0 -102
  274. package/src/doc/templates/dateEvent.ts +0 -147
  275. package/src/doc/templates/definitionCard.ts +0 -140
  276. package/src/doc/templates/diagramBlock.ts +0 -403
  277. package/src/doc/templates/diagramLayout.ts +0 -159
  278. package/src/doc/templates/drawingBlock.ts +0 -325
  279. package/src/doc/templates/drawingLayout.ts +0 -539
  280. package/src/doc/templates/factCard.ts +0 -146
  281. package/src/doc/templates/fallbackBlock.ts +0 -123
  282. package/src/doc/templates/featureBlock.ts +0 -209
  283. package/src/doc/templates/fullBleedQuote.ts +0 -83
  284. package/src/doc/templates/imageWithCaption.ts +0 -186
  285. package/src/doc/templates/index.ts +0 -642
  286. package/src/doc/templates/inputDescriptors.ts +0 -451
  287. package/src/doc/templates/layoutBlock.ts +0 -43
  288. package/src/doc/templates/layoutLayout.ts +0 -243
  289. package/src/doc/templates/listBlock.ts +0 -207
  290. package/src/doc/templates/mapBlock.ts +0 -138
  291. package/src/doc/templates/metadata.ts +0 -130
  292. package/src/doc/templates/persistentLayers.ts +0 -526
  293. package/src/doc/templates/photoGrid.ts +0 -190
  294. package/src/doc/templates/pullQuote.ts +0 -141
  295. package/src/doc/templates/quoteBlock.ts +0 -141
  296. package/src/doc/templates/registry.ts +0 -88
  297. package/src/doc/templates/sectionHeader.ts +0 -120
  298. package/src/doc/templates/statHighlight.ts +0 -146
  299. package/src/doc/templates/templateNames.ts +0 -22
  300. package/src/doc/templates/timelineBlock.ts +0 -613
  301. package/src/doc/templates/titleBlock.ts +0 -150
  302. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +0 -487
  303. package/src/doc/templates/tokens/resolveTokens.ts +0 -453
  304. package/src/doc/templates/treeBlock.ts +0 -131
  305. package/src/doc/templates/twoColumn.ts +0 -263
  306. package/src/doc/templates/videoPullQuote.ts +0 -142
  307. package/src/doc/templates/videoWithCaption.ts +0 -135
  308. package/src/doc/treeview/chars.ts +0 -72
  309. package/src/doc/treeview/detect.ts +0 -145
  310. package/src/doc/treeview/index.ts +0 -30
  311. package/src/doc/treeview/mapping.ts +0 -95
  312. package/src/doc/treeview/parse.ts +0 -201
  313. package/src/doc/treeview/render.ts +0 -41
  314. package/src/doc/treeview/types.ts +0 -43
  315. package/src/doc/utils/animationUtils.ts +0 -399
  316. package/src/doc/utils/applyRenderStyle.ts +0 -106
  317. package/src/doc/utils/diagramText.ts +0 -78
  318. package/src/doc/utils/imageTreatment.ts +0 -70
  319. package/src/doc/utils/nearest.ts +0 -59
  320. package/src/doc/utils/shapeGeometry.ts +0 -614
  321. package/src/doc/utils/themeUtils.ts +0 -243
  322. package/src/doc/validate.ts +0 -575
  323. package/src/generate/contentExtractor.ts +0 -921
  324. package/src/generate/index.ts +0 -18
  325. package/src/generate/templateMapper.ts +0 -177
  326. package/src/icons/iconData.ts +0 -31
  327. package/src/icons/index.ts +0 -26
  328. package/src/icons/inlineIconMarker.ts +0 -73
  329. package/src/icons/resolve.ts +0 -139
  330. package/src/imageEdit/ImageEditVersionManager.ts +0 -73
  331. package/src/imageEdit/export.ts +0 -410
  332. package/src/imageEdit/index.ts +0 -72
  333. package/src/imageEdit/persistence.ts +0 -113
  334. package/src/imageEdit/state.ts +0 -112
  335. package/src/imageEdit/versionPaths.ts +0 -44
  336. package/src/imageEdit/versions.ts +0 -152
  337. package/src/index.ts +0 -15
  338. package/src/internal/immutable.ts +0 -58
  339. package/src/jsonForm/chooseControl.ts +0 -88
  340. package/src/jsonForm/evaluateWhen.ts +0 -59
  341. package/src/jsonForm/index.ts +0 -32
  342. package/src/jsonForm/inferSchema.ts +0 -25
  343. package/src/jsonForm/pointer.ts +0 -154
  344. package/src/jsonForm/tokens.ts +0 -78
  345. package/src/jsonForm/types.ts +0 -167
  346. package/src/markdown/annotationCoercion.ts +0 -295
  347. package/src/markdown/attrTokens.ts +0 -243
  348. package/src/markdown/convert.ts +0 -1353
  349. package/src/markdown/htmlParse.ts +0 -212
  350. package/src/markdown/index.ts +0 -152
  351. package/src/markdown/parse.ts +0 -103
  352. package/src/markdown/sanitize.ts +0 -318
  353. package/src/markdown/stringify.ts +0 -207
  354. package/src/markdown/types.ts +0 -710
  355. package/src/markdown/utils.ts +0 -459
  356. package/src/narration/align.ts +0 -514
  357. package/src/narration/features.ts +0 -231
  358. package/src/narration/index.ts +0 -86
  359. package/src/narration/nuclei.ts +0 -118
  360. package/src/narration/pacing.ts +0 -211
  361. package/src/narration/script.ts +0 -239
  362. package/src/narration/session.ts +0 -100
  363. package/src/narration/sidecar.ts +0 -187
  364. package/src/narration/syllables.ts +0 -85
  365. package/src/narration/trace.ts +0 -50
  366. package/src/narration/types.ts +0 -299
  367. package/src/narration/vad.ts +0 -131
  368. package/src/ngeohash.d.ts +0 -11
  369. package/src/random/SeededRandom.ts +0 -127
  370. package/src/random/index.ts +0 -1
  371. package/src/recommend/index.ts +0 -16
  372. package/src/recommend/numberHighlight.ts +0 -25
  373. package/src/recommend/templates.ts +0 -393
  374. package/src/schemas/BlockTemplates.ts +0 -1046
  375. package/src/schemas/CustomTemplates.ts +0 -126
  376. package/src/schemas/Doc.ts +0 -1052
  377. package/src/schemas/ImageEditDoc.ts +0 -102
  378. package/src/schemas/LayoutStrategy.ts +0 -178
  379. package/src/schemas/Media.ts +0 -179
  380. package/src/schemas/MediaProvider.ts +0 -70
  381. package/src/schemas/Theme.ts +0 -406
  382. package/src/schemas/Transitions.ts +0 -224
  383. package/src/schemas/Types.ts +0 -15
  384. package/src/schemas/Viewport.ts +0 -91
  385. package/src/schemas/colorUtils.ts +0 -219
  386. package/src/schemas/fontStacks.ts +0 -339
  387. package/src/schemas/index.ts +0 -29
  388. package/src/schemas/themeCompile.ts +0 -237
  389. package/src/schemas/themeConstants.ts +0 -2
  390. package/src/schemas/themeLibrary.ts +0 -60
  391. package/src/schemas/themeValidator.ts +0 -426
  392. package/src/schemas/themes/bold.json +0 -48
  393. package/src/schemas/themes/cinematic.json +0 -111
  394. package/src/schemas/themes/documentary.json +0 -96
  395. package/src/schemas/themes/gezellig.json +0 -93
  396. package/src/schemas/themes/index.ts +0 -53
  397. package/src/schemas/themes/magazine.json +0 -89
  398. package/src/schemas/themes/minimalist.json +0 -45
  399. package/src/schemas/themes/morning-light.json +0 -85
  400. package/src/schemas/themes/standard-dark.json +0 -85
  401. package/src/schemas/themes/standard.json +0 -74
  402. package/src/schemas/themes/tech-dark.json +0 -101
  403. package/src/schemas/themes/warm-earth.json +0 -50
  404. package/src/spatial/Geohash.ts +0 -196
  405. package/src/spatial/Haversine.ts +0 -59
  406. package/src/spatial/index.ts +0 -2
  407. package/src/storage/ContentContainer.ts +0 -220
  408. package/src/storage/LocalForageAdapter.ts +0 -103
  409. package/src/storage/LocalStorageAdapter.ts +0 -57
  410. package/src/storage/MediaProviderFromContainer.ts +0 -92
  411. package/src/storage/MemoryStorageAdapter.ts +0 -37
  412. package/src/storage/ScopedContentContainer.ts +0 -143
  413. package/src/storage/Storage.ts +0 -28
  414. package/src/storage/index.ts +0 -9
  415. package/src/timing/index.ts +0 -17
  416. package/src/timing/narrationTiming.ts +0 -220
  417. package/src/timing/readingTime.ts +0 -94
  418. package/src/transform/applyTransform.ts +0 -122
  419. package/src/transform/blockAnalyzer.ts +0 -181
  420. package/src/transform/index.ts +0 -23
  421. package/src/transform/registry.ts +0 -198
  422. package/src/transform/styles/dataDriven.ts +0 -17
  423. package/src/transform/styles/documentary.ts +0 -17
  424. package/src/transform/styles/index.ts +0 -5
  425. package/src/transform/styles/magazine.ts +0 -18
  426. package/src/transform/styles/minimal.ts +0 -17
  427. package/src/transform/styles/narrative.ts +0 -18
  428. package/src/transform/templateSelector.ts +0 -389
  429. package/src/transform/timingAllocator.ts +0 -257
  430. package/src/transform/types.ts +0 -151
  431. package/src/versions/DocumentVersionManager.ts +0 -70
  432. package/src/versions/gitignore.ts +0 -34
  433. package/src/versions/index.ts +0 -56
  434. package/src/versions/operations.ts +0 -144
  435. package/src/versions/paths.ts +0 -51
  436. package/src/versions/snapshotStore.ts +0 -202
  437. package/src/versions/timestamp.ts +0 -48
  438. 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
- }