zombie-mermaid 2.2.1 → 2.2.2

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 (251) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +7 -2
  3. package/dist/ascii.cjs +9 -9
  4. package/dist/ascii.cjs.map +1 -1
  5. package/dist/ascii.js +4069 -3969
  6. package/dist/ascii.js.map +1 -1
  7. package/dist/cli.js +7605 -7381
  8. package/dist/index.cjs +52 -52
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.js +7613 -7459
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.cjs +14142 -13717
  13. package/dist/mcp.cjs.map +1 -1
  14. package/dist/mcp.js +14142 -13717
  15. package/dist/mcp.js.map +1 -1
  16. package/package.json +27 -5
  17. package/packages/ascii-renderer/LICENSE +22 -0
  18. package/{src/ascii → packages/ascii-renderer/src}/class-diagram.ts +277 -57
  19. package/{src/ascii → packages/ascii-renderer/src}/er-diagram.ts +2 -2
  20. package/packages/ascii-renderer/src/flowchart.ts +94 -0
  21. package/{src/ascii → packages/ascii-renderer/src}/grid.ts +23 -0
  22. package/{src/ascii → packages/ascii-renderer/src}/index.ts +13 -84
  23. package/{src/ascii → packages/ascii-renderer/src}/registry.ts +23 -12
  24. package/{src/ascii → packages/ascii-renderer/src}/sequence.ts +55 -8
  25. package/{src/ascii → packages/ascii-renderer/src}/xychart.ts +6 -3
  26. package/packages/core/LICENSE +22 -0
  27. package/packages/core/src/click-directive.ts +106 -0
  28. package/packages/core/src/color-utils.ts +205 -0
  29. package/packages/core/src/diagram-type.ts +35 -0
  30. package/packages/core/src/direction-override.ts +46 -0
  31. package/packages/core/src/direction.ts +56 -0
  32. package/packages/core/src/index.ts +34 -0
  33. package/packages/core/src/init-directive.ts +248 -0
  34. package/packages/core/src/multiline-utils.ts +275 -0
  35. package/packages/core/src/statements.ts +197 -0
  36. package/packages/core/src/style-directives.ts +214 -0
  37. package/packages/core/src/text-metrics.ts +498 -0
  38. package/packages/core/src/theme.ts +696 -0
  39. package/packages/core/src/types.ts +670 -0
  40. package/packages/mcp/LICENSE +22 -0
  41. package/{src/mcp → packages/mcp/src}/server.ts +34 -2
  42. package/{src/mcp → packages/mcp/src}/tools/check-sequence-activations.ts +8 -5
  43. package/packages/mcp/src/tools/fix-sequence-activations.ts +77 -0
  44. package/{src/mcp → packages/mcp/src}/tools/render-ascii.ts +2 -2
  45. package/{src/mcp → packages/mcp/src}/tools/render-svg.ts +1 -1
  46. package/packages/mermaid-parser/LICENSE +22 -0
  47. package/{src → packages/mermaid-parser/src}/class/parser.ts +86 -8
  48. package/{src → packages/mermaid-parser/src}/er/parser.ts +59 -26
  49. package/packages/mermaid-parser/src/index.ts +48 -0
  50. package/{src → packages/mermaid-parser/src}/sequence/activation-check.ts +4 -3
  51. package/packages/mermaid-parser/src/sequence/activation-fix.ts +112 -0
  52. package/{src → packages/mermaid-parser/src}/sequence/parser.ts +74 -16
  53. package/{src → packages/mermaid-parser/src}/xychart/parser.ts +84 -6
  54. package/packages/svg-renderer/LICENSE +22 -0
  55. package/{src → packages/svg-renderer/src}/class/layout.ts +8 -4
  56. package/{src → packages/svg-renderer/src}/class/renderer.ts +7 -6
  57. package/packages/svg-renderer/src/edge-curves.ts +204 -0
  58. package/packages/svg-renderer/src/elk-instance.ts +292 -0
  59. package/{src → packages/svg-renderer/src}/er/layout.ts +6 -4
  60. package/{src → packages/svg-renderer/src}/er/renderer.ts +4 -4
  61. package/packages/svg-renderer/src/index.ts +58 -0
  62. package/packages/svg-renderer/src/layout-engine/constants.ts +19 -0
  63. package/packages/svg-renderer/src/layout-engine/edge-bundling.ts +379 -0
  64. package/packages/svg-renderer/src/layout-engine/elk-adapter-utils.ts +81 -0
  65. package/packages/svg-renderer/src/layout-engine/elk-graph-builder.ts +240 -0
  66. package/packages/svg-renderer/src/layout-engine/from-elk.ts +685 -0
  67. package/packages/svg-renderer/src/layout-engine/layer-alignment.ts +174 -0
  68. package/packages/svg-renderer/src/layout-engine/to-elk.ts +695 -0
  69. package/packages/svg-renderer/src/layout-engine.ts +74 -0
  70. package/packages/svg-renderer/src/layout.ts +8 -0
  71. package/packages/svg-renderer/src/renderer.ts +1485 -0
  72. package/packages/svg-renderer/src/resolve-colors.ts +339 -0
  73. package/{src → packages/svg-renderer/src}/sequence/layout.ts +2 -6
  74. package/{src → packages/svg-renderer/src}/sequence/renderer.ts +4 -4
  75. package/packages/svg-renderer/src/shape-clipping.ts +197 -0
  76. package/packages/svg-renderer/src/styles.ts +118 -0
  77. package/{src → packages/svg-renderer/src}/xychart/layout.ts +2 -2
  78. package/{src → packages/svg-renderer/src}/xychart/renderer.ts +6 -6
  79. package/src/__tests__/ascii-ansi.test.ts +6 -2
  80. package/src/__tests__/ascii-canvas-write.test.ts +2 -2
  81. package/src/__tests__/ascii-charset-border-junctions.test.ts +1 -1
  82. package/src/__tests__/ascii-cjk-width.test.ts +1 -1
  83. package/src/__tests__/ascii-class-box-occupancy.test.ts +147 -45
  84. package/src/__tests__/ascii-class-column-width-488-489.test.ts +1 -1
  85. package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
  86. package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +1 -1
  87. package/src/__tests__/ascii-class-diagram-compartments.test.ts +1 -1
  88. package/src/__tests__/ascii-class-label-box-collision.test.ts +1 -1
  89. package/src/__tests__/ascii-class-label-row-collision-531.test.ts +1 -1
  90. package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +1 -1
  91. package/src/__tests__/ascii-class-padding.test.ts +1 -1
  92. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +86 -1
  93. package/src/__tests__/ascii-combining-mark-width.test.ts +2 -2
  94. package/src/__tests__/ascii-coords-overlay.test.ts +2 -2
  95. package/src/__tests__/ascii-decision-lr-box-start.test.ts +2 -2
  96. package/src/__tests__/ascii-display-width-unit.test.ts +1 -1
  97. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +1 -1
  98. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +12 -5
  99. package/src/__tests__/ascii-draw-bundles.test.ts +5 -5
  100. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +1 -1
  101. package/src/__tests__/ascii-edge-bundling-routing.test.ts +9 -5
  102. package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +10 -6
  103. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +1 -1
  104. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +2 -2
  105. package/src/__tests__/ascii-edge-routing-fixes.test.ts +1 -1
  106. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +8 -4
  107. package/src/__tests__/ascii-edge-styles.test.ts +1 -1
  108. package/src/__tests__/ascii-emoji-cluster-width.test.ts +2 -2
  109. package/src/__tests__/ascii-er-box-occupancy.test.ts +1 -1
  110. package/src/__tests__/ascii-er-cardinality.test.ts +1 -1
  111. package/src/__tests__/ascii-er-corner-glyphs.test.ts +1 -1
  112. package/src/__tests__/ascii-er-jog-stray-line.test.ts +1 -1
  113. package/src/__tests__/ascii-er-label-padding.test.ts +1 -1
  114. package/src/__tests__/ascii-er-padding.test.ts +1 -1
  115. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +1 -1
  116. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +1 -1
  117. package/src/__tests__/ascii-er-stray-connectors.test.ts +7 -4
  118. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +1 -1
  119. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +1 -1
  120. package/src/__tests__/ascii-grid-deep-chain.test.ts +4 -4
  121. package/src/__tests__/ascii-hyperlinks.test.ts +9 -6
  122. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +8 -4
  123. package/src/__tests__/ascii-lane-search.test.ts +1 -1
  124. package/src/__tests__/ascii-multibox-cjk-width.test.ts +1 -1
  125. package/src/__tests__/ascii-multiline.test.ts +6 -3
  126. package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +8 -5
  127. package/src/__tests__/ascii-package-boundary.test.ts +47 -37
  128. package/src/__tests__/ascii-padding-edge-cases.test.ts +1 -1
  129. package/src/__tests__/ascii-parallel-edges-329.test.ts +8 -5
  130. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +14 -4
  131. package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +9 -5
  132. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +1 -1
  133. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +1 -1
  134. package/src/__tests__/ascii-sequence-box-group.test.ts +1 -1
  135. package/src/__tests__/ascii-sequence-cjk-width.test.ts +1 -1
  136. package/src/__tests__/ascii-sequence-create-destroy.test.ts +1 -1
  137. package/src/__tests__/ascii-sequence-form-invariants.test.ts +48 -50
  138. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +1 -1
  139. package/src/__tests__/ascii-sequence-notes.test.ts +1 -1
  140. package/src/__tests__/ascii-sequence-padding.test.ts +1 -1
  141. package/src/__tests__/ascii-sequence-self-arrow.test.ts +1 -1
  142. package/src/__tests__/ascii-shape-diamond.test.ts +3 -3
  143. package/src/__tests__/ascii-shape-rectangle.test.ts +5 -5
  144. package/src/__tests__/ascii-shape-rounded.test.ts +3 -3
  145. package/src/__tests__/ascii-shapes-circle.test.ts +3 -3
  146. package/src/__tests__/ascii-shapes-hexagon.test.ts +3 -3
  147. package/src/__tests__/ascii-shapes-special.test.ts +10 -5
  148. package/src/__tests__/ascii-shapes-stadium.test.ts +3 -3
  149. package/src/__tests__/ascii-shapes-state.test.ts +15 -5
  150. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +1 -1
  151. package/src/__tests__/ascii-subgraph-bbox.test.ts +8 -4
  152. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +1 -1
  153. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +88 -25
  154. package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +8 -4
  155. package/src/__tests__/ascii-subgraph-title-padding.test.ts +1 -1
  156. package/src/__tests__/ascii-territory-unit.test.ts +2 -2
  157. package/src/__tests__/ascii-validate.test.ts +1 -1
  158. package/src/__tests__/ascii.test.ts +5 -2
  159. package/src/__tests__/box-color.test.ts +1 -1
  160. package/src/__tests__/class-arrow-directions.test.ts +1 -1
  161. package/src/__tests__/class-click-interactions.test.ts +3 -6
  162. package/src/__tests__/class-integration.test.ts +1 -1
  163. package/src/__tests__/class-notes.test.ts +3 -3
  164. package/src/__tests__/class-parser-error-messages.test.ts +162 -0
  165. package/src/__tests__/class-parser.test.ts +6 -6
  166. package/src/__tests__/class-styling.test.ts +2 -2
  167. package/src/__tests__/cli-render.test.ts +16 -1
  168. package/src/__tests__/direction-override.test.ts +1 -1
  169. package/src/__tests__/draw-lines.test.ts +2 -2
  170. package/src/__tests__/edge-cell-styles.test.ts +5 -2
  171. package/src/__tests__/elk-layout-cache.test.ts +6 -4
  172. package/src/__tests__/er-click-unsupported.test.ts +5 -8
  173. package/src/__tests__/er-parser-error-messages.test.ts +82 -0
  174. package/src/__tests__/er-parser.test.ts +3 -6
  175. package/src/__tests__/expanded-shapes.test.ts +1 -1
  176. package/src/__tests__/grid-occupancy.test.ts +1 -1
  177. package/src/__tests__/mcp-check-sequence-activations-tool.test.ts +1 -1
  178. package/src/__tests__/mcp-fix-sequence-activations-tool.test.ts +134 -0
  179. package/src/__tests__/mcp-render-ascii-tool.test.ts +5 -4
  180. package/src/__tests__/mcp-render-svg-theme-guard.test.ts +3 -3
  181. package/src/__tests__/mcp-render-svg-tool.test.ts +1 -1
  182. package/src/__tests__/mcp-server.test.ts +30 -1
  183. package/src/__tests__/multiline-labels.test.ts +29 -18
  184. package/src/__tests__/parser.test.ts +117 -5
  185. package/src/__tests__/pathfinder.test.ts +11 -4
  186. package/src/__tests__/render-options-overrides.test.ts +28 -21
  187. package/src/__tests__/render-options-per-type.test.ts +13 -9
  188. package/src/__tests__/resolve-colors.test.ts +1 -1
  189. package/src/__tests__/self-loop-edges.test.ts +112 -0
  190. package/src/__tests__/sequence-activation-check.test.ts +7 -8
  191. package/src/__tests__/sequence-activation-fix.test.ts +91 -0
  192. package/src/__tests__/sequence-layout.test.ts +4 -7
  193. package/src/__tests__/sequence-parser-syntax-errors.test.ts +179 -0
  194. package/src/__tests__/sequence-parser.test.ts +3 -6
  195. package/src/__tests__/statements.test.ts +53 -35
  196. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +21 -21
  197. package/src/__tests__/theme-palette-contrast.test.ts +84 -0
  198. package/src/__tests__/workspace-package-boundaries.test.ts +199 -25
  199. package/src/__tests__/xychart-ascii.test.ts +1 -1
  200. package/src/__tests__/xychart-colors.test.ts +3 -2
  201. package/src/__tests__/xychart-layout.test.ts +1 -1
  202. package/src/__tests__/xychart-parser-errors.test.ts +95 -0
  203. package/src/browser.ts +8 -2
  204. package/src/cli/mcp.ts +1 -1
  205. package/src/cli/render.ts +59 -25
  206. package/src/cli/web.ts +1 -1
  207. package/src/diagram-registry.ts +190 -25
  208. package/src/index.ts +34 -131
  209. package/src/parser.ts +102 -40
  210. /package/{src/ascii → packages/ascii-renderer/src}/ansi.ts +0 -0
  211. /package/{src/ascii → packages/ascii-renderer/src}/canvas.ts +0 -0
  212. /package/{src/ascii → packages/ascii-renderer/src}/converter.ts +0 -0
  213. /package/{src/ascii → packages/ascii-renderer/src}/coords.ts +0 -0
  214. /package/{src/ascii → packages/ascii-renderer/src}/display-width.ts +0 -0
  215. /package/{src/ascii → packages/ascii-renderer/src}/draw-arrows.ts +0 -0
  216. /package/{src/ascii → packages/ascii-renderer/src}/draw-boxes.ts +0 -0
  217. /package/{src/ascii → packages/ascii-renderer/src}/draw-bundles.ts +0 -0
  218. /package/{src/ascii → packages/ascii-renderer/src}/draw-lines.ts +0 -0
  219. /package/{src/ascii → packages/ascii-renderer/src}/draw-subgraphs.ts +0 -0
  220. /package/{src/ascii → packages/ascii-renderer/src}/draw.ts +0 -0
  221. /package/{src/ascii → packages/ascii-renderer/src}/edge-bundling.ts +0 -0
  222. /package/{src/ascii → packages/ascii-renderer/src}/edge-cell-styles.ts +0 -0
  223. /package/{src/ascii → packages/ascii-renderer/src}/edge-routing.ts +0 -0
  224. /package/{src/ascii → packages/ascii-renderer/src}/grid-occupancy.ts +0 -0
  225. /package/{src/ascii → packages/ascii-renderer/src}/hyperlinks.ts +0 -0
  226. /package/{src/ascii → packages/ascii-renderer/src}/lane-search.ts +0 -0
  227. /package/{src/ascii → packages/ascii-renderer/src}/multiline-utils.ts +0 -0
  228. /package/{src/ascii → packages/ascii-renderer/src}/pathfinder.ts +0 -0
  229. /package/{src/ascii → packages/ascii-renderer/src}/shapes/circle.ts +0 -0
  230. /package/{src/ascii → packages/ascii-renderer/src}/shapes/corners.ts +0 -0
  231. /package/{src/ascii → packages/ascii-renderer/src}/shapes/diamond.ts +0 -0
  232. /package/{src/ascii → packages/ascii-renderer/src}/shapes/hexagon.ts +0 -0
  233. /package/{src/ascii → packages/ascii-renderer/src}/shapes/index.ts +0 -0
  234. /package/{src/ascii → packages/ascii-renderer/src}/shapes/rectangle.ts +0 -0
  235. /package/{src/ascii → packages/ascii-renderer/src}/shapes/rounded.ts +0 -0
  236. /package/{src/ascii → packages/ascii-renderer/src}/shapes/special.ts +0 -0
  237. /package/{src/ascii → packages/ascii-renderer/src}/shapes/stadium.ts +0 -0
  238. /package/{src/ascii → packages/ascii-renderer/src}/shapes/state.ts +0 -0
  239. /package/{src/ascii → packages/ascii-renderer/src}/shapes/types.ts +0 -0
  240. /package/{src/ascii → packages/ascii-renderer/src}/territory.ts +0 -0
  241. /package/{src/ascii → packages/ascii-renderer/src}/types.ts +0 -0
  242. /package/{src/ascii → packages/ascii-renderer/src}/validate.ts +0 -0
  243. /package/{src/mcp → packages/mcp/src}/index.ts +0 -0
  244. /package/{src → packages/mermaid-parser/src}/class/format.ts +0 -0
  245. /package/{src → packages/mermaid-parser/src}/class/types.ts +0 -0
  246. /package/{src → packages/mermaid-parser/src}/er/types.ts +0 -0
  247. /package/{src → packages/mermaid-parser/src}/expanded-shapes.ts +0 -0
  248. /package/{src → packages/mermaid-parser/src}/sequence/box-color.ts +0 -0
  249. /package/{src → packages/mermaid-parser/src}/sequence/types.ts +0 -0
  250. /package/{src → packages/mermaid-parser/src}/xychart/colors.ts +0 -0
  251. /package/{src → packages/mermaid-parser/src}/xychart/types.ts +0 -0
