zombie-mermaid 2.2.1 → 2.2.3

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 (253) 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 +4072 -3942
  6. package/dist/ascii.js.map +1 -1
  7. package/dist/cli.js +6724 -6464
  8. package/dist/index.cjs +52 -52
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.js +7653 -7469
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.cjs +14201 -13705
  13. package/dist/mcp.cjs.map +1 -1
  14. package/dist/mcp.js +14201 -13705
  15. package/dist/mcp.js.map +1 -1
  16. package/package.json +28 -6
  17. package/packages/ascii-renderer/LICENSE +22 -0
  18. package/{src/ascii → packages/ascii-renderer/src}/class-diagram.ts +469 -69
  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 +89 -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 +194 -53
  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 +9 -9
  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-parent-alignment-971.test.ts +139 -0
  93. package/src/__tests__/ascii-class-parent-alignment-972.test.ts +208 -0
  94. package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +86 -1
  95. package/src/__tests__/ascii-combining-mark-width.test.ts +2 -2
  96. package/src/__tests__/ascii-coords-overlay.test.ts +2 -2
  97. package/src/__tests__/ascii-decision-lr-box-start.test.ts +2 -2
  98. package/src/__tests__/ascii-display-width-unit.test.ts +1 -1
  99. package/src/__tests__/ascii-draw-arrows-coverage.test.ts +1 -1
  100. package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +12 -5
  101. package/src/__tests__/ascii-draw-bundles.test.ts +5 -5
  102. package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +1 -1
  103. package/src/__tests__/ascii-edge-bundling-routing.test.ts +9 -5
  104. package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +10 -6
  105. package/src/__tests__/ascii-edge-ending-glyphs.test.ts +1 -1
  106. package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +2 -2
  107. package/src/__tests__/ascii-edge-routing-fixes.test.ts +1 -1
  108. package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +8 -4
  109. package/src/__tests__/ascii-edge-styles.test.ts +1 -1
  110. package/src/__tests__/ascii-emoji-cluster-width.test.ts +2 -2
  111. package/src/__tests__/ascii-er-box-occupancy.test.ts +1 -1
  112. package/src/__tests__/ascii-er-cardinality.test.ts +1 -1
  113. package/src/__tests__/ascii-er-corner-glyphs.test.ts +1 -1
  114. package/src/__tests__/ascii-er-jog-stray-line.test.ts +1 -1
  115. package/src/__tests__/ascii-er-label-padding.test.ts +1 -1
  116. package/src/__tests__/ascii-er-padding.test.ts +1 -1
  117. package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +1 -1
  118. package/src/__tests__/ascii-er-relationship-overwrite.test.ts +1 -1
  119. package/src/__tests__/ascii-er-stray-connectors.test.ts +7 -4
  120. package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +1 -1
  121. package/src/__tests__/ascii-er-vertical-one-marker.test.ts +1 -1
  122. package/src/__tests__/ascii-grid-deep-chain.test.ts +4 -4
  123. package/src/__tests__/ascii-hyperlinks.test.ts +9 -6
  124. package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +8 -4
  125. package/src/__tests__/ascii-lane-search.test.ts +1 -1
  126. package/src/__tests__/ascii-multibox-cjk-width.test.ts +1 -1
  127. package/src/__tests__/ascii-multiline.test.ts +6 -3
  128. package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +8 -5
  129. package/src/__tests__/ascii-package-boundary.test.ts +47 -37
  130. package/src/__tests__/ascii-padding-edge-cases.test.ts +1 -1
  131. package/src/__tests__/ascii-parallel-edges-329.test.ts +8 -5
  132. package/src/__tests__/ascii-pathfinder-route-edge.test.ts +14 -4
  133. package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +9 -5
  134. package/src/__tests__/ascii-sequence-alt-else-label.test.ts +1 -1
  135. package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +1 -1
  136. package/src/__tests__/ascii-sequence-box-group.test.ts +1 -1
  137. package/src/__tests__/ascii-sequence-cjk-width.test.ts +1 -1
  138. package/src/__tests__/ascii-sequence-create-destroy.test.ts +1 -1
  139. package/src/__tests__/ascii-sequence-form-invariants.test.ts +99 -50
  140. package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +1 -1
  141. package/src/__tests__/ascii-sequence-notes.test.ts +1 -1
  142. package/src/__tests__/ascii-sequence-padding.test.ts +1 -1
  143. package/src/__tests__/ascii-sequence-self-arrow.test.ts +1 -1
  144. package/src/__tests__/ascii-shape-diamond.test.ts +3 -3
  145. package/src/__tests__/ascii-shape-rectangle.test.ts +5 -5
  146. package/src/__tests__/ascii-shape-rounded.test.ts +3 -3
  147. package/src/__tests__/ascii-shapes-circle.test.ts +3 -3
  148. package/src/__tests__/ascii-shapes-hexagon.test.ts +3 -3
  149. package/src/__tests__/ascii-shapes-special.test.ts +10 -5
  150. package/src/__tests__/ascii-shapes-stadium.test.ts +3 -3
  151. package/src/__tests__/ascii-shapes-state.test.ts +15 -5
  152. package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +1 -1
  153. package/src/__tests__/ascii-subgraph-bbox.test.ts +8 -4
  154. package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +1 -1
  155. package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +88 -25
  156. package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +8 -4
  157. package/src/__tests__/ascii-subgraph-title-padding.test.ts +1 -1
  158. package/src/__tests__/ascii-territory-unit.test.ts +2 -2
  159. package/src/__tests__/ascii-validate.test.ts +1 -1
  160. package/src/__tests__/ascii.test.ts +5 -2
  161. package/src/__tests__/box-color.test.ts +1 -1
  162. package/src/__tests__/class-arrow-directions.test.ts +1 -1
  163. package/src/__tests__/class-click-interactions.test.ts +3 -6
  164. package/src/__tests__/class-integration.test.ts +1 -1
  165. package/src/__tests__/class-notes.test.ts +3 -3
  166. package/src/__tests__/class-parser-error-messages.test.ts +162 -0
  167. package/src/__tests__/class-parser.test.ts +6 -6
  168. package/src/__tests__/class-styling.test.ts +2 -2
  169. package/src/__tests__/cli-render.test.ts +16 -1
  170. package/src/__tests__/direction-override.test.ts +1 -1
  171. package/src/__tests__/draw-lines.test.ts +2 -2
  172. package/src/__tests__/edge-cell-styles.test.ts +5 -2
  173. package/src/__tests__/elk-layout-cache.test.ts +6 -4
  174. package/src/__tests__/er-click-unsupported.test.ts +5 -8
  175. package/src/__tests__/er-parser-error-messages.test.ts +82 -0
  176. package/src/__tests__/er-parser.test.ts +3 -6
  177. package/src/__tests__/expanded-shapes.test.ts +1 -1
  178. package/src/__tests__/grid-occupancy.test.ts +1 -1
  179. package/src/__tests__/mcp-check-sequence-activations-tool.test.ts +1 -1
  180. package/src/__tests__/mcp-fix-sequence-activations-tool.test.ts +134 -0
  181. package/src/__tests__/mcp-render-ascii-tool.test.ts +5 -4
  182. package/src/__tests__/mcp-render-svg-theme-guard.test.ts +3 -3
  183. package/src/__tests__/mcp-render-svg-tool.test.ts +1 -1
  184. package/src/__tests__/mcp-server.test.ts +30 -1
  185. package/src/__tests__/multiline-labels.test.ts +29 -18
  186. package/src/__tests__/parser.test.ts +117 -5
  187. package/src/__tests__/pathfinder.test.ts +11 -4
  188. package/src/__tests__/render-options-overrides.test.ts +28 -21
  189. package/src/__tests__/render-options-per-type.test.ts +13 -9
  190. package/src/__tests__/resolve-colors.test.ts +1 -1
  191. package/src/__tests__/self-loop-edges.test.ts +112 -0
  192. package/src/__tests__/sequence-activation-check.test.ts +7 -8
  193. package/src/__tests__/sequence-activation-fix.test.ts +91 -0
  194. package/src/__tests__/sequence-layout.test.ts +4 -7
  195. package/src/__tests__/sequence-parser-syntax-errors.test.ts +179 -0
  196. package/src/__tests__/sequence-parser.test.ts +3 -6
  197. package/src/__tests__/statements.test.ts +53 -35
  198. package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +21 -21
  199. package/src/__tests__/theme-palette-contrast.test.ts +84 -0
  200. package/src/__tests__/workspace-package-boundaries.test.ts +199 -25
  201. package/src/__tests__/xychart-ascii.test.ts +1 -1
  202. package/src/__tests__/xychart-colors.test.ts +3 -2
  203. package/src/__tests__/xychart-layout.test.ts +1 -1
  204. package/src/__tests__/xychart-parser-errors.test.ts +95 -0
  205. package/src/browser.ts +8 -2
  206. package/src/cli/mcp.ts +1 -1
  207. package/src/cli/render.ts +59 -25
  208. package/src/cli/web.ts +1 -1
  209. package/src/diagram-registry.ts +190 -25
  210. package/src/index.ts +34 -131
  211. package/src/parser.ts +102 -40
  212. /package/{src/ascii → packages/ascii-renderer/src}/ansi.ts +0 -0
  213. /package/{src/ascii → packages/ascii-renderer/src}/canvas.ts +0 -0
  214. /package/{src/ascii → packages/ascii-renderer/src}/converter.ts +0 -0
  215. /package/{src/ascii → packages/ascii-renderer/src}/coords.ts +0 -0
  216. /package/{src/ascii → packages/ascii-renderer/src}/display-width.ts +0 -0
  217. /package/{src/ascii → packages/ascii-renderer/src}/draw-arrows.ts +0 -0
  218. /package/{src/ascii → packages/ascii-renderer/src}/draw-boxes.ts +0 -0
  219. /package/{src/ascii → packages/ascii-renderer/src}/draw-bundles.ts +0 -0
  220. /package/{src/ascii → packages/ascii-renderer/src}/draw-lines.ts +0 -0
  221. /package/{src/ascii → packages/ascii-renderer/src}/draw-subgraphs.ts +0 -0
  222. /package/{src/ascii → packages/ascii-renderer/src}/draw.ts +0 -0
  223. /package/{src/ascii → packages/ascii-renderer/src}/edge-bundling.ts +0 -0
  224. /package/{src/ascii → packages/ascii-renderer/src}/edge-cell-styles.ts +0 -0
  225. /package/{src/ascii → packages/ascii-renderer/src}/edge-routing.ts +0 -0
  226. /package/{src/ascii → packages/ascii-renderer/src}/grid-occupancy.ts +0 -0
  227. /package/{src/ascii → packages/ascii-renderer/src}/hyperlinks.ts +0 -0
  228. /package/{src/ascii → packages/ascii-renderer/src}/lane-search.ts +0 -0
  229. /package/{src/ascii → packages/ascii-renderer/src}/multiline-utils.ts +0 -0
  230. /package/{src/ascii → packages/ascii-renderer/src}/pathfinder.ts +0 -0
  231. /package/{src/ascii → packages/ascii-renderer/src}/shapes/circle.ts +0 -0
  232. /package/{src/ascii → packages/ascii-renderer/src}/shapes/corners.ts +0 -0
  233. /package/{src/ascii → packages/ascii-renderer/src}/shapes/diamond.ts +0 -0
  234. /package/{src/ascii → packages/ascii-renderer/src}/shapes/hexagon.ts +0 -0
  235. /package/{src/ascii → packages/ascii-renderer/src}/shapes/index.ts +0 -0
  236. /package/{src/ascii → packages/ascii-renderer/src}/shapes/rectangle.ts +0 -0
  237. /package/{src/ascii → packages/ascii-renderer/src}/shapes/rounded.ts +0 -0
  238. /package/{src/ascii → packages/ascii-renderer/src}/shapes/special.ts +0 -0
  239. /package/{src/ascii → packages/ascii-renderer/src}/shapes/stadium.ts +0 -0
  240. /package/{src/ascii → packages/ascii-renderer/src}/shapes/state.ts +0 -0
  241. /package/{src/ascii → packages/ascii-renderer/src}/shapes/types.ts +0 -0
  242. /package/{src/ascii → packages/ascii-renderer/src}/territory.ts +0 -0
  243. /package/{src/ascii → packages/ascii-renderer/src}/types.ts +0 -0
  244. /package/{src/ascii → packages/ascii-renderer/src}/validate.ts +0 -0
  245. /package/{src/mcp → packages/mcp/src}/index.ts +0 -0
  246. /package/{src → packages/mermaid-parser/src}/class/format.ts +0 -0
  247. /package/{src → packages/mermaid-parser/src}/class/types.ts +0 -0
  248. /package/{src → packages/mermaid-parser/src}/er/types.ts +0 -0
  249. /package/{src → packages/mermaid-parser/src}/expanded-shapes.ts +0 -0
  250. /package/{src → packages/mermaid-parser/src}/sequence/box-color.ts +0 -0
  251. /package/{src → packages/mermaid-parser/src}/sequence/types.ts +0 -0
  252. /package/{src → packages/mermaid-parser/src}/xychart/colors.ts +0 -0
  253. /package/{src → packages/mermaid-parser/src}/xychart/types.ts +0 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tests for the sequence diagram activation/deactivation auto-fix
