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.
- package/LICENSE +1 -0
- package/README.md +7 -2
- package/dist/ascii.cjs +9 -9
- package/dist/ascii.cjs.map +1 -1
- package/dist/ascii.js +4069 -3969
- package/dist/ascii.js.map +1 -1
- package/dist/cli.js +7605 -7381
- package/dist/index.cjs +52 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7613 -7459
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +14142 -13717
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +14142 -13717
- package/dist/mcp.js.map +1 -1
- package/package.json +27 -5
- package/packages/ascii-renderer/LICENSE +22 -0
- package/{src/ascii → packages/ascii-renderer/src}/class-diagram.ts +277 -57
- package/{src/ascii → packages/ascii-renderer/src}/er-diagram.ts +2 -2
- package/packages/ascii-renderer/src/flowchart.ts +94 -0
- package/{src/ascii → packages/ascii-renderer/src}/grid.ts +23 -0
- package/{src/ascii → packages/ascii-renderer/src}/index.ts +13 -84
- package/{src/ascii → packages/ascii-renderer/src}/registry.ts +23 -12
- package/{src/ascii → packages/ascii-renderer/src}/sequence.ts +55 -8
- package/{src/ascii → packages/ascii-renderer/src}/xychart.ts +6 -3
- package/packages/core/LICENSE +22 -0
- package/packages/core/src/click-directive.ts +106 -0
- package/packages/core/src/color-utils.ts +205 -0
- package/packages/core/src/diagram-type.ts +35 -0
- package/packages/core/src/direction-override.ts +46 -0
- package/packages/core/src/direction.ts +56 -0
- package/packages/core/src/index.ts +34 -0
- package/packages/core/src/init-directive.ts +248 -0
- package/packages/core/src/multiline-utils.ts +275 -0
- package/packages/core/src/statements.ts +197 -0
- package/packages/core/src/style-directives.ts +214 -0
- package/packages/core/src/text-metrics.ts +498 -0
- package/packages/core/src/theme.ts +696 -0
- package/packages/core/src/types.ts +670 -0
- package/packages/mcp/LICENSE +22 -0
- package/{src/mcp → packages/mcp/src}/server.ts +34 -2
- package/{src/mcp → packages/mcp/src}/tools/check-sequence-activations.ts +8 -5
- package/packages/mcp/src/tools/fix-sequence-activations.ts +77 -0
- package/{src/mcp → packages/mcp/src}/tools/render-ascii.ts +2 -2
- package/{src/mcp → packages/mcp/src}/tools/render-svg.ts +1 -1
- package/packages/mermaid-parser/LICENSE +22 -0
- package/{src → packages/mermaid-parser/src}/class/parser.ts +86 -8
- package/{src → packages/mermaid-parser/src}/er/parser.ts +59 -26
- package/packages/mermaid-parser/src/index.ts +48 -0
- package/{src → packages/mermaid-parser/src}/sequence/activation-check.ts +4 -3
- package/packages/mermaid-parser/src/sequence/activation-fix.ts +112 -0
- package/{src → packages/mermaid-parser/src}/sequence/parser.ts +74 -16
- package/{src → packages/mermaid-parser/src}/xychart/parser.ts +84 -6
- package/packages/svg-renderer/LICENSE +22 -0
- package/{src → packages/svg-renderer/src}/class/layout.ts +8 -4
- package/{src → packages/svg-renderer/src}/class/renderer.ts +7 -6
- package/packages/svg-renderer/src/edge-curves.ts +204 -0
- package/packages/svg-renderer/src/elk-instance.ts +292 -0
- package/{src → packages/svg-renderer/src}/er/layout.ts +6 -4
- package/{src → packages/svg-renderer/src}/er/renderer.ts +4 -4
- package/packages/svg-renderer/src/index.ts +58 -0
- package/packages/svg-renderer/src/layout-engine/constants.ts +19 -0
- package/packages/svg-renderer/src/layout-engine/edge-bundling.ts +379 -0
- package/packages/svg-renderer/src/layout-engine/elk-adapter-utils.ts +81 -0
- package/packages/svg-renderer/src/layout-engine/elk-graph-builder.ts +240 -0
- package/packages/svg-renderer/src/layout-engine/from-elk.ts +685 -0
- package/packages/svg-renderer/src/layout-engine/layer-alignment.ts +174 -0
- package/packages/svg-renderer/src/layout-engine/to-elk.ts +695 -0
- package/packages/svg-renderer/src/layout-engine.ts +74 -0
- package/packages/svg-renderer/src/layout.ts +8 -0
- package/packages/svg-renderer/src/renderer.ts +1485 -0
- package/packages/svg-renderer/src/resolve-colors.ts +339 -0
- package/{src → packages/svg-renderer/src}/sequence/layout.ts +2 -6
- package/{src → packages/svg-renderer/src}/sequence/renderer.ts +4 -4
- package/packages/svg-renderer/src/shape-clipping.ts +197 -0
- package/packages/svg-renderer/src/styles.ts +118 -0
- package/{src → packages/svg-renderer/src}/xychart/layout.ts +2 -2
- package/{src → packages/svg-renderer/src}/xychart/renderer.ts +6 -6
- package/src/__tests__/ascii-ansi.test.ts +6 -2
- package/src/__tests__/ascii-canvas-write.test.ts +2 -2
- package/src/__tests__/ascii-charset-border-junctions.test.ts +1 -1
- package/src/__tests__/ascii-cjk-width.test.ts +1 -1
- package/src/__tests__/ascii-class-box-occupancy.test.ts +147 -45
- package/src/__tests__/ascii-class-column-width-488-489.test.ts +1 -1
- package/src/__tests__/ascii-class-cross-level-jog-corruption.test.ts +187 -0
- package/src/__tests__/ascii-class-detour-label-routing-487.test.ts +1 -1
- package/src/__tests__/ascii-class-diagram-compartments.test.ts +1 -1
- package/src/__tests__/ascii-class-label-box-collision.test.ts +1 -1
- package/src/__tests__/ascii-class-label-row-collision-531.test.ts +1 -1
- package/src/__tests__/ascii-class-label-territory-row-awareness.test.ts +1 -1
- package/src/__tests__/ascii-class-padding.test.ts +1 -1
- package/src/__tests__/ascii-class-reciprocal-relationships-448.test.ts +86 -1
- package/src/__tests__/ascii-combining-mark-width.test.ts +2 -2
- package/src/__tests__/ascii-coords-overlay.test.ts +2 -2
- package/src/__tests__/ascii-decision-lr-box-start.test.ts +2 -2
- package/src/__tests__/ascii-display-width-unit.test.ts +1 -1
- package/src/__tests__/ascii-draw-arrows-coverage.test.ts +1 -1
- package/src/__tests__/ascii-draw-arrows-single-point-path.test.ts +12 -5
- package/src/__tests__/ascii-draw-bundles.test.ts +5 -5
- package/src/__tests__/ascii-edge-bundling-rank-violation-454.test.ts +1 -1
- package/src/__tests__/ascii-edge-bundling-routing.test.ts +9 -5
- package/src/__tests__/ascii-edge-cross-style-overlap.test.ts +10 -6
- package/src/__tests__/ascii-edge-ending-glyphs.test.ts +1 -1
- package/src/__tests__/ascii-edge-label-diagonal-fallback-418.test.ts +2 -2
- package/src/__tests__/ascii-edge-routing-fixes.test.ts +1 -1
- package/src/__tests__/ascii-edge-routing-single-point-path.test.ts +8 -4
- package/src/__tests__/ascii-edge-styles.test.ts +1 -1
- package/src/__tests__/ascii-emoji-cluster-width.test.ts +2 -2
- package/src/__tests__/ascii-er-box-occupancy.test.ts +1 -1
- package/src/__tests__/ascii-er-cardinality.test.ts +1 -1
- package/src/__tests__/ascii-er-corner-glyphs.test.ts +1 -1
- package/src/__tests__/ascii-er-jog-stray-line.test.ts +1 -1
- package/src/__tests__/ascii-er-label-padding.test.ts +1 -1
- package/src/__tests__/ascii-er-padding.test.ts +1 -1
- package/src/__tests__/ascii-er-relationship-label-corruption-350.test.ts +1 -1
- package/src/__tests__/ascii-er-relationship-overwrite.test.ts +1 -1
- package/src/__tests__/ascii-er-stray-connectors.test.ts +7 -4
- package/src/__tests__/ascii-er-unrelated-stem-separation-411.test.ts +1 -1
- package/src/__tests__/ascii-er-vertical-one-marker.test.ts +1 -1
- package/src/__tests__/ascii-grid-deep-chain.test.ts +4 -4
- package/src/__tests__/ascii-hyperlinks.test.ts +9 -6
- package/src/__tests__/ascii-label-line-terminal-fallback.test.ts +8 -4
- package/src/__tests__/ascii-lane-search.test.ts +1 -1
- package/src/__tests__/ascii-multibox-cjk-width.test.ts +1 -1
- package/src/__tests__/ascii-multiline.test.ts +6 -3
- package/src/__tests__/ascii-nested-subgraph-sibling-order-444.test.ts +8 -5
- package/src/__tests__/ascii-package-boundary.test.ts +47 -37
- package/src/__tests__/ascii-padding-edge-cases.test.ts +1 -1
- package/src/__tests__/ascii-parallel-edges-329.test.ts +8 -5
- package/src/__tests__/ascii-pathfinder-route-edge.test.ts +14 -4
- package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +9 -5
- package/src/__tests__/ascii-sequence-alt-else-label.test.ts +1 -1
- package/src/__tests__/ascii-sequence-block-wall-clearance.test.ts +1 -1
- package/src/__tests__/ascii-sequence-box-group.test.ts +1 -1
- package/src/__tests__/ascii-sequence-cjk-width.test.ts +1 -1
- package/src/__tests__/ascii-sequence-create-destroy.test.ts +1 -1
- package/src/__tests__/ascii-sequence-form-invariants.test.ts +48 -50
- package/src/__tests__/ascii-sequence-mermaid-parity.test.ts +1 -1
- package/src/__tests__/ascii-sequence-notes.test.ts +1 -1
- package/src/__tests__/ascii-sequence-padding.test.ts +1 -1
- package/src/__tests__/ascii-sequence-self-arrow.test.ts +1 -1
- package/src/__tests__/ascii-shape-diamond.test.ts +3 -3
- package/src/__tests__/ascii-shape-rectangle.test.ts +5 -5
- package/src/__tests__/ascii-shape-rounded.test.ts +3 -3
- package/src/__tests__/ascii-shapes-circle.test.ts +3 -3
- package/src/__tests__/ascii-shapes-hexagon.test.ts +3 -3
- package/src/__tests__/ascii-shapes-special.test.ts +10 -5
- package/src/__tests__/ascii-shapes-stadium.test.ts +3 -3
- package/src/__tests__/ascii-shapes-state.test.ts +15 -5
- package/src/__tests__/ascii-state-bidirectional-label-swap-530.test.ts +1 -1
- package/src/__tests__/ascii-subgraph-bbox.test.ts +8 -4
- package/src/__tests__/ascii-subgraph-direction-honored-445.test.ts +1 -1
- package/src/__tests__/ascii-subgraph-label-border-clip.test.ts +88 -25
- package/src/__tests__/ascii-subgraph-later-edgeless-301.test.ts +8 -4
- package/src/__tests__/ascii-subgraph-title-padding.test.ts +1 -1
- package/src/__tests__/ascii-territory-unit.test.ts +2 -2
- package/src/__tests__/ascii-validate.test.ts +1 -1
- package/src/__tests__/ascii.test.ts +5 -2
- package/src/__tests__/box-color.test.ts +1 -1
- package/src/__tests__/class-arrow-directions.test.ts +1 -1
- package/src/__tests__/class-click-interactions.test.ts +3 -6
- package/src/__tests__/class-integration.test.ts +1 -1
- package/src/__tests__/class-notes.test.ts +3 -3
- package/src/__tests__/class-parser-error-messages.test.ts +162 -0
- package/src/__tests__/class-parser.test.ts +6 -6
- package/src/__tests__/class-styling.test.ts +2 -2
- package/src/__tests__/cli-render.test.ts +16 -1
- package/src/__tests__/direction-override.test.ts +1 -1
- package/src/__tests__/draw-lines.test.ts +2 -2
- package/src/__tests__/edge-cell-styles.test.ts +5 -2
- package/src/__tests__/elk-layout-cache.test.ts +6 -4
- package/src/__tests__/er-click-unsupported.test.ts +5 -8
- package/src/__tests__/er-parser-error-messages.test.ts +82 -0
- package/src/__tests__/er-parser.test.ts +3 -6
- package/src/__tests__/expanded-shapes.test.ts +1 -1
- package/src/__tests__/grid-occupancy.test.ts +1 -1
- package/src/__tests__/mcp-check-sequence-activations-tool.test.ts +1 -1
- package/src/__tests__/mcp-fix-sequence-activations-tool.test.ts +134 -0
- package/src/__tests__/mcp-render-ascii-tool.test.ts +5 -4
- package/src/__tests__/mcp-render-svg-theme-guard.test.ts +3 -3
- package/src/__tests__/mcp-render-svg-tool.test.ts +1 -1
- package/src/__tests__/mcp-server.test.ts +30 -1
- package/src/__tests__/multiline-labels.test.ts +29 -18
- package/src/__tests__/parser.test.ts +117 -5
- package/src/__tests__/pathfinder.test.ts +11 -4
- package/src/__tests__/render-options-overrides.test.ts +28 -21
- package/src/__tests__/render-options-per-type.test.ts +13 -9
- package/src/__tests__/resolve-colors.test.ts +1 -1
- package/src/__tests__/self-loop-edges.test.ts +112 -0
- package/src/__tests__/sequence-activation-check.test.ts +7 -8
- package/src/__tests__/sequence-activation-fix.test.ts +91 -0
- package/src/__tests__/sequence-layout.test.ts +4 -7
- package/src/__tests__/sequence-parser-syntax-errors.test.ts +179 -0
- package/src/__tests__/sequence-parser.test.ts +3 -6
- package/src/__tests__/statements.test.ts +53 -35
- package/src/__tests__/testdata/ascii/subgraph_direction_override.txt +21 -21
- package/src/__tests__/theme-palette-contrast.test.ts +84 -0
- package/src/__tests__/workspace-package-boundaries.test.ts +199 -25
- package/src/__tests__/xychart-ascii.test.ts +1 -1
- package/src/__tests__/xychart-colors.test.ts +3 -2
- package/src/__tests__/xychart-layout.test.ts +1 -1
- package/src/__tests__/xychart-parser-errors.test.ts +95 -0
- package/src/browser.ts +8 -2
- package/src/cli/mcp.ts +1 -1
- package/src/cli/render.ts +59 -25
- package/src/cli/web.ts +1 -1
- package/src/diagram-registry.ts +190 -25
- package/src/index.ts +34 -131
- package/src/parser.ts +102 -40
- /package/{src/ascii → packages/ascii-renderer/src}/ansi.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/canvas.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/converter.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/coords.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/display-width.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/draw-arrows.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/draw-boxes.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/draw-bundles.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/draw-lines.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/draw-subgraphs.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/draw.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/edge-bundling.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/edge-cell-styles.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/edge-routing.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/grid-occupancy.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/hyperlinks.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/lane-search.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/multiline-utils.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/pathfinder.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/circle.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/corners.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/diamond.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/hexagon.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/index.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/rectangle.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/rounded.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/special.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/stadium.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/state.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/shapes/types.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/territory.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/types.ts +0 -0
- /package/{src/ascii → packages/ascii-renderer/src}/validate.ts +0 -0
- /package/{src/mcp → packages/mcp/src}/index.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/class/format.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/class/types.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/er/types.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/expanded-shapes.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/sequence/box-color.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/sequence/types.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/xychart/colors.ts +0 -0
- /package/{src → packages/mermaid-parser/src}/xychart/types.ts +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error-message quality tests for the ER diagram parser (issue #541).
|
|
3
|
+
*
|
|
4
|
+
* Before this pass, `parseErDiagram` had zero `throw`/`Error` statements in
|
|
5
|
+
* the entire file (see docs/parser-error-audit-541.md): a relationship line
|
|
6
|
+
* with invalid cardinality tokens or a missing `: label` silently dropped
|
|
7
|
+
* the *entire* line — both entities included — with no indication anything
|
|
8
|
+
* was wrong. These tests assert the parser now throws an actionable error
|
|
9
|
+
* instead, while confirming valid syntax and genuinely unrelated lines
|
|
10
|
+
* (e.g. `click`, which ER's own grammar has no directive for) are
|
|
11
|
+
* unaffected.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect } from 'vitest'
|
|
14
|
+
import { parseErDiagram } from '@zombie-mermaid/mermaid-parser'
|
|
15
|
+
import { splitStatements } from '@zombie-mermaid/core'
|
|
16
|
+
|
|
17
|
+
function parse(text: string) {
|
|
18
|
+
return parseErDiagram(splitStatements(text))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('parseErDiagram – invalid cardinality tokens', () => {
|
|
22
|
+
it('throws on non-crow-foot cardinality characters', () => {
|
|
23
|
+
expect(() =>
|
|
24
|
+
parse(`erDiagram
|
|
25
|
+
CUSTOMER ??--?? ORDER : places`),
|
|
26
|
+
).toThrow(/Invalid ER relationship cardinality "\?\?--\?\?"/)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('throws on a left-side cardinality that is not one of the four valid tokens', () => {
|
|
30
|
+
expect(() =>
|
|
31
|
+
parse(`erDiagram
|
|
32
|
+
CUSTOMER |--|| ORDER : places`),
|
|
33
|
+
).toThrow(/Invalid ER relationship cardinality "\|--\|\|"/)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('mentions the valid cardinality tokens in the message', () => {
|
|
37
|
+
expect(() =>
|
|
38
|
+
parse(`erDiagram
|
|
39
|
+
CUSTOMER ??--?? ORDER : places`),
|
|
40
|
+
).toThrow(/\|\|, \|o, \}\|, \}o/)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('parseErDiagram – missing relationship label', () => {
|
|
45
|
+
it('throws when the colon has no label after it', () => {
|
|
46
|
+
expect(() =>
|
|
47
|
+
parse(`erDiagram
|
|
48
|
+
CUSTOMER ||--o{ ORDER :`),
|
|
49
|
+
).toThrow(/missing a ": label"/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('throws when there is no colon at all', () => {
|
|
53
|
+
expect(() =>
|
|
54
|
+
parse(`erDiagram
|
|
55
|
+
CUSTOMER ||--o{ ORDER`),
|
|
56
|
+
).toThrow(/missing a ": label"/)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('parseErDiagram – valid syntax and unrelated lines are unaffected', () => {
|
|
61
|
+
it('still parses a fully valid relationship without throwing', () => {
|
|
62
|
+
const d = parse(`erDiagram
|
|
63
|
+
CUSTOMER ||--o{ ORDER : places`)
|
|
64
|
+
expect(d.relationships).toHaveLength(1)
|
|
65
|
+
expect(d.entities.map((e) => e.id).sort()).toEqual(['CUSTOMER', 'ORDER'])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('does not throw for a click line (no ER click grammar; see er-click-unsupported.test.ts)', () => {
|
|
69
|
+
expect(() =>
|
|
70
|
+
parse(`erDiagram
|
|
71
|
+
CUSTOMER ||--o{ ORDER : places
|
|
72
|
+
click CUSTOMER "https://example.com" "Tooltip"`),
|
|
73
|
+
).not.toThrow()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('does not throw for a standalone entity name with no relationship or block', () => {
|
|
77
|
+
expect(() =>
|
|
78
|
+
parse(`erDiagram
|
|
79
|
+
CUSTOMER`),
|
|
80
|
+
).not.toThrow()
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -5,15 +5,12 @@
|
|
|
5
5
|
* relationships with all cardinality types, identifying/non-identifying lines.
|
|
6
6
|
*/
|
|
7
7
|
import { describe, it, expect } from 'vitest'
|
|
8
|
-
import { parseErDiagram } from '
|
|
8
|
+
import { parseErDiagram } from '@zombie-mermaid/mermaid-parser'
|
|
9
|
+
import { splitStatements } from '@zombie-mermaid/core'
|
|
9
10
|
|
|
10
11
|
/** Helper to parse — preprocesses text the same way index.ts does */
|
|
11
12
|
function parse(text: string) {
|
|
12
|
-
|
|
13
|
-
.split('\n')
|
|
14
|
-
.map((l) => l.trim())
|
|
15
|
-
.filter((l) => l.length > 0 && !l.startsWith('%%'))
|
|
16
|
-
return parseErDiagram(lines)
|
|
13
|
+
return parseErDiagram(splitStatements(text))
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
// ============================================================================
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { checkSequenceActivationsHandler } from '
|
|
2
|
+
import { checkSequenceActivationsHandler } from '../../packages/mcp/src/tools/check-sequence-activations.ts'
|
|
3
3
|
|
|
4
4
|
function parseReport(text: string): { ok: boolean; issues: unknown[] } {
|
|
5
5
|
return JSON.parse(text) as { ok: boolean; issues: unknown[] }
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { fixSequenceActivationsHandler } from '../../packages/mcp/src/tools/fix-sequence-activations.ts'
|
|
3
|
+
|
|
4
|
+
interface FixReport {
|
|
5
|
+
ok: boolean
|
|
6
|
+
fixedDiagram: string
|
|
7
|
+
fixesApplied: string[]
|
|
8
|
+
remainingIssues: unknown[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseReport(text: string): FixReport {
|
|
12
|
+
return JSON.parse(text) as FixReport
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('fixSequenceActivationsHandler', () => {
|
|
16
|
+
it('reports ok: true with no fixes for an already-balanced diagram', () => {
|
|
17
|
+
const result = fixSequenceActivationsHandler({
|
|
18
|
+
diagram: 'sequenceDiagram\n A->>+B: hello\n B-->>-A: world',
|
|
19
|
+
})
|
|
20
|
+
expect(result.isError).toBeUndefined()
|
|
21
|
+
const [content] = result.content
|
|
22
|
+
if (content?.type !== 'text') {
|
|
23
|
+
throw new Error('Expected text content')
|
|
24
|
+
}
|
|
25
|
+
const report = parseReport(content.text)
|
|
26
|
+
expect(report.ok).toBe(true)
|
|
27
|
+
expect(report.fixesApplied).toEqual([])
|
|
28
|
+
expect(report.remainingIssues).toEqual([])
|
|
29
|
+
expect(report.fixedDiagram).toBe(
|
|
30
|
+
'sequenceDiagram\n A->>+B: hello\n B-->>-A: world',
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('appends a matching deactivate for a dangling activation', () => {
|
|
35
|
+
const result = fixSequenceActivationsHandler({
|
|
36
|
+
diagram: 'sequenceDiagram\n activate A\n A->>B: hello',
|
|
37
|
+
})
|
|
38
|
+
expect(result.isError).toBeUndefined()
|
|
39
|
+
const [content] = result.content
|
|
40
|
+
if (content?.type !== 'text') {
|
|
41
|
+
throw new Error('Expected text content')
|
|
42
|
+
}
|
|
43
|
+
const report = parseReport(content.text)
|
|
44
|
+
expect(report.ok).toBe(true)
|
|
45
|
+
expect(report.fixesApplied).toHaveLength(1)
|
|
46
|
+
expect(report.remainingIssues).toEqual([])
|
|
47
|
+
expect(report.fixedDiagram.trim().endsWith('deactivate A')).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('fixes multiple dangling activations across different actors', () => {
|
|
51
|
+
const result = fixSequenceActivationsHandler({
|
|
52
|
+
diagram: 'sequenceDiagram\n activate A\n activate B\n A->>B: hello',
|
|
53
|
+
})
|
|
54
|
+
expect(result.isError).toBeUndefined()
|
|
55
|
+
const [content] = result.content
|
|
56
|
+
if (content?.type !== 'text') {
|
|
57
|
+
throw new Error('Expected text content')
|
|
58
|
+
}
|
|
59
|
+
const report = parseReport(content.text)
|
|
60
|
+
expect(report.ok).toBe(true)
|
|
61
|
+
expect(report.fixesApplied).toHaveLength(2)
|
|
62
|
+
expect(report.fixedDiagram).toContain('deactivate A')
|
|
63
|
+
expect(report.fixedDiagram).toContain('deactivate B')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('does not auto-fix an unmatched deactivation, reporting it instead', () => {
|
|
67
|
+
const result = fixSequenceActivationsHandler({
|
|
68
|
+
diagram: 'sequenceDiagram\n A->>B: hello\n deactivate A',
|
|
69
|
+
})
|
|
70
|
+
expect(result.isError).toBeUndefined()
|
|
71
|
+
const [content] = result.content
|
|
72
|
+
if (content?.type !== 'text') {
|
|
73
|
+
throw new Error('Expected text content')
|
|
74
|
+
}
|
|
75
|
+
const report = parseReport(content.text)
|
|
76
|
+
expect(report.ok).toBe(false)
|
|
77
|
+
expect(report.fixesApplied).toEqual([])
|
|
78
|
+
expect(report.fixedDiagram).toBe(
|
|
79
|
+
'sequenceDiagram\n A->>B: hello\n deactivate A',
|
|
80
|
+
)
|
|
81
|
+
expect(report.remainingIssues).toEqual([
|
|
82
|
+
expect.objectContaining({
|
|
83
|
+
code: 'UNMATCHED_DEACTIVATION',
|
|
84
|
+
actorId: 'A',
|
|
85
|
+
}),
|
|
86
|
+
])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('fixes the dangling activation while still reporting an unmatched one', () => {
|
|
90
|
+
const result = fixSequenceActivationsHandler({
|
|
91
|
+
diagram: 'sequenceDiagram\n activate A\n A->>B: hello\n deactivate B',
|
|
92
|
+
})
|
|
93
|
+
expect(result.isError).toBeUndefined()
|
|
94
|
+
const [content] = result.content
|
|
95
|
+
if (content?.type !== 'text') {
|
|
96
|
+
throw new Error('Expected text content')
|
|
97
|
+
}
|
|
98
|
+
const report = parseReport(content.text)
|
|
99
|
+
expect(report.ok).toBe(false)
|
|
100
|
+
expect(report.fixesApplied).toHaveLength(1)
|
|
101
|
+
expect(report.fixedDiagram).toContain('deactivate A')
|
|
102
|
+
expect(report.remainingIssues).toEqual([
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
code: 'UNMATCHED_DEACTIVATION',
|
|
105
|
+
actorId: 'B',
|
|
106
|
+
}),
|
|
107
|
+
])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('rejects a non-sequence diagram with a clear error instead of a false report', () => {
|
|
111
|
+
const result = fixSequenceActivationsHandler({
|
|
112
|
+
diagram: 'graph LR\n A --> B',
|
|
113
|
+
})
|
|
114
|
+
expect(result.isError).toBe(true)
|
|
115
|
+
const [content] = result.content
|
|
116
|
+
if (content?.type !== 'text') {
|
|
117
|
+
throw new Error('Expected text content')
|
|
118
|
+
}
|
|
119
|
+
expect(content.text).toContain('only fixes sequence diagrams')
|
|
120
|
+
expect(content.text).toContain('"flowchart"')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('returns an MCP tool error instead of throwing on invalid sequence syntax', () => {
|
|
124
|
+
const result = fixSequenceActivationsHandler({
|
|
125
|
+
diagram: 'sequenceDiagram\n box\n box',
|
|
126
|
+
})
|
|
127
|
+
expect(result.isError).toBe(true)
|
|
128
|
+
const [content] = result.content
|
|
129
|
+
if (content?.type !== 'text') {
|
|
130
|
+
throw new Error('Expected text content')
|
|
131
|
+
}
|
|
132
|
+
expect(content.text).toContain('Failed to fix diagram activations')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { renderAsciiHandler } from '
|
|
3
|
-
import { renderMermaidASCII } from '
|
|
2
|
+
import { renderAsciiHandler } from '../../packages/mcp/src/tools/render-ascii.ts'
|
|
3
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
4
4
|
|
|
5
5
|
// ============================================================================
|
|
6
6
|
// renderMermaidASCII is wrapped (not fully replaced) so every test above
|
|
@@ -8,8 +8,9 @@ import { renderMermaidASCII } from '../ascii/index.ts'
|
|
|
8
8
|
// non-Error throw (impossible to provoke from real Mermaid source, since
|
|
9
9
|
// the renderer only ever throws Error instances) overrides it per-call.
|
|
10
10
|
// ============================================================================
|
|
11
|
-
vi.mock('
|
|
12
|
-
const actual =
|
|
11
|
+
vi.mock('@zombie-mermaid/ascii-renderer', async (importOriginal) => {
|
|
12
|
+
const actual =
|
|
13
|
+
await importOriginal<typeof import('@zombie-mermaid/ascii-renderer')>()
|
|
13
14
|
return { ...actual, renderMermaidASCII: vi.fn(actual.renderMermaidASCII) }
|
|
14
15
|
})
|
|
15
16
|
|
|
@@ -29,8 +29,8 @@ describe('render-svg.ts module load — theme registry invariant', () => {
|
|
|
29
29
|
}))
|
|
30
30
|
vi.resetModules()
|
|
31
31
|
|
|
32
|
-
await expect(
|
|
33
|
-
'
|
|
34
|
-
)
|
|
32
|
+
await expect(
|
|
33
|
+
import('../../packages/mcp/src/tools/render-svg.ts'),
|
|
34
|
+
).rejects.toThrow('Expected at least one built-in theme to be registered')
|
|
35
35
|
})
|
|
36
36
|
})
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest'
|
|
|
2
2
|
import {
|
|
3
3
|
renderSvgHandler,
|
|
4
4
|
renderSvgInputShape,
|
|
5
|
-
} from '
|
|
5
|
+
} from '../../packages/mcp/src/tools/render-svg.ts'
|
|
6
6
|
import { THEMES } from '@zombie-mermaid/core'
|
|
7
7
|
import { renderMermaidSVG } from '../index.ts'
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
|
11
11
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
12
12
|
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
13
13
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
14
|
-
import { createMcpServer } from '
|
|
14
|
+
import { createMcpServer } from '../../packages/mcp/src/server.ts'
|
|
15
15
|
|
|
16
16
|
let server: McpServer
|
|
17
17
|
let client: Client
|
|
@@ -39,6 +39,7 @@ describe('createMcpServer', () => {
|
|
|
39
39
|
const names = tools.map((t) => t.name).sort()
|
|
40
40
|
expect(names).toEqual([
|
|
41
41
|
'check_mermaid_sequence_activations',
|
|
42
|
+
'fix_mermaid_sequence_activations',
|
|
42
43
|
'render_mermaid_ascii',
|
|
43
44
|
'render_mermaid_svg',
|
|
44
45
|
])
|
|
@@ -131,4 +132,32 @@ describe('createMcpServer', () => {
|
|
|
131
132
|
expect.objectContaining({ code: 'DANGLING_ACTIVATION', actorId: 'A' }),
|
|
132
133
|
])
|
|
133
134
|
})
|
|
135
|
+
|
|
136
|
+
it('fixes a dangling activation via a real tool call', async () => {
|
|
137
|
+
const result = await client.callTool({
|
|
138
|
+
name: 'fix_mermaid_sequence_activations',
|
|
139
|
+
arguments: {
|
|
140
|
+
diagram: 'sequenceDiagram\n activate A\n A->>B: hello',
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
expect(result.isError).toBeFalsy()
|
|
144
|
+
const content = result.content
|
|
145
|
+
if (
|
|
146
|
+
!Array.isArray(content) ||
|
|
147
|
+
content[0]?.type !== 'text' ||
|
|
148
|
+
typeof content[0].text !== 'string'
|
|
149
|
+
) {
|
|
150
|
+
throw new Error('Expected text content')
|
|
151
|
+
}
|
|
152
|
+
const report = JSON.parse(content[0].text) as {
|
|
153
|
+
ok: boolean
|
|
154
|
+
fixedDiagram: string
|
|
155
|
+
fixesApplied: string[]
|
|
156
|
+
remainingIssues: unknown[]
|
|
157
|
+
}
|
|
158
|
+
expect(report.ok).toBe(true)
|
|
159
|
+
expect(report.fixedDiagram).toContain('deactivate A')
|
|
160
|
+
expect(report.fixesApplied).toHaveLength(1)
|
|
161
|
+
expect(report.remainingIssues).toEqual([])
|
|
162
|
+
})
|
|
134
163
|
})
|
|
@@ -10,18 +10,26 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { describe, it, expect } from 'vitest'
|
|
12
12
|
import { parseMermaid } from '../parser.ts'
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import {
|
|
14
|
+
parseSequenceDiagram,
|
|
15
|
+
parseClassDiagram,
|
|
16
|
+
parseErDiagram,
|
|
17
|
+
} from '@zombie-mermaid/mermaid-parser'
|
|
16
18
|
import {
|
|
17
19
|
measureMultilineText,
|
|
18
20
|
LINE_HEIGHT_RATIO,
|
|
19
21
|
measureTextWidth,
|
|
20
22
|
normalizeBrTags,
|
|
21
23
|
stripFormattingTags,
|
|
24
|
+
splitStatements,
|
|
22
25
|
} from '@zombie-mermaid/core'
|
|
23
26
|
import { renderMermaid } from '../index.ts'
|
|
24
27
|
|
|
28
|
+
/** Build a `Statement[]` from plain source lines, one statement per line. */
|
|
29
|
+
function toLines(sourceLines: string[]) {
|
|
30
|
+
return splitStatements(sourceLines.join('\n'))
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
// ============================================================================
|
|
26
34
|
// Parser: <br> tag normalization
|
|
27
35
|
// ============================================================================
|
|
@@ -131,45 +139,45 @@ describe('parseMermaid – <br> tag normalization', () => {
|
|
|
131
139
|
|
|
132
140
|
describe('sequence diagram labels', () => {
|
|
133
141
|
it('normalizes <br> in participant alias labels', () => {
|
|
134
|
-
const lines = [
|
|
142
|
+
const lines = toLines([
|
|
135
143
|
'sequenceDiagram',
|
|
136
144
|
'participant A as First<br>Line',
|
|
137
145
|
'A->>A: test',
|
|
138
|
-
]
|
|
146
|
+
])
|
|
139
147
|
const diagram = parseSequenceDiagram(lines)
|
|
140
148
|
expect(diagram.actors[0]!.label).toBe('First\nLine')
|
|
141
149
|
})
|
|
142
150
|
|
|
143
151
|
it('normalizes <br> in message labels', () => {
|
|
144
|
-
const lines = ['sequenceDiagram', 'A->>B: Hello<br>World']
|
|
152
|
+
const lines = toLines(['sequenceDiagram', 'A->>B: Hello<br>World'])
|
|
145
153
|
const diagram = parseSequenceDiagram(lines)
|
|
146
154
|
expect(diagram.messages[0]!.label).toBe('Hello\nWorld')
|
|
147
155
|
})
|
|
148
156
|
|
|
149
157
|
it('normalizes <br> in note text', () => {
|
|
150
|
-
const lines = [
|
|
158
|
+
const lines = toLines([
|
|
151
159
|
'sequenceDiagram',
|
|
152
160
|
'A->>B: Hello',
|
|
153
161
|
'Note over A,B: First<br>Second',
|
|
154
|
-
]
|
|
162
|
+
])
|
|
155
163
|
const diagram = parseSequenceDiagram(lines)
|
|
156
164
|
expect(diagram.notes[0]!.text).toBe('First\nSecond')
|
|
157
165
|
})
|
|
158
166
|
|
|
159
167
|
it('normalizes <br> in block labels', () => {
|
|
160
|
-
const lines = [
|
|
168
|
+
const lines = toLines([
|
|
161
169
|
'sequenceDiagram',
|
|
162
170
|
'A->>B: Hello',
|
|
163
171
|
'loop Every<br>30s',
|
|
164
172
|
'A->>B: Ping',
|
|
165
173
|
'end',
|
|
166
|
-
]
|
|
174
|
+
])
|
|
167
175
|
const diagram = parseSequenceDiagram(lines)
|
|
168
176
|
expect(diagram.blocks[0]!.label).toBe('Every\n30s')
|
|
169
177
|
})
|
|
170
178
|
|
|
171
179
|
it('normalizes <br> in divider labels', () => {
|
|
172
|
-
const lines = [
|
|
180
|
+
const lines = toLines([
|
|
173
181
|
'sequenceDiagram',
|
|
174
182
|
'A->>B: Hello',
|
|
175
183
|
'alt First<br>case',
|
|
@@ -177,7 +185,7 @@ describe('parseMermaid – <br> tag normalization', () => {
|
|
|
177
185
|
'else Second<br>case',
|
|
178
186
|
'A->>B: b',
|
|
179
187
|
'end',
|
|
180
|
-
]
|
|
188
|
+
])
|
|
181
189
|
const diagram = parseSequenceDiagram(lines)
|
|
182
190
|
expect(diagram.blocks[0]!.dividers[0]!.label).toBe('Second\ncase')
|
|
183
191
|
})
|
|
@@ -185,19 +193,19 @@ describe('parseMermaid – <br> tag normalization', () => {
|
|
|
185
193
|
|
|
186
194
|
describe('class diagram labels', () => {
|
|
187
195
|
it('normalizes <br> in relationship labels', () => {
|
|
188
|
-
const lines = ['classDiagram', 'A --> B : uses<br>internally']
|
|
196
|
+
const lines = toLines(['classDiagram', 'A --> B : uses<br>internally'])
|
|
189
197
|
const diagram = parseClassDiagram(lines)
|
|
190
198
|
expect(diagram.relationships[0]!.label).toBe('uses\ninternally')
|
|
191
199
|
})
|
|
192
200
|
|
|
193
201
|
it('normalizes <br> in fromCardinality labels', () => {
|
|
194
|
-
const lines = ['classDiagram', 'A "one<br>to" --> B']
|
|
202
|
+
const lines = toLines(['classDiagram', 'A "one<br>to" --> B'])
|
|
195
203
|
const diagram = parseClassDiagram(lines)
|
|
196
204
|
expect(diagram.relationships[0]!.fromCardinality).toBe('one\nto')
|
|
197
205
|
})
|
|
198
206
|
|
|
199
207
|
it('normalizes <br> in toCardinality labels', () => {
|
|
200
|
-
const lines = ['classDiagram', 'A --> "many<br>items" B']
|
|
208
|
+
const lines = toLines(['classDiagram', 'A --> "many<br>items" B'])
|
|
201
209
|
const diagram = parseClassDiagram(lines)
|
|
202
210
|
expect(diagram.relationships[0]!.toCardinality).toBe('many\nitems')
|
|
203
211
|
})
|
|
@@ -205,18 +213,21 @@ describe('parseMermaid – <br> tag normalization', () => {
|
|
|
205
213
|
|
|
206
214
|
describe('ER diagram labels', () => {
|
|
207
215
|
it('normalizes <br> in relationship labels', () => {
|
|
208
|
-
const lines = [
|
|
216
|
+
const lines = toLines([
|
|
217
|
+
'erDiagram',
|
|
218
|
+
'CUSTOMER ||--o{ ORDER : places<br>orders',
|
|
219
|
+
])
|
|
209
220
|
const diagram = parseErDiagram(lines)
|
|
210
221
|
expect(diagram.relationships[0]!.label).toBe('places\norders')
|
|
211
222
|
})
|
|
212
223
|
|
|
213
224
|
it('normalizes <br> in attribute comments', () => {
|
|
214
|
-
const lines = [
|
|
225
|
+
const lines = toLines([
|
|
215
226
|
'erDiagram',
|
|
216
227
|
'CUSTOMER {',
|
|
217
228
|
'int id PK "primary<br>key"',
|
|
218
229
|
'}',
|
|
219
|
-
]
|
|
230
|
+
])
|
|
220
231
|
const diagram = parseErDiagram(lines)
|
|
221
232
|
expect(diagram.entities[0]!.attributes[0]!.comment).toBe('primary\nkey')
|
|
222
233
|
})
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
* - Comments and error cases
|
|
11
11
|
*/
|
|
12
12
|
import { describe, it, expect } from 'vitest'
|
|
13
|
-
import { parseMermaid
|
|
14
|
-
// `isDirection` moved to @zombie-mermaid/core under #625
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
import
|
|
13
|
+
import { parseMermaid } from '../parser.ts'
|
|
14
|
+
// `isDirection` moved to @zombie-mermaid/core under #625; `toDirection`
|
|
15
|
+
// followed it under #624 once `er/parser.ts` (now
|
|
16
|
+
// packages/mermaid-parser/src/er/parser.ts) became a second caller that
|
|
17
|
+
// can't import this file without a cycle. `../parser.ts` still re-exports
|
|
18
|
+
// it for backward compatibility, but the tests below import it straight
|
|
19
|
+
// from `core`, same as `isDirection`.
|
|
20
|
+
import { isDirection, toDirection } from '@zombie-mermaid/core'
|
|
18
21
|
|
|
19
22
|
// ============================================================================
|
|
20
23
|
// Graph header parsing
|
|
@@ -374,6 +377,115 @@ describe('parseMermaid – edges (original)', () => {
|
|
|
374
377
|
})
|
|
375
378
|
})
|
|
376
379
|
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// Continuation-line edge chains (mermaid-js/mermaid#6049)
|
|
382
|
+
//
|
|
383
|
+
// Mermaid lets a vertex-chain statement wrap across lines, with the link
|
|
384
|
+
// operator leading the next line rather than trailing the previous one.
|
|
385
|
+
// `splitStatements` only splits on newlines/`;`, so without merging these
|
|
386
|
+
// back together the continuation lines look like they start with a bare
|
|
387
|
+
// arrow and no source node — parseEdgeLine can't find a node group, so it
|
|
388
|
+
// dropped the node and edge entirely instead of continuing the chain.
|
|
389
|
+
// ============================================================================
|
|
390
|
+
|
|
391
|
+
describe('parseMermaid – continuation-line edge chains (mermaid-js/mermaid#6049)', () => {
|
|
392
|
+
it('chains a vertex statement split across lines, same as one line', () => {
|
|
393
|
+
const g = parseMermaid(
|
|
394
|
+
[
|
|
395
|
+
'flowchart TB',
|
|
396
|
+
' start([Start])',
|
|
397
|
+
' ==> green([Change some code])',
|
|
398
|
+
' ==> finish([Finish])',
|
|
399
|
+
].join('\n'),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
expect([...g.nodes.keys()]).toEqual(['start', 'green', 'finish'])
|
|
403
|
+
expect(g.nodes.get('start')!.label).toBe('Start')
|
|
404
|
+
expect(g.nodes.get('green')!.label).toBe('Change some code')
|
|
405
|
+
expect(g.nodes.get('finish')!.label).toBe('Finish')
|
|
406
|
+
|
|
407
|
+
expect(g.edges).toHaveLength(2)
|
|
408
|
+
expect(g.edges[0]).toMatchObject({
|
|
409
|
+
source: 'start',
|
|
410
|
+
target: 'green',
|
|
411
|
+
style: 'thick',
|
|
412
|
+
})
|
|
413
|
+
expect(g.edges[1]).toMatchObject({
|
|
414
|
+
source: 'green',
|
|
415
|
+
target: 'finish',
|
|
416
|
+
style: 'thick',
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('keeps a self-loop on a later, unrelated line intact', () => {
|
|
421
|
+
const g = parseMermaid(
|
|
422
|
+
[
|
|
423
|
+
'flowchart TB',
|
|
424
|
+
' start([Start])',
|
|
425
|
+
' ==> green([Change some code])',
|
|
426
|
+
' ==> finish([Finish])',
|
|
427
|
+
'',
|
|
428
|
+
' green -.->|Incomplete ?| green',
|
|
429
|
+
].join('\n'),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
expect(g.edges).toHaveLength(3)
|
|
433
|
+
expect(g.edges[2]).toMatchObject({
|
|
434
|
+
source: 'green',
|
|
435
|
+
target: 'green',
|
|
436
|
+
label: 'Incomplete ?',
|
|
437
|
+
style: 'dotted',
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('matches the equivalent single-line chain', () => {
|
|
442
|
+
const multiline = parseMermaid(
|
|
443
|
+
[
|
|
444
|
+
'flowchart TB',
|
|
445
|
+
' start([Start])',
|
|
446
|
+
' ==> green([Change some code])',
|
|
447
|
+
' ==> finish([Finish])',
|
|
448
|
+
].join('\n'),
|
|
449
|
+
)
|
|
450
|
+
const singleLine = parseMermaid(
|
|
451
|
+
'flowchart TB\n start([Start]) ==> green([Change some code]) ==> finish([Finish])',
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
expect([...multiline.nodes.entries()]).toEqual([
|
|
455
|
+
...singleLine.nodes.entries(),
|
|
456
|
+
])
|
|
457
|
+
expect(multiline.edges).toEqual(singleLine.edges)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// Regressions caught in review (PR #730): the continuation-merge heuristic
|
|
461
|
+
// above must not (a) mistake a bare `x`/`o` node id for the `x`/`o`
|
|
462
|
+
// start-marker ARROW_REGEX also accepts, or (b) merge across an explicit
|
|
463
|
+
// `;` statement boundary on the same physical line.
|
|
464
|
+
|
|
465
|
+
it('does not misread a bare node id "x" as a marked-continuation opener', () => {
|
|
466
|
+
const g = parseMermaid('flowchart TD\nx-->Y')
|
|
467
|
+
expect(g.direction).toBe('TD')
|
|
468
|
+
expect([...g.nodes.keys()]).toEqual(['x', 'Y'])
|
|
469
|
+
expect(g.edges).toHaveLength(1)
|
|
470
|
+
expect(g.edges[0]).toMatchObject({ source: 'x', target: 'Y' })
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('does not misread a bare node id "o" as a marked-continuation opener', () => {
|
|
474
|
+
const g = parseMermaid('flowchart TD\no-->Y')
|
|
475
|
+
expect(g.direction).toBe('TD')
|
|
476
|
+
expect([...g.nodes.keys()]).toEqual(['o', 'Y'])
|
|
477
|
+
expect(g.edges).toHaveLength(1)
|
|
478
|
+
expect(g.edges[0]).toMatchObject({ source: 'o', target: 'Y' })
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('keeps an explicit `;` a real statement boundary, not a continuation', () => {
|
|
482
|
+
const g = parseMermaid('flowchart TD\nA --> B; --> C')
|
|
483
|
+
expect([...g.nodes.keys()]).toEqual(['A', 'B'])
|
|
484
|
+
expect(g.edges).toHaveLength(1)
|
|
485
|
+
expect(g.edges[0]).toMatchObject({ source: 'A', target: 'B' })
|
|
486
|
+
})
|
|
487
|
+
})
|
|
488
|
+
|
|
377
489
|
// ============================================================================
|
|
378
490
|
// Circle-end / cross-end edges: --o, --x (issue #65)
|
|
379
491
|
//
|
|
@@ -5,10 +5,17 @@ import {
|
|
|
5
5
|
mergePath,
|
|
6
6
|
createPathBudget,
|
|
7
7
|
DEFAULT_PATH_BUDGET,
|
|
8
|
-
} from '
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
} from '../../packages/ascii-renderer/src/pathfinder.ts'
|
|
9
|
+
import type {
|
|
10
|
+
GridCoord,
|
|
11
|
+
PathBudget,
|
|
12
|
+
} from '../../packages/ascii-renderer/src/types.ts'
|
|
13
|
+
import {
|
|
14
|
+
createGrid,
|
|
15
|
+
placeBlock,
|
|
16
|
+
type Grid,
|
|
17
|
+
} from '../../packages/ascii-renderer/src/grid-occupancy.ts'
|
|
18
|
+
import { renderMermaidASCII } from '@zombie-mermaid/ascii-renderer'
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* Helper to build an occupied grid from a list of coordinates, going through
|