@zombie-mermaid/ascii-renderer 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/LICENSE +22 -0
  2. package/dist/index.cjs +8 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +146 -0
  5. package/dist/index.d.ts +146 -0
  6. package/dist/index.js +5392 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +36 -0
  9. package/src/__tests__/ascii-arrowhead-direction-1083.test.ts +51 -0
  10. package/src/__tests__/ascii-canvas-first-claim-wins-1093.test.ts +89 -0
  11. package/src/__tests__/ascii-canvas-size-offset-1093.test.ts +95 -0
  12. package/src/__tests__/ascii-canvas-write.test.ts +132 -0
  13. package/src/__tests__/ascii-chain-edge-overlap-1067.test.ts +100 -0
  14. package/src/__tests__/ascii-charset-border-junctions.test.ts +63 -0
  15. package/src/__tests__/ascii-cjk-width.test.ts +150 -0
  16. package/src/__tests__/ascii-class-box-occupancy.test.ts +462 -0
  17. package/src/__tests__/ascii-class-column-width-488-489.test.ts +639 -0
  18. package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
  19. package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +114 -0
  20. package/src/__tests__/ascii-class-diagram-compartments.test.ts +70 -0
  21. package/src/__tests__/ascii-class-label-box-collision.test.ts +60 -0
  22. package/src/__tests__/ascii-class-label-row-collision-531.test.ts +135 -0
  23. package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +57 -0
  24. package/src/__tests__/ascii-class-padding.test.ts +104 -0
  25. package/src/__tests__/ascii-class-parent-alignment-971.test.ts +139 -0
  26. package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
  27. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +169 -0
  28. package/src/__tests__/ascii-combining-mark-width.test.ts +70 -0
  29. package/src/__tests__/ascii-coords-overlay.test.ts +64 -0
  30. package/src/__tests__/ascii-decision-lr-box-start.test.ts +106 -0
  31. package/src/__tests__/ascii-display-width-unit.test.ts +154 -0
  32. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +224 -0
  33. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +105 -0
  34. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +72 -0
  35. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +206 -0
  36. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +95 -0
  37. package/src/__tests__/ascii-edge-routing-fixes.test.ts +178 -0
  38. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +103 -0
  39. package/src/__tests__/ascii-edge-style-consistency-1067.test.ts +89 -0
  40. package/src/__tests__/ascii-edge-styles.test.ts +149 -0
  41. package/src/__tests__/ascii-emoji-cluster-width.test.ts +101 -0
  42. package/src/__tests__/ascii-er-box-occupancy.test.ts +251 -0
  43. package/src/__tests__/ascii-er-cardinality.test.ts +40 -0
  44. package/src/__tests__/ascii-er-corner-glyphs.test.ts +249 -0
  45. package/src/__tests__/ascii-er-jog-stray-line.test.ts +111 -0
  46. package/src/__tests__/ascii-er-label-padding.test.ts +118 -0
  47. package/src/__tests__/ascii-er-padding.test.ts +114 -0
  48. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +458 -0
  49. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +325 -0
  50. package/src/__tests__/ascii-er-stray-connectors.test.ts +344 -0
  51. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +98 -0
  52. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +110 -0
  53. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +168 -0
  54. package/src/__tests__/ascii-lane-search.test.ts +207 -0
  55. package/src/__tests__/ascii-multibox-cjk-width.test.ts +184 -0
  56. package/src/__tests__/ascii-multiline.test.ts +288 -0
  57. package/src/__tests__/ascii-padding-edge-cases.test.ts +154 -0
  58. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +184 -0
  59. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +236 -0
  60. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +217 -0
  61. package/src/__tests__/ascii-sequence-box-group.test.ts +159 -0
  62. package/src/__tests__/ascii-sequence-cjk-width.test.ts +235 -0
  63. package/src/__tests__/ascii-sequence-create-destroy.test.ts +114 -0
  64. package/src/__tests__/ascii-sequence-form-invariants.test.ts +432 -0
  65. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +219 -0
  66. package/src/__tests__/ascii-sequence-notes.test.ts +61 -0
  67. package/src/__tests__/ascii-sequence-padding.test.ts +119 -0
  68. package/src/__tests__/ascii-sequence-self-arrow.test.ts +206 -0
  69. package/src/__tests__/ascii-shape-diamond.test.ts +38 -0
  70. package/src/__tests__/ascii-shape-rectangle.test.ts +258 -0
  71. package/src/__tests__/ascii-shape-rounded.test.ts +36 -0
  72. package/src/__tests__/ascii-shapes-circle.test.ts +41 -0
  73. package/src/__tests__/ascii-shapes-hexagon.test.ts +42 -0
  74. package/src/__tests__/ascii-shapes-special.test.ts +344 -0
  75. package/src/__tests__/ascii-shapes-stadium.test.ts +217 -0
  76. package/src/__tests__/ascii-shapes-state.test.ts +224 -0
  77. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +130 -0
  78. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +90 -0
  79. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +152 -0
  80. package/src/__tests__/ascii-subgraph-title-padding.test.ts +77 -0
  81. package/src/__tests__/ascii-territory-unit.test.ts +219 -0
  82. package/src/__tests__/ascii-validate.test.ts +220 -0
  83. package/src/__tests__/ascii.test.ts +325 -0
  84. package/src/__tests__/class-arrow-directions.test.ts +505 -0
  85. package/src/__tests__/draw-lines.test.ts +93 -0
  86. package/src/__tests__/edge-cell-styles.test.ts +278 -0
  87. package/src/__tests__/grid-occupancy.test.ts +240 -0
  88. package/src/__tests__/helpers/ascii-form.ts +142 -0
  89. package/src/__tests__/helpers/terminal-display-width.ts +74 -0
  90. package/src/__tests__/pathfinder.test.ts +239 -0
  91. package/src/__tests__/testdata/ascii/ampersand_lhs.txt +18 -0
  92. package/src/__tests__/testdata/ascii/ampersand_lhs_and_rhs.txt +18 -0
  93. package/src/__tests__/testdata/ascii/ampersand_rhs.txt +18 -0
  94. package/src/__tests__/testdata/ascii/ampersand_td_fanin.txt +18 -0
  95. package/src/__tests__/testdata/ascii/ampersand_td_fanout.txt +18 -0
  96. package/src/__tests__/testdata/ascii/ampersand_without_edge.txt +18 -0
  97. package/src/__tests__/testdata/ascii/back_reference_from_child.txt +10 -0
  98. package/src/__tests__/testdata/ascii/backlink_from_bottom.txt +22 -0
  99. package/src/__tests__/testdata/ascii/backlink_from_top.txt +22 -0
  100. package/src/__tests__/testdata/ascii/backlink_with_short_y_padding.txt +20 -0
  101. package/src/__tests__/testdata/ascii/cls_all_relationships.txt +19 -0
  102. package/src/__tests__/testdata/ascii/cls_annotation.txt +29 -0
  103. package/src/__tests__/testdata/ascii/cls_association.txt +14 -0
  104. package/src/__tests__/testdata/ascii/cls_basic.txt +15 -0
  105. package/src/__tests__/testdata/ascii/cls_dependency.txt +14 -0
  106. package/src/__tests__/testdata/ascii/cls_inheritance.txt +20 -0
  107. package/src/__tests__/testdata/ascii/cls_methods.txt +21 -0
  108. package/src/__tests__/testdata/ascii/comments.txt +23 -0
  109. package/src/__tests__/testdata/ascii/custom_padding.txt +10 -0
  110. package/src/__tests__/testdata/ascii/duplicate_labels.txt +19 -0
  111. package/src/__tests__/testdata/ascii/er_attributes.txt +21 -0
  112. package/src/__tests__/testdata/ascii/er_basic.txt +8 -0
  113. package/src/__tests__/testdata/ascii/er_identifying.txt +18 -0
  114. package/src/__tests__/testdata/ascii/flowchart_tb_simple.txt +29 -0
  115. package/src/__tests__/testdata/ascii/graph_bt_direction.txt +28 -0
  116. package/src/__tests__/testdata/ascii/graph_tb_direction.txt +26 -0
  117. package/src/__tests__/testdata/ascii/nested_subgraphs_with_labels.txt +36 -0
  118. package/src/__tests__/testdata/ascii/preserve_order_of_definition.txt +23 -0
  119. package/src/__tests__/testdata/ascii/self_reference.txt +10 -0
  120. package/src/__tests__/testdata/ascii/self_reference_with_edge.txt +10 -0
  121. package/src/__tests__/testdata/ascii/seq_basic.txt +17 -0
  122. package/src/__tests__/testdata/ascii/seq_multiple_messages.txt +25 -0
  123. package/src/__tests__/testdata/ascii/seq_self_message.txt +18 -0
  124. package/src/__tests__/testdata/ascii/single_node.txt +8 -0
  125. package/src/__tests__/testdata/ascii/single_node_longer_name.txt +8 -0
  126. package/src/__tests__/testdata/ascii/subgraph_complex_mixed.txt +38 -0
  127. package/src/__tests__/testdata/ascii/subgraph_complex_nested.txt +49 -0
  128. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +47 -0
  129. package/src/__tests__/testdata/ascii/subgraph_empty.txt +10 -0
  130. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes.txt +20 -0
  131. package/src/__tests__/testdata/ascii/subgraph_mixed_nodes_td.txt +48 -0
  132. package/src/__tests__/testdata/ascii/subgraph_multiple_edges.txt +32 -0
  133. package/src/__tests__/testdata/ascii/subgraph_multiple_nodes.txt +16 -0
  134. package/src/__tests__/testdata/ascii/subgraph_nested.txt +24 -0
  135. package/src/__tests__/testdata/ascii/subgraph_nested_with_external.txt +30 -0
  136. package/src/__tests__/testdata/ascii/subgraph_node_outside_lr.txt +17 -0
  137. package/src/__tests__/testdata/ascii/subgraph_single_node.txt +16 -0
  138. package/src/__tests__/testdata/ascii/subgraph_td_direction.txt +26 -0
  139. package/src/__tests__/testdata/ascii/subgraph_td_multiple.txt +44 -0
  140. package/src/__tests__/testdata/ascii/subgraph_td_multiple_paddingy.txt +42 -0
  141. package/src/__tests__/testdata/ascii/subgraph_three_levels_nested.txt +32 -0
  142. package/src/__tests__/testdata/ascii/subgraph_three_separate.txt +24 -0
  143. package/src/__tests__/testdata/ascii/subgraph_two_separate.txt +20 -0
  144. package/src/__tests__/testdata/ascii/subgraph_with_labels.txt +20 -0
  145. package/src/__tests__/testdata/ascii/three_nodes.txt +9 -0
  146. package/src/__tests__/testdata/ascii/three_nodes_single_line.txt +8 -0
  147. package/src/__tests__/testdata/ascii/two_layer_single_graph.txt +19 -0
  148. package/src/__tests__/testdata/ascii/two_layer_single_graph_longer_names.txt +19 -0
  149. package/src/__tests__/testdata/ascii/two_nodes_linked.txt +8 -0
  150. package/src/__tests__/testdata/ascii/two_nodes_longer_names.txt +8 -0
  151. package/src/__tests__/testdata/ascii/two_root_nodes.txt +19 -0
  152. package/src/__tests__/testdata/ascii/two_root_nodes_longer_names.txt +19 -0
  153. package/src/__tests__/testdata/ascii/two_single_root_nodes.txt +19 -0
  154. package/src/__tests__/testdata/unicode/ampersand_lhs.txt +18 -0
  155. package/src/__tests__/testdata/unicode/ampersand_lhs_and_rhs.txt +18 -0
  156. package/src/__tests__/testdata/unicode/ampersand_rhs.txt +18 -0
  157. package/src/__tests__/testdata/unicode/ampersand_without_edge.txt +18 -0
  158. package/src/__tests__/testdata/unicode/back_reference_from_child.txt +10 -0
  159. package/src/__tests__/testdata/unicode/backlink_from_bottom.txt +22 -0
  160. package/src/__tests__/testdata/unicode/backlink_from_top.txt +22 -0
  161. package/src/__tests__/testdata/unicode/cls_all_relationships.txt +19 -0
  162. package/src/__tests__/testdata/unicode/cls_annotation.txt +29 -0
  163. package/src/__tests__/testdata/unicode/cls_association.txt +14 -0
  164. package/src/__tests__/testdata/unicode/cls_basic.txt +15 -0
  165. package/src/__tests__/testdata/unicode/cls_dependency.txt +14 -0
  166. package/src/__tests__/testdata/unicode/cls_inheritance.txt +20 -0
  167. package/src/__tests__/testdata/unicode/cls_methods.txt +21 -0
  168. package/src/__tests__/testdata/unicode/comments.txt +23 -0
  169. package/src/__tests__/testdata/unicode/duplicate_labels.txt +19 -0
  170. package/src/__tests__/testdata/unicode/er_attributes.txt +21 -0
  171. package/src/__tests__/testdata/unicode/er_basic.txt +8 -0
  172. package/src/__tests__/testdata/unicode/er_identifying.txt +18 -0
  173. package/src/__tests__/testdata/unicode/graph_bt_direction.txt +28 -0
  174. package/src/__tests__/testdata/unicode/preserve_order_of_definition.txt +23 -0
  175. package/src/__tests__/testdata/unicode/self_reference.txt +10 -0
  176. package/src/__tests__/testdata/unicode/self_reference_with_edge.txt +10 -0
  177. package/src/__tests__/testdata/unicode/seq_basic.txt +17 -0
  178. package/src/__tests__/testdata/unicode/seq_multiple_messages.txt +25 -0
  179. package/src/__tests__/testdata/unicode/seq_self_message.txt +18 -0
  180. package/src/__tests__/testdata/unicode/single_node.txt +8 -0
  181. package/src/__tests__/testdata/unicode/single_node_longer_name.txt +8 -0
  182. package/src/__tests__/testdata/unicode/three_nodes.txt +9 -0
  183. package/src/__tests__/testdata/unicode/three_nodes_single_line.txt +8 -0
  184. package/src/__tests__/testdata/unicode/two_layer_single_graph.txt +19 -0
  185. package/src/__tests__/testdata/unicode/two_layer_single_graph_longer_names.txt +19 -0
  186. package/src/__tests__/testdata/unicode/two_nodes_linked.txt +8 -0
  187. package/src/__tests__/testdata/unicode/two_nodes_longer_names.txt +8 -0
  188. package/src/__tests__/testdata/unicode/two_root_nodes.txt +19 -0
  189. package/src/__tests__/testdata/unicode/two_root_nodes_longer_names.txt +19 -0
  190. package/src/__tests__/testdata/unicode/two_single_root_nodes.txt +19 -0
  191. package/src/__tests__/xychart-ascii.test.ts +376 -0
  192. package/src/ansi.ts +490 -0
  193. package/src/canvas.ts +757 -0
  194. package/src/class-diagram.ts +2001 -0
  195. package/src/converter.ts +446 -0
  196. package/src/coords.ts +58 -0
  197. package/src/display-width.ts +151 -0
  198. package/src/draw-arrows.ts +593 -0
  199. package/src/draw-boxes.ts +267 -0
  200. package/src/draw-bundles.ts +611 -0
  201. package/src/draw-lines.ts +174 -0
  202. package/src/draw-subgraphs.ts +108 -0
  203. package/src/draw.ts +350 -0
  204. package/src/edge-bundling.ts +435 -0
  205. package/src/edge-cell-styles.ts +209 -0
  206. package/src/edge-routing.ts +1070 -0
  207. package/src/er-diagram.ts +1488 -0
  208. package/src/flowchart.ts +94 -0
  209. package/src/grid-occupancy.ts +234 -0
  210. package/src/grid.ts +1309 -0
  211. package/src/hyperlinks.ts +248 -0
  212. package/src/index.ts +163 -0
  213. package/src/lane-search.ts +68 -0
  214. package/src/multiline-utils.ts +82 -0
  215. package/src/pathfinder.ts +448 -0
  216. package/src/registry.ts +88 -0
  217. package/src/sequence.ts +1318 -0
  218. package/src/shapes/circle.ts +31 -0
  219. package/src/shapes/corners.ts +273 -0
  220. package/src/shapes/diamond.ts +31 -0
  221. package/src/shapes/hexagon.ts +35 -0
  222. package/src/shapes/index.ts +123 -0
  223. package/src/shapes/rectangle.ts +199 -0
  224. package/src/shapes/rounded.ts +31 -0
  225. package/src/shapes/special.ts +360 -0
  226. package/src/shapes/stadium.ts +122 -0
  227. package/src/shapes/state.ts +204 -0
  228. package/src/shapes/types.ts +78 -0
  229. package/src/territory.ts +136 -0
  230. package/src/types.ts +454 -0
  231. package/src/validate.ts +189 -0
  232. package/src/xychart.ts +1085 -0
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Independently re-implemented terminal display-width calculation, shared by
3
+ * the ASCII wide-character regression suites (issues #66, #182, #205, #214).
4
+ *
5
+ * Deliberately does NOT import from `src/ascii/display-width.ts` or
6
+ * `packages/core/src/text-metrics.ts`: these suites verify rendered output against an
7
+ * authority separate from the implementation under test, so that a bug in the
8
+ * renderer's own width helper cannot make its tests pass by agreeing with
9
+ * itself. Mirrors the East Asian Width ranges a real monospace terminal uses
10
+ * to decide whether a code point renders as 1 or 2 columns.
11
+ *
12
+ * Measures per grapheme cluster (`Intl.Segmenter`), not per code point: a
13
+ * real terminal gives zero columns to a combining mark (it attaches to the
14
+ * preceding base character — issue #205) and exactly one glyph's worth of
15
+ * columns to a composed multi-code-point sequence like a ZWJ emoji, flag, or
16
+ * skin-tone modifier (issue #214), not one glyph's worth per code point.
17
+ */
18
+ import { expect } from 'vitest'
19
+
20
+ const EMOJI_PATTERN = /\p{Extended_Pictographic}|\p{Emoji_Presentation}/u
21
+ /** Unicode general categories Mn (Nonspacing_Mark) and Me (Enclosing_Mark). */
22
+ const COMBINING_MARK_PATTERN = /\p{Mn}|\p{Me}/u
23
+
24
+ const graphemeSegmenter = new Intl.Segmenter(undefined, {
25
+ granularity: 'grapheme',
26
+ })
27
+
28
+ function isWideCodePoint(ch: string): boolean {
29
+ const code = ch.codePointAt(0)!
30
+ return (
31
+ (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
32
+ (code >= 0x2e80 && code <= 0xa4cf) || // CJK radicals .. Yi
33
+ (code >= 0xac00 && code <= 0xd7a3) || // Hangul syllables
34
+ (code >= 0xf900 && code <= 0xfaff) || // CJK compatibility ideographs
35
+ (code >= 0xff00 && code <= 0xff60) || // Fullwidth forms
36
+ (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth signs
37
+ code >= 0x20000 || // CJK extension B+
38
+ EMOJI_PATTERN.test(ch)
39
+ )
40
+ }
41
+
42
+ /** Number of terminal columns one grapheme cluster occupies: 0, 1, or 2. */
43
+ function clusterWidth(cluster: string): number {
44
+ let sawWide = false
45
+ let sawNonMark = false
46
+ for (const ch of cluster) {
47
+ if (isWideCodePoint(ch)) sawWide = true
48
+ if (!COMBINING_MARK_PATTERN.test(ch)) sawNonMark = true
49
+ }
50
+ if (sawWide) return 2
51
+ return sawNonMark ? 1 : 0
52
+ }
53
+
54
+ /** Number of terminal columns `text` occupies. */
55
+ export function terminalDisplayWidth(text: string): number {
56
+ let width = 0
57
+ for (const { segment } of graphemeSegmenter.segment(text)) {
58
+ width += clusterWidth(segment)
59
+ }
60
+ return width
61
+ }
62
+
63
+ /** All rendered lines should occupy the same number of terminal columns. */
64
+ export function assertUniformDisplayWidth(ascii: string): void {
65
+ const lines = ascii.split('\n')
66
+ const widths = lines.map(terminalDisplayWidth)
67
+ const expected = widths[0]
68
+ for (let i = 0; i < lines.length; i++) {
69
+ expect(
70
+ widths[i],
71
+ `line ${i} (${JSON.stringify(lines[i])}) has display width ${widths[i]}, expected ${expected}`,
72
+ ).toBe(expected)
73
+ }
74
+ }
@@ -0,0 +1,239 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ getPath,
4
+ heuristic,
5
+ mergePath,
6
+ createPathBudget,
7
+ DEFAULT_PATH_BUDGET,
8
+ } from '../pathfinder.ts'
9
+ import type { GridCoord, PathBudget } from '../types.ts'
10
+ import { createGrid, placeBlock, type Grid } from '../grid-occupancy.ts'
11
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
12
+
13
+ /**
14
+ * Helper to build an occupied grid from a list of coordinates, going through
15
+ * the real `Grid` abstraction rather than hand-rolling a raw Map/Set — a
16
+ * size-1 "block" reserves exactly the one requested cell.
17
+ */
18
+ function buildGrid(occupied: GridCoord[]): Grid {
19
+ const grid = createGrid()
20
+ for (const c of occupied) {
21
+ placeBlock(grid, c, 1)
22
+ }
23
+ return grid
24
+ }
25
+
26
+ describe('pathfinder', () => {
27
+ describe('getPath', () => {
28
+ it('finds a straight-line path on an empty grid', () => {
29
+ const grid = buildGrid([])
30
+ const path = getPath(grid, { x: 0, y: 0 }, { x: 0, y: 3 })
31
+ expect(path).not.toBeNull()
32
+ expect(path![0]).toEqual({ x: 0, y: 0 })
33
+ expect(path![path!.length - 1]).toEqual({ x: 0, y: 3 })
34
+ })
35
+
36
+ it('routes around an obstacle', () => {
37
+ // Block the straight path at (0,1)
38
+ const grid = buildGrid([{ x: 0, y: 1 }])
39
+ const path = getPath(grid, { x: 0, y: 0 }, { x: 0, y: 2 })
40
+ expect(path).not.toBeNull()
41
+ expect(path![0]).toEqual({ x: 0, y: 0 })
42
+ expect(path![path!.length - 1]).toEqual({ x: 0, y: 2 })
43
+ // Path must detour (length > 3)
44
+ expect(path!.length).toBeGreaterThan(3)
45
+ })
46
+
47
+ it('returns null when destination is completely enclosed', () => {
48
+ // Surround (2,2) with occupied cells on all 4 sides
49
+ const grid = buildGrid([
50
+ { x: 1, y: 2 },
51
+ { x: 3, y: 2 },
52
+ { x: 2, y: 1 },
53
+ { x: 2, y: 3 },
54
+ ])
55
+ const path = getPath(grid, { x: 0, y: 0 }, { x: 2, y: 2 })
56
+ // Destination is occupied-surrounded; A* should return null within iteration limit
57
+ // (not throw RangeError: Map maximum size exceeded)
58
+ expect(path).toBeNull()
59
+ })
60
+
61
+ it('returns null for unreachable destination without exhausting memory', () => {
62
+ // Create a wall that blocks access to the target region
63
+ const occupied: GridCoord[] = []
64
+ for (let i = 0; i < 100; i++) {
65
+ occupied.push({ x: 5, y: i }) // vertical wall at x=5
66
+ }
67
+ const grid = buildGrid(occupied)
68
+ // Target is behind the wall; only reachable via y<0 (disallowed) or y>=100 (very far)
69
+ getPath(grid, { x: 0, y: 50 }, { x: 10, y: 50 })
70
+ // Should find a path going around the wall (via y>=100), or return null if
71
+ // the iteration limit is hit. Either outcome is acceptable; the key is no
72
+ // crash / unbounded memory growth.
73
+ expect(true).toBe(true)
74
+ })
75
+ })
76
+
77
+ describe('heuristic', () => {
78
+ it('returns 0 for same point', () => {
79
+ expect(heuristic({ x: 3, y: 5 }, { x: 3, y: 5 })).toBe(0)
80
+ })
81
+
82
+ it('returns manhattan distance for axis-aligned points', () => {
83
+ expect(heuristic({ x: 0, y: 0 }, { x: 0, y: 5 })).toBe(5)
84
+ expect(heuristic({ x: 0, y: 0 }, { x: 3, y: 0 })).toBe(3)
85
+ })
86
+
87
+ it('adds +1 penalty for diagonal displacement', () => {
88
+ expect(heuristic({ x: 0, y: 0 }, { x: 2, y: 3 })).toBe(6) // 2+3+1
89
+ })
90
+ })
91
+
92
+ describe('mergePath', () => {
93
+ it('removes collinear intermediate points', () => {
94
+ const path: GridCoord[] = [
95
+ { x: 0, y: 0 },
96
+ { x: 1, y: 0 },
97
+ { x: 2, y: 0 },
98
+ { x: 2, y: 1 },
99
+ ]
100
+ const merged = mergePath(path)
101
+ expect(merged).toEqual([
102
+ { x: 0, y: 0 },
103
+ { x: 2, y: 0 },
104
+ { x: 2, y: 1 },
105
+ ])
106
+ })
107
+
108
+ it('returns short paths unchanged', () => {
109
+ expect(mergePath([{ x: 0, y: 0 }])).toEqual([{ x: 0, y: 0 }])
110
+ expect(
111
+ mergePath([
112
+ { x: 0, y: 0 },
113
+ { x: 1, y: 0 },
114
+ ]),
115
+ ).toEqual([
116
+ { x: 0, y: 0 },
117
+ { x: 1, y: 0 },
118
+ ])
119
+ })
120
+ })
121
+
122
+ describe('PathBudget (render-wide iteration budget)', () => {
123
+ // These test the deterministic mechanism that actually bounds total
124
+ // pathfinding work for a render — not wall-clock time. See the
125
+ // ascii-edge-routing-fixes.test.ts "much larger dense fan-in" test
126
+ // (PR #143) for the wall-clock version of this same regression guard,
127
+ // which flaked under CPU contention because it measures how long the
128
+ // work took rather than how much work happened. Asserting on
129
+ // budget.remaining instead is immune to machine speed/load entirely.
130
+
131
+ it('createPathBudget defaults to DEFAULT_PATH_BUDGET', () => {
132
+ expect(createPathBudget()).toEqual({ remaining: DEFAULT_PATH_BUDGET })
133
+ })
134
+
135
+ it('createPathBudget accepts a custom total', () => {
136
+ expect(createPathBudget(500)).toEqual({ remaining: 500 })
137
+ })
138
+
139
+ it('decrements a shared budget cumulatively across multiple getPath calls', () => {
140
+ const grid = buildGrid([])
141
+ const budget = createPathBudget(1000)
142
+ const initial = budget.remaining
143
+
144
+ getPath(grid, { x: 0, y: 0 }, { x: 0, y: 20 }, budget)
145
+ const afterFirst = budget.remaining
146
+ expect(afterFirst).toBeLessThan(initial)
147
+
148
+ getPath(grid, { x: 0, y: 0 }, { x: 0, y: 20 }, budget)
149
+ const afterSecond = budget.remaining
150
+ // Same search run twice costs the same iterations each time, and the
151
+ // budget is cumulative across calls, not reset per call — so the
152
+ // second call must spend down further from where the first left off.
153
+ expect(afterSecond).toBeLessThan(afterFirst)
154
+ })
155
+
156
+ it('returns null immediately once the budget is exhausted, without spending further iterations', () => {
157
+ const grid = buildGrid([])
158
+ const budget: PathBudget = { remaining: 0 }
159
+ const result = getPath(grid, { x: 0, y: 0 }, { x: 0, y: 5 }, budget)
160
+ expect(result).toBeNull()
161
+ // The exhausted-budget check is the first thing getPath does, before
162
+ // the search loop runs at all, so remaining stays unchanged.
163
+ expect(budget.remaining).toBe(0)
164
+ })
165
+
166
+ it('never lets budget.remaining go negative', () => {
167
+ const grid = buildGrid([])
168
+ const budget = createPathBudget(3) // deliberately smaller than this search needs
169
+ getPath(grid, { x: 0, y: 0 }, { x: 0, y: 50 }, budget)
170
+ expect(budget.remaining).toBeGreaterThanOrEqual(0)
171
+ })
172
+
173
+ it('bounds total pathfinding work across many edges to the budget, regardless of edge count', () => {
174
+ // Structural equivalent of the "much larger dense fan-in" wall-clock
175
+ // test: 120 fan-in edges, each independently getting its own
176
+ // unbounded allowance, is what caused the real OOM crashes this
177
+ // budget exists to prevent (see pathfinder.ts's PathBudget doc
178
+ // comment). Here, a deliberately small shared budget stands in for
179
+ // "more total demand than the budget allows" — the same shape of
180
+ // problem as 120 real edges against the production default.
181
+ const grid = buildGrid([])
182
+ const budget = createPathBudget(500)
183
+ for (let i = 0; i < 120; i++) {
184
+ getPath(grid, { x: i, y: 0 }, { x: i, y: 40 }, budget)
185
+ }
186
+ // Total demand (120 searches x ~40+ iterations each) far exceeds the
187
+ // 500-iteration budget, so it must land at exactly 0 — proving later
188
+ // calls were actually cut off partway through, not merely that none
189
+ // of them individually happened to exceed it.
190
+ expect(budget.remaining).toBe(0)
191
+ })
192
+ })
193
+
194
+ describe('dense graph regression', () => {
195
+ it('does not crash on dense TD graph with multiple fan-in bundles', () => {
196
+ // Regression test for: RangeError: Map maximum size exceeded
197
+ // https://github.com/lukilabs/beautiful-mermaid/issues/64
198
+ const code = `graph TD
199
+ A["AAA<br>(keita)"] --> C["CCC"]
200
+ B["BBB<br>(yuriko)"] --> C
201
+ C --> D["DDDD"]
202
+ D --> E["EEEE"]
203
+
204
+ A1["1 / 2"] --> A
205
+ A2["3 / 4"] --> A
206
+ A3["5 / 6"] --> A
207
+ A4["XXX<br>(YYY ZZZ)"] --> A
208
+
209
+ B1["77 77<br>(7 / 7 / 7)"] --> B
210
+ B2["88-88<br>(99 99)"] --> B
211
+ B3["111s 222s"] --> B
212
+
213
+ D --> F{"F?"}
214
+ F -->|Yes| G["High level<br>Tr"]
215
+ F -->|No| H["Dumb Tr<br>S"]`
216
+
217
+ // Should not throw (previously threw RangeError: Map maximum size exceeded)
218
+ const result = renderMermaidASCII(code, { useAscii: false })
219
+ expect(result).toBeDefined()
220
+ expect(result.length).toBeGreaterThan(0)
221
+ // Verify most node labels appear (some may be visually clipped by edge routing)
222
+ for (const label of [
223
+ 'CCC',
224
+ 'DDDD',
225
+ 'EEEE',
226
+ '1 / 2',
227
+ '3 / 4',
228
+ '5 / 6',
229
+ 'XXX',
230
+ '77 77',
231
+ '88-88',
232
+ '111s 222s',
233
+ 'F?',
234
+ ]) {
235
+ expect(result).toContain(label)
236
+ }
237
+ })
238
+ })
239
+ })
@@ -0,0 +1,18 @@
1
+ graph LR
2
+ A --> B & C
3
+ ---
4
+ +---+ +---+
5
+ | | | |
6
+ | A +---->| B |
7
+ | | | |
8
+ +-+-+ +---+
9
+ |
10
+ |
11
+ |
12
+ |
13
+ |
14
+ | +---+
15
+ | | |
16
+ +------>| C |
17
+ | |
18
+ +---+
@@ -0,0 +1,18 @@
1
+ graph LR
2
+ A & B --> C & D
3
+ ---
4
+ +---+ +---+
5
+ | | | |
6
+ | A +--+->| C |
7
+ | | | | |
8
+ +-+-+ | +---+
9
+ | |
10
+ | |
11
+ +----+
12
+ | |
13
+ | |
14
+ +-+-+ | +---+
15
+ | | | | |
16
+ | B +--+->| D |
17
+ | | | |
18
+ +---+ +---+
@@ -0,0 +1,18 @@
1
+ graph LR
2
+ A & B --> C
3
+ ---
4
+ +---+ +---+
5
+ | | | |
6
+ | A +---->| C |
7
+ | | | |
8
+ +---+ +---+
9
+ ^
10
+ |
11
+ |
12
+ |
13
+ |
14
+ +---+ |
15
+ | | |
16
+ | B +-------+
17
+ | |
18
+ +---+
@@ -0,0 +1,18 @@
1
+ graph TD
2
+ A & B --> C
3
+ ---
4
+ +---+ +---+
5
+ | | | |
6
+ | A | | B |
7
+ | | | |
8
+ +-+-+ +-+-+
9
+ | |
10
+ | |
11
+ +---------+
12
+ |
13
+ v
14
+ +---+
15
+ | |
16
+ | C |
17
+ | |
18
+ +---+
@@ -0,0 +1,18 @@
1
+ graph TD
2
+ A --> B & C
3
+ ---
4
+ +---+
5
+ | |
6
+ | A |
7
+ | |
8
+ +---+
9
+ |
10
+ |
11
+ +---------+
12
+ | |
13
+ v v
14
+ +---+ +---+
15
+ | | | |
16
+ | B | | C |
17
+ | | | |
18
+ +---+ +---+
@@ -0,0 +1,18 @@
1
+ graph LR
2
+ A & B
3
+ ---
4
+ +---+
5
+ | |
6
+ | A |
7
+ | |
8
+ +---+
9
+
10
+
11
+
12
+
13
+
14
+ +---+
15
+ | |
16
+ | B |
17
+ | |
18
+ +---+
@@ -0,0 +1,10 @@
1
+ graph LR
2
+ A --> B --> C --> A
3
+ ---
4
+ +---+ +---+ +---+
5
+ | | | | | |
6
+ | A +---->| B +---->| C |
7
+ | | | | | |
8
+ +---+ +---+ +-+-+
9
+ ^ |
10
+ +-------------------+
@@ -0,0 +1,22 @@
1
+ graph LR
2
+ A --> B
3
+ B --> C
4
+ A --> C
5
+ B --> D
6
+ C --> D
7
+ ---
8
+ +---+ +---+ +---+
9
+ | | | | | |
10
+ | A +---->| B +---->| D |
11
+ | | | | | |
12
+ +-+-+ +-+-+ +---+
13
+ | | ^
14
+ | | |
15
+ | | |
16
+ | | |
17
+ | v |
18
+ | +---+ |
19
+ | | | |
20
+ +------>| C +-------+
21
+ | |
22
+ +---+
@@ -0,0 +1,22 @@
1
+ graph LR
2
+ A --> B
3
+ B --> C
4
+ A --> C
5
+ B --> D
6
+ D --> C
7
+ ---
8
+ +---+ +---+ +---+
9
+ | | | | | |
10
+ | A +---->| B +--+->+ D |
11
+ | | | | | | |
12
+ +-+-+ +-+-+ | +---+
13
+ | | |
14
+ | | |
15
+ | +----+
16
+ | |
17
+ | v
18
+ | +---+
19
+ | | |
20
+ +------>| C |
21
+ | |
22
+ +---+
@@ -0,0 +1,20 @@
1
+ paddingX=5
2
+ paddingY=3
3
+ graph LR
4
+ A --> B & C
5
+ B --> C & D
6
+ D --> C
7
+ ---
8
+ +---+ +---+ +---+
9
+ | | | | | |
10
+ | A +---->| B +--+->+ D |
11
+ | | | | | | |
12
+ +-+-+ +-+-+ | +---+
13
+ | | |
14
+ | +----+
15
+ | v
16
+ | +---+
17
+ | | |
18
+ +------>| C |
19
+ | |
20
+ +---+
@@ -0,0 +1,19 @@
1
+ classDiagram
2
+ A <|-- B : inherits
3
+ C *-- D : owns
4
+ E o-- F : has
5
+ G --> H : uses
6
+ I ..> J : depends
7
+ K ..|> L : implements
8
+ ---
9
+ +---+ +---+ +---+ +---+ +---+ +---+
10
+ | A | | C | | E | | G | | I | | K |
11
+ +---+ +---+ +---+ +---+ +---+ +---+
12
+ ^ * o | : :
13
+ inherits owns has uses depends implements
14
+ | | | v v v
15
+ +---+ +---+ +---+ +---+ +---+ +---+
16
+ | B | | D | | F | | H | | J | | L |
17
+ +---+ +---+ +---+ +---+ +---+ +---+
18
+
19
+
@@ -0,0 +1,29 @@
1
+ classDiagram
2
+ class Shape {
3
+ <<abstract>>
4
+ +draw() void
5
+ }
6
+ class Circle {
7
+ +radius int
8
+ +draw() void
9
+ }
10
+ Shape <|-- Circle
11
+ ---
12
+ +----------------+
13
+ | <<abstract>> |
14
+ | Shape |
15
+ +----------------+
16
+ | + draw(): void |
17
+ +----------------+
18
+ ^
19
+ |
20
+ |
21
+ +----------------+
22
+ | Circle |
23
+ +----------------+
24
+ | + int: radius |
25
+ +----------------+
26
+ | + draw(): void |
27
+ +----------------+
28
+
29
+
@@ -0,0 +1,14 @@
1
+ classDiagram
2
+ Student --> Course : enrolls
3
+ ---
4
+ +---------+
5
+ | Student |
6
+ +---------+
7
+ |
8
+ enrolls
9
+ v
10
+ +--------+
11
+ | Course |
12
+ +--------+
13
+
14
+
@@ -0,0 +1,15 @@
1
+ classDiagram
2
+ class Animal {
3
+ +String name
4
+ +eat() void
5
+ }
6
+ ---
7
+ +----------------+
8
+ | Animal |
9
+ +----------------+
10
+ | + name: String |
11
+ +----------------+
12
+ | + eat(): void |
13
+ +----------------+
14
+
15
+
@@ -0,0 +1,14 @@
1
+ classDiagram
2
+ Client ..> Server : uses
3
+ ---
4
+ +--------+
5
+ | Client |
6
+ +--------+
7
+ :
8
+ uses
9
+ v
10
+ +--------+
11
+ | Server |
12
+ +--------+
13
+
14
+
@@ -0,0 +1,20 @@
1
+ classDiagram
2
+ Animal <|-- Dog
3
+ Animal : +String name
4
+ Dog : +bark() void
5
+ ---
6
+ +----------------+
7
+ | Animal |
8
+ +----------------+
9
+ | + name: String |
10
+ +----------------+
11
+ ^
12
+ |
13
+ |
14
+ +----------------+
15
+ | Dog |
16
+ +----------------+
17
+ | + bark(): void |
18
+ +----------------+
19
+
20
+
@@ -0,0 +1,21 @@
1
+ classDiagram
2
+ class BankAccount {
3
+ +String owner
4
+ -int balance
5
+ +deposit(int amount) void
6
+ +withdraw(int amount) bool
7
+ -validate() bool
8
+ }
9
+ ---
10
+ +------------------------------+
11
+ | BankAccount |
12
+ +------------------------------+
13
+ | + owner: String |
14
+ | - balance: int |
15
+ +------------------------------+
16
+ | + deposit(int amount): void |
17
+ | + withdraw(int amount): bool |
18
+ | - validate(): bool |
19
+ +------------------------------+
20
+
21
+
@@ -0,0 +1,23 @@
1
+ graph LR
2
+ %% This is a comment
3
+ A --> B
4
+ %% Another comment
5
+ B --> C
6
+ A --> C
7
+ %% Final comment
8
+ ---
9
+ +---+ +---+
10
+ | | | |
11
+ | A +---->| B |
12
+ | | | |
13
+ +-+-+ +-+-+
14
+ | |
15
+ | |
16
+ | |
17
+ | |
18
+ | v
19
+ | +---+
20
+ | | |
21
+ +------>| C |
22
+ | |
23
+ +---+
@@ -0,0 +1,10 @@
1
+ paddingX=2
2
+ paddingY=1
3
+ graph LR
4
+ A --> B
5
+ ---
6
+ +---+ +---+
7
+ | | | |
8
+ | A +->| B |
9
+ | | | |
10
+ +---+ +---+