3
+ * (packages/mermaid-parser/src/sequence/activation-fix.ts). Operates on raw
4
+ * Mermaid source text (unlike checkActivationBalance, which takes an
5
+ * already-parsed diagram), since the fix needs to append to the original
6
+ * source rather than just report against the parsed model.
7
+ *
8
+ * Covers: an already-balanced diagram (no-op), a single dangling
9
+ * activation (auto-fixed), multiple dangling activations across actors, an
10
+ * unmatched deactivation (deliberately NOT auto-fixed — see module header),
11
+ * and a mix of both issue types together.
12
+ */
13
+ import { describe, it, expect } from 'vitest'
14
+ import { fixActivationBalance } from '@zombie-mermaid/mermaid-parser'
15
+
16
+ describe('fixActivationBalance', () => {
17
+ it('returns the diagram unchanged when already balanced', () => {
18
+ const source = 'sequenceDiagram\n A->>+B: hello\n B-->>-A: world'
19
+ const result = fixActivationBalance(source)
20
+ expect(result.ok).toBe(true)
21
+ expect(result.fixedDiagram).toBe(source)
22
+ expect(result.fixesApplied).toEqual([])
23
+ expect(result.remainingIssues).toEqual([])
24
+ })
25
+
26
+ it('appends a deactivate statement for a single dangling activation', () => {
27
+ const source = 'sequenceDiagram\n activate A\n A->>B: hello'
28
+ const result = fixActivationBalance(source)
29
+ expect(result.ok).toBe(true)
30
+ expect(result.originalDiagram).toBe(source)
31
+ expect(result.fixedDiagram).toBe(`${source}\n deactivate A`)
32
+ expect(result.fixesApplied).toEqual([
33
+ expect.stringContaining('Appended "deactivate A"'),
34
+ ])
35
+ expect(result.remainingIssues).toEqual([])
36
+ })
37
+
38
+ it('makes the fixed diagram re-check as balanced', () => {
39
+ const source = 'sequenceDiagram\n activate A\n A->>B: hello'
40
+ const { fixedDiagram } = fixActivationBalance(source)
41
+ const reChecked = fixActivationBalance(fixedDiagram)
42
+ expect(reChecked.ok).toBe(true)
43
+ expect(reChecked.fixesApplied).toEqual([])
44
+ })
45
+
46
+ it('closes multiple dangling activations for different actors', () => {
47
+ const source = 'sequenceDiagram\n activate A\n activate B\n A->>B: hello'
48
+ const result = fixActivationBalance(source)
49
+ expect(result.ok).toBe(true)
50
+ expect(result.fixesApplied).toHaveLength(2)
51
+ expect(result.fixedDiagram).toContain(' deactivate A')
52
+ expect(result.fixedDiagram).toContain(' deactivate B')
53
+ })
54
+
55
+ it('does not fix an unmatched deactivation and reports it back', () => {
56
+ const source = 'sequenceDiagram\n A->>B: hello\n deactivate A'
57
+ const result = fixActivationBalance(source)
58
+ expect(result.ok).toBe(false)
59
+ expect(result.fixedDiagram).toBe(source)
60
+ expect(result.fixesApplied).toEqual([])
61
+ expect(result.remainingIssues).toEqual([
62
+ expect.objectContaining({
63
+ code: 'UNMATCHED_DEACTIVATION',
64
+ actorId: 'A',
65
+ }),
66
+ ])
67
+ })
68
+
69
+ it('fixes the dangling half of a mixed-issue diagram, leaving the unmatched half reported', () => {
70
+ const source =
71
+ 'sequenceDiagram\n activate A\n A->>B: hello\n deactivate B'
72
+ const result = fixActivationBalance(source)
73
+ expect(result.ok).toBe(false)
74
+ expect(result.fixesApplied).toEqual([
75
+ expect.stringContaining('Appended "deactivate A"'),
76
+ ])
77
+ expect(result.fixedDiagram).toContain(' deactivate A')
78
+ expect(result.remainingIssues).toEqual([
79
+ expect.objectContaining({
80
+ code: 'UNMATCHED_DEACTIVATION',
81
+ actorId: 'B',
82
+ }),
83
+ ])
84
+ })
85
+
86
+ it('throws a clear error for a non-sequence diagram', () => {
87
+ expect(() => fixActivationBalance('graph LR\n A --> B')).toThrow(
88
+ /only supports sequence diagrams/,
89
+ )
90
+ })
91
+ })
@@ -6,16 +6,13 @@
6
6
  * to inspect Y coordinates, rather than checking SVG output.
