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
@@ -15,13 +15,18 @@ import { describe, it, expect } from 'vitest'
15
15
  import { splitStatements, detectDiagramType } from '@zombie-mermaid/core'
16
16
  import { renderMermaidASCII, renderMermaidSVG } from '../index.ts'
17
17
 
18
+ /** Project a `splitStatements` result down to just the text, for assertions
19
+ * that don't care about position (see the dedicated `line` tests below for
20
+ * position coverage). */
21
+ const texts = (source: string) => splitStatements(source).map((s) => s.text)
22
+
18
23
  describe('splitStatements (issue #181)', () => {
19
24
  it('splits on newlines', () => {
20
- expect(splitStatements('graph TD\nA-->B')).toEqual(['graph TD', 'A-->B'])
25
+ expect(texts('graph TD\nA-->B')).toEqual(['graph TD', 'A-->B'])
21
26
  })
22
27
 
23
28
  it('splits on semicolons', () => {
24
- expect(splitStatements('graph TD;A-->B;B-->C')).toEqual([
29
+ expect(texts('graph TD;A-->B;B-->C')).toEqual([
25
30
  'graph TD',
26
31
  'A-->B',
27
32
  'B-->C',
@@ -29,59 +34,53 @@ describe('splitStatements (issue #181)', () => {
29
34
  })
30
35
 
31
36
  it('drops empty statements from trailing or repeated separators', () => {
32
- expect(splitStatements('graph TD;A-->B;')).toEqual(['graph TD', 'A-->B'])
33
- expect(splitStatements('graph TD;;;A-->B')).toEqual(['graph TD', 'A-->B'])
34
- expect(splitStatements('graph TD\n\n\nA-->B')).toEqual([
35
- 'graph TD',
36
- 'A-->B',
37
- ])
37
+ expect(texts('graph TD;A-->B;')).toEqual(['graph TD', 'A-->B'])
38
+ expect(texts('graph TD;;;A-->B')).toEqual(['graph TD', 'A-->B'])
39
+ expect(texts('graph TD\n\n\nA-->B')).toEqual(['graph TD', 'A-->B'])
38
40
  })
39
41
 
40
42
  it('trims whitespace around each statement', () => {
41
- expect(splitStatements(' graph TD ; A-->B ')).toEqual([
42
- 'graph TD',
43
- 'A-->B',
44
- ])
43
+ expect(texts(' graph TD ; A-->B ')).toEqual(['graph TD', 'A-->B'])
45
44
  })
46
45
 
47
46
  describe('does not split a semicolon that is not a separator', () => {
48
47
  it('inside a double-quoted label', () => {
49
- expect(splitStatements('graph TD\nA["a; b"]-->B')).toEqual([
48
+ expect(texts('graph TD\nA["a; b"]-->B')).toEqual([
50
49
  'graph TD',
51
50
  'A["a; b"]-->B',
52
51
  ])
53
52
  })
54
53
 
55
54
  it('inside a single-quoted label', () => {
56
- expect(splitStatements("graph TD\nA['x; y']-->B")).toEqual([
55
+ expect(texts("graph TD\nA['x; y']-->B")).toEqual([
57
56
  'graph TD',
58
57
  "A['x; y']-->B",
59
58
  ])
60
59
  })
61
60
 
62
61
  it('terminating a named character reference', () => {
63
- expect(splitStatements('graph TD\nA[&]-->B')).toEqual([
62
+ expect(texts('graph TD\nA[&]-->B')).toEqual([
64
63
  'graph TD',
65
64
  'A[&]-->B',
66
65
  ])
67
66
  })
68
67
 
69
68
  it('terminating a numeric character reference', () => {
70
- expect(splitStatements('graph TD\nA[#]-->B')).toEqual([
69
+ expect(texts('graph TD\nA[#]-->B')).toEqual([
71
70
  'graph TD',
72
71
  'A[#]-->B',
73
72
  ])
74
73
  })
75
74
 
76
75
  it('terminating a hex character reference', () => {
77
- expect(splitStatements('graph TD\nA[😀]-->B')).toEqual([
76
+ expect(texts('graph TD\nA[😀]-->B')).toEqual([
78
77
  'graph TD',
79
78
  'A[😀]-->B',
80
79
  ])
81
80
  })
82
81
 
83
82
  it('but still splits a real separator on the same line as an entity', () => {
84
- expect(splitStatements('graph TD;A[&]-->B;B-->C')).toEqual([
83
+ expect(texts('graph TD;A[&]-->B;B-->C')).toEqual([
85
84
  'graph TD',
86
85
  'A[&]-->B',
87
86
  'B-->C',
@@ -90,26 +89,20 @@ describe('splitStatements (issue #181)', () => {
90
89
 
91
90
  it('and treats a lone ampersand as an ordinary character', () => {
92
91
  // `A & B` is flowchart's parallel-link syntax, not an entity.
93
- expect(splitStatements('graph TD;A-->B & C')).toEqual([
94
- 'graph TD',
95
- 'A-->B & C',
96
- ])
92
+ expect(texts('graph TD;A-->B & C')).toEqual(['graph TD', 'A-->B & C'])
97
93
  })
98
94
  })
99
95
 
100
96
  describe('comments', () => {
101
97
  it('drops whole-line comments without splitting their semicolons', () => {
102
- expect(splitStatements('graph TD\n%% note; not code\nA-->B')).toEqual([
98
+ expect(texts('graph TD\n%% note; not code\nA-->B')).toEqual([
103
99
  'graph TD',
104
100
  'A-->B',
105
101
  ])
106
102
  })
107
103
 
108
104
  it('drops a comment that only begins after a semicolon', () => {
109
- expect(splitStatements('graph TD\nA-->B; %% note')).toEqual([
110
- 'graph TD',
111
- 'A-->B',
112
- ])
105
+ expect(texts('graph TD\nA-->B; %% note')).toEqual(['graph TD', 'A-->B'])
113
106
  })
114
107
 
115
108
  it('lets the comment swallow the rest of the line', () => {
@@ -118,14 +111,14 @@ describe('splitStatements (issue #181)', () => {
118
111
  * resurrect what follows. Splitting first and then discarding fragments
119
112
  * that merely begin with `%%` left `C-->D` parsing as code.
120
113
  */
121
- expect(splitStatements('graph TD\nA-->B; %% note; C-->D')).toEqual([
114
+ expect(texts('graph TD\nA-->B; %% note; C-->D')).toEqual([
122
115
  'graph TD',
123
116
  'A-->B',
124
117
  ])
125
118
  })
126
119
 
127
120
  it('treats %% inside a quoted label as text, not a comment', () => {
128
- expect(splitStatements('graph TD\nA["100%% done"]-->B')).toEqual([
121
+ expect(texts('graph TD\nA["100%% done"]-->B')).toEqual([
129
122
  'graph TD',
130
123
  'A["100%% done"]-->B',
131
124
  ])
@@ -144,14 +137,11 @@ describe('splitStatements (issue #181)', () => {
144
137
  ['A[heart #9829; x]-->B', 'a symbol reference'],
145
138
  ['A[say #quot;hi#quot;]-->B', 'a named reference'],
146
139
  ])('keeps %s intact (%s)', (statement) => {
147
- expect(splitStatements(`graph TD\n ${statement}`)).toEqual([
148
- 'graph TD',
149
- statement,
150
- ])
140
+ expect(texts(`graph TD\n ${statement}`)).toEqual(['graph TD', statement])
151
141
  })
152
142
 
153
143
  it('still splits a real separator on the same line as one', () => {
154
- expect(splitStatements('graph TD;A[#59;]-->B;B-->C')).toEqual([
144
+ expect(texts('graph TD;A[#59;]-->B;B-->C')).toEqual([
155
145
  'graph TD',
156
146
  'A[#59;]-->B',
157
147
  'B-->C',
@@ -159,7 +149,7 @@ describe('splitStatements (issue #181)', () => {
159
149
  })
160
150
 
161
151
  it('leaves a bare # that is not a reference alone', () => {
162
- expect(splitStatements('graph TD;A[C# lang]-->B')).toEqual([
152
+ expect(texts('graph TD;A[C# lang]-->B')).toEqual([
163
153
  'graph TD',
164
154
  'A[C# lang]-->B',
165
155
  ])
@@ -167,6 +157,34 @@ describe('splitStatements (issue #181)', () => {
167
157
  })
168
158
  })
169
159
 
160
+ describe('splitStatements — line numbers (issue #760)', () => {
161
+ it('reports the 1-based physical source line for each newline-separated statement', () => {
162
+ const stmts = splitStatements('graph TD\nA-->B\nB-->C')
163
+ expect(stmts.map((s) => s.line)).toEqual([1, 2, 3])
164
+ })
165
+
166
+ it('carries forward the original line number across dropped blank lines', () => {
167
+ const stmts = splitStatements('graph TD\n\n\nA-->B')
168
+ expect(stmts.map((s) => ({ text: s.text, line: s.line }))).toEqual([
169
+ { text: 'graph TD', line: 1 },
170
+ { text: 'A-->B', line: 4 },
171
+ ])
172
+ })
173
+
174
+ it('carries forward the original line number across a dropped comment line', () => {
175
+ const stmts = splitStatements('graph TD\n%% a comment\nA-->B')
176
+ expect(stmts.map((s) => ({ text: s.text, line: s.line }))).toEqual([
177
+ { text: 'graph TD', line: 1 },
178
+ { text: 'A-->B', line: 3 },
179
+ ])
180
+ })
181
+
182
+ it('gives every semicolon-separated statement on one physical line the same line number', () => {
183
+ const stmts = splitStatements('graph TD\nA-->B;B-->C;C-->D')
184
+ expect(stmts.map((s) => s.line)).toEqual([1, 2, 2, 2])
185
+ })
186
+ })
187
+
170
188
  describe('semicolon-separated diagrams render their bodies (issue #181)', () => {
171
189
  const cases = [
172
190
  {
@@ -16,27 +16,27 @@ B --> Y
16
16
  |
17
17
  |
18
18
  |
19
- +---|---+
20
- |LR Grou|
21
- | | |
22
- | v |
23
- | +---+ |
24
- | | | |
25
- | | A | |
26
- | | | |
27
- | +-+-+ |
28
- | | |
29
- | | |
30
- | | |
31
- | | |
32
- | v |
33
- | +---+ |
34
- | | | |
35
- | | B | |
36
- | | | |
37
- | +-+-+ |
38
- | | |
39
- +---|---+
19
+ +---|----+
20
+ |LR Group|
21
+ | | |
22
+ | v |
23
+ | +---+ |
24
+ | | | |
25
+ | | A | |
26
+ | | | |
27
+ | +-+-+ |
28
+ | | |
29
+ | | |
30
+ | | |
31
+ | | |
32
+ | v |
33
+ | +---+ |
34
+ | | | |
35
+ | | B | |
36
+ | | | |
37
+ | +-+-+ |
38
+ | | |
39
+ +---|----+
40
40
  |
41
41
  |
42
42
  v
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Guards the "legible, readable contrast" acceptance criterion the #684
3
+ * theme-selector restoration's 16 per-theme verification sub-issues
4
+ * (#691-706) each individually ask for, systematically across every
5
+ * built-in theme at once rather than eyeballing 16 screenshots one at a
6
+ * time — see docs/decisions/theme-selector-shared-state.md and this
7
+ * repo's CLAUDE.md's "favor tooling over manual scanning" convention.
8
+ *
9
+ * Computes the actual WCAG 2.x contrast ratio (relative luminance, per
10
+ * https://www.w3.org/TR/WCAG21/#dfn-relative-luminance) between each
11
+ * theme's `bg`/`fg` pair — not a hand-picked pass/fail per theme, so a
12
+ * future THEMES addition with a genuinely illegible pair fails this test
13
+ * automatically instead of only being caught by a human noticing a
14
+ * screenshot looks wrong.
15
+ */
16
+ import { describe, it, expect } from 'vitest'
17
+ import { THEMES } from '@zombie-mermaid/core'
18
+
19
+ /**
20
+ * The threshold every theme must clear. WCAG AA's own thresholds are 4.5:1
21
+ * for normal text and 3:1 for large/bold text; diagram node labels here
22
+ * are typically medium, semi-bold text, closer to the "large text"
23
+ * category than dense body copy. 4.0 sits between the two -- strict enough
24
+ * to catch a genuinely illegible pair, loose enough not to fail
25
+ * `solarized-light` (4.13:1), whose muted bg/fg pair matches Ethan
26
+ * Schoonover's actual published Solarized values rather than being a
27
+ * mistake in this repo's THEMES data -- #684's own non-goals rule out
28
+ * changing THEMES' colors to "fix" a curated editor-theme's real values.
29
+ */
30
+ const MIN_CONTRAST_RATIO = 4.0
31
+
32
+ function hexToRgb(hex: string): [number, number, number] {
33
+ const clean = hex.replace('#', '')
34
+ return [
35
+ parseInt(clean.slice(0, 2), 16),
36
+ parseInt(clean.slice(2, 4), 16),
37
+ parseInt(clean.slice(4, 6), 16),
38
+ ]
39
+ }
40
+
41
+ /** sRGB channel -> linear-light value, the WCAG relative-luminance step. */
42
+ function linearize(channel: number): number {
43
+ const c = channel / 255
44
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
45
+ }
46
+
47
+ function relativeLuminance(hex: string): number {
48
+ const [r, g, b] = hexToRgb(hex)
49
+ return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
50
+ }
51
+
52
+ /** WCAG contrast ratio between two colors, always >= 1 (order-independent). */
53
+ function contrastRatio(hexA: string, hexB: string): number {
54
+ const lumA = relativeLuminance(hexA)
55
+ const lumB = relativeLuminance(hexB)
56
+ const lighter = Math.max(lumA, lumB)
57
+ const darker = Math.min(lumA, lumB)
58
+ return (lighter + 0.05) / (darker + 0.05)
59
+ }
60
+
61
+ describe('every built-in theme has a legible bg/fg contrast ratio', () => {
62
+ for (const [themeKey, colors] of Object.entries(THEMES)) {
63
+ it(`${themeKey}: bg/fg contrast is at least ${MIN_CONTRAST_RATIO}:1`, () => {
64
+ const ratio = contrastRatio(colors.bg, colors.fg)
65
+ expect(ratio).toBeGreaterThanOrEqual(MIN_CONTRAST_RATIO)
66
+ })
67
+ }
68
+ })
69
+
70
+ describe('contrastRatio() itself', () => {
71
+ it('returns 1 for identical colors', () => {
72
+ expect(contrastRatio('#808080', '#808080')).toBeCloseTo(1, 5)
73
+ })
74
+
75
+ it('returns 21 for pure black vs pure white (the WCAG maximum)', () => {
76
+ expect(contrastRatio('#000000', '#FFFFFF')).toBeCloseTo(21, 1)
77
+ })
78
+
79
+ it('is symmetric regardless of argument order', () => {
80
+ const a = contrastRatio('#1a1b26', '#a9b1d6')
81
+ const b = contrastRatio('#a9b1d6', '#1a1b26')
82
+ expect(a).toBeCloseTo(b, 10)
83
+ })
84
+ })
@@ -14,12 +14,21 @@
14
14
  //
15
15
  // So: walk what the files actually import, and assert the shape.
16
16
  //
17
- // core -> nothing in this repo, only `elkjs` (type-only)
18
- // svg-renderer -> `@zombie-mermaid/core` and `elkjs`, nothing else
17
+ // core -> nothing in this repo, only `elkjs` (type-only)
18
+ // mermaid-parser -> `@zombie-mermaid/core`, nothing else
19
+ // svg-renderer -> `@zombie-mermaid/core`, `@zombie-mermaid/mermaid-parser`,
20
+ // and `elkjs`, nothing else
19
21
  //
20
- // Neither may reach back into `src/` by relative path or by importing
21
- // `zombie-mermaid` itself — the umbrella depends on them, never the
22
- // reverse.
22
+ // `svg-renderer` depending on `mermaid-parser` (added under #624) is a new
23
+ // edge, not a violation of the sink property above: it is an ordinary,
24
+ // acyclic dependency (both `svg-renderer` and the future `ascii-renderer`
25
+ // depend on `mermaid-parser` directly — the scoping doc's finding 2), and
26
+ // `mermaid-parser` itself stays a sink exactly like `core` — it does not
27
+ // import `svg-renderer` back.
28
+ //
29
+ // None of the three may reach back into `src/` by relative path or by
30
+ // importing `zombie-mermaid` itself — the umbrella depends on them, never
31
+ // the reverse.
23
32
 
24
33
  import { describe, it, expect } from 'vitest'
25
34
  import { readdirSync, readFileSync, statSync } from 'node:fs'
@@ -43,6 +52,20 @@ const PACKAGES = resolve(REPO_ROOT, 'packages')
43
52
  const SPECIFIER_RE =
44
53
  /(?:^|\n)[ \t]*(?:import|export)\b[^'"\n]*?from[ \t]*['"]([^'"]+)['"]|(?:^|\n)[ \t]*import[ \t]*['"]([^'"]+)['"]|\bimport\([ \t]*['"]([^'"]+)['"]/g
45
54
 
55
+ /**
56
+ * Same three shapes as `SPECIFIER_RE`'s first alternative, but split into
57
+ * the `import`/`export` keyword and the clause between it and `from`, so a
58
+ * whole-clause type-only import (`import type { X } from '...'` /
59
+ * `export type { X } from '...'`) can be told apart from a real value one.
60
+ * Used only by `valueSpecifiersOf` below — `specifiersOf`/`SPECIFIER_RE`
61
+ * stay untouched since the import-graph tests above are deliberately
62
+ * over-approximating.
63
+ */
64
+ const FROM_CLAUSE_RE =
65
+ /(?:^|\n)[ \t]*(?:import|export)\b([^'"\n]*?)from[ \t]*['"]([^'"]+)['"]/g
66
+ const SIDE_EFFECT_IMPORT_RE = /(?:^|\n)[ \t]*import[ \t]*['"]([^'"]+)['"]/g
67
+ const DYNAMIC_IMPORT_RE = /\bimport\([ \t]*['"]([^'"]+)['"]/g
68
+
46
69
  function tsFilesUnder(dir: string): string[] {
47
70
  const out: string[] = []
48
71
  for (const entry of readdirSync(dir)) {
@@ -77,12 +100,63 @@ function externalSpecifiers(pkg: string): string[] {
77
100
  .sort()
78
101
  }
79
102
 
103
+ /**
104
+ * Every specifier named by a *value* import in `packages/<name>/src` —
105
+ * i.e. excluding a whole-clause `import type { X } from '...'` /
106
+ * `export type { X } from '...'`, which is erased before bundling and so
107
+ * shouldn't obligate a `dependencies` entry (#742). A per-symbol inline
108
+ * `import { type X, y } from '...'` still counts as a value import here,
109
+ * since `y` binds to a real runtime export from that module — this repo
110
+ * doesn't currently mix the two on one specifier, so that distinction
111
+ * isn't exercised, but the whole-clause check stays conservative rather
112
+ * than trying to parse the individual specifiers in the braces.
113
+ */
114
+ function valueSpecifiersOf(pkg: string): Map<string, string[]> {
115
+ const bySpecifier = new Map<string, string[]>()
116
+ const record = (specifier: string, file: string) => {
117
+ const seen = bySpecifier.get(specifier) ?? []
118
+ seen.push(relative(REPO_ROOT, file))
119
+ bySpecifier.set(specifier, seen)
120
+ }
121
+ for (const file of tsFilesUnder(resolve(PACKAGES, pkg, 'src'))) {
122
+ const source = readFileSync(file, 'utf8')
123
+ for (const match of source.matchAll(FROM_CLAUSE_RE)) {
124
+ const [, clause, specifier] = match
125
+ if (/^\s*type\b/.test(clause ?? '')) continue
126
+ record(specifier!, file)
127
+ }
128
+ for (const match of source.matchAll(SIDE_EFFECT_IMPORT_RE)) {
129
+ record(match[1]!, file)
130
+ }
131
+ for (const match of source.matchAll(DYNAMIC_IMPORT_RE)) {
132
+ record(match[1]!, file)
133
+ }
134
+ }
135
+ return bySpecifier
136
+ }
137
+
138
+ /**
139
+ * Non-relative value-import specifiers only — what a package's
140
+ * `dependencies` should actually list, since a type-only import is erased
141
+ * before anything is bundled and never needs a runtime package present.
142
+ */
143
+ function externalValueSpecifiers(pkg: string): string[] {
144
+ return [...valueSpecifiersOf(pkg).keys()]
145
+ .filter((s) => !s.startsWith('.'))
146
+ .map((s) => (s.startsWith('@') ? s : s.split('/')[0]!))
147
+ .filter((s, i, all) => all.indexOf(s) === i)
148
+ .sort()
149
+ }
150
+
80
151
  /**
81
152
  * A relative specifier that climbs out of `packages/<name>/src` — i.e. a
82
153
  * reach-around into the umbrella's `src/` or into a sibling package. The
83
- * three legitimate `../` hops inside `svg-renderer` (`layout-engine/*.ts`
84
- * importing `../styles.ts` and friends) stay within the package and are
85
- * resolved, not pattern-matched, so this can't be fooled by depth.
154
+ * many legitimate `../` hops inside `svg-renderer` (`layout-engine/*.ts`
155
+ * importing `../styles.ts`, and — since #624 — each `class/`, `er/`,
156
+ * `sequence/`, `xychart/` subdirectory's `layout.ts`/`renderer.ts`
157
+ * importing sibling modules like `../elk-instance.ts`/`../renderer.ts`)
158
+ * stay within the package and are resolved, not pattern-matched, so this
159
+ * can't be fooled by depth.
86
160
  */
87
161
  function escapingRelativeImports(pkg: string): string[] {
88
162
  const packageSrc = resolve(PACKAGES, pkg, 'src')
@@ -115,10 +189,27 @@ describe('@zombie-mermaid/core is a workspace sink', () => {
115
189
  })
116
190
  })
117
191
 
118
- describe('@zombie-mermaid/svg-renderer depends only on core', () => {
119
- it('imports no workspace package other than @zombie-mermaid/core', () => {
192
+ describe('@zombie-mermaid/mermaid-parser is a workspace sink', () => {
193
+ // Verified (#624, umbrella #620): no file under packages/mermaid-parser/src
194
+ // imports `elkjs`, `@zombie-mermaid/svg-renderer`, or anything from the
195
+ // umbrella — the property that lets both `svg-renderer` and the future
196
+ // `ascii-renderer` depend on it without either dragging the other in.
197
+ it('imports no workspace package and no npm dependency other than @zombie-mermaid/core', () => {
198
+ expect(externalSpecifiers('mermaid-parser')).toEqual([
199
+ '@zombie-mermaid/core',
200
+ ])
201
+ })
202
+
203
+ it('never reaches outside its own src/ by relative path', () => {
204
+ expect(escapingRelativeImports('mermaid-parser')).toEqual([])
205
+ })
206
+ })
207
+
208
+ describe('@zombie-mermaid/svg-renderer depends only on core and mermaid-parser', () => {
209
+ it('imports no workspace package other than @zombie-mermaid/core and @zombie-mermaid/mermaid-parser', () => {
120
210
  expect(externalSpecifiers('svg-renderer')).toEqual([
121
211
  '@zombie-mermaid/core',
212
+ '@zombie-mermaid/mermaid-parser',
122
213
  'elkjs',
123
214
  ])
124
215
  })
@@ -129,27 +220,110 @@ describe('@zombie-mermaid/svg-renderer depends only on core', () => {
129
220
  })
130
221
 
131
222
  describe('workspace package manifests', () => {
132
- // The umbrella bundles both packages into its own `dist/` (they are
133
- // absent from `isExternal` in vite.config.lib.ts), so nothing resolves
134
- // these names at install time and publishing them would be misleading.
135
- // Recommendation 2 of the scoping doc; the umbrella declares them as
136
- // devDependencies for the same reason.
137
- it.each(['core', 'svg-renderer'])('%s is private and unpublished', (pkg) => {
138
- const manifest = JSON.parse(
139
- readFileSync(resolve(PACKAGES, pkg, 'package.json'), 'utf8'),
140
- ) as { name: string; private: boolean }
141
- expect(manifest.name).toBe(`@zombie-mermaid/${pkg}`)
142
- expect(manifest.private).toBe(true)
143
- })
223
+ // As of #769, each of the five `@zombie-mermaid/*` packages has a
224
+ // genuinely publishable `package.json` shape — real `name`/`exports`
225
+ // pointing at its own built `dist/`, `publishConfig.access: "public"`,
226
+ // no longer `"private": true` — and its own independent build
227
+ // (`packages/<name>/vite.config.ts`, via `vite.config.package.ts`).
228
+ //
229
+ // That is preparation only. The umbrella (`vite.config.lib.ts`) still
230
+ // bundles all five packages' source directly into its own `dist/` (they
231
+ // are absent from `isExternal` there, same as before #769) — none of
232
+ // them is a real external `dependencies` entry of `zombie-mermaid`, none
233
+ // is part of `.changeset/config.json`'s `fixed` group (still `[]`), and
234
+ // none has ever actually been published to npm. Externalizing them for
235
+ // real is a separate, future PR gated on completing npm's one-time
236
+ // trusted-publishing setup for each new package name — see
237
+ // RELEASING.md's "Future: multi-package publish" section.
238
+ it.each(['core', 'mermaid-parser', 'svg-renderer', 'ascii-renderer', 'mcp'])(
239
+ '%s is publish-ready in shape under the @zombie-mermaid/ scope (not yet externalized or published)',
240
+ (pkg) => {
241
+ const manifest = JSON.parse(
242
+ readFileSync(resolve(PACKAGES, pkg, 'package.json'), 'utf8'),
243
+ ) as {
244
+ name: string
245
+ private?: boolean
246
+ publishConfig?: { access?: string }
247
+ }
248
+ expect(manifest.name).toBe(`@zombie-mermaid/${pkg}`)
249
+ // Not `"private": true` — a scoped package still defaults to a
250
+ // *private* npm publish unless `publishConfig.access` says otherwise,
251
+ // which is the actual thing preventing an accidental unscoped-style
252
+ // public leak here; `private` merely gates `pnpm publish`/`npm publish`
253
+ // from running against this package at all.
254
+ expect(manifest.private).not.toBe(true)
255
+ expect(manifest.publishConfig?.access).toBe('public')
256
+ },
257
+ )
144
258
 
145
- it('declares every external specifier its source actually imports', () => {
146
- for (const pkg of ['core', 'svg-renderer'] as const) {
259
+ // Declared `dependencies` must match actual *runtime* imports, not the
260
+ // over-approximating `externalSpecifiers` above — a type-only import
261
+ // (e.g. `mermaid-parser`'s hypothetical future one) is erased before
262
+ // bundling and belongs in `devDependencies` instead, or a published
263
+ // consumer of the package would pull in a runtime dependency it never
264
+ // executes (#742) — UNLESS that type is itself part of this package's own
265
+ // published public surface (its rolled-up `dist/index.d.ts`, built via
266
+ // api-extractor's `bundleTypes` — see vite.config.package.ts), in which
267
+ // case a real downstream `tsc` consumer needs the type resolvable and a
268
+ // `devDependency` (never installed for a consumer) won't do. `core` is
269
+ // exactly this case: `LayoutCache`'s public field types reference
270
+ // `elkjs`'s `ElkNode` (`import type { ElkNode } from 'elkjs'` in
271
+ // `types.ts`), so `elkjs` is a real `dependencies` entry there despite
272
+ // never being a *value* import in `core`'s own source.
273
+ it('declares every runtime (non-type-only) external specifier its source actually imports', () => {
274
+ for (const pkg of ['mermaid-parser', 'svg-renderer'] as const) {
147
275
  const manifest = JSON.parse(
148
276
  readFileSync(resolve(PACKAGES, pkg, 'package.json'), 'utf8'),
149
277
  ) as { dependencies?: Record<string, string> }
150
278
  expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual(
151
- externalSpecifiers(pkg),
279
+ externalValueSpecifiers(pkg),
152
280
  )
153
281
  }
154
282
  })
283
+
284
+ it('core additionally declares elkjs — type-only in source, but part of its own published public types', () => {
285
+ const manifest = JSON.parse(
286
+ readFileSync(resolve(PACKAGES, 'core', 'package.json'), 'utf8'),
287
+ ) as { dependencies?: Record<string, string> }
288
+ expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual([
289
+ 'elkjs',
290
+ ...externalValueSpecifiers('core'),
291
+ ])
292
+ })
293
+
294
+ // `ascii-renderer` and `mcp` each bundle one (`packages/ascii-renderer/src/flowchart.ts`
295
+ // -> `../../../src/parser.ts`) or two (`packages/mcp/src/{server,tools/render-svg}.ts`
296
+ // -> `../../../src/{package-info,index}.ts`) files from *outside* their
297
+ // own `packages/<name>/src/` — see vite.config.package.ts's header and
298
+ // each package's own vite.config.ts for why (their build still compiles
299
+ // that source in directly, same as before #769). `externalValueSpecifiers`
300
+ // above only scans each package's own `src/`, so it can't see what those
301
+ // reach-through files themselves import — this asserts the full,
302
+ // hand-traced transitive set instead of extending that scanner to follow
303
+ // an out-of-package relative import (which `escapingRelativeImports`
304
+ // above deliberately treats as a violation for the other three packages).
305
+ it('ascii-renderer declares every specifier its reach-through into src/parser.ts needs, on top of its own', () => {
306
+ const manifest = JSON.parse(
307
+ readFileSync(resolve(PACKAGES, 'ascii-renderer', 'package.json'), 'utf8'),
308
+ ) as { dependencies?: Record<string, string> }
309
+ expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual([
310
+ '@zombie-mermaid/core',
311
+ '@zombie-mermaid/mermaid-parser',
312
+ ])
313
+ })
314
+
315
+ it('mcp declares every specifier its reach-through into src/{package-info,index}.ts needs, on top of its own', () => {
316
+ const manifest = JSON.parse(
317
+ readFileSync(resolve(PACKAGES, 'mcp', 'package.json'), 'utf8'),
318
+ ) as { dependencies?: Record<string, string> }
319
+ expect(Object.keys(manifest.dependencies ?? {}).sort()).toEqual([
320
+ '@modelcontextprotocol/sdk',
321
+ '@zombie-mermaid/ascii-renderer',
322
+ '@zombie-mermaid/core',
323
+ '@zombie-mermaid/mermaid-parser',
324
+ '@zombie-mermaid/svg-renderer',
325
+ 'entities',
326
+ 'zod',
327
+ ])
328
+ })
155
329
  })
@@ -5,7 +5,7 @@
5
5
  * multi-series support, staircase line routing, and edge cases.
6
6
  */
7
7
  import { describe, it, expect } from 'vitest'
8
- import { renderMermaidASCII } from '../ascii/index.ts'
8
+ import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
9
9
 
10
10
  // ============================================================================
11
11
  // Helper — render with no colors for easy string matching
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Tests for xychart color palette generation (src/xychart/colors.ts).
2
+ * Tests for xychart color palette generation
3
+ * (packages/mermaid-parser/src/xychart/colors.ts).
3
4
  */
4
5
  import { describe, it, expect } from 'vitest'
5
6
  import {
@@ -8,7 +9,7 @@ import {
8
9
  isDarkBackground,
9
10
  mixHexColors,
10
11
  getSeriesColor,
11
- } from '../xychart/colors.ts'
12
+ } from '@zombie-mermaid/mermaid-parser'
12
13
 
13
14
  describe('isValidHex', () => {
14
15
  it('accepts a well-formed 6-digit hex color', () => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Coverage-focused tests for src/xychart/layout.ts.
2
+ * Coverage-focused tests for packages/svg-renderer/src/xychart/layout.ts.
3
3
  *
4
4
  * Exercises the horizontal layout path (previously untested), numeric
5
5
  * x-axis ranges (category label interpolation), multi-series bar groups,