@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,174 @@
1
+ // ============================================================================
2
+ // ASCII renderer — line drawing (8-directional lines on the canvas)
3
+ //
4
+ // Split out of draw.ts.
5
+ // ============================================================================
6
+
7
+ import type { Canvas, DrawingCoord, AsciiEdgeStyle } from './types.ts'
8
+ import {
9
+ Up,
10
+ Down,
11
+ Left,
12
+ Right,
13
+ UpperLeft,
14
+ UpperRight,
15
+ LowerLeft,
16
+ LowerRight,
17
+ } from './types.ts'
18
+ import { determineDirection, dirEquals } from './edge-routing.ts'
19
+ import { write } from './canvas.ts'
20
+
21
+ /**
22
+ * Line character sets for different edge styles.
23
+ * Each style has horizontal, vertical, and diagonal characters for both
24
+ * Unicode (box-drawing) and ASCII (basic punctuation) modes.
25
+ *
26
+ * Unicode dotted: ┄ (horizontal), ┆ (vertical) — U+2504, U+2506
27
+ * Unicode thick: ━ (horizontal), ┃ (vertical) — U+2501, U+2503
28
+ */
29
+ /**
30
+ * Line character sets for different edge styles.
31
+ * Only horizontal and vertical characters - no diagonals.
32
+ * All edges use orthogonal Manhattan routing (90° bends only).
33
+ */
34
+ const LINE_CHARS = {
35
+ solid: {
36
+ h: { unicode: '─', ascii: '-' },
37
+ v: { unicode: '│', ascii: '|' },
38
+ },
39
+ dotted: {
40
+ h: { unicode: '┄', ascii: '.' },
41
+ v: { unicode: '┆', ascii: ':' },
42
+ },
43
+ thick: {
44
+ h: { unicode: '━', ascii: '=' },
45
+ v: { unicode: '┃', ascii: '‖' },
46
+ },
47
+ /*
48
+ * `A ~~~ B` — an invisible link. It routes and reserves grid cells like any
49
+ * other edge (so the nodes it connects stay ranked apart), but every cell it
50
+ * would occupy is written as a space, leaving no visible line.
51
+ */
52
+ invisible: {
53
+ h: { unicode: ' ', ascii: ' ' },
54
+ v: { unicode: ' ', ascii: ' ' },
55
+ },
56
+ } as const
57
+
58
+ /**
59
+ * Draw a line between two drawing coordinates using orthogonal Manhattan routing.
60
+ * Returns the list of coordinates that were drawn on.
61
+ * offsetFrom/offsetTo control how many cells to skip at the start/end.
62
+ *
63
+ * All lines use 90° bends only - no diagonal lines are produced.
64
+ * For diagonal directions, uses horizontal-first routing (draws horizontal
65
+ * segment, then vertical segment).
66
+ */
67
+ export function drawLine(
68
+ canvas: Canvas,
69
+ from: DrawingCoord,
70
+ to: DrawingCoord,
71
+ offsetFrom: number,
72
+ offsetTo: number,
73
+ useAscii: boolean,
74
+ style: AsciiEdgeStyle = 'solid',
75
+ ): DrawingCoord[] {
76
+ const dir = determineDirection(from, to)
77
+ const drawnCoords: DrawingCoord[] = []
78
+
79
+ // Select character set based on style (horizontal and vertical only)
80
+ const chars = LINE_CHARS[style]
81
+ const hChar = useAscii ? chars.h.ascii : chars.h.unicode
82
+ const vChar = useAscii ? chars.v.ascii : chars.v.unicode
83
+
84
+ // Pure vertical directions
85
+ if (dirEquals(dir, Up)) {
86
+ for (let y = from.y - offsetFrom; y >= to.y - offsetTo; y--) {
87
+ drawnCoords.push({ x: from.x, y })
88
+ write(canvas, from.x, y, vChar)
89
+ }
90
+ } else if (dirEquals(dir, Down)) {
91
+ for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y++) {
92
+ drawnCoords.push({ x: from.x, y })
93
+ write(canvas, from.x, y, vChar)
94
+ }
95
+ }
96
+ // Pure horizontal directions
97
+ else if (dirEquals(dir, Left)) {
98
+ for (let x = from.x - offsetFrom; x >= to.x - offsetTo; x--) {
99
+ drawnCoords.push({ x, y: from.y })
100
+ write(canvas, x, from.y, hChar)
101
+ }
102
+ } else if (dirEquals(dir, Right)) {
103
+ for (let x = from.x + offsetFrom; x <= to.x + offsetTo; x++) {
104
+ drawnCoords.push({ x, y: from.y })
105
+ write(canvas, x, from.y, hChar)
106
+ }
107
+ }
108
+ // Diagonal directions: use Manhattan routing (horizontal-first, then vertical)
109
+ // UpperLeft: go left first, then up
110
+ else if (dirEquals(dir, UpperLeft)) {
111
+ // Horizontal segment: from.x -> to.x (going left)
112
+ for (let x = from.x - offsetFrom; x >= to.x; x--) {
113
+ drawnCoords.push({ x, y: from.y })
114
+ write(canvas, x, from.y, hChar)
115
+ }
116
+ // Vertical segment: from.y -> to.y (going up)
117
+ for (let y = from.y - 1; y >= to.y - offsetTo; y--) {
118
+ drawnCoords.push({ x: to.x, y })
119
+ write(canvas, to.x, y, vChar)
120
+ }
121
+ }
122
+ // UpperRight: go right first, then up
123
+ else if (dirEquals(dir, UpperRight)) {
124
+ // Horizontal segment: from.x -> to.x (going right)
125
+ for (let x = from.x + offsetFrom; x <= to.x; x++) {
126
+ drawnCoords.push({ x, y: from.y })
127
+ write(canvas, x, from.y, hChar)
128
+ }
129
+ // Vertical segment: from.y -> to.y (going up)
130
+ for (let y = from.y - 1; y >= to.y - offsetTo; y--) {
131
+ drawnCoords.push({ x: to.x, y })
132
+ write(canvas, to.x, y, vChar)
133
+ }
134
+ }
135
+ // LowerLeft: go left first, then down
136
+ else if (dirEquals(dir, LowerLeft)) {
137
+ // Horizontal segment: from.x -> to.x (going left)
138
+ for (let x = from.x - offsetFrom; x >= to.x; x--) {
139
+ drawnCoords.push({ x, y: from.y })
140
+ write(canvas, x, from.y, hChar)
141
+ }
142
+ // Vertical segment: from.y -> to.y (going down)
143
+ for (let y = from.y + 1; y <= to.y + offsetTo; y++) {
144
+ drawnCoords.push({ x: to.x, y })
145
+ write(canvas, to.x, y, vChar)
146
+ }
147
+ }
148
+ // LowerRight: go right first, then down
149
+ // Special case: if x difference is small (1), draw straight vertical at from.x
150
+ // This keeps edges visually aligned with the source node
151
+ else if (dirEquals(dir, LowerRight)) {
152
+ const dx = to.x - from.x
153
+ if (dx <= 1) {
154
+ // Draw vertical line at from.x (source's x-coordinate)
155
+ for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y++) {
156
+ drawnCoords.push({ x: from.x, y })
157
+ write(canvas, from.x, y, vChar)
158
+ }
159
+ } else {
160
+ // Horizontal segment: from.x -> to.x (going right)
161
+ for (let x = from.x + offsetFrom; x <= to.x; x++) {
162
+ drawnCoords.push({ x, y: from.y })
163
+ write(canvas, x, from.y, hChar)
164
+ }
165
+ // Vertical segment: from.y -> to.y (going down)
166
+ for (let y = from.y + 1; y <= to.y + offsetTo; y++) {
167
+ drawnCoords.push({ x: to.x, y })
168
+ write(canvas, to.x, y, vChar)
169
+ }
170
+ }
171
+ }
172
+
173
+ return drawnCoords
174
+ }
@@ -0,0 +1,108 @@
1
+ // ============================================================================
2
+ // ASCII renderer — subgraph drawing (border rectangle + label)
3
+ //
4
+ // Split out of draw.ts.
5
+ // ============================================================================
6
+
7
+ import type {
8
+ Canvas,
9
+ DrawingCoord,
10
+ AsciiGraph,
11
+ AsciiSubgraph,
12
+ } from './types.ts'
13
+ import { mkCanvas, write } from './canvas.ts'
14
+ import { splitLines } from './multiline-utils.ts'
15
+ import { displayWidth, toDisplayCells } from './display-width.ts'
16
+
17
+ /** Draw a subgraph border rectangle. */
18
+ export function drawSubgraphBox(sg: AsciiSubgraph, graph: AsciiGraph): Canvas {
19
+ const width = sg.maxX - sg.minX
20
+ const height = sg.maxY - sg.minY
21
+ if (width <= 0 || height <= 0) return mkCanvas(0, 0)
22
+
23
+ const from: DrawingCoord = { x: 0, y: 0 }
24
+ const to: DrawingCoord = { x: width, y: height }
25
+ const canvas = mkCanvas(width, height)
26
+
27
+ if (!graph.config.useAscii) {
28
+ for (let x = from.x + 1; x < to.x; x++) write(canvas, x, from.y, '─')
29
+ for (let x = from.x + 1; x < to.x; x++) write(canvas, x, to.y, '─')
30
+ for (let y = from.y + 1; y < to.y; y++) write(canvas, from.x, y, '│')
31
+ for (let y = from.y + 1; y < to.y; y++) write(canvas, to.x, y, '│')
32
+ write(canvas, from.x, from.y, '┌')
33
+ write(canvas, to.x, from.y, '┐')
34
+ write(canvas, from.x, to.y, '└')
35
+ write(canvas, to.x, to.y, '┘')
36
+ } else {
37
+ for (let x = from.x + 1; x < to.x; x++) write(canvas, x, from.y, '-')
38
+ for (let x = from.x + 1; x < to.x; x++) write(canvas, x, to.y, '-')
39
+ for (let y = from.y + 1; y < to.y; y++) write(canvas, from.x, y, '|')
40
+ for (let y = from.y + 1; y < to.y; y++) write(canvas, to.x, y, '|')
41
+ write(canvas, from.x, from.y, '+')
42
+ write(canvas, to.x, from.y, '+')
43
+ write(canvas, from.x, to.y, '+')
44
+ write(canvas, to.x, to.y, '+')
45
+ }
46
+
47
+ return canvas
48
+ }
49
+
50
+ /**
51
+ * A single cell of a subgraph title's own footprint — including cells whose
52
+ * character is a literal space (e.g. the space inside "US West Region").
53
+ * Callers use this to force those cells to win over whatever was already
54
+ * drawn underneath (see the caller in `draw.ts` for why: `mergeCanvases`
55
+ * treats overlay spaces as transparent, which lets a connector line drawn
56
+ * earlier show through a label's internal spaces — issue #447).
57
+ */
58
+ export interface SubgraphLabelCell {
59
+ x: number
60
+ y: number
61
+ }
62
+
63
+ /** Draw a subgraph label centered in its header area. Supports multi-line labels. */
64
+ // `_graph` isn't read here but is kept to match the `(sg, graph)` signature
65
+ // shared by the other `draw*` subgraph helpers in this file.
66
+ export function drawSubgraphLabel(
67
+ sg: AsciiSubgraph,
68
+ _graph: AsciiGraph,
69
+ ): [Canvas, DrawingCoord, SubgraphLabelCell[]] {
70
+ const width = sg.maxX - sg.minX
71
+ const height = sg.maxY - sg.minY
72
+ if (width <= 0 || height <= 0) return [mkCanvas(0, 0), { x: 0, y: 0 }, []]
73
+
74
+ const canvas = mkCanvas(width, height)
75
+ const footprint: SubgraphLabelCell[] = []
76
+
77
+ // Support multi-line subgraph labels
78
+ const lines = splitLines(sg.name)
79
+
80
+ // Start at row 1 inside subgraph, expand downward for multiple lines
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const line = lines[i]!
83
+ const labelY = 1 + i
84
+ // Center the label within the interior columns (1..width-1, i.e.
85
+ // excluding both border columns). When the interior width minus the
86
+ // label width is odd, the leftover column can't be split evenly — bias
87
+ // it to the right (keep >=1 space on the left) so the title never hugs
88
+ // the left border. Using floor(width/2) - floor(label/2) here instead
89
+ // biases the leftover to the left, which can zero out the left padding
90
+ // entirely for even-length labels in an odd-width interior.
91
+ let labelX = 1 + Math.ceil((width - 1 - displayWidth(line)) / 2)
92
+ if (labelX < 1) labelX = 1
93
+
94
+ // Unlike `write()`'s own bounds (inclusive of the canvas edge — correct
95
+ // for the border-drawing calls in `drawSubgraphBox` above, which write
96
+ // the border itself at x == width / y == height), the label must stay
97
+ // strictly inside the border, so this loop enforces the tighter,
98
+ // exclusive bound itself rather than relying on `write()`'s clip.
99
+ const cells = toDisplayCells(line)
100
+ for (let j = 0; j < cells.length; j++) {
101
+ if (labelX + j >= width || labelY >= height) continue
102
+ write(canvas, labelX + j, labelY, cells[j]!)
103
+ footprint.push({ x: labelX + j, y: labelY })
104
+ }
105
+ }
106
+
107
+ return [canvas, { x: sg.minX, y: sg.minY }, footprint]
108
+ }
package/src/draw.ts ADDED
@@ -0,0 +1,350 @@
1
+ // ============================================================================
2
+ // ASCII renderer — drawing operations
3
+ //
4
+ // Ported from AlexanderGrooff/mermaid-ascii cmd/draw.go + cmd/arrow.go.
5
+ // Contains the top-level draw orchestrator (drawGraph) that layers node,
6
+ // edge, bundle, and subgraph canvases together, plus the character-role
7
+ // tracking used for colored output.
8
+ //
9
+ // The individual drawing operations live in sibling modules, split out for
10
+ // navigability:
11
+ // - draw-boxes.ts node/box drawing, multi-section boxes
12
+ // - draw-lines.ts 8-directional line drawing
13
+ // - draw-arrows.ts arrow paths, corners, arrowheads, labels
14
+ // - draw-bundles.ts bundled (fan-in/fan-out) edge drawing
15
+ // - draw-subgraphs.ts subgraph border + label drawing
16
+ // This file re-exports their public entry points so `./draw.ts` remains a
17
+ // stable import path for the rest of the ASCII renderer.
18
+ // ============================================================================
19
+
20
+ import type {
21
+ Canvas,
22
+ DrawingCoord,
23
+ AsciiGraph,
24
+ AsciiSubgraph,
25
+ EdgeBundle,
26
+ } from './types.ts'
27
+ import { mergeCanvases, firstClaimWins, write } from './canvas.ts'
28
+ import type { RoleCanvas, CharRole } from './types.ts'
29
+ import { setRole } from './canvas.ts'
30
+ import { drawArrow } from './draw-arrows.ts'
31
+ import {
32
+ drawBundledEdgeSegment,
33
+ drawBundleSharedPath,
34
+ drawBundleArrowhead,
35
+ drawBundleArrowheadStart,
36
+ drawBundledEdgeArrowhead,
37
+ drawBundledEdgeArrowheadStart,
38
+ drawJunctionCharacter,
39
+ } from './draw-bundles.ts'
40
+ import { drawSubgraphBox, drawSubgraphLabel } from './draw-subgraphs.ts'
41
+
42
+ export {
43
+ drawNode,
44
+ drawBox,
45
+ drawMultiBox,
46
+ measureMultiBox,
47
+ classifyBoxChar,
48
+ } from './draw-boxes.ts'
49
+ export { drawLine } from './draw-lines.ts'
50
+ export { drawArrow, drawSubgraphBox, drawSubgraphLabel }
51
+
52
+ // ============================================================================
53
+ // Top-level draw orchestrator
54
+ // ============================================================================
55
+
56
+ /** Sort subgraphs by nesting depth (shallowest first) for correct layered rendering. */
57
+ function sortSubgraphsByDepth(subgraphs: AsciiSubgraph[]): AsciiSubgraph[] {
58
+ function getDepth(sg: AsciiSubgraph): number {
59
+ return sg.parent === null ? 0 : 1 + getDepth(sg.parent)
60
+ }
61
+ const sorted = [...subgraphs]
62
+ sorted.sort((a, b) => getDepth(a) - getDepth(b))
63
+ return sorted
64
+ }
65
+
66
+ // ============================================================================
67
+ // Role tracking helpers for colored output
68
+ // ============================================================================
69
+
70
+ /**
71
+ * Fill roles for all non-space characters in a canvas region.
72
+ * Used after drawing a layer to record what role those characters have.
73
+ */
74
+ function fillRolesFromCanvas(
75
+ roleCanvas: RoleCanvas,
76
+ canvas: Canvas,
77
+ offset: DrawingCoord,
78
+ role: CharRole,
79
+ ): void {
80
+ for (let x = 0; x < canvas.length; x++) {
81
+ for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
82
+ const char = canvas[x]?.[y]
83
+ if (char && char !== ' ') {
84
+ const rx = x + offset.x
85
+ const ry = y + offset.y
86
+ // Use setRole which auto-expands the role canvas if needed
87
+ if (rx >= 0 && ry >= 0) {
88
+ setRole(roleCanvas, rx, ry, role)
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Fill roles for multiple canvases with the same role.
97
+ */
98
+ function fillRolesFromCanvases(
99
+ roleCanvas: RoleCanvas,
100
+ canvases: Canvas[],
101
+ offset: DrawingCoord,
102
+ role: CharRole,
103
+ ): void {
104
+ for (const canvas of canvases) {
105
+ fillRolesFromCanvas(roleCanvas, canvas, offset, role)
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Special handling for node boxes: border chars get 'border' role, text gets 'text' role.
111
+ * Detects text by checking if character is alphanumeric or common punctuation.
112
+ */
113
+ function fillRolesForNodeBox(
114
+ roleCanvas: RoleCanvas,
115
+ canvas: Canvas,
116
+ offset: DrawingCoord,
117
+ ): void {
118
+ const isBorderChar = (c: string) => /^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|.':]$/.test(c)
119
+
120
+ for (let x = 0; x < canvas.length; x++) {
121
+ for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
122
+ const char = canvas[x]?.[y]
123
+ if (char && char !== ' ') {
124
+ const rx = x + offset.x
125
+ const ry = y + offset.y
126
+ // Use setRole which auto-expands the role canvas if needed
127
+ if (rx >= 0 && ry >= 0) {
128
+ setRole(roleCanvas, rx, ry, isBorderChar(char) ? 'border' : 'text')
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Main draw function — renders the entire graph onto the canvas.
137
+ * Drawing order matters for correct layering:
138
+ * 1. Subgraph borders (bottom layer)
139
+ * 2. Node boxes
140
+ * 3. Edge paths (lines)
141
+ * 4. Edge corners
142
+ * 5. Arrowheads
143
+ * 6. Box-start junctions
144
+ * 7. Edge labels
145
+ * 8. Subgraph labels (top layer)
146
+ *
147
+ * Also fills the roleCanvas with character roles for colored output.
148
+ */
149
+ export function drawGraph(graph: AsciiGraph): Canvas {
150
+ const useAscii = graph.config.useAscii
151
+ const zero: DrawingCoord = { x: 0, y: 0 }
152
+
153
+ // Draw subgraph borders
154
+ const sortedSgs = sortSubgraphsByDepth(graph.subgraphs)
155
+ for (const sg of sortedSgs) {
156
+ const sgCanvas = drawSubgraphBox(sg, graph)
157
+ const offset: DrawingCoord = { x: sg.minX, y: sg.minY }
158
+ graph.canvas = mergeCanvases(graph.canvas, offset, useAscii, sgCanvas)
159
+ // Subgraph borders get 'border' role
160
+ fillRolesFromCanvas(graph.roleCanvas, sgCanvas, offset, 'border')
161
+ }
162
+
163
+ // Draw node boxes
164
+ for (const node of graph.nodes) {
165
+ if (!node.drawn && node.drawingCoord && node.drawing) {
166
+ graph.canvas = mergeCanvases(
167
+ graph.canvas,
168
+ node.drawingCoord,
169
+ useAscii,
170
+ node.drawing,
171
+ )
172
+ // Node boxes: detect border vs text characters
173
+ fillRolesForNodeBox(graph.roleCanvas, node.drawing, node.drawingCoord)
174
+ node.drawn = true
175
+ }
176
+ }
177
+
178
+ // Collect all edge drawing layers
179
+ const lineCanvases: Canvas[] = []
180
+ const cornerCanvases: Canvas[] = []
181
+ const arrowHeadEndCanvases: Canvas[] = []
182
+ const arrowHeadStartCanvases: Canvas[] = []
183
+ const boxStartCanvases: Canvas[] = []
184
+ const labelCanvases: Canvas[] = []
185
+ const junctionCanvases: Canvas[] = []
186
+
187
+ // Track which bundles have been processed (to draw shared paths only once)
188
+ const processedBundles = new Set<EdgeBundle>()
189
+
190
+ for (const edge of graph.edges) {
191
+ // Handle bundled edges specially
192
+ if (edge.bundle && edge.pathToJunction) {
193
+ const bundle = edge.bundle
194
+
195
+ // Draw this edge's individual path (source → junction for fan-in, junction → target for fan-out)
196
+ const [pathC, boxStartC, , , cornersC, labelC] = drawBundledEdgeSegment(
197
+ graph,
198
+ edge,
199
+ bundle,
200
+ )
201
+ lineCanvases.push(pathC)
202
+ cornerCanvases.push(cornersC)
203
+ boxStartCanvases.push(boxStartC)
204
+ labelCanvases.push(labelC)
205
+
206
+ // For fan-in bundles, draw a start arrowhead at each individual source
207
+ if (bundle.type === 'fan-in' && edge.hasArrowStart) {
208
+ const arrowHeadStartC = drawBundledEdgeArrowheadStart(graph, edge)
209
+ arrowHeadStartCanvases.push(arrowHeadStartC)
210
+ }
211
+
212
+ // Draw the bundle's shared path and arrowhead only once
213
+ if (!processedBundles.has(bundle)) {
214
+ processedBundles.add(bundle)
215
+
216
+ // Draw shared path (junction → target for fan-in, source → junction for fan-out)
217
+ const [sharedPathC, sharedCornersC] = drawBundleSharedPath(
218
+ graph,
219
+ bundle,
220
+ )
221
+ lineCanvases.push(sharedPathC)
222
+ cornerCanvases.push(sharedCornersC)
223
+
224
+ // Draw arrowhead at target for fan-in (once for all edges in bundle)
225
+ if (bundle.type === 'fan-in') {
226
+ const arrowHeadC = drawBundleArrowhead(graph, bundle)
227
+ arrowHeadEndCanvases.push(arrowHeadC)
228
+ }
229
+
230
+ // Draw start arrowhead at the shared source for fan-out (once for all
231
+ // edges in the bundle — the trunk leaving the source is drawn once,
232
+ // so it only gets a start arrowhead when every edge agrees on one).
233
+ if (
234
+ bundle.type === 'fan-out' &&
235
+ bundle.edges.every((e) => e.hasArrowStart)
236
+ ) {
237
+ const arrowHeadStartC = drawBundleArrowheadStart(graph, bundle)
238
+ arrowHeadStartCanvases.push(arrowHeadStartC)
239
+ }
240
+
241
+ // Draw junction character
242
+ const junctionC = drawJunctionCharacter(graph, bundle)
243
+ junctionCanvases.push(junctionC)
244
+ }
245
+
246
+ // For fan-out bundles, draw arrowhead at each target
247
+ if (bundle.type === 'fan-out' && edge.hasArrowEnd) {
248
+ const arrowHeadC = drawBundledEdgeArrowhead(graph, edge)
249
+ arrowHeadEndCanvases.push(arrowHeadC)
250
+ }
251
+ } else {
252
+ // Non-bundled edge: use standard drawing
253
+ const [
254
+ pathC,
255
+ boxStartC,
256
+ arrowHeadEndC,
257
+ arrowHeadStartC,
258
+ cornersC,
259
+ labelC,
260
+ ] = drawArrow(graph, edge)
261
+ lineCanvases.push(pathC)
262
+ cornerCanvases.push(cornersC)
263
+ arrowHeadEndCanvases.push(arrowHeadEndC)
264
+ arrowHeadStartCanvases.push(arrowHeadStartC)
265
+ boxStartCanvases.push(boxStartC)
266
+ labelCanvases.push(labelC)
267
+ }
268
+ }
269
+
270
+ // Merge edge layers in order and track roles
271
+ // Note: arrowHeadStart is merged AFTER boxStart so bidirectional arrows
272
+ // properly overwrite the box connector at the source end
273
+ //
274
+ // `firstClaimWins` resolves any leftover cross-style overlap between two
275
+ // edges' own line canvases (see its doc) before they're merged onto the
276
+ // shared canvas — applied here, and not to the other edge layers below,
277
+ // because only the line layer has this "first claim wins" rule; corners,
278
+ // arrowheads, box-start connectors and labels keep mergeCanvases' normal
279
+ // last-overlay-wins behavior.
280
+ const resolvedLineCanvases = firstClaimWins(lineCanvases)
281
+ graph.canvas = mergeCanvases(
282
+ graph.canvas,
283
+ zero,
284
+ useAscii,
285
+ ...resolvedLineCanvases,
286
+ )
287
+ fillRolesFromCanvases(graph.roleCanvas, resolvedLineCanvases, zero, 'line')
288
+
289
+ graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...cornerCanvases)
290
+ fillRolesFromCanvases(graph.roleCanvas, cornerCanvases, zero, 'corner')
291
+
292
+ graph.canvas = mergeCanvases(
293
+ graph.canvas,
294
+ zero,
295
+ useAscii,
296
+ ...junctionCanvases,
297
+ )
298
+ fillRolesFromCanvases(graph.roleCanvas, junctionCanvases, zero, 'junction')
299
+
300
+ graph.canvas = mergeCanvases(
301
+ graph.canvas,
302
+ zero,
303
+ useAscii,
304
+ ...arrowHeadEndCanvases,
305
+ )
306
+ fillRolesFromCanvases(graph.roleCanvas, arrowHeadEndCanvases, zero, 'arrow')
307
+
308
+ graph.canvas = mergeCanvases(
309
+ graph.canvas,
310
+ zero,
311
+ useAscii,
312
+ ...boxStartCanvases,
313
+ )
314
+ fillRolesFromCanvases(graph.roleCanvas, boxStartCanvases, zero, 'junction')
315
+
316
+ graph.canvas = mergeCanvases(
317
+ graph.canvas,
318
+ zero,
319
+ useAscii,
320
+ ...arrowHeadStartCanvases,
321
+ )
322
+ fillRolesFromCanvases(graph.roleCanvas, arrowHeadStartCanvases, zero, 'arrow')
323
+
324
+ graph.canvas = mergeCanvases(graph.canvas, zero, useAscii, ...labelCanvases)
325
+ fillRolesFromCanvases(graph.roleCanvas, labelCanvases, zero, 'text')
326
+
327
+ // Draw subgraph labels last (on top)
328
+ for (const sg of graph.subgraphs) {
329
+ if (sg.nodes.length === 0) continue
330
+ const [labelCanvas, offset, footprint] = drawSubgraphLabel(sg, graph)
331
+ graph.canvas = mergeCanvases(graph.canvas, offset, useAscii, labelCanvas)
332
+ fillRolesFromCanvas(graph.roleCanvas, labelCanvas, offset, 'text')
333
+ // mergeCanvases treats an overlay's space characters as transparent (so
334
+ // sparse edge/arrow layers don't blank each other out) — but the title's
335
+ // own footprint should win outright, including cells whose character is
336
+ // a literal space (e.g. "US West Region"). Otherwise a connector line
337
+ // already drawn at that cell shows through the label's internal space
338
+ // (issue #447: rendered as "US West│Region"). Force those specific
339
+ // cells, bypassing mergeCanvases's space-skip.
340
+ for (const { x, y } of footprint) {
341
+ const ch = labelCanvas[x]?.[y]
342
+ if (ch === ' ') {
343
+ write(graph.canvas, x + offset.x, y + offset.y, ch)
344
+ setRole(graph.roleCanvas, x + offset.x, y + offset.y, 'text')
345
+ }
346
+ }
347
+ }
348
+
349
+ return graph.canvas
350
+ }