7
7
  */
8
8
  import { describe, it, expect } from 'vitest'
9
- import { parseSequenceDiagram } from '../sequence/parser.ts'
10
- import { layoutSequenceDiagram } from '../sequence/layout.ts'
9
+ import { parseSequenceDiagram } from '@zombie-mermaid/mermaid-parser'
10
+ import { layoutSequenceDiagram } from '@zombie-mermaid/svg-renderer'
11
+ import { splitStatements } from '@zombie-mermaid/core'
11
12
 
12
13
  /** Helper: parse and layout a sequence diagram from source lines */
13
14
  function layout(source: string) {
14
- const lines = source
15
- .split('\n')
16
- .map((l) => l.trim())
17
- .filter((l) => l.length > 0 && !l.startsWith('%%'))
18
- return layoutSequenceDiagram(parseSequenceDiagram(lines))
15
+ return layoutSequenceDiagram(parseSequenceDiagram(splitStatements(source)))
19
16
  }
20
17
 
21
18
  describe('sequence layout – block spacing', () => {
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Syntactic error-detection tests for the sequence diagram parser
3
+ * (issue #762).
4
+ *
5
+ * The 8 pre-existing `throw` sites in `parseSequenceDiagram` all guard
6
+ * *semantic* rules mirrored from mermaid.js (duplicate actor ids, box
7
+ * nesting, create/destroy message mismatch — see
8
+ * sequence-parser.test.ts/sequence-activation-check.test.ts for those). No
9
+ * *syntactic* error was ever raised: a malformed arrow like `->>>` silently
10
+ * absorbed the extra character into the actor name instead of being
11
+ * rejected (verified: `Alice->>>Bob: Hello` created an actor literally
12
+ * named `">Bob"`), an unmatched `end` was silently ignored, and an
13
+ * `alt`/`loop`/`box` left open at EOF was silently accepted as if closed.
14
+ * These tests assert the parser now throws an actionable error instead,
15
+ * while confirming valid syntax — including short arrows, activation
16
+ * shorthand, and actor names that legitimately start with "x" — is
17
+ * unaffected.
18
+ */
19
+ import { describe, it, expect } from 'vitest'
20
+ import { parseSequenceDiagram } from '@zombie-mermaid/mermaid-parser'
21
+ import { splitStatements } from '@zombie-mermaid/core'
22
+
23
+ function parse(text: string) {
24
+ return parseSequenceDiagram(splitStatements(text))
25
+ }
26
+
27
+ describe('parseSequenceDiagram – malformed arrows', () => {
28
+ it('throws on an extra ">" past the longest known long-arrow token', () => {
29
+ // The issue's own verified repro: silently created an actor named
30
+ // `">Bob"` before this pass.
31
+ expect(() =>
32
+ parse(`sequenceDiagram
33
+ Alice->>>Bob: Hello`),
34
+ ).toThrow(/Malformed sequence-diagram arrow.*"->>>"/)
35
+ })
36
+
37
+ it('throws on an extra ")" past a short open-arrow token', () => {
38
+ expect(() =>
39
+ parse(`sequenceDiagram
40
+ Alice-))Bob: Hello`),
41
+ ).toThrow(/Malformed sequence-diagram arrow.*"-\)\)"/)
42
+ })
43
+
44
+ it('throws on an extra ")" past a short dashed-open-arrow token', () => {
45
+ expect(() =>
46
+ parse(`sequenceDiagram
47
+ Alice--))Bob: Hello`),
48
+ ).toThrow(/Malformed sequence-diagram arrow/)
49
+ })
50
+
51
+ it('mentions the known-good arrow set in the message', () => {
52
+ expect(() =>
53
+ parse(`sequenceDiagram
54
+ Alice->>>Bob: Hello`),
55
+ ).toThrow(/->, -->, ->>, -->>, -x, --x, -\), --\)/)
56
+ })
57
+
58
+ it('reports the 1-based line number', () => {
59
+ expect(() =>
60
+ parse(`sequenceDiagram
61
+ participant Alice
62
+ Alice->>>Bob: Hello`),
63
+ ).toThrow(/^Line 3:/)
64
+ })
65
+ })
66
+
67
+ describe('parseSequenceDiagram – unmatched "end"', () => {
68
+ it('throws on an "end" with no open block or box', () => {
69
+ expect(() =>
70
+ parse(`sequenceDiagram
71
+ Alice->>Bob: Hi
72
+ end`),
73
+ ).toThrow(/"end" does not match any open block/)
74
+ })
75
+ })
76
+
77
+ describe('parseSequenceDiagram – unclosed blocks/boxes at EOF', () => {
78
+ it('throws when a loop is never closed', () => {
79
+ expect(() =>
80
+ parse(`sequenceDiagram
81
+ loop Every 5s
82
+ Alice->>Bob: Hi`),
83
+ ).toThrow(/unclosed "loop" block/)
84
+ })
85
+
86
+ it('throws when an alt is never closed', () => {
87
+ expect(() =>
88
+ parse(`sequenceDiagram
89
+ alt Happy path
90
+ Alice->>Bob: Hi`),
91
+ ).toThrow(/unclosed "alt" block/)
92
+ })
93
+
94
+ it('throws when a box is never closed', () => {
95
+ expect(() =>
96
+ parse(`sequenceDiagram
97
+ box Group1
98
+ participant Alice
99
+ Alice->>Alice: Hi`),
100
+ ).toThrow(/unclosed "box Group1"/)
101
+ })
102
+
103
+ it('reports the line the block opened on', () => {
104
+ expect(() =>
105
+ parse(`sequenceDiagram
106
+ Alice->>Bob: Hi
107
+ loop Every 5s
108
+ Bob->>Alice: Pong`),
109
+ ).toThrow(/^Line 3:/)
110
+ })
111
+ })
112
+
113
+ describe('parseSequenceDiagram – valid syntax is unaffected', () => {
114
+ it('still parses correct long arrows without throwing', () => {
115
+ expect(() =>
116
+ parse(`sequenceDiagram
117
+ Alice->>Bob: Hi
118
+ Bob-->>Alice: Hi back`),
119
+ ).not.toThrow()
120
+ })
121
+
122
+ it('still parses activation shorthand (+/-) without throwing', () => {
123
+ const d = parse(`sequenceDiagram
124
+ Alice->>+Bob: Hi
125
+ Bob-->>-Alice: Hi back`)
126
+ expect(d.messages[0]!.activate).toBe(true)
127
+ expect(d.messages[1]!.deactivate).toBe(true)
128
+ })
129
+
130
+ it('still parses short open/cross arrows without throwing', () => {
131
+ const d = parse(`sequenceDiagram
132
+ Alice-)Bob: fire and forget
133
+ Bob--xAlice: lost message`)
134
+ expect(d.messages).toHaveLength(2)
135
+ })
136
+
137
+ it('still parses bidirectional arrows without throwing', () => {
138
+ expect(() =>
139
+ parse(`sequenceDiagram
140
+ Alice<<->>Bob: sync
141
+ Alice<<-->>Bob: async sync`),
142
+ ).not.toThrow()
143
+ })
144
+
145
+ it('does not throw for an actor name that legitimately starts with "x" after a -x arrow', () => {
146
+ const d = parse(`sequenceDiagram
147
+ Alice-xXavier: lost`)
148
+ expect(d.actors.map((a) => a.id)).toContain('Xavier')
149
+ })
150
+
151
+ it('still parses a properly closed loop/alt/box without throwing', () => {
152
+ expect(() =>
153
+ parse(`sequenceDiagram
154
+ box Group1
155
+ participant Alice
156
+ participant Bob
157
+ end
158
+ loop Every 5s
159
+ alt Happy path
160
+ Alice->>Bob: Hi
161
+ else Sad path
162
+ Alice-xBob: nope
163
+ end
164
+ end`),
165
+ ).not.toThrow()
166
+ })
167
+
168
+ it('still closes an "end" that matches a block nested inside a box', () => {
169
+ const d = parse(`sequenceDiagram
170
+ box Group1
171
+ participant Alice
172
+ loop Every 5s
173
+ Alice->>Alice: ping
174
+ end
175
+ end`)
176
+ expect(d.blocks).toHaveLength(1)
177
+ expect(d.boxes).toHaveLength(1)
178
+ })
179
+ })
@@ -9,15 +9,12 @@ import {
9
9
  parseSequenceDiagram,
10
10
  isBlockType,
11
11
  toBlockType,
12
- } from '../sequence/parser.ts'
12
+ } from '@zombie-mermaid/mermaid-parser'
13
+ import { splitStatements } from '@zombie-mermaid/core'
13
14
 
