@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
package/src/types.ts ADDED
@@ -0,0 +1,454 @@
1
+ // ============================================================================
2
+ // ASCII renderer — type definitions
3
+ //
4
+ // Ported from AlexanderGrooff/mermaid-ascii (Go).
5
+ // These types model the grid-based coordinate system, 2D text canvas,
6
+ // and graph structures used by the ASCII/Unicode renderer.
7
+ // ============================================================================
8
+
9
+ import type { NodeShape } from '@zombie-mermaid/core'
10
+ import type { Grid } from './grid-occupancy.ts'
11
+
12
+ // Re-export NodeShape for convenience
13
+ export type { NodeShape }
14
+
15
+ /**
16
+ * Shape type for ASCII rendering — maps parser shapes to ASCII renderers.
17
+ * Most shapes from the parser are supported, with fallback to 'rectangle'.
18
+ */
19
+ export type AsciiNodeShape = NodeShape
20
+
21
+ /** Logical grid coordinate — nodes occupy 3x3 blocks on this grid. */
22
+ export interface GridCoord {
23
+ x: number
24
+ y: number
25
+ }
26
+
27
+ /**
28
+ * A mutable, render-wide A* iteration budget shared across every getPath
29
+ * call made while routing one graph's edges (see pathfinder.ts's getPath
30
+ * and grid.ts's createMapping). Bounds total pathfinding work for the whole
31
+ * render, independent of any single call's own per-call iteration cap.
32
+ */
33
+ export interface PathBudget {
34
+ remaining: number
35
+ }
36
+
37
+ /** Character-level coordinate on the 2D text canvas. */
38
+ export interface DrawingCoord {
39
+ x: number
40
+ y: number
41
+ }
42
+
43
+ /**
44
+ * Direction constants model positions on a node's 3x3 grid block.
45
+ * Each node occupies grid cells [x..x+2, y..y+2].
46
+ * Directions are offsets into that block, used for edge attachment points.
47
+ *
48
+ * (0,0) UL (1,0) Up (2,0) UR
49
+ * (0,1) Left (1,1) Mid (2,1) Right
50
+ * (0,2) LL (1,2) Down (2,2) LR
51
+ */
52
+ export interface Direction {
53
+ readonly x: number
54
+ readonly y: number
55
+ }
56
+
57
+ export const Up: Direction = { x: 1, y: 0 }
58
+ export const Down: Direction = { x: 1, y: 2 }
59
+ export const Left: Direction = { x: 0, y: 1 }
60
+ export const Right: Direction = { x: 2, y: 1 }
61
+ export const UpperRight: Direction = { x: 2, y: 0 }
62
+ export const UpperLeft: Direction = { x: 0, y: 0 }
63
+ export const LowerRight: Direction = { x: 2, y: 2 }
64
+ export const LowerLeft: Direction = { x: 0, y: 2 }
65
+ export const Middle: Direction = { x: 1, y: 1 }
66
+
67
+ /** All named directions for iteration. */
68
+ export const ALL_DIRECTIONS: readonly Direction[] = [
69
+ Up,
70
+ Down,
71
+ Left,
72
+ Right,
73
+ UpperRight,
74
+ UpperLeft,
75
+ LowerRight,
76
+ LowerLeft,
77
+ Middle,
78
+ ]
79
+
80
+ /** Compare directions by value (not reference). */
81
+ export function dirEquals(a: Direction, b: Direction): boolean {
82
+ return a.x === b.x && a.y === b.y
83
+ }
84
+
85
+ declare const cardinalDirectionBrand: unique symbol
86
+
87
+ /**
88
+ * The 4 pure cardinal directions — Up/Down/Left/Right — the only Direction
89
+ * values pathfinder.ts's routeEdge/tryDirectPath know how to turn into a
90
+ * single L-shaped route (horizontal-first for Left/Right, vertical-first
91
+ * for Up/Down). The other 5 Direction values (the four diagonals, plus
92
+ * Middle) don't correspond to one routing axis. Direction can't
93
+ * structurally distinguish these on its own — all 9 constants share the
94
+ * same {x, y} shape — so this is a nominal brand: requireCardinalDirection
95
+ * below is the only way to produce one, and it checks the actual value
96
+ * rather than trusting a caller's claim.
97
+ */
98
+ export type CardinalDirection = Direction & {
99
+ readonly [cardinalDirectionBrand]: true
100
+ }
101
+
102
+ function isCardinalDirection(d: Direction): d is CardinalDirection {
103
+ return (
104
+ dirEquals(d, Up) ||
105
+ dirEquals(d, Down) ||
106
+ dirEquals(d, Left) ||
107
+ dirEquals(d, Right)
108
+ )
109
+ }
110
+
111
+ /**
112
+ * Narrow a Direction into a CardinalDirection, throwing if it isn't one of
113
+ * Up/Down/Left/Right. Mirrors requireGridCoord/requirePathBudget elsewhere
114
+ * in this module family: the invariant that routeEdge only ever receives a
115
+ * cardinal direction lives in caller logic spread across edge-routing.ts
116
+ * and edge-bundling.ts, not in Direction's type, so it's checked explicitly
117
+ * at the boundary rather than trusted silently across modules.
118
+ */
119
+ export function requireCardinalDirection(d: Direction): CardinalDirection {
120
+ if (!isCardinalDirection(d)) {
121
+ throw new Error(
122
+ `Expected a cardinal direction (Up/Down/Left/Right); got {x:${d.x}, y:${d.y}}`,
123
+ )
124
+ }
125
+ return d
126
+ }
127
+
128
+ /**
129
+ * 2D text canvas — column-major (canvas[x][y]).
130
+ * Each cell holds a single character (or space).
131
+ */
132
+ export type Canvas = string[][]
133
+
134
+ /** A node in the ASCII graph, positioned on the grid. */
135
+ export interface AsciiNode {
136
+ /** Unique identity key — the original node ID from the parser (e.g. "A", "B"). */
137
+ name: string
138
+ /** Human-readable label for rendering inside the box (e.g. "Web Server"). */
139
+ displayLabel: string
140
+ /** Node shape from the parser (e.g. "rectangle", "diamond", "circle"). */
141
+ shape: AsciiNodeShape
142
+ index: number
143
+ gridCoord: GridCoord | null
144
+ drawingCoord: DrawingCoord | null
145
+ drawing: Canvas | null
146
+ drawn: boolean
147
+ styleClassName: string
148
+ styleClass: AsciiStyleClass
149
+ }
150
+
151
+ /** Style class for colored node text (ported from Go's classDef). */
152
+ export interface AsciiStyleClass {
153
+ name: string
154
+ styles: Record<string, string>
155
+ }
156
+
157
+ /** Edge line style for ASCII rendering. */
158
+ export type AsciiEdgeStyle = 'solid' | 'dotted' | 'thick' | 'invisible'
159
+
160
+ /** An edge in the ASCII graph, with a routed path. */
161
+ export interface AsciiEdge {
162
+ from: AsciiNode
163
+ to: AsciiNode
164
+ text: string
165
+ path: GridCoord[]
166
+ labelLine: GridCoord[]
167
+ startDir: Direction
168
+ endDir: Direction
169
+ /** Line style: solid (default), dotted (-.->) or thick (==>) */
170
+ style: AsciiEdgeStyle
171
+ /** Whether to render an arrowhead at the start (source end) of the edge */
172
+ hasArrowStart: boolean
173
+ /** Whether to render an arrowhead at the end (target end) of the edge */
174
+ hasArrowEnd: boolean
175
+ /**
176
+ * Terminator shape at the source end when it's a circle/cross marker
177
+ * (`o--`/`x--`) rather than a plain arrowhead. Undefined draws the
178
+ * regular arrowhead glyph (per `hasArrowStart`). See issue #330.
179
+ */
180
+ startMarker?: 'circle' | 'cross'
181
+ /** Terminator shape at the target end (`--o`/`--x`). See `startMarker`. */
182
+ endMarker?: 'circle' | 'cross'
183
+ /** Bundle this edge belongs to (if any). Set during bundling analysis. */
184
+ bundle?: EdgeBundle
185
+ /**
186
+ * For bundled edges: path from source/target to the junction point.
187
+ * The full visual path is: pathToJunction + bundle.sharedPath (for fan-in)
188
+ * or bundle.sharedPath + pathToJunction (for fan-out).
189
+ */
190
+ pathToJunction?: GridCoord[]
191
+ /**
192
+ * Set when this edge connects the same *pair* of nodes as one or more
193
+ * sibling edges — either in the same direction (`A -->|One| B` and
194
+ * `A -->|Two| B`) or, for a pair laid out side by side, in opposite
195
+ * directions (`A -->|req| B` and `B -->|res| A`). See
196
+ * `parallelGroupKey` in edge-routing.ts for the exact rule. This is as
197
+ * opposed to edges that merely share one endpoint, which `bundle` above
198
+ * already handles via fan-in/fan-out junctions.
199
+ *
200
+ * `index` is this edge's 0-based position within the group (declaration
201
+ * order); `total` is the group's size. `index === 0` keeps the ordinary
202
+ * single-edge route (determinePath's ordinary preferred/alternative
203
+ * search), so a graph with no parallel edges renders identically to
204
+ * before this field existed. `index > 0` routes through an offset lane
205
+ * instead — see determinePath in edge-routing.ts — so sibling edges never
206
+ * compute the identical path (and therefore identically-positioned,
207
+ * mutually-corrupting labels; see #329 and #629).
208
+ *
209
+ * `usedOffsets` is the *same* Set object, by reference, on every edge in
210
+ * the group (assigned once in assignParallelEdgeLanes) — the lane-offset
211
+ * search in buildParallelLanePath can land two different lane indices on
212
+ * the same actual offset when both have to detour around the same
213
+ * obstacle (see that function's doc), so each successful search records
214
+ * its chosen offset here and skips any offset a sibling already claimed,
215
+ * keeping every lane in the group on a genuinely distinct path.
216
+ */
217
+ parallelLane?: { index: number; total: number; usedOffsets: Set<number> }
218
+ }
219
+
220
+ /** A subgraph container with bounding box for rendering. */
221
+ export interface AsciiSubgraph {
222
+ name: string
223
+ nodes: AsciiNode[]
224
+ parent: AsciiSubgraph | null
225
+ children: AsciiSubgraph[]
226
+ minX: number
227
+ minY: number
228
+ maxX: number
229
+ maxY: number
230
+ /** Optional direction override for layout within this subgraph (LR or TD). */
231
+ direction?: 'LR' | 'TD'
232
+ }
233
+
234
+ // ============================================================================
235
+ // Padding defaults
236
+ //
237
+ // Shared by every renderer that reads AsciiConfig's padding fields. Layouts
238
+ // that predate `paddingX`/`paddingY`/`boxBorderPadding` becoming configurable
239
+ // (sequence, class, ER — see issue #343) compute their own spacing as an
240
+ // offset from these defaults rather than substituting the raw config value
241
+ // directly, so that *not* passing a padding option still renders exactly as
242
+ // it always has (no baseline/snapshot churn) while explicitly passing one
243
+ // still visibly changes spacing. The flowchart/state grid layout (grid.ts)
244
+ // predates this convention and uses `paddingX`/`paddingY` directly as its
245
+ // column/row gap — don't "fix" that to go through these constants too, its
246
+ // existing default already equals them, so behavior is identical.
247
+ // ============================================================================
248
+
249
+ /** `AsciiConfig.paddingX`'s default — see the block comment above. */
250
+ export const DEFAULT_PADDING_X = 5
251
+ /** `AsciiConfig.paddingY`'s default — see the block comment above. */
252
+ export const DEFAULT_PADDING_Y = 5
253
+ /** `AsciiConfig.boxBorderPadding`'s default — see the block comment above. */
254
+ export const DEFAULT_BOX_BORDER_PADDING = 1
255
+
256
+ /**
257
+ * Derive a diagram-local spacing constant from a padding option, offset from
258
+ * that option's default so a diagram whose own historical constant differs
259
+ * from the shared default (e.g. class diagrams' 4-column gap vs. the shared
260
+ * default of 5) still renders unchanged when the caller passes no explicit
261
+ * padding override. `-x`/`-y`/`-p` still visibly widen or tighten spacing
262
+ * because they shift `configValue` away from `defaultValue`.
263
+ *
264
+ * Shared by sequence.ts/class-diagram.ts/er-diagram.ts so the three
265
+ * renderers wired up for issue #343 apply padding the same way instead of
266
+ * each re-deriving this arithmetic.
267
+ *
268
+ * @param floor - Minimum result, so a large negative padding can't collapse
269
+ * the spacing to zero or negative (which would overlap adjacent elements).
270
+ */
271
+ export function paddingOffset(
272
+ configValue: number,
273
+ defaultValue: number,
274
+ base: number,
275
+ floor: number,
276
+ ): number {
277
+ return Math.max(floor, base + (configValue - defaultValue))
278
+ }
279
+
280
+ /** Configuration for ASCII rendering. */
281
+ export interface AsciiConfig {
282
+ /** true = ASCII chars (+,-,|), false = Unicode box-drawing (┌,─,│). Default: false */
283
+ useAscii: boolean
284
+ /** Horizontal spacing between nodes. Default: 5 */
285
+ paddingX: number
286
+ /** Vertical spacing between nodes. Default: 5 */
287
+ paddingY: number
288
+ /** Padding inside node boxes. Default: 1 */
289
+ boxBorderPadding: number
290
+ /** Graph direction: "LR" or "TD". */
291
+ graphDirection: 'LR' | 'TD'
292
+ }
293
+
294
+ /** Full ASCII graph state used during layout and rendering. */
295
+ export interface AsciiGraph {
296
+ nodes: AsciiNode[]
297
+ edges: AsciiEdge[]
298
+ canvas: Canvas
299
+ /** Role canvas — tracks the role of each character for colored output. */
300
+ roleCanvas: RoleCanvas
301
+ /** Grid occupancy map — tracks which "x,y" cells are reserved. */
302
+ grid: Grid
303
+ columnWidth: Map<number, number>
304
+ rowHeight: Map<number, number>
305
+ subgraphs: AsciiSubgraph[]
306
+ config: AsciiConfig
307
+ /** Offset applied to all drawing coords to accommodate subgraph borders. */
308
+ offsetX: number
309
+ offsetY: number
310
+ /** Edge bundles for parallel link visualization. Set during bundling analysis. */
311
+ bundles: EdgeBundle[]
312
+ /**
313
+ * Render-wide A* iteration budget shared across every getPath call made
314
+ * while routing this graph's edges. Bounds total pathfinding work (and
315
+ * therefore memory) for graphs with many edges, independent of any
316
+ * single call's own iteration cap. Set at the start of createMapping's
317
+ * edge-routing phase; undefined before then. See pathfinder.ts's
318
+ * PathBudget for details.
319
+ */
320
+ pathBudget?: PathBudget
321
+ }
322
+
323
+ // ============================================================================
324
+ // Coordinate helpers
325
+ // ============================================================================
326
+
327
+ export function gridCoordEquals(a: GridCoord, b: GridCoord): boolean {
328
+ return a.x === b.x && a.y === b.y
329
+ }
330
+
331
+ export function drawingCoordEquals(a: DrawingCoord, b: DrawingCoord): boolean {
332
+ return a.x === b.x && a.y === b.y
333
+ }
334
+
335
+ /** Apply a direction offset to a grid coordinate (move into the 3x3 block). */
336
+ export function gridCoordDirection(c: GridCoord, dir: Direction): GridCoord {
337
+ return { x: c.x + dir.x, y: c.y + dir.y }
338
+ }
339
+
340
+ /** Key for storing GridCoord in a Map. */
341
+ export function gridKey(c: GridCoord): string {
342
+ return `${c.x},${c.y}`
343
+ }
344
+
345
+ /**
346
+ * Get a node's grid coordinate. Every consumer of a node's `gridCoord` after
347
+ * layout (edge routing, edge bundling, drawing) runs strictly after
348
+ * `createMapping` (grid.ts) has placed every node, so this is always defined
349
+ * for a real graph — but that guarantee lives in layout's control flow, not
350
+ * in `AsciiNode`'s type (`gridCoord: GridCoord | null`), so it's narrowed
351
+ * here explicitly rather than trusted silently across the module boundary.
352
+ *
353
+ * Lives here (not in grid.ts, where it originated) because it's a pure
354
+ * predicate over `AsciiNode` with zero dependency on grid/layout state —
355
+ * keeping it here lets `draw-boxes.ts` and `draw-bundles.ts` reuse it
356
+ * without pulling in all of grid.ts's transitive imports.
357
+ */
358
+ export function requireGridCoord(node: AsciiNode): GridCoord {
359
+ const gc = node.gridCoord
360
+ if (gc === null) {
361
+ /* v8 ignore next */
362
+ throw new Error(
363
+ `Node "${node.name}" has no gridCoord; grid layout must run before it is read`,
364
+ )
365
+ }
366
+ return gc
367
+ }
368
+
369
+ /** Default empty style class. */
370
+ export const EMPTY_STYLE: AsciiStyleClass = { name: '', styles: {} }
371
+
372
+ // ============================================================================
373
+ // Character role types for colored output
374
+ // ============================================================================
375
+
376
+ /**
377
+ * Role of a character in the ASCII diagram, used for theming.
378
+ * Each role maps to a different color when colors are enabled.
379
+ */
380
+ export type CharRole =
381
+ | 'text' // Node labels, edge labels
382
+ | 'border' // Node box borders, subgraph borders
383
+ | 'line' // Edge lines (paths between nodes)
384
+ | 'arrow' // Arrowheads (▲▼◄► or ^v<>)
385
+ | 'corner' // Corner characters at path bends
386
+ | 'junction' // Junction characters (┬┴├┤ where edges meet boxes)
387
+
388
+ /**
389
+ * Role canvas — parallel to Canvas, tracks the role of each character.
390
+ * Same column-major structure: roleCanvas[x][y] gives the role at (x, y).
391
+ * null means the character has no role (whitespace).
392
+ */
393
+ export type RoleCanvas = (CharRole | null)[][]
394
+
395
+ /**
396
+ * Theme colors for ASCII output — hex color strings.
397
+ * Derived from the SVG theme system for visual consistency.
398
+ */
399
+ export interface AsciiTheme {
400
+ /** Text color (node labels, edge labels) */
401
+ fg: string
402
+ /** Box border color (node borders, subgraph borders) */
403
+ border: string
404
+ /** Edge line color (paths between nodes) */
405
+ line: string
406
+ /** Arrowhead color (▲▼◄► or ^v<>) */
407
+ arrow: string
408
+ /** Theme accent color (optional, used by xycharts for series 0) */
409
+ accent?: string
410
+ /** Background color (optional, used by xycharts for dark-mode-aware shading) */
411
+ bg?: string
412
+ /** Corner character color (optional, defaults to line) */
413
+ corner?: string
414
+ /** Junction character color (optional, defaults to border) */
415
+ junction?: string
416
+ }
417
+
418
+ /** Color mode for output. */
419
+ export type ColorMode =
420
+ | 'none' // No colors (plain text)
421
+ | 'ansi16' // 16-color ANSI (basic terminals)
422
+ | 'ansi256' // 256-color ANSI (xterm)
423
+ | 'truecolor' // 24-bit RGB (modern terminals)
424
+ | 'html' // HTML <span> tags with inline color styles (browsers)
425
+
426
+ // ============================================================================
427
+ // Edge bundling types
428
+ // ============================================================================
429
+
430
+ /**
431
+ * Edge bundle — groups edges that share a common source or target.
432
+ * Used to visually merge parallel links before they reach the shared node.
433
+ *
434
+ * For fan-in (A & B --> C): multiple sources converge to one target.
435
+ * For fan-out (A --> B & C): one source diverges to multiple targets.
436
+ */
437
+ export interface EdgeBundle {
438
+ /** Bundle type: fan-in = many→one, fan-out = one→many */
439
+ type: 'fan-in' | 'fan-out'
440
+ /** Edges in this bundle */
441
+ edges: AsciiEdge[]
442
+ /** The common node (target for fan-in, source for fan-out) */
443
+ sharedNode: AsciiNode
444
+ /** The non-shared nodes (sources for fan-in, targets for fan-out) */
445
+ otherNodes: AsciiNode[]
446
+ /** Junction point where edges merge/split — set during routing */
447
+ junctionPoint: GridCoord | null
448
+ /** Path from junction to shared node (drawn once for all edges) */
449
+ sharedPath: GridCoord[]
450
+ /** Direction when entering/exiting the junction */
451
+ junctionDir: Direction
452
+ /** Direction when entering/exiting the shared node */
453
+ sharedNodeDir: Direction
454
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * ASCII Rendering Validation Utilities
3
+ *
4
+ * Provides validation functions for ASCII diagram output,
5
+ * including diagonal line detection to ensure orthogonal-only routing.
6
+ */
7
+
8
+ /**
9
+ * Characters that represent diagonal lines in ASCII and Unicode modes.
10
+ * These should never appear in properly rendered diagrams.
11
+ */
12
+ export const DIAGONAL_CHARS = {
13
+ ascii: ['/', '\\'],
14
+ unicode: ['\u2571', '\u2572'], // ╱ ╲
15
+ all: ['/', '\\', '\u2571', '\u2572'],
16
+ } as const
17
+
18
+ /**
19
+ * Position of a diagonal character in ASCII output.
20
+ */
21
+ export interface DiagonalPosition {
22
+ line: number
23
+ col: number
24
+ char: string
25
+ }
26
+
27
+ /**
28
+ * Type guard narrowing a single character to one of `DIAGONAL_CHARS.all`.
29
+ */
30
+ function isDiagonalChar(
31
+ char: string,
32
+ ): char is (typeof DIAGONAL_CHARS.all)[number] {
33
+ return (DIAGONAL_CHARS.all as readonly string[]).includes(char)
34
+ }
35
+
36
+ /**
37
+ * Check if ASCII output contains any diagonal line characters.
38
+ * Returns true if diagonals are found (which is an error condition).
39
+ *
40
+ * @param asciiOutput - The rendered ASCII diagram string
41
+ * @returns true if diagonal characters are present, false otherwise
42
+ */
43
+ export function hasDiagonalLines(asciiOutput: string): boolean {
44
+ return DIAGONAL_CHARS.all.some((char) => asciiOutput.includes(char))
45
+ }
46
+
47
+ /**
48
+ * Find all diagonal line character positions in ASCII output.
49
+ * Useful for debugging when diagonals are detected.
50
+ *
51
+ * Skips diagonal characters that appear inside node labels (between box borders).
52
+ * This prevents false positives from labels like "feature/auth" or "release/1.0".
53
+ *
54
+ * @param asciiOutput - The rendered ASCII diagram string
55
+ * @returns Array of positions where diagonal characters were found
56
+ */
57
+ export function findDiagonalLines(asciiOutput: string): DiagonalPosition[] {
58
+ const positions: DiagonalPosition[] = []
59
+ const lines = asciiOutput.split('\n')
60
+
61
+ // Box-drawing characters that indicate node boundaries
62
+ const boxBorders = new Set(['│', '┤', '├', '║', '┃', '|'])
63
+
64
+ for (const [lineNum, line] of lines.entries()) {
65
+ // Find all box border positions in this line
66
+ const borderPositions: number[] = []
67
+ for (let col = 0; col < line.length; col++) {
68
+ if (boxBorders.has(line[col]!)) {
69
+ borderPositions.push(col)
70
+ }
71
+ }
72
+
73
+ for (let col = 0; col < line.length; col++) {
74
+ const char = line[col]!
75
+ if (isDiagonalChar(char)) {
76
+ // Check if this position is inside a node (between two box borders)
77
+ // Find the nearest borders before and after this position
78
+ let insideNode = false
79
+ for (let i = 0; i < borderPositions.length - 1; i++) {
80
+ const leftBorder = borderPositions[i]!
81
+ const rightBorder = borderPositions[i + 1]!
82
+ if (col > leftBorder && col < rightBorder) {
83
+ // This diagonal char is between two borders - likely inside a node label
84
+ insideNode = true
85
+ break
86
+ }
87
+ }
88
+
89
+ if (!insideNode) {
90
+ positions.push({
91
+ line: lineNum + 1, // 1-indexed for human readability
92
+ col: col + 1,
93
+ char,
94
+ })
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return positions
101
+ }
102
+
103
+ /**
104
+ * Assert that ASCII output contains no diagonal lines.
105
+ * Throws an error with detailed position information if diagonals are found.
106
+ *
107
+ * @param asciiOutput - The rendered ASCII diagram string
108
+ * @param context - Optional context string for error message (e.g., diagram name)
109
+ * @throws Error if diagonal characters are present
110
+ */
111
+ export function assertNoDiagonals(asciiOutput: string, context?: string): void {
112
+ if (!hasDiagonalLines(asciiOutput)) {
113
+ return
114
+ }
115
+
116
+ const positions = findDiagonalLines(asciiOutput)
117
+ const contextStr = context ? ` in "${context}"` : ''
118
+ const positionStr = positions
119
+ .map((p) => ` Line ${p.line}, Col ${p.col}: '${p.char}'`)
120
+ .join('\n')
121
+
122
+ throw new Error(
123
+ `Diagonal lines detected${contextStr}. ` +
124
+ `Edges must use orthogonal Manhattan routing (90° bends only).\n` +
125
+ `Found ${positions.length} diagonal character(s):\n${positionStr}`,
126
+ )
127
+ }
128
+
129
+ /**
130
+ * Position of an orphaned tee/junction character in ASCII output.
131
+ */
132
+ export interface OrphanedJunctionPosition {
133
+ line: number
134
+ col: number
135
+ char: string
136
+ }
137
+
138
+ /**
139
+ * Find "orphaned" tee/junction characters — a ├/┤/┬/┴ whose perpendicular
140
+ * arm (the part of the glyph that's supposed to come from a pre-existing
141
+ * border or line, not from the edge that merged into it) has nothing on
142
+ * either side. A genuine junction always has *something* immediately on
143
+ * both sides of that arm (a border character, another line, an arrowhead,
144
+ * etc.); a blank cell on both sides means the character was written as a
145
+ * junction without a real line to justify it (see issue #86).
146
+ *
147
+ * - ├ / ┤ carry a vertical arm, so both sides means the row above and below.
148
+ * - ┬ / ┴ carry a horizontal arm, so both sides means the column left and right.
149
+ *
150
+ * @param asciiOutput - The rendered ASCII diagram string
151
+ * @returns Array of positions where an orphaned junction was found
152
+ */
153
+ export function findOrphanedJunctions(
154
+ asciiOutput: string,
155
+ ): OrphanedJunctionPosition[] {
156
+ const lines = asciiOutput.split('\n')
157
+ const positions: OrphanedJunctionPosition[] = []
158
+
159
+ for (const [lineNum, line] of lines.entries()) {
160
+ for (let col = 0; col < line.length; col++) {
161
+ const char = line[col]!
162
+ if (char === '├' || char === '┤') {
163
+ const above = lines[lineNum - 1]?.[col] ?? ' '
164
+ const below = lines[lineNum + 1]?.[col] ?? ' '
165
+ if (above === ' ' && below === ' ') {
166
+ positions.push({ line: lineNum + 1, col: col + 1, char })
167
+ }
168
+ } else if (char === '┬' || char === '┴') {
169
+ const left = line[col - 1] ?? ' '
170
+ const right = line[col + 1] ?? ' '
171
+ if (left === ' ' && right === ' ') {
172
+ positions.push({ line: lineNum + 1, col: col + 1, char })
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ return positions
179
+ }
180
+
181
+ /**
182
+ * Check if ASCII output contains any orphaned tee/junction characters.
183
+ * Returns true if any are found (which is an error condition).
184
+ *
185
+ * @param asciiOutput - The rendered ASCII diagram string
186
+ */
187
+ export function hasOrphanedJunctions(asciiOutput: string): boolean {
188
+ return findOrphanedJunctions(asciiOutput).length > 0
189
+ }