@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,448 @@
1
+ // ============================================================================
2
+ // ASCII renderer — A* pathfinding for edge routing
3
+ //
4
+ // Ported from AlexanderGrooff/mermaid-ascii cmd/arrow.go.
5
+ // Uses A* search with a corner-penalizing heuristic to find clean
6
+ // paths between nodes on the grid. Prefers straight lines over zigzags.
7
+ // ============================================================================
8
+
9
+ import type {
10
+ GridCoord,
11
+ PathBudget,
12
+ AsciiGraph,
13
+ CardinalDirection,
14
+ } from './types.ts'
15
+ import { gridKey, gridCoordEquals, dirEquals, Left, Right } from './types.ts'
16
+ import { isFree, type Grid } from './grid-occupancy.ts'
17
+
18
+ // ============================================================================
19
+ // Priority queue (min-heap) for A* open set
20
+ // ============================================================================
21
+
22
+ interface PQItem {
23
+ coord: GridCoord
24
+ priority: number
25
+ }
26
+
27
+ /**
28
+ * Simple min-heap priority queue.
29
+ * For the grid sizes we handle (~100s of cells), this is more than fast enough.
30
+ */
31
+ class MinHeap {
32
+ private items: PQItem[] = []
33
+
34
+ get length(): number {
35
+ return this.items.length
36
+ }
37
+
38
+ push(item: PQItem): void {
39
+ this.items.push(item)
40
+ this.bubbleUp(this.items.length - 1)
41
+ }
42
+
43
+ pop(): PQItem | undefined {
44
+ if (this.items.length === 0) return undefined
45
+ const top = this.items[0]!
46
+ const last = this.items.pop()!
47
+ if (this.items.length > 0) {
48
+ this.items[0] = last
49
+ this.sinkDown(0)
50
+ }
51
+ return top
52
+ }
53
+
54
+ private bubbleUp(start: number): void {
55
+ let i = start
56
+ while (i > 0) {
57
+ const parent = (i - 1) >> 1
58
+ if (this.items[i]!.priority < this.items[parent]!.priority) {
59
+ ;[this.items[i], this.items[parent]] = [
60
+ this.items[parent]!,
61
+ this.items[i]!,
62
+ ]
63
+ i = parent
64
+ } else {
65
+ break
66
+ }
67
+ }
68
+ }
69
+
70
+ private sinkDown(start: number): void {
71
+ const n = this.items.length
72
+ let i = start
73
+ while (true) {
74
+ let smallest = i
75
+ const left = 2 * i + 1
76
+ const right = 2 * i + 2
77
+ if (
78
+ left < n &&
79
+ this.items[left]!.priority < this.items[smallest]!.priority
80
+ ) {
81
+ smallest = left
82
+ }
83
+ if (
84
+ right < n &&
85
+ this.items[right]!.priority < this.items[smallest]!.priority
86
+ ) {
87
+ smallest = right
88
+ }
89
+ if (smallest !== i) {
90
+ ;[this.items[i], this.items[smallest]] = [
91
+ this.items[smallest]!,
92
+ this.items[i]!,
93
+ ]
94
+ i = smallest
95
+ } else {
96
+ break
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ // ============================================================================
103
+ // A* heuristic
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Manhattan distance with a +1 penalty when both dx and dy are non-zero.
108
+ * This encourages the pathfinder to prefer straight lines and minimize corners.
109
+ */
110
+ export function heuristic(a: GridCoord, b: GridCoord): number {
111
+ const absX = Math.abs(a.x - b.x)
112
+ const absY = Math.abs(a.y - b.y)
113
+ if (absX === 0 || absY === 0) {
114
+ return absX + absY
115
+ }
116
+ return absX + absY + 1
117
+ }
118
+
119
+ // ============================================================================
120
+ // A* pathfinding
121
+ // ============================================================================
122
+
123
+ /** 4-directional movement (no diagonals in grid pathfinding). */
124
+ const MOVE_DIRS: GridCoord[] = [
125
+ { x: 1, y: 0 },
126
+ { x: -1, y: 0 },
127
+ { x: 0, y: 1 },
128
+ { x: 0, y: -1 },
129
+ ]
130
+
131
+ /**
132
+ * Maximum number of A* iterations before giving up on a *single* getPath
133
+ * call. Prevents unbounded memory growth when the destination is
134
+ * unreachable through free cells (the grid has no positive upper-bound
135
+ * check).
136
+ *
137
+ * This alone is not sufficient to bound a whole render: a dense fan-in/out
138
+ * graph can call getPath hundreds of times (once per edge, sometimes twice
139
+ * — see determinePath's preferred + alternative attempts), and each call is
140
+ * independently allowed to spend up to MAX_ITERATIONS work searching a grid
141
+ * that's mostly occupied by other nodes on the same row. That per-call cap
142
+ * bounds each search but not their sum, so total time/memory across a
143
+ * render still scales with (edge count × MAX_ITERATIONS) and can exhaust
144
+ * the heap well before any single call hits its own limit. See PathBudget
145
+ * below for the render-wide bound that fixes this.
146
+ */
147
+ const MAX_ITERATIONS = 50_000
148
+
149
+ /**
150
+ * A mutable, render-wide budget shared across every getPath call made while
151
+ * laying out one graph (see grid.ts's createMapping, which creates one
152
+ * fresh budget per render and threads it through edge-routing.ts and
153
+ * edge-bundling.ts). Each A* iteration — in any call — decrements
154
+ * `remaining`; once it hits zero, all subsequent getPath calls return null
155
+ * immediately (falling back to the direct-path logic already used when A*
156
+ * can't find a route at all), which hard-bounds total pathfinding work for
157
+ * the whole render regardless of how many edges it has.
158
+ *
159
+ * (Type defined in types.ts alongside the other shared ASCII-renderer
160
+ * types, to avoid a circular import between this module and types.ts.)
161
+ */
162
+
163
+ /**
164
+ * Default render-wide iteration budget. Generous enough to fully route
165
+ * ordinary diagrams (tens to low hundreds of edges) without ever being
166
+ * hit, while still keeping total pathfinding work — and thus memory use —
167
+ * bounded for pathological dense fan-in/out graphs with hundreds of edges.
168
+ */
169
+ export const DEFAULT_PATH_BUDGET = 200_000
170
+
171
+ /** Create a fresh render-wide path budget. */
172
+ export function createPathBudget(
173
+ total: number = DEFAULT_PATH_BUDGET,
174
+ ): PathBudget {
175
+ return { remaining: total }
176
+ }
177
+
178
+ /**
179
+ * Find a path from `from` to `to` on the grid using A*.
180
+ * Returns the path as an array of GridCoords, or null if no path exists.
181
+ *
182
+ * `budget`, if provided, is a render-wide iteration budget shared across
183
+ * all getPath calls for the current layout — see PathBudget above. Once
184
+ * exhausted, this (and every subsequent call sharing it) returns null
185
+ * right away instead of searching.
186
+ */
187
+ export function getPath(
188
+ grid: Grid,
189
+ from: GridCoord,
190
+ to: GridCoord,
191
+ budget?: PathBudget,
192
+ ): GridCoord[] | null {
193
+ if (budget && budget.remaining <= 0) {
194
+ return null
195
+ }
196
+
197
+ const pq = new MinHeap()
198
+ pq.push({ coord: from, priority: 0 })
199
+
200
+ const costSoFar = new Map<string, number>()
201
+ costSoFar.set(gridKey(from), 0)
202
+
203
+ const cameFrom = new Map<string, GridCoord | null>()
204
+ cameFrom.set(gridKey(from), null)
205
+
206
+ let iterations = 0
207
+ while (pq.length > 0) {
208
+ if (++iterations > MAX_ITERATIONS) {
209
+ return null
210
+ }
211
+ if (budget) {
212
+ if (budget.remaining <= 0) return null
213
+ budget.remaining--
214
+ }
215
+
216
+ const current = pq.pop()!.coord
217
+
218
+ if (gridCoordEquals(current, to)) {
219
+ // Reconstruct path by walking backwards through cameFrom
220
+ const path: GridCoord[] = []
221
+ let c: GridCoord | null = current
222
+ while (c !== null) {
223
+ path.unshift(c)
224
+ c = cameFrom.get(gridKey(c)) ?? null
225
+ }
226
+ return path
227
+ }
228
+
229
+ // Every coord ever pushed onto `pq` has its costSoFar entry set
230
+ // immediately beforehand — either the initial `from` push above, or in
231
+ // the neighbor-expansion loop below (`costSoFar.set` precedes
232
+ // `pq.push`) — so a coord just popped from `pq` always has a recorded
233
+ // cost. That invariant lives in this function's control flow, not in
234
+ // the Map's type, so it's checked explicitly rather than trusted via
235
+ // `!`.
236
+ const currentCost = costSoFar.get(gridKey(current))
237
+ if (currentCost === undefined) {
238
+ /* v8 ignore next */
239
+ throw new Error(
240
+ `A* pathfinding: missing cost for visited cell ${gridKey(current)}`,
241
+ )
242
+ }
243
+
244
+ for (const dir of MOVE_DIRS) {
245
+ const next: GridCoord = { x: current.x + dir.x, y: current.y + dir.y }
246
+
247
+ // Allow moving to the destination even if it's occupied (it's a node boundary)
248
+ if (!isFree(grid, next) && !gridCoordEquals(next, to)) {
249
+ continue
250
+ }
251
+
252
+ const newCost = currentCost + 1
253
+ const nextKey = gridKey(next)
254
+ const existingCost = costSoFar.get(nextKey)
255
+
256
+ if (existingCost === undefined || newCost < existingCost) {
257
+ costSoFar.set(nextKey, newCost)
258
+ const priority = newCost + heuristic(next, to)
259
+ pq.push({ coord: next, priority })
260
+ cameFrom.set(nextKey, current)
261
+ }
262
+ }
263
+ }
264
+
265
+ return null // No path found
266
+ }
267
+
268
+ /**
269
+ * Simplify a path by removing intermediate waypoints on straight segments.
270
+ * E.g., [(0,0), (1,0), (2,0), (2,1)] becomes [(0,0), (2,0), (2,1)].
271
+ * This reduces the number of line-drawing operations.
272
+ */
273
+ export function mergePath(path: GridCoord[]): GridCoord[] {
274
+ if (path.length <= 2) return path
275
+
276
+ const toRemove = new Set<number>()
277
+ let step0 = path[0]!
278
+ let step1 = path[1]!
279
+
280
+ for (let idx = 2; idx < path.length; idx++) {
281
+ const step2 = path[idx]!
282
+ const prevDx = step1.x - step0.x
283
+ const prevDy = step1.y - step0.y
284
+ const dx = step2.x - step1.x
285
+ const dy = step2.y - step1.y
286
+
287
+ // Same direction — the middle point is redundant
288
+ if (prevDx === dx && prevDy === dy) {
289
+ // In Go: indexToRemove = append(indexToRemove, idx+1) but idx is 0-based from path[2:]
290
+ // which corresponds to index idx in the full path. Go uses idx+1 because idx iterates
291
+ // from 0 in the [2:] slice, mapping to full-array index idx+1.
292
+ // Actually re-checking Go code: the loop is `for idx, step2 := range path[2:]`
293
+ // so idx=0 → path[2], and it removes idx+1 which is index 1 in the full array.
294
+ // Wait, that doesn't look right. Let me re-read:
295
+ // step0 = path[0], step1 = path[1]
296
+ // for idx, step2 := range path[2:] { ... indexToRemove = append(indexToRemove, idx+1) ... }
297
+ // When idx=0, step2=path[2], and it removes index 1 (step1 = path[1]) if directions match
298
+ // So it removes the middle point (step1) which is at index idx+1 in the original array
299
+ // when counting from the 2-ahead loop. Let me just track which middle indices to remove.
300
+ toRemove.add(idx - 1) // Remove the middle point (step1's position)
301
+ }
302
+
303
+ step0 = step1
304
+ step1 = step2
305
+ }
306
+
307
+ return path.filter((_, i) => !toRemove.has(i))
308
+ }
309
+
310
+ // ============================================================================
311
+ // Direct (unobstructed L-shape) path — shared fast path for edge-routing.ts
312
+ // and edge-bundling.ts
313
+ // ============================================================================
314
+
315
+ /**
316
+ * Check every cell strictly after `from` up to (and optionally including)
317
+ * `to` along a single axis-aligned run. `from` and `to` must share an x or
318
+ * a y coordinate. `includeTo` should be false when `to` is the edge's
319
+ * final destination point (which is expected to sit on/adjacent to the
320
+ * target node's own border and so may legitimately be "occupied" in
321
+ * graph.grid — mirroring the same allowance A*'s getPath makes for its
322
+ * destination cell).
323
+ */
324
+ function isAxisRunFree(
325
+ grid: Grid,
326
+ from: GridCoord,
327
+ to: GridCoord,
328
+ includeTo: boolean,
329
+ ): boolean {
330
+ if (from.x === to.x && from.y === to.y) return true
331
+ const alongY = from.x === to.x
332
+ if (!alongY && from.y !== to.y) return false // not axis-aligned
333
+
334
+ const step = alongY ? (to.y > from.y ? 1 : -1) : to.x > from.x ? 1 : -1
335
+ let pos = alongY ? from.y : from.x
336
+ const end = alongY ? to.y : to.x
337
+
338
+ for (pos += step; ; pos += step) {
339
+ const isLast = pos === end
340
+ const cell: GridCoord = alongY
341
+ ? { x: from.x, y: pos }
342
+ : { x: pos, y: from.y }
343
+ if (!(isLast && !includeTo) && !isFree(grid, cell)) return false
344
+ if (isLast) break
345
+ }
346
+ return true
347
+ }
348
+
349
+ /** Try one L-shaped corner order; null if any leg is obstructed. */
350
+ function tryCornerPath(
351
+ grid: Grid,
352
+ from: GridCoord,
353
+ to: GridCoord,
354
+ horizontalFirst: boolean,
355
+ ): GridCoord[] | null {
356
+ const corner: GridCoord = horizontalFirst
357
+ ? { x: to.x, y: from.y }
358
+ : { x: from.x, y: to.y }
359
+
360
+ if (!isAxisRunFree(grid, from, corner, true)) return null
361
+ if (!isAxisRunFree(grid, corner, to, false)) return null
362
+ return [from, corner, to]
363
+ }
364
+
365
+ /**
366
+ * Try a direct two-segment (L-shaped) route between `from` and `to`. `dir`
367
+ * indicates the preferred corner order (horizontal-first for Left/Right,
368
+ * vertical-first for Up/Down), but when that orientation is blocked the
369
+ * *other* orientation is tried too before giving up.
370
+ *
371
+ * Trying both orientations (rather than trusting `dir` alone) matters
372
+ * because `dir` is not always the true geometric departure direction from
373
+ * `from`: edge-routing.ts's determinePath always passes it correctly, but
374
+ * edge-bundling.ts's junction segments sometimes pass the *arrival* anchor
375
+ * at `to` instead (the two only coincide when `from`/`to` happen to already
376
+ * be axis-aligned, which is common but not guaranteed — see the PR #178
377
+ * review that found the fast path dead at 3 of 4 bundled call sites because
378
+ * of exactly this mix-up). Trying both orientations makes the result
379
+ * correct regardless of which direction a caller actually knows, instead of
380
+ * requiring every caller to recompute true geometric departure.
381
+ *
382
+ * Returns null when `from` and `to` are already axis-aligned (no corner
383
+ * needed — A*'s result is already optimal there) or when both orientations
384
+ * are blocked.
385
+ *
386
+ * A* always finds a *shortest* path, but when several equally-short routes
387
+ * exist — e.g. a straight line and a zigzag with the same total step count
388
+ * — which one it returns depends on priority-queue tie-breaking order, not
389
+ * on which "looks" straighter. That's why sibling edges from the same
390
+ * source can end up with visually inconsistent routing (one goes straight,
391
+ * another zigzags) even though nothing actually blocks a straight route
392
+ * for either. A direct L-shaped route is, by construction, exactly as
393
+ * short as any route between two points can be, so whenever it's
394
+ * unobstructed we prefer it outright over whatever A* found — this also
395
+ * skips the A* search entirely for the common unobstructed case.
396
+ */
397
+ function tryDirectPath(
398
+ graph: AsciiGraph,
399
+ from: GridCoord,
400
+ to: GridCoord,
401
+ dir: CardinalDirection,
402
+ ): GridCoord[] | null {
403
+ if (from.x === to.x || from.y === to.y) return null
404
+
405
+ const preferHorizontalFirst = dirEquals(dir, Left) || dirEquals(dir, Right)
406
+ return (
407
+ tryCornerPath(graph.grid, from, to, preferHorizontalFirst) ??
408
+ tryCornerPath(graph.grid, from, to, !preferHorizontalFirst)
409
+ )
410
+ }
411
+
412
+ /**
413
+ * Route a single directed edge segment from `from` to `to`: try an
414
+ * unobstructed direct L-shaped route first (tryDirectPath, above), falling
415
+ * back to A* search (getPath) when every direct orientation is blocked or
416
+ * unavailable (from/to already axis-aligned). Returns the merged path, or
417
+ * null when neither strategy finds a route at all (e.g. the destination
418
+ * is unreachable through free cells).
419
+ *
420
+ * This is the single seam both edge-routing.ts (determinePath's
421
+ * preferred/alternative candidates) and edge-bundling.ts
422
+ * (routeBundledEdges' junction-based segments) route through, so regular
423
+ * and bundled edges share the same fast-path-then-A* behavior instead of
424
+ * each independently calling getPath.
425
+ *
426
+ * `graph.pathBudget` is required, not silently optional: it's the
427
+ * render-wide bound against pathological/hostile diagrams (see PathBudget
428
+ * above), and routeEdge is the shared entry point for two modules now — an
429
+ * absent budget here would silently disable that bound for both instead of
430
+ * failing loudly, the way requireGridCoord fails loudly for a missing grid
431
+ * coordinate rather than treating it as some default.
432
+ */
433
+ export function routeEdge(
434
+ graph: AsciiGraph,
435
+ from: GridCoord,
436
+ to: GridCoord,
437
+ dir: CardinalDirection,
438
+ ): GridCoord[] | null {
439
+ if (!graph.pathBudget) {
440
+ throw new Error(
441
+ 'routeEdge requires graph.pathBudget to be set; call createPathBudget() (see grid.ts createMapping) before routing edges',
442
+ )
443
+ }
444
+ const path =
445
+ tryDirectPath(graph, from, to, dir) ??
446
+ getPath(graph.grid, from, to, graph.pathBudget)
447
+ return path ? mergePath(path) : null
448
+ }
@@ -0,0 +1,88 @@
1
+ // ============================================================================
2
+ // zombie-mermaid — ASCII half of the per-diagram-type registry (issue #533)
3
+ //
4
+ // `src/diagram-registry.ts` used to hold both halves of each registered
5
+ // diagram type: `renderSvg` (plus `parse`/`layoutForSvg`) AND `renderAscii`.
6
+ // That made the SVG-side module import `renderXYChartAscii`/`renderErAscii`
7
+ // out of `src/ascii/`, while `src/ascii/index.ts` imported `diagramRegistry`
8
+ // back out of `src/`. A cycle:
9
+ //
10
+ // src/ascii/index.ts -> src/diagram-registry.ts -> src/ascii/xychart.ts
11
+ // -> src/ascii/er-diagram.ts
12
+ //
13
+ // Benign while both halves live in one package; fatal to the monorepo split
14
+ // scoped in docs/decisions/monorepo-conversion-scoping.md (#416/#620), where
15
+ // `src/ascii/**` becomes `@zombie-mermaid/ascii-renderer` and the SVG side
16
+ // stays behind — a package-level import cycle, not just a module-level one.
17
+ // It also had a measurable cost today: `dist/ascii.js` carried
18
+ // `import "elkjs/lib/elk.bundled.js"` purely because the registry dragged
19
+ // `src/er/layout.ts` -> `src/elk-instance.ts` into the ASCII entry's module
20
+ // graph, which is precisely what the `./ascii` subpath export (#300) exists
21
+ // to avoid.
22
+ //
23
+ // Splitting the table by renderer removes the back-edge: the ASCII side owns
24
+ // its own dispatch here, and `src/diagram-registry.ts` keeps the SVG side and
25
+ // no longer names anything under `src/ascii/`. Both directions are now
26
+ // one-way, and the set of registered types stays a single decision per
27
+ // renderer rather than a `switch` re-listed at each front door — the point of
28
+ // #533. Adding a type to one renderer's table without the other is
29
+ // expressible in principle, though in practice 'xychart'/'er'/'sequence'/
30
+ // 'class'/'flowchart' now move together on both sides.
31
+ // ============================================================================
32
+
33
+ import type { DiagramType, Direction } from '@zombie-mermaid/core'
34
+ import type { AsciiConfig, AsciiTheme, ColorMode } from './types.ts'
35
+ import { renderXYChartAscii } from './xychart.ts'
36
+ import { renderErAscii } from './er-diagram.ts'
37
+ import { renderSequenceAscii } from './sequence.ts'
38
+ import { renderClassAscii } from './class-diagram.ts'
39
+ import { renderFlowchartAscii } from './flowchart.ts'
40
+
41
+ /**
42
+ * Small, closed set of ASCII-only extras not every type needs — `class`
43
+ * reads `hyperlinks` (see `ClassAsciiOptions` in
44
+ * src/ascii/class-diagram.ts), `flowchart` reads both `direction` and
45
+ * `hyperlinks` (see `FlowchartAsciiExtras` in src/ascii/flowchart.ts). Kept
46
+ * as its own type rather than reusing `AsciiRenderOptions` from
47
+ * src/ascii/index.ts so this module stays a leaf of the ASCII tree: index.ts
48
+ * imports it, never the other way round.
49
+ */
50
+ export interface AsciiRenderExtras {
51
+ hyperlinks?: boolean
52
+ direction?: Direction
53
+ }
54
+
55
+ /**
56
+ * One registered diagram type's ASCII entry point. Every ASCII renderer
57
+ * reruns its own parse + grid layout + draw from raw text — there is no
58
+ * shared positioned model to hand it, unlike the SVG side's
59
+ * `parse`/`layoutForSvg` split (see `DiagramModule` in
60
+ * src/diagram-registry.ts for why a shared layout step would be fiction
61
+ * here).
62
+ */
63
+ export type AsciiRenderer = (
64
+ text: string,
65
+ config: AsciiConfig,
66
+ colorMode: ColorMode,
67
+ theme: AsciiTheme,
68
+ extras: AsciiRenderExtras,
69
+ ) => string
70
+
71
+ /**
72
+ * Every diagram type `renderMermaidASCII` supports, dispatched through this
73
+ * table — see that function in src/ascii/index.ts, which has no fallback
74
+ * switch left now that 'flowchart' (the last holdout — see
75
+ * docs/decisions/diagram-type-registry-partial.md) is registered here too.
76
+ */
77
+ export const asciiRegistry: Record<DiagramType, AsciiRenderer> = {
78
+ xychart: (text, config, colorMode, theme) =>
79
+ renderXYChartAscii(text, config, colorMode, theme),
80
+ er: (text, config, colorMode, theme) =>
81
+ renderErAscii(text, config, colorMode, theme),
82
+ sequence: (text, config, colorMode, theme) =>
83
+ renderSequenceAscii(text, config, colorMode, theme),
84
+ class: (text, config, colorMode, theme, extras) =>
85
+ renderClassAscii(text, config, colorMode, theme, extras),
86
+ flowchart: (text, config, colorMode, theme, extras) =>
87
+ renderFlowchartAscii(text, config, colorMode, theme, extras),
88
+ }