@@ -8,9 +8,9 @@ import type {
8
8
  GridLine,
9
9
  PlotArea,
10
10
  LegendItem,
11
- } from './types.ts'
11
+ } from '@zombie-mermaid/mermaid-parser'
12
12
  import type { XyChartRenderOptions } from '@zombie-mermaid/core'
13
- import { estimateTextWidth } from '@zombie-mermaid/svg-renderer'
13
+ import { estimateTextWidth } from '../styles.ts'
14
14
 
15
15
  // ============================================================================
16
16
  // XY Chart layout engine
@@ -1,12 +1,12 @@
1
- import type { PositionedXYChart } from './types.ts'
1
+ import type { PositionedXYChart } from '@zombie-mermaid/mermaid-parser'
2
2
  import type { DiagramColors, SvgEmitOptions } from '@zombie-mermaid/core'
3
3
  import { svgOpenTag, buildStyleBlock, styleOpenTag } from '@zombie-mermaid/core'
4
+ import { withDataSrc } from '../renderer.ts'
5
+ import { TEXT_BASELINE_SHIFT, estimateTextWidth } from '../styles.ts'
4
6
  import {
5
- withDataSrc,
6
- TEXT_BASELINE_SHIFT,
7
- estimateTextWidth,
8
- } from '@zombie-mermaid/svg-renderer'
9
- import { getSeriesColor, CHART_ACCENT_FALLBACK } from './colors.ts'
7
+ getSeriesColor,
8
+ CHART_ACCENT_FALLBACK,
9
+ } from '@zombie-mermaid/mermaid-parser'
10
10
 
