@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,267 @@
1
+ // ============================================================================
2
+ // ASCII renderer — node/box drawing
3
+ //
4
+ // Split out of draw.ts. Renders individual node shapes and the
5
+ // multi-section boxes used by class/ER diagram nodes.
6
+ // ============================================================================
7
+
8
+ import type {
9
+ Canvas,
10
+ DrawingCoord,
11
+ AsciiGraph,
12
+ AsciiNode,
13
+ CharRole,
14
+ } from './types.ts'
15
+ import { requireGridCoord } from './types.ts'
16
+ import { mkCanvas, write } from './canvas.ts'
17
+ import { splitLines } from './multiline-utils.ts'
18
+ import { getCorners } from './shapes/corners.ts'
19
+ import { displayWidth, toDisplayCells } from './display-width.ts'
20
+
21
+ // ============================================================================
22
+ // Node drawing — renders a node using shape-aware rendering
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Draw a node using its shape type.
27
+ * Returns a standalone canvas containing the rendered shape.
28
+ *
29
+ * For basic shapes (rectangle, rounded), uses grid-determined dimensions
30
+ * to ensure consistent sizing across nodes in the same column.
31
+ * For special shapes (diamond, circle, state pseudo-states, etc.),
32
+ * uses shape-specific dimension calculation but centers the content
33
+ * within the grid cell dimensions to ensure proper vertical alignment.
34
+ */
35
+ export function drawNode(node: AsciiNode, graph: AsciiGraph): Canvas {
36
+ // All shapes use grid-determined dimensions to fill their allocated space.
37
+ // This ensures consistent sizing across nodes and eliminates gaps between
38
+ // nodes and subgraph borders. All shapes are rectangles with distinctive
39
+ // corner characters (defined in corners.ts) to indicate shape type.
40
+ return drawBoxWithGridDimensions(node, graph)
41
+ }
42
+
43
+ /**
44
+ * Draw a box shape using grid-determined dimensions.
45
+ * This ensures consistent sizing when multiple nodes share a column,
46
+ * and eliminates gaps between nodes and subgraph borders by filling
47
+ * the entire allocated grid space.
48
+ *
49
+ * All shapes are rendered as rectangles with distinctive corner characters
50
+ * (defined in corners.ts) to indicate shape type.
51
+ */
52
+ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
53
+ // Every node is guaranteed a gridCoord before drawing starts —
54
+ // createMapping (grid.ts) seeds even nodes in otherwise-unreachable
55
+ // cycles with a pseudo-root (see addPseudoRootsForUnreachableCycles)
56
+ // specifically so every node gets placed. requireGridCoord validates that
57
+ // invariant explicitly rather than trusting it silently across the module
58
+ // boundary, since the type checker can't see it (`gridCoord: GridCoord |
59
+ // null`).
60
+ const gc = requireGridCoord(node)
61
+ const useAscii = graph.config.useAscii
62
+
63
+ // Width spans 2 columns (border + content) - matching original behavior
64
+ let w = 0
65
+ for (let i = 0; i < 2; i++) {
66
+ w += graph.columnWidth.get(gc.x + i) ?? 0
67
+ }
68
+ // Height spans 2 rows (border + content)
69
+ let h = 0
70
+ for (let i = 0; i < 2; i++) {
71
+ h += graph.rowHeight.get(gc.y + i) ?? 0
72
+ }
73
+
74
+ const from: DrawingCoord = { x: 0, y: 0 }
75
+ const to: DrawingCoord = { x: w, y: h }
76
+ const box = mkCanvas(Math.max(from.x, to.x), Math.max(from.y, to.y))
77
+
78
+ // Get corner characters for this shape type
79
+ const corners = getCorners(node.shape, useAscii)
80
+
81
+ // State-end uses double border to differentiate from state-start
82
+ const isDoubleBox = node.shape === 'state-end'
83
+ const hChar = useAscii ? (isDoubleBox ? '=' : '-') : isDoubleBox ? '═' : '─'
84
+ const vChar = useAscii ? (isDoubleBox ? '‖' : '|') : isDoubleBox ? '║' : '│'
85
+
86
+ // Double-box corners (for state-end)
87
+ const doubleCorners = useAscii
88
+ ? { tl: '#', tr: '#', bl: '#', br: '#' }
89
+ : { tl: '╔', tr: '╗', bl: '╚', br: '╝' }
90
+ const effectiveCorners = isDoubleBox ? doubleCorners : corners
91
+
92
+ // Draw box border with shape-specific corners
93
+ for (let x = from.x + 1; x < to.x; x++) write(box, x, from.y, hChar)
94
+ for (let x = from.x + 1; x < to.x; x++) write(box, x, to.y, hChar)
95
+ for (let y = from.y + 1; y < to.y; y++) write(box, from.x, y, vChar)
96
+ for (let y = from.y + 1; y < to.y; y++) write(box, to.x, y, vChar)
97
+ write(box, from.x, from.y, effectiveCorners.tl)
98
+ write(box, to.x, from.y, effectiveCorners.tr)
99
+ write(box, from.x, to.y, effectiveCorners.bl)
100
+ write(box, to.x, to.y, effectiveCorners.br)
101
+
102
+ // Center the multi-line display label inside the box
103
+ const label = node.displayLabel
104
+ const lines = splitLines(label)
105
+ const textCenterY = from.y + Math.floor(h / 2)
106
+ const startY = textCenterY - Math.floor((lines.length - 1) / 2)
107
+
108
+ for (let i = 0; i < lines.length; i++) {
109
+ const line = lines[i]!
110
+ const lineWidth = displayWidth(line)
111
+ const textX = from.x + Math.floor(w / 2) - Math.ceil(lineWidth / 2) + 1
112
+ const cells = toDisplayCells(line)
113
+ for (let j = 0; j < cells.length; j++) {
114
+ write(box, textX + j, startY + i, cells[j]!)
115
+ }
116
+ }
117
+
118
+ return box
119
+ }
120
+
121
+ /**
122
+ * Draw a node box with centered label text.
123
+ * Returns a standalone canvas containing just the box.
124
+ * Box size is determined by the grid column/row sizes for the node's position.
125
+ */
126
+ export function drawBox(node: AsciiNode, graph: AsciiGraph): Canvas {
127
+ return drawNode(node, graph)
128
+ }
129
+
130
+ // ============================================================================
131
+ // Multi-section box drawing — for class and ER diagram nodes
132
+ // ============================================================================
133
+
134
+ /**
135
+ * Classify a character from a multi-section box drawing as 'border' or 'text'.
136
+ * Shared by the class and ER diagram ASCII renderers, which both copy a
137
+ * `drawMultiBox` canvas onto their main canvas and need to tag each
138
+ * non-blank cell with its role for colored output.
139
+ */
140
+ export function classifyBoxChar(ch: string): CharRole {
141
+ if (/^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|]$/.test(ch)) return 'border'
142
+ return 'text'
143
+ }
144
+
145
+ /**
146
+ * Outer dimensions, in grid cells, of the box `drawMultiBox` would draw for
147
+ * `sections`.
148
+ *
149
+ * Class and ER diagrams both need these dimensions *before* drawing, to
150
+ * reserve grid space for the box during layout. Deriving them here — rather
151
+ * than each caller re-implementing the same arithmetic — keeps the reserved
152
+ * space and the drawn box in lockstep. When the two disagree (as they did
153
+ * while the callers measured `line.length` and this module measured display
154
+ * width), wide-character boxes overlap their neighbours.
155
+ */
156
+ export function measureMultiBox(
157
+ sections: string[][],
158
+ padding: number = 1,
159
+ ): { width: number; height: number } {
160
+ // A caller-supplied padding (ultimately AsciiConfig.boxBorderPadding, which
161
+ // reaches here unclamped from the public renderMermaidASCII options) could
162
+ // be negative — clamp rather than let it produce a negative box width for
163
+ // a short label (see issue #343's CodeRabbit review).
164
+ const clampedPadding = Math.max(0, padding)
165
+ // Width: widest line across all sections + 2*padding + 2 border chars.
166
+ // displayWidth (not line.length) because CJK/fullwidth glyphs occupy two
167
+ // terminal columns each, so a code-unit count under-measures them.
168
+ let maxTextWidth = 0
169
+ for (const section of sections) {
170
+ for (const line of section) {
171
+ maxTextWidth = Math.max(maxTextWidth, displayWidth(line))
172
+ }
173
+ }
174
+
175
+ // Height: sum of all section line counts + dividers + 2 border rows
176
+ let totalLines = 0
177
+ for (const section of sections) {
178
+ totalLines += Math.max(section.length, 1) // at least 1 row per section
179
+ }
180
+
181
+ return {
182
+ width: maxTextWidth + 2 * clampedPadding + 2,
183
+ height: totalLines + (sections.length - 1) + 2,
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Draw a multi-section box with horizontal dividers between sections.
189
+ * Used by class diagrams (header | attributes | methods) and ER diagrams (header | attributes).
190
+ * Each section is an array of text lines to render left-aligned with padding.
191
+ *
192
+ * @param sections - Array of sections, each section is an array of text lines
193
+ * @param useAscii - true for ASCII chars, false for Unicode box-drawing
194
+ * @param padding - horizontal padding inside the box (default 1)
195
+ * @returns A standalone Canvas containing the multi-section box
196
+ */
197
+ export function drawMultiBox(
198
+ sections: string[][],
199
+ useAscii: boolean,
200
+ padding: number = 1,
201
+ ): Canvas {
202
+ // Clamp here too (not just inside measureMultiBox) since `padding` is also
203
+ // used directly below for each line's startX — see the comment in
204
+ // measureMultiBox for why a negative value can reach this function.
205
+ const clampedPadding = Math.max(0, padding)
206
+ const { width: boxWidth, height: boxHeight } = measureMultiBox(
207
+ sections,
208
+ clampedPadding,
209
+ )
210
+
211
+ // Box-drawing characters
212
+ const hLine = useAscii ? '-' : '─'
213
+ const vLine = useAscii ? '|' : '│'
214
+ const tl = useAscii ? '+' : '┌'
215
+ const tr = useAscii ? '+' : '┐'
216
+ const bl = useAscii ? '+' : '└'
217
+ const br = useAscii ? '+' : '┘'
218
+ const divL = useAscii ? '+' : '├'
219
+ const divR = useAscii ? '+' : '┤'
220
+
221
+ const canvas = mkCanvas(boxWidth - 1, boxHeight - 1)
222
+
223
+ // Top border
224
+ write(canvas, 0, 0, tl)
225
+ for (let x = 1; x < boxWidth - 1; x++) write(canvas, x, 0, hLine)
226
+ write(canvas, boxWidth - 1, 0, tr)
227
+
228
+ // Bottom border
229
+ write(canvas, 0, boxHeight - 1, bl)
230
+ for (let x = 1; x < boxWidth - 1; x++) write(canvas, x, boxHeight - 1, hLine)
231
+ write(canvas, boxWidth - 1, boxHeight - 1, br)
232
+
233
+ // Left and right borders (full height)
234
+ for (let y = 1; y < boxHeight - 1; y++) {
235
+ write(canvas, 0, y, vLine)
236
+ write(canvas, boxWidth - 1, y, vLine)
237
+ }
238
+
239
+ // Render sections with dividers
240
+ let row = 1 // current y position (starts after top border)
241
+ for (let s = 0; s < sections.length; s++) {
242
+ const section = sections[s]!
243
+ const lines = section.length > 0 ? section : ['']
244
+
245
+ // Draw section text lines. toDisplayCells (not line[i]) so a wide glyph
246
+ // claims the two grid cells it renders across, and a surrogate pair is
247
+ // never split across two cells.
248
+ for (const line of lines) {
249
+ const startX = 1 + clampedPadding
250
+ const cells = toDisplayCells(line)
251
+ for (let i = 0; i < cells.length; i++) {
252
+ write(canvas, startX + i, row, cells[i]!)
253
+ }
254
+ row++
255
+ }
256
+
257
+ // Draw divider after each section except the last
258
+ if (s < sections.length - 1) {
259
+ write(canvas, 0, row, divL)
260
+ for (let x = 1; x < boxWidth - 1; x++) write(canvas, x, row, hLine)
261
+ write(canvas, boxWidth - 1, row, divR)
262
+ row++
263
+ }
264
+ }
265
+
266
+ return canvas
267
+ }