@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,458 @@
1
+ // ============================================================================
2
+ // ASCII ER diagram relationship-label/box-corruption regression tests
3
+ // (GitHub issue #350)
4
+ //
5
+ // erDiagram relationship lines and labels were drawn without checking
6
+ // whether the straight-line path they took actually stayed clear of other
7
+ // entities' boxes. Two entities placed in the same row of the layout grid
8
+ // were always connected with a straight horizontal line even when a third
9
+ // entity sat physically between them (e.g. ORDER↔SHIPMENT with LINE_ITEM in
10
+ // between), so the line and its label were composited directly on top of
11
+ // that third entity's border and attribute text — a well-formed-looking but
12
+ // wrong attribute name (e.g. "int totalCents" silently became
13
+ // "int hasalCents"). A parallel bug existed for vertical connections: the
14
+ // horizontal jog between two entities in different rows used the naive
15
+ // vertical midpoint, which could still land inside a row-mate that was
16
+ // taller than the connection's own upper entity.
17
+ //
18
+ // The fix makes relationship routing obstruction-aware (detour around a
19
+ // row-mate that sits between two connected entities; clamp the vertical
20
+ // jog into the row-gap band that's actually free of every entity) and adds
21
+ // a last-resort occupancy guard so a relationship line, marker, or label
22
+ // can never overwrite a cell already reserved by an entity box or by an
23
+ // earlier relationship's label — see setCGuarded in src/ascii/er-diagram.ts.
24
+ // ============================================================================
25
+
26
+ import { describe, it, expect } from 'vitest'
27
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
28
+
29
+ /** Assert every one of `needles` appears verbatim, exactly once, in `text`. */
30
+ function expectAllPresentOnce(text: string, needles: string[]): void {
31
+ for (const needle of needles) {
32
+ const count = text.split(needle).length - 1
33
+ expect(count, `expected "${needle}" to appear exactly once`).toBe(1)
34
+ }
35
+ }
36
+
37
+ // The exact minimal repro from issue #350.
38
+ const REPRO_MINIMAL = `erDiagram
39
+ ORDER ||--|{ LINE_ITEM : contains
40
+ ORDER ||--o| SHIPMENT : fulfilled_by
41
+ PRODUCT ||--o{ LINE_ITEM : appears_in
42
+ WAREHOUSE ||--o{ SHIPMENT : dispatches
43
+ ORDER {
44
+ string id
45
+ string customerId
46
+ int totalCents
47
+ string status
48
+ }
49
+ LINE_ITEM {
50
+ string orderId
51
+ string productId
52
+ int quantity
53
+ int unitPriceCents
54
+ }
55
+ PRODUCT {
56
+ string sku
57
+ string title
58
+ int priceCents
59
+ }
60
+ `
61
+
62
+ // The fuller repro from issue #350 — more entities, more relationships
63
+ // crowding the same rows, including two labels ("has" / "fulfilled_by")
64
+ // that both need to route through the same row-gap band.
65
+ const REPRO_FULL = `erDiagram
66
+ CUSTOMER ||--o{ ORDER : places
67
+ CUSTOMER ||--o{ ADDRESS : has
68
+ ORDER ||--|{ LINE_ITEM : contains
69
+ ORDER ||--o| SHIPMENT : fulfilled_by
70
+ PRODUCT ||--o{ LINE_ITEM : appears_in
71
+ PRODUCT }o--|| CATEGORY : belongs_to
72
+ WAREHOUSE ||--o{ SHIPMENT : dispatches
73
+
74
+ CUSTOMER {
75
+ string id
76
+ string email
77
+ string name
78
+ date createdAt
79
+ }
80
+ ORDER {
81
+ string id
82
+ string customerId
83
+ int totalCents
84
+ string status
85
+ }
86
+ LINE_ITEM {
87
+ string orderId
88
+ string productId
89
+ int quantity
90
+ int unitPriceCents
91
+ }
92
+ PRODUCT {
93
+ string sku
94
+ string title
95
+ int priceCents
96
+ }
97
+ `
98
+
99
+ describe('ASCII ER relationship labels never corrupt entity boxes (issue #350)', () => {
100
+ it('renders every attribute name verbatim in the minimal repro', () => {
101
+ const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
102
+ expectAllPresentOnce(ascii, [
103
+ 'string id',
104
+ 'string customerId',
105
+ 'int totalCents',
106
+ 'string status',
107
+ 'string orderId',
108
+ 'string productId',
109
+ 'int quantity',
110
+ 'int unitPriceCents',
111
+ 'string sku',
112
+ 'string title',
113
+ 'int priceCents',
114
+ ])
115
+ })
116
+
117
+ it('renders every relationship label in full in the minimal repro', () => {
118
+ const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
119
+ expect(ascii).toContain('contains')
120
+ expect(ascii).toContain('fulfilled_by')
121
+ expect(ascii).toContain('appears_in')
122
+ expect(ascii).toContain('dispatches')
123
+ // The original bug doubled a trailing character when a label was
124
+ // partially overwritten (e.g. "fulfilled_byy").
125
+ expect(ascii).not.toContain('fulfilled_byy')
126
+ })
127
+
128
+ it('does not produce the specific corrupted values from issue #350', () => {
129
+ const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
130
+ // "int unitPriceCents" (LINE_ITEM) must never read as though a label
131
+ // overwrote part of it, and "productId"/"quantity" must survive.
132
+ expect(ascii).not.toContain('fulfilled_byy')
133
+ expect(ascii).toContain('productId')
134
+ expect(ascii).toContain('quantity')
135
+ })
136
+
137
+ it('renders every attribute name verbatim in the fuller repro', () => {
138
+ const ascii = renderMermaidASCII(REPRO_FULL, { colorMode: 'none' })
139
+ // "string id" is intentionally excluded here — CUSTOMER and ORDER both
140
+ // declare it, so it legitimately appears twice.
141
+ expectAllPresentOnce(ascii, [
142
+ 'string email',
143
+ 'string name',
144
+ 'date createdAt',
145
+ 'string customerId',
146
+ 'int totalCents',
147
+ 'string status',
148
+ 'string orderId',
149
+ 'string productId',
150
+ 'int quantity',
151
+ 'int unitPriceCents',
152
+ 'string sku',
153
+ 'string title',
154
+ 'int priceCents',
155
+ ])
156
+ // The issue's headline corruption: "int totalCents" silently became
157
+ // "int hasalCents" when the "has" label (CUSTOMER↔ADDRESS, skipping
158
+ // over ORDER) was composited into ORDER's box.
159
+ expect(ascii).not.toContain('hasalCents')
160
+ })
161
+
162
+ it('renders every relationship label in full in the fuller repro', () => {
163
+ const ascii = renderMermaidASCII(REPRO_FULL, { colorMode: 'none' })
164
+ expect(ascii).toContain('places')
165
+ expect(ascii).toContain('has')
166
+ expect(ascii).toContain('contains')
167
+ expect(ascii).toContain('fulfilled_by')
168
+ expect(ascii).toContain('appears_in')
169
+ expect(ascii).toContain('belongs_to')
170
+ expect(ascii).toContain('dispatches')
171
+ })
172
+
173
+ it('keeps every entity box border fully intact (no line/label punched through a border)', () => {
174
+ const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
175
+ const lines = ascii.split('\n')
176
+ // Every box's header row (e.g. "│ LINE_ITEM ") must still open with the
177
+ // left border directly followed by a space, never a relationship glyph.
178
+ for (const name of [
179
+ 'ORDER',
180
+ 'LINE_ITEM',
181
+ 'SHIPMENT',
182
+ 'PRODUCT',
183
+ 'WAREHOUSE',
184
+ ]) {
185
+ const headerLine = lines.find((l) => l.includes(`│ ${name}`))
186
+ expect(headerLine, `expected a header line for ${name}`).toBeDefined()
187
+ }
188
+ })
189
+
190
+ it('detects attribute corruption the same way the issue does: every declared attribute name must appear verbatim', () => {
191
+ // Mirrors the issue's own "Detection" section: erDiagram attribute names
192
+ // are known at parse time, so corruption shows up as a missing name.
193
+ const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
194
+ const declaredAttributeNames = [
195
+ 'id',
196
+ 'customerId',
197
+ 'totalCents',
198
+ 'status',
199
+ 'orderId',
200
+ 'productId',
201
+ 'quantity',
202
+ 'unitPriceCents',
203
+ 'sku',
204
+ 'title',
205
+ 'priceCents',
206
+ ]
207
+ for (const name of declaredAttributeNames) {
208
+ expect(
209
+ ascii,
210
+ `expected declared attribute "${name}" to survive`,
211
+ ).toContain(name)
212
+ }
213
+ })
214
+ })
215
+
216
+ describe('ASCII ER relationship label routing — adjacent-entity regression (issue #350)', () => {
217
+ it('keeps a short label between two adjacent entities compact (no unnecessary detour)', () => {
218
+ const ascii = renderMermaidASCII(
219
+ `erDiagram
220
+ A ||--o{ B : x`,
221
+ { colorMode: 'none' },
222
+ )
223
+ const lines = ascii.split('\n')
224
+ const line = lines.find((l) => l.includes('A') && l.includes('│'))
225
+ expect(line).toBeDefined()
226
+ expect(line!.length).toBeLessThan(30)
227
+ expect(ascii).toContain('x')
228
+ })
229
+
230
+ it('renders a normal two-entity diagram exactly as before (direct line, no detour)', () => {
231
+ const ascii = renderMermaidASCII(
232
+ `erDiagram
233
+ CUSTOMER ||--o{ ORDER : places`,
234
+ { colorMode: 'none' },
235
+ )
236
+ // No third entity to dodge, so the connector should sit on a single row
237
+ // directly below the connecting line — not detoured further down.
238
+ const lines = ascii.split('\n')
239
+ const placesIdx = lines.findIndex((l) => l.includes('places'))
240
+ const connectorIdx = lines.findIndex(
241
+ (l) => l.includes('○') || l.includes('╢') || l.includes('╟'),
242
+ )
243
+ expect(placesIdx).toBeGreaterThan(-1)
244
+ expect(connectorIdx).toBeGreaterThan(-1)
245
+ // The label sits exactly one row below the connector row in the direct
246
+ // (undetoured) case.
247
+ expect(placesIdx).toBe(connectorIdx + 1)
248
+ })
249
+
250
+ it('renders three entities in a row with adjacent-only relationships without detouring', () => {
251
+ // B sits between A and C, but the only relationships are A-B and B-C —
252
+ // both adjacent pairs, so neither needs to route around anything.
253
+ const ascii = renderMermaidASCII(
254
+ `erDiagram
255
+ A ||--o{ B : ab
256
+ B ||--o{ C : bc`,
257
+ { colorMode: 'none' },
258
+ )
259
+ expect(ascii).toContain('ab')
260
+ expect(ascii).toContain('bc')
261
+ })
262
+ })
263
+
264
+ describe('ASCII ER relationship label routing — long labels and crowded areas (issue #350)', () => {
265
+ it('routes a long label between two non-adjacent same-row entities around the entity between them', () => {
266
+ // ORDER and SHIPMENT are placed in the same row with LINE_ITEM between
267
+ // them (5-entity component ⇒ 3 columns per row); "fulfilled_by" must
268
+ // detour around LINE_ITEM rather than being written into its box.
269
+ const ascii = renderMermaidASCII(REPRO_MINIMAL, { colorMode: 'none' })
270
+ expect(ascii).toContain('fulfilled_by')
271
+ expect(ascii).toContain('string productId')
272
+ expect(ascii).toContain('int quantity')
273
+ })
274
+
275
+ it('keeps multiple relationship labels distinct when several need to route through the same row-gap band', () => {
276
+ // In REPRO_FULL, CUSTOMER↔ADDRESS ("has") and ORDER↔SHIPMENT
277
+ // ("fulfilled_by") both detour through the row-gap band below
278
+ // CUSTOMER/ORDER/ADDRESS's row. Neither should silently disappear or
279
+ // get merged into the other.
280
+ const ascii = renderMermaidASCII(REPRO_FULL, { colorMode: 'none' })
281
+ expect(ascii).toContain('has')
282
+ expect(ascii).toContain('fulfilled_by')
283
+ })
284
+
285
+ it('does not corrupt attribute text with a longer, multi-word relationship label', () => {
286
+ const ascii = renderMermaidASCII(
287
+ `erDiagram
288
+ CUSTOMER ||--o{ ORDER : places
289
+ CUSTOMER ||--o{ ADDRESS : "ships to"
290
+ ORDER {
291
+ string id
292
+ int totalCents
293
+ }
294
+ CUSTOMER {
295
+ string id
296
+ string name
297
+ }`,
298
+ { colorMode: 'none' },
299
+ )
300
+ expect(ascii).toContain('ships to')
301
+ expect(ascii).toContain('int totalCents')
302
+ expect(ascii).toContain('string name')
303
+ })
304
+ })
305
+
306
+ // ============================================================================
307
+ // Follow-up findings from CodeRabbit's review of the #350 fix (PR #391) —
308
+ // both are the same "silently overwritten" shape as the original issue,
309
+ // just between two relationships instead of a relationship and a box.
310
+ // ============================================================================
311
+
312
+ describe('ASCII ER relationship label routing — parallel relationships (CodeRabbit follow-up)', () => {
313
+ it('keeps both labels when two relationships connect the exact same adjacent pair (same-row)', () => {
314
+ // A↔B are adjacent (no obstruction), so both "first" and "second" land
315
+ // on the identical direct-path row unless the label search kicks in.
316
+ const ascii = renderMermaidASCII(
317
+ `erDiagram
318
+ A ||--o{ B : first
319
+ A ||--o{ B : second`,
320
+ { colorMode: 'none' },
321
+ )
322
+ expect(ascii).toContain('first')
323
+ expect(ascii).toContain('second')
324
+ })
325
+
326
+ it('keeps both labels when two relationships connect the exact same vertical pair', () => {
327
+ // A (row1) and C (row2) are directly above/below each other; both
328
+ // "first" and "second" would compute the identical band midpoint
329
+ // without pickBandY's collision search.
330
+ const ascii = renderMermaidASCII(
331
+ `erDiagram
332
+ A ||--o{ B : sibling
333
+ A ||--o{ C : first
334
+ A ||--o{ C : second`,
335
+ { colorMode: 'none' },
336
+ )
337
+ expect(ascii).toContain('sibling')
338
+ expect(ascii).toContain('first')
339
+ expect(ascii).toContain('second')
340
+ })
341
+ })
342
+
343
+ describe('ASCII ER relationship routing — multi-row obstruction (CodeRabbit follow-up)', () => {
344
+ it('keeps a relationship connected and boxes uncorrupted when it skips two rows', () => {
345
+ // 7 entities -> maxPerRow = 3 -> row1: A,B,C; row2: D,E,F; row3: G.
346
+ // A-G shares D's column two rows down, so the single-row-gap clamp
347
+ // alone doesn't cover it — this needs the free-column bypass.
348
+ const ascii = renderMermaidASCII(
349
+ `erDiagram
350
+ A ||--o{ B : ab
351
+ B ||--o{ C : bc
352
+ C ||--o{ D : cd
353
+ D ||--o{ E : de
354
+ E ||--o{ F : ef
355
+ F ||--o{ G : fg
356
+ A ||--o{ G : ag
357
+ A {
358
+ string id
359
+ string name
360
+ }
361
+ D {
362
+ string id
363
+ string title
364
+ }
365
+ G {
366
+ string id
367
+ string label
368
+ }`,
369
+ { colorMode: 'none' },
370
+ )
371
+ // No box content lost.
372
+ expect(ascii).toContain('string id')
373
+ expect(ascii).toContain('string name')
374
+ expect(ascii).toContain('string title')
375
+ expect(ascii).toContain('string label')
376
+ // Every relationship label survives, including the multi-row one.
377
+ expect(ascii).toContain('ab')
378
+ expect(ascii).toContain('bc')
379
+ expect(ascii).toContain('cd')
380
+ expect(ascii).toContain('de')
381
+ expect(ascii).toContain('ef')
382
+ expect(ascii).toContain('fg')
383
+ expect(ascii).toContain('ag')
384
+ })
385
+
386
+ it('routes the multi-row relationship through a column with no box at all (not a coincidentally-empty cell)', () => {
387
+ // Same shape as above, but every entity carries an attribute so a
388
+ // "disconnected" line landing inside a box (rather than routing
389
+ // around it) would corrupt visible attribute text, not just an empty
390
+ // interior cell.
391
+ const ascii = renderMermaidASCII(
392
+ `erDiagram
393
+ A ||--o{ B : ab
394
+ B ||--o{ C : bc
395
+ C ||--o{ D : cd
396
+ D ||--o{ E : de
397
+ E ||--o{ F : ef
398
+ F ||--o{ G : fg
399
+ A ||--o{ G : ag
400
+ A { string a1 }
401
+ B { string b1 }
402
+ C { string c1 }
403
+ D { string d1 }
404
+ E { string e1 }
405
+ F { string f1 }
406
+ G { string g1 }`,
407
+ { colorMode: 'none' },
408
+ )
409
+ for (const attr of ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1']) {
410
+ expect(ascii, `expected attribute "${attr}" to survive`).toContain(attr)
411
+ }
412
+ expect(ascii).toContain('ag')
413
+ })
414
+
415
+ it('draws the multi-row line outside the intervening box, not just around a coincidental gap', () => {
416
+ // A precise, geometry-aware check (not just "content survives"):
417
+ // setCGuarded alone would already stop attribute-text corruption even
418
+ // with the multi-row bypass disabled — it just leaves the line
419
+ // discontinuous through D's box, which "content survives" assertions
420
+ // can't tell apart from a real, routed connection. This test locates
421
+ // D's own top-border row and asserts a line/marker character exists
422
+ // *outside* D's box width on that exact row — proof the connector
423
+ // steps around D's box rather than merely being guarded away inside
424
+ // it (verified against a version of this fix with the bypass
425
+ // disabled: the extra character outside D's box does not appear).
426
+ const ascii = renderMermaidASCII(
427
+ `erDiagram
428
+ A ||--o{ B : ab
429
+ B ||--o{ C : bc
430
+ C ||--o{ D : cd
431
+ D ||--o{ E : de
432
+ E ||--o{ F : ef
433
+ F ||--o{ G : fg
434
+ A ||--o{ G : ag
435
+ A {
436
+ string id
437
+ string name
438
+ }
439
+ D {
440
+ string id
441
+ string title
442
+ }
443
+ G {
444
+ string id
445
+ string label
446
+ }`,
447
+ { colorMode: 'none' },
448
+ )
449
+ const lines = ascii.split('\n')
450
+ const dTopIdx = lines.findIndex(
451
+ (l, i) => l.includes('┌') && lines[i + 1]?.includes('│ D'),
452
+ )
453
+ expect(dTopIdx, 'expected to find D’s top-border row').toBeGreaterThan(-1)
454
+ const dTopLine = lines[dTopIdx]!
455
+ const afterDBox = dTopLine.slice(dTopLine.indexOf('┐') + 1)
456
+ expect(afterDBox).toMatch(/[│╢╟○]/)
457
+ })
458
+ })