11
11
  // ============================================================================
12
12
  // XY Chart SVG renderer
@@ -11,8 +11,12 @@ import {
11
11
  colorizeChar,
12
12
  colorizeLine,
13
13
  colorizeText,
14
- } from '../ascii/ansi.ts'
15
- import type { CharRole, AsciiTheme, ColorMode } from '../ascii/types.ts'
14
+ } from '../../packages/ascii-renderer/src/ansi.ts'
15
+ import type {
16
+ CharRole,
17
+ AsciiTheme,
18
+ ColorMode,
19
+ } from '../../packages/ascii-renderer/src/types.ts'
16
20
  import type { DiagramColors } from '@zombie-mermaid/core'
17
21
 
18
22
  const RESET = '\x1b[0m'
@@ -17,8 +17,8 @@ import {
17
17
  mkRoleCanvas,
18
18
  write,
19
19
  getCanvasSize,
20
- } from '../ascii/canvas.ts'
21
- import type { CharRole } from '../ascii/types.ts'
20
+ } from '../../packages/ascii-renderer/src/canvas.ts'
21
+ import type { CharRole } from '../../packages/ascii-renderer/src/types.ts'
22
22
 
23
23
  describe('canvas.ts write()', () => {
24
24
  it('writes within bounds, including exactly on the inclusive max edge', () => {
@@ -9,7 +9,7 @@
9
9
  * the ASCII equivalent ('+').
10
10
  */
11
11
  import { describe, it, expect } from 'vitest'
12
- import { renderMermaidASCII } from '../ascii/index.ts'
12
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
13
13
 
14
14
  describe('ASCII vs Unicode charset: box-start border junctions', () => {
15
15
  it('draws a "+" junction on a plain top-down edge, matching the Unicode T-junction', () => {
@@ -14,7 +14,7 @@
14
14
  * just that the fix's own helper agrees with itself.
15
15
  */
16
16
  import { describe, it, expect } from 'vitest'
17
- import { renderMermaidASCII } from '../ascii/index.ts'
17
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
18
18
  import {
19
19
  terminalDisplayWidth,
20
20
  assertUniformDisplayWidth,
@@ -1,20 +1,20 @@
1
1
  // ============================================================================
2
2
  // ASCII class diagram box-occupancy invariants
3
3
  //
4
- // Unlike src/ascii/er-diagram.ts (which gained an occupancy guard —
5
- // `setCGuarded`/`boxCells`/`regionClear` — fixing issue #350),
6
- // src/ascii/class-diagram.ts has no equivalent mechanism: its relationship
7
- // line/marker/label writes all go through the plain `setC`, which only
8
- // bounds-checks, never occupancy-checks. This suite sweeps the same "does a
9
- // relationship line/label ever land on top of an unrelated class's box"
10
- // question issue #350 asked for ER diagrams, generalized to class diagrams —
11
- // where it has never been asked before. Most cases pass today (no unguarded
12
- // code path is exercised); two are confirmed-broken and marked `it.fails`
13
- // with the exact code path they hit.
4
+ // packages/ascii-renderer/src/er-diagram.ts has an occupancy guard —
5
+ // `setCGuarded`/`boxCells`/`regionClear` — fixing issue #350.
6
+ // packages/ascii-renderer/src/class-diagram.ts now generalizes that same
7
+ // guard (see its own `boxCells`/`setCGuarded`, plus an obstruction-aware
8
+ // same-level detour row) to close issue #953's class-diagram cases. This
9
+ // suite sweeps the same "does a relationship line/label ever land on top of
10
+ // an unrelated class's box" question issue #350 asked for ER diagrams,
11
+ // generalized to class diagrams. Two cases below (the same-level detour
12
+ // crossing a taller, in-between class's box) used to be confirmed-broken and
13
+ // marked `it.fails`; they're now plain `it()` cases, verifying the fix.
14
14
  // ============================================================================
15
15
 
16
16
  import { describe, it, expect } from 'vitest'
17
- import { renderMermaidASCII } from '../ascii/index.ts'
17
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
18
18
  import { findBoxRect, rectsOverlap } from './helpers/ascii-form.ts'
19
19
 
20
20
  function expectNoBoxOverlap(ascii: string, names: string[]): void {
@@ -101,18 +101,15 @@ describe('ASCII class diagrams — class boxes never overlap', () => {
101
101
  expectNoBoxOverlap(ascii, ['Alpha', 'Mid', 'Charlie'])
102
102
  })
103
103
 
104
- it.fails(
105
- // Confirmed by rendering (not just reading the code): the same-level
106
- // branch's detour row — `Math.max(fromBY, toP.y + toP.height - 1) + 2`
107
- // — is computed only from the *relationship's own* two endpoints
108
- // (Alpha, Charlie). It never checks whether Mid, sitting between them
109
- // in the same row, is taller and extends below that row. The detour's
110
- // horizontal segment is drawn through plain `setC` with no occupancy
111
- // check, so it cuts straight through Mid's box — see the middle
112
- // attribute line missing from the assertion below.
113
- '3-node cycle, Mid (middle) has 6 attributes: detour line corrupts an attribute row',
114
- () => {
115
- const src = `classDiagram
104
+ // The same-level branch's detour row used to be computed only from the
105
+ // relationship's own two endpoints (Alpha, Charlie), never checking
106
+ // whether Mid — sitting between them in the same row — was taller and
107
+ // extended below that row. class-diagram.ts now searches for such a
108
+ // same-row obstruction and routes the detour below its bottom edge too
109
+ // (generalizing er-diagram.ts's obstructionBottom guard, issue #350),
110
+ // so the detour's horizontal segment no longer cuts through Mid's box.
111
+ it('3-node cycle, Mid (middle) has 6 attributes: detour line corrupts an attribute row', () => {
112
+ const src = `classDiagram
116
113
  class Alpha
117
114
  class Mid {
118
115
  ${manyAttrs('b', 6)}
@@ -121,7 +118,79 @@ ${manyAttrs('b', 6)}
121
118
  Alpha --> Mid
122
119
  Mid --> Charlie
123
120
  Charlie --> Alpha`
124
- const ascii = renderMermaidASCII(src, { useAscii: true })
121
+ const ascii = renderMermaidASCII(src, { useAscii: true })
122
+ expectAllPresentOnce(ascii, [
123
+ '+ b0: String',
124
+ '+ b1: String',
125
+ '+ b2: String',
126
+ '+ b3: String',
127
+ '+ b4: String',
128
+ '+ b5: String',
129
+ ])
130
+ })
131
+
132
+ // The same-row obstruction search (see the test above) must skip a class
133
+ // on a *different* level/row, even when that class's box geometrically
134
+ // overlaps the detour's horizontal span — not just when it happens to sit
135
+ // off to the side where the search would ignore it anyway regardless of
136
+ // the `other.y !== fromP.y` filter. `Root`/`Kid` are a real root/child
137
+ // pair via the level-BFS (Root has no parent, Kid is its level-1 child),
138
+ // placed on a different row from the Alpha/Mid/Charlie cycle.
139
+ //
140
+ // Two orderings of the same Root/Kid pair are compared: declaring Root
141
+ // first places Kid's column off to the side, outside the Alpha-to-Charlie
142
+ // detour span; declaring Root between Alpha and Mid places Kid's column
143
+ // so it overlaps Alpha's own range instead (confirmed below via
144
+ // `findBoxRect`). If the different-row filter worked, both orderings
145
+ // render the cycle identically — Root/Kid's actual row is irrelevant to
146
+ // it either way. A test that only used the non-overlapping ordering would
147
+ // pass even with the filter deleted (verified: an earlier version of this
148
+ // test did exactly that, and CodeRabbit correctly flagged it as not
149
+ // actually exercising the filter — see the PR discussion). Sabotage-
150
+ // verified: deleting the filter leaves both orderings' `+ bN: String`
151
+ // lines intact but makes the overlapping ordering one row taller than the
152
+ // non-overlapping one (19 vs. 18), which the equality assertion below
153
+ // catches.
154
+ it('3-node cycle, unrelated Root->Kid pair on a different level: obstruction search ignores it regardless of whether it overlaps the detour span', () => {
155
+ function withRootKid(rootDeclaredBetweenAlphaAndMid: boolean): string {
156
+ const rootAndKid = ` class Root
157
+ class Kid
158
+ `
159
+ return `classDiagram
160
+ ${rootDeclaredBetweenAlphaAndMid ? '' : rootAndKid} class Alpha
161
+ ${rootDeclaredBetweenAlphaAndMid ? rootAndKid : ''} class Mid {
162
+ ${manyAttrs('b', 6)}
163
+ }
164
+ class Charlie
165
+ Alpha --> Mid
166
+ Mid --> Charlie
167
+ Charlie --> Alpha
168
+ Root --> Kid`
169
+ }
170
+
171
+ const nonOverlapping = renderMermaidASCII(withRootKid(false), {
172
+ useAscii: true,
173
+ })
174
+ const overlapping = renderMermaidASCII(withRootKid(true), {
175
+ useAscii: true,
176
+ })
177
+
178
+ // Confirms the geometry this test relies on: in the "overlapping"
179
+ // ordering, Kid really does sit on a different row from the cycle, and
180
+ // really does overlap Alpha's own column range (i.e. the obstruction
181
+ // search has something to incorrectly notice if the different-row
182
+ // filter is missing); in the "non-overlapping" ordering, it doesn't.
183
+ const overlappingAlphaRect = findBoxRect(overlapping, 'Alpha')
184
+ const overlappingKidRect = findBoxRect(overlapping, 'Kid')
185
+ expect(overlappingKidRect.y0).toBeGreaterThan(overlappingAlphaRect.y1)
186
+ expect(overlappingKidRect.x0).toBeLessThan(overlappingAlphaRect.x1)
187
+ const nonOverlappingAlphaRect = findBoxRect(nonOverlapping, 'Alpha')
188
+ const nonOverlappingKidRect = findBoxRect(nonOverlapping, 'Kid')
189
+ expect(nonOverlappingKidRect.x1).toBeLessThanOrEqual(
190
+ nonOverlappingAlphaRect.x0,
191
+ )
192
+
193
+ for (const ascii of [nonOverlapping, overlapping]) {
125
194
  expectAllPresentOnce(ascii, [
126
195
  '+ b0: String',
127
196
  '+ b1: String',
@@ -130,8 +199,43 @@ ${manyAttrs('b', 6)}
130
199
  '+ b4: String',
131
200
  '+ b5: String',
132
201
  ])
133
- },
134
- )
202
+ expectNoBoxOverlap(ascii, ['Alpha', 'Mid', 'Charlie', 'Root', 'Kid'])
203
+ }
204
+
205
+ // The real check: Kid overlapping the detour span must not change the
206
+ // cycle's own rendered height compared to Kid sitting clear of it.
207
+ expect(overlapping.split('\n').length).toBe(
208
+ nonOverlapping.split('\n').length,
209
+ )
210
+ })
211
+
212
+ // `computeLabelAnchor`'s same-level branch used to recompute the label's
213
+ // row from scratch (`Math.max(fromBY, toBY) + 2`) instead of reusing the
214
+ // connector's own obstruction-aware `detourY` (`sameLevelDetourY` above)
215
+ // — so a *labeled* same-level relationship still landed its label inside
216
+ // Mid's box even after the unlabeled connector-line fix above, the same
217
+ // corruption in a sibling code path (caught in review on PR #957).
218
+ it('3-node cycle, Mid (middle) has 6 attributes, labeled wrap-around relationship: label no longer corrupts an attribute row', () => {
219
+ const src = `classDiagram
220
+ class Alpha
221
+ class Mid {
222
+ ${manyAttrs('b', 6)}
223
+ }
224
+ class Charlie
225
+ Alpha --> Mid
226
+ Mid --> Charlie
227
+ Charlie --> Alpha : a rather long descriptive label`
228
+ const ascii = renderMermaidASCII(src, { useAscii: true })
229
+ expectAllPresentOnce(ascii, [
230
+ '+ b0: String',
231
+ '+ b1: String',
232
+ '+ b2: String',
233
+ '+ b3: String',
234
+ '+ b4: String',
235
+ '+ b5: String',
236
+ ])
237
+ expect(ascii).toContain('a rather long descriptive label')
238
+ })
135
239
 
136
240
  it('3-node cycle, long relationship label, no tall obstruction', () => {
137
241
  const src = `classDiagram
@@ -176,14 +280,13 @@ ${manyAttrs('c', 6)}
176
280
  expectNoBoxOverlap(ascii, ['A', 'B', 'C', 'D'])
177
281
  })
178
282
 
179
- it.fails(
180
- // Confirmed by rendering: a 4-node cycle (A->B->C->D->A) plus a skip
181
- // edge (A->C) also falls into the same unguarded same-level detour as
182
- // the 3-node case above — a second, independently-verified topology
183
- // hitting the same code gap, this time skipping over B's box.
184
- '4-node cycle with a skip edge, tall middle class: detour corrupts an attribute row',
185
- () => {
186
- const src = `classDiagram
283
+ // A 4-node cycle (A->B->C->D->A) plus a skip edge (A->C) also fell into
284
+ // the same unguarded same-level detour as the 3-node case above — a
285
+ // second, independently-verified topology hitting the same code gap,
286
+ // this time skipping over B's box. Fixed by the same obstruction-aware
287
+ // detour routing (see the 3-node cycle test above).
288
+ it('4-node cycle with a skip edge, tall middle class: detour corrupts an attribute row', () => {
289
+ const src = `classDiagram
187
290
  class A
188
291
  class B {
189
292
  ${manyAttrs('b', 6)}
@@ -195,17 +298,16 @@ ${manyAttrs('b', 6)}
195
298
  C --> D
196
299
  D --> A
197
300
  A --> C`
198
- const ascii = renderMermaidASCII(src, { useAscii: true })
199
- expectAllPresentOnce(ascii, [
200
- '+ b0: String',
201
- '+ b1: String',
202
- '+ b2: String',
203
- '+ b3: String',
204
- '+ b4: String',
205
- '+ b5: String',
206
- ])
207
- },
208
- )
301
+ const ascii = renderMermaidASCII(src, { useAscii: true })
302
+ expectAllPresentOnce(ascii, [
303
+ '+ b0: String',
304
+ '+ b1: String',
305
+ '+ b2: String',
306
+ '+ b3: String',
307
+ '+ b4: String',
308
+ '+ b5: String',
309
+ ])
310
+ })
209
311
 
210
312
  it('3 siblings + 2 children, crossing inheritance edges', () => {
211
313
  const src = `classDiagram
@@ -21,7 +21,7 @@
21
21
  */
22
22
 
23
23
  import { describe, it, expect } from 'vitest'
24
- import { renderMermaidASCII } from '../ascii/index.ts'
24
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
25
25
  import { assertUniformDisplayWidth } from './helpers/terminal-display-width.ts'
26
26
 
27
27
  /** Same source as the "Class: All 6 Relationship Types" sample (samples-data.ts). */
@@ -0,0 +1,187 @@
1
+ // ============================================================================
2
+ // ASCII class diagram — cross-level connector jog corrupting an intervening
3
+ // box's text
4
+ //
5
+ // Discovered incidentally while writing ascii-class-box-occupancy.test.ts's
6
+ // coverage for issue #953/PR #957 (the *same-level* relationship cycle's
7
+ // detour line lacking an occupancy guard). This is a distinct bug in a
8
+ // different branch of src/ascii/class-diagram.ts's relationship-drawing
9
+ // pass — the "target is below source" (and, by the same missing guard,
10
+ // "target is above source") branch, used whenever a relationship's two
11
+ // classes land on different levels.
12
+ //
13
+ // That branch's "no collision" routing draws a straight horizontal jog
14
+ // connecting the source's lane column to the target's lane column at some
15
+ // row between them. The *only* check that decided this was safe to draw
16
+ // without a detour (`findClearColumn`) tests whether the source's own lane
17
+ // column is itself clear of boxes across the full row range — it says
18
+ // nothing about whether the horizontal jog's row is clear all the way over
19
+ // to the target's column. When a class ends up positioned far from its
20
+ // actual parent's column — e.g. a level's only child is always placed at
21
+ // the leftmost slot regardless of which sibling at the level above is its
22
+ // parent — and a *different*, taller same-level sibling's box sits between
23
+ // those two columns, the jog cuts straight through that sibling's box,
24
+ // silently replacing whichever attribute/method row it crosses.
25
+ //
26
+ // Fixed by giving src/ascii/class-diagram.ts the same kind of box-occupancy
27
+ // guard (`boxCells`/`setCGuarded`) src/ascii/er-diagram.ts already has for
28
+ // issue #350, scoped to the cross-level branches only — the same-level
29
+ // branch's own detour (PR #957's scope) is untouched here.
30
+ // ============================================================================
31
+
32
+ import { describe, it, expect } from 'vitest'
33
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
34
+ import { findBoxRect, rectsOverlap } from './helpers/ascii-form.ts'
35
+
36
+ function expectNoBoxOverlap(ascii: string, names: string[]): void {
37
+ const rects = names.map((n) => ({ n, r: findBoxRect(ascii, n) }))
38
+ for (let i = 0; i < rects.length; i++) {
39
+ for (let j = i + 1; j < rects.length; j++) {
40
+ expect(
41
+ rectsOverlap(rects[i]!.r, rects[j]!.r),
42
+ `${rects[i]!.n} and ${rects[j]!.n} boxes overlap`,
43
+ ).toBe(false)
44
+ }
45
+ }
46
+ }
47
+
48
+ function manyAttrs(prefix: string, count: number): string {
49
+ return Array.from(
50
+ { length: count },
51
+ (_, i) => ` +String ${prefix}${i}`,
52
+ ).join('\n')
53
+ }
54
+
55
+ /** Assert every one of `needles` appears verbatim, exactly once, in `text`. */
56
+ function expectAllPresentOnce(text: string, needles: string[]): void {
57
+ for (const needle of needles) {
58
+ const count = text.split(needle).length - 1
59
+ expect(count, `expected "${needle}" to appear exactly once`).toBe(1)
60
+ }
61
+ }
62
+
63
+ describe('ASCII class diagrams — cross-level connector jog never corrupts a box', () => {
64
+ it('root-level sibling sits between a source and its far-off, sole child (minimal repro)', () => {
65
+ // Left/TallMid/RightSrc are three unrelated level-0 roots (no cycle
66
+ // involved, unlike the original report — isolates this bug from the
67
+ // separately-tracked same-level detour issue). Kid is RightSrc's only
68
+ // child, so it lands at the leftmost slot of level 1 regardless of
69
+ // RightSrc's own (rightmost) column — the "no collision" routing then
70
+ // has to jog the whole width of the canvas, straight through TallMid.
71
+ const src = `classDiagram
72
+ class Left
73
+ class TallMid {
74
+ ${manyAttrs('b', 6)}
75
+ }
76
+ class RightSrc
77
+ class Kid
78
+ RightSrc --> Kid`
79
+ const ascii = renderMermaidASCII(src, { useAscii: true })
80
+ expectAllPresentOnce(ascii, [
81
+ '+ b0: String',
82
+ '+ b1: String',
83
+ '+ b2: String',
84
+ '+ b3: String',
85
+ '+ b4: String',
86
+ '+ b5: String',
87
+ ])
88
+ expectNoBoxOverlap(ascii, ['Left', 'TallMid', 'RightSrc', 'Kid'])
89
+ })
90
+
91
+ it('originally-reported repro: same-level cycle plus an unrelated root/child pair', () => {
92
+ // The exact topology this bug was first reported against. Alpha/Mid/
93
+ // Charlie form a same-level relationship cycle (falls to level 0 via
94
+ // the level-BFS's "no root" fallback); Root is a genuine level-0 root
95
+ // (declared after the cycle) with its own child Kid at level 1. Only
96
+ // "+ b4: String" is asserted here — the cross-level bug this file
97
+ // covers. "+ b1: String" is a *separate*, already-tracked corruption
98
+ // (the same-level cycle's own detour, issue #953/PR #957) and is
99
+ // deliberately not asserted on so this suite stays independent of
100
+ // that fix landing.
101
+ const src = `classDiagram
102
+ class Alpha
103
+ class Mid {
104
+ ${manyAttrs('b', 6)}
105
+ }
106
+ class Charlie
107
+ class Root
108
+ class Kid
109
+ Alpha --> Mid
110
+ Mid --> Charlie
111
+ Charlie --> Alpha
112
+ Root --> Kid`
113
+ const ascii = renderMermaidASCII(src, { useAscii: true })
114
+ expect(
115
+ ascii,
116
+ '"+ b4: String" was overwritten by Root->Kid\'s jog',
117
+ ).toContain('+ b4: String')
118
+ })
119
+
120
+ it('deeper tree: a tall level-0 sibling between a level-1 source and its level-2 grandchild', () => {
121
+ const src = `classDiagram
122
+ class A
123
+ class TallMid {
124
+ ${manyAttrs('b', 6)}
125
+ }
126
+ class B
127
+ class Kid1
128
+ class GrandKid
129
+ B --> Kid1
130
+ Kid1 --> GrandKid`
131
+ const ascii = renderMermaidASCII(src, { useAscii: true })
132
+ expectAllPresentOnce(ascii, [
133
+ '+ b0: String',
134
+ '+ b1: String',
135
+ '+ b2: String',
136
+ '+ b3: String',
137
+ '+ b4: String',
138
+ '+ b5: String',
139
+ ])
140
+ expectNoBoxOverlap(ascii, ['A', 'TallMid', 'B', 'Kid1', 'GrandKid'])
141
+ })
142
+
143
+ it('more siblings: several unrelated roots plus a separate parent/child pair, inheritance marker', () => {
144
+ const src = `classDiagram
145
+ class Left
146
+ class TallMid {
147
+ ${manyAttrs('d', 6)}
148
+ }
149
+ class RightSrc
150
+ class Kid
151
+ RightSrc <|-- Kid`
152
+ const ascii = renderMermaidASCII(src, { useAscii: true })
153
+ expectAllPresentOnce(ascii, [
154
+ '+ d0: String',
155
+ '+ d1: String',
156
+ '+ d2: String',
157
+ '+ d3: String',
158
+ '+ d4: String',
159
+ '+ d5: String',
160
+ ])
161
+ expectNoBoxOverlap(ascii, ['Left', 'TallMid', 'RightSrc', 'Kid'])
162
+ })
163
+
164
+ it('"target above source" branch with a from-side marker renders without corrupting a box', () => {
165
+ // The "above" branch (this file's second `else if`) has no
166
+ // findClearColumn collision-avoidance at all — it always draws the
167
+ // straight jog/vertical/horizontal/vertical/jog path, so it's just as
168
+ // exposed to this bug as the "below" branch, and is guarded the same
169
+ // way here. A `Root --> A` / `A <|-- B` / `B --> A` back-edge forces
170
+ // A's level above B's (the level-assignment BFS keeps the *longest*
171
+ // path to a node, so B->A's back edge pushes A past B) — the
172
+ // `A <|-- B` edge then runs from the now-deeper A up to the
173
+ // shallower B, taking the "above" branch with its hollow-triangle
174
+ // marker at the 'from' end (A), a combination no other test in this
175
+ // file exercises.
176
+ const src = `classDiagram
177
+ class Root
178
+ class A
179
+ class B
180
+ class Extra
181
+ Root --> A
182
+ A <|-- B
183
+ B --> A`
184
+ const ascii = renderMermaidASCII(src, { useAscii: true })
185
+ expectNoBoxOverlap(ascii, ['Root', 'A', 'B', 'Extra'])
186
+ })
187
+ })
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import { describe, it, expect } from 'vitest'
28
- import { renderMermaidASCII } from '../ascii/index.ts'
28
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
29
29
 
30
30
  const MVC_SOURCE = `classDiagram
31
31
  class Model {
@@ -3,7 +3,7 @@
3
3
  // ============================================================================
4
4
 
5
5
  import { describe, it, expect } from 'vitest'
6
- import { renderMermaidASCII } from '../ascii/index.ts'
6
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
7
7
 
8
8
  describe('ASCII class diagram box compartments', () => {
9
9
  it('omits the attrs compartment for a class with methods but no attributes (regression)', () => {
@@ -21,7 +21,7 @@
21
21
  */
22
22
 
23
23
  import { describe, it, expect } from 'vitest'
24
- import { renderMermaidASCII } from '../ascii/index.ts'
24
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
25
25
 
26
26
  describe('ASCII class diagram — label collision with an intermediate box', () => {
27
27
  it('moves a skip-edge label out of an intermediate class box instead of overwriting it', () => {
@@ -40,7 +40,7 @@
40
40
  */
41
41
 
42
42
  import { describe, it, expect } from 'vitest'
43
- import { renderMermaidASCII } from '../ascii/index.ts'
43
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
44
44
 
45
45
  /** paddingY values below the default (5) that the issue found all reproduce
46
46
  * the same corruption — the vertical gap floors at 1 row, so nothing about
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { describe, it, expect } from 'vitest'
19
- import { renderMermaidASCII } from '../ascii/index.ts'
19
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
20
20
 
21
21
  describe('ASCII class diagram — label territory is row-aware (issue #447 follow-up)', () => {
22
22
  it('does not truncate two labels whose relationships share a source but target different, differently-positioned classes', () => {
@@ -11,7 +11,7 @@
11
11
  // ============================================================================
12
12
 
13
13
  import { describe, it, expect } from 'vitest'
14
- import { renderMermaidASCII } from '../ascii/index.ts'
14
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
15
15
 
16
16
  // Two independent, same-level classes — exercises the horizontal gap.
17
17
  const SIDE_BY_SIDE = `classDiagram