14
15
  /** Helper to parse — preprocesses text the same way index.ts does */
15
16
  function parse(text: string) {
16
- const lines = text
17
- .split('\n')
18
- .map((l) => l.trim())
19
- .filter((l) => l.length > 0 && !l.startsWith('%%'))
20
- return parseSequenceDiagram(lines)
17
+ return parseSequenceDiagram(splitStatements(text))
21
18
  }
22
19
 
23
20
  // ============================================================================
@@ -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[&amp;]-->B')).toEqual([
62
+ expect(texts('graph TD\nA[&amp;]-->B')).toEqual([
64
63
  'graph TD',
65
64
  'A[&amp;]-->B',
66
65
  ])
67
66
  })
68
67
 
69
68
  it('terminating a numeric character reference', () => {
70
- expect(splitStatements('graph TD\nA[&#35;]-->B')).toEqual([
69
+ expect(texts('graph TD\nA[&#35;]-->B')).toEqual([
71
70
  'graph TD',
72
71
  'A[&#35;]-->B',
73
72
  ])
74
73
  })
75
74
 
76
75
  it('terminating a hex character reference', () => {
77
- expect(splitStatements('graph TD\nA[&#x1F600;]-->B')).toEqual([
76
+ expect(texts('graph TD\nA[&#x1F600;]-->B')).toEqual([
78
77
  'graph TD',
79
78
  'A[&#x1F600;]-->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[&amp;]-->B;B-->C')).toEqual([
83
+ expect(texts('graph TD;A[&amp;]-->B;B-->C')).toEqual([
85
84
  'graph TD',
86
85
  'A[&amp;]-->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
+ })