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
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
// Relationship lines use Manhattan routing between entity boxes.
|
|
10
10
|
// ============================================================================
|
|
11
11
|
|
|
12
|
-
import { parseErDiagram } from '
|
|
12
|
+
import { parseErDiagram } from '@zombie-mermaid/mermaid-parser'
|
|
13
13
|
import type {
|
|
14
14
|
ErDiagram,
|
|
15
15
|
ErEntity,
|
|
16
16
|
ErAttribute,
|
|
17
17
|
Cardinality,
|
|
18
|
-
} from '
|
|
18
|
+
} from '@zombie-mermaid/mermaid-parser'
|
|
19
19
|
import type {
|
|
20
20
|
AsciiConfig,
|
|
21
21
|
CharRole,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ASCII renderer — Flowchart / state diagram
|
|
3
|
+
//
|
|
4
|
+
// Renders flowcharts (graph TD / flowchart LR) and state diagrams
|
|
5
|
+
// (stateDiagram-v2) to ASCII/Unicode box-drawing art via the shared
|
|
6
|
+
// grid-based layout + A* pathfinding pipeline (converter -> grid -> draw).
|
|
7
|
+
//
|
|
8
|
+
// Extracted from the inline sequence that used to live directly in
|
|
9
|
+
// `renderMermaidASCII`'s switch/fallback (src/ascii/index.ts) so flowchart
|
|
10
|
+
// can be registered in `asciiRegistry` (src/ascii/registry.ts) alongside
|
|
11
|
+
// every other diagram type — see
|
|
12
|
+
// docs/decisions/diagram-type-registry-partial.md and issue #745. A pure,
|
|
13
|
+
// behavior-preserving extraction: same functions, same call order, same
|
|
14
|
+
// arguments.
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
import { parseMermaid } from '../../../src/parser.ts'
|
|
18
|
+
import { withDirectionOverride } from '@zombie-mermaid/core'
|
|
19
|
+
import type { Direction } from '@zombie-mermaid/core'
|
|
20
|
+
import { convertToAsciiGraph } from './converter.ts'
|
|
21
|
+
import { createMapping } from './grid.ts'
|
|
22
|
+
import { drawGraph } from './draw.ts'
|
|
23
|
+
import {
|
|
24
|
+
canvasToString,
|
|
25
|
+
flipCanvasVertically,
|
|
26
|
+
flipRoleCanvasVertically,
|
|
27
|
+
} from './canvas.ts'
|
|
28
|
+
import { buildNodeLinkCanvas, flipLinkCanvasVertically } from './hyperlinks.ts'
|
|
29
|
+
import type { AsciiConfig, AsciiTheme, ColorMode } from './types.ts'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Flowchart-only ASCII extras: `direction` (override the parsed top-level
|
|
33
|
+
* direction, same semantics as `RenderOptions.direction` for SVG — see
|
|
34
|
+
* issue #276) and `hyperlinks` (OSC 8 terminal links). No other registered
|
|
35
|
+
* ASCII type reads `direction`; `hyperlinks` is shared with `class` (see
|
|
36
|
+
* `AsciiRenderExtras` in ./registry.ts).
|
|
37
|
+
*/
|
|
38
|
+
export interface FlowchartAsciiExtras {
|
|
39
|
+
direction?: Direction
|
|
40
|
+
hyperlinks?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render a flowchart or state diagram to ASCII/Unicode text art.
|
|
45
|
+
*
|
|
46
|
+
* Matches every other diagram type's ASCII entry-point shape
|
|
47
|
+
* (`text, config, colorMode, theme, extras`) so it can slot into
|
|
48
|
+
* `asciiRegistry` — see ./registry.ts.
|
|
49
|
+
*/
|
|
50
|
+
export function renderFlowchartAscii(
|
|
51
|
+
text: string,
|
|
52
|
+
config: AsciiConfig,
|
|
53
|
+
colorMode: ColorMode,
|
|
54
|
+
theme: AsciiTheme,
|
|
55
|
+
extras: FlowchartAsciiExtras = {},
|
|
56
|
+
): string {
|
|
57
|
+
// `extras.direction` replaces the parsed top-level direction before
|
|
58
|
+
// layout; see packages/core/src/direction-override.ts.
|
|
59
|
+
const parsed = withDirectionOverride(parseMermaid(text), extras.direction)
|
|
60
|
+
|
|
61
|
+
// Normalize direction for grid layout.
|
|
62
|
+
// BT is laid out as TD then flipped vertically after drawing.
|
|
63
|
+
// RL is treated as LR (full RL support not yet implemented).
|
|
64
|
+
if (parsed.direction === 'LR' || parsed.direction === 'RL') {
|
|
65
|
+
config.graphDirection = 'LR'
|
|
66
|
+
} else {
|
|
67
|
+
config.graphDirection = 'TD'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const graph = convertToAsciiGraph(parsed, config)
|
|
71
|
+
createMapping(graph)
|
|
72
|
+
drawGraph(graph)
|
|
73
|
+
|
|
74
|
+
// Opt-in OSC 8 hyperlinks: mark each `click`-linked node's label cells
|
|
75
|
+
// now, from the drawn node positions, before any flip below moves them.
|
|
76
|
+
const linkCanvas = extras.hyperlinks
|
|
77
|
+
? buildNodeLinkCanvas(graph, parsed.interactions)
|
|
78
|
+
: undefined
|
|
79
|
+
|
|
80
|
+
// BT: flip the finished canvas vertically so the flow runs bottom→top.
|
|
81
|
+
// The grid layout ran as TD; flipping + character remapping produces BT.
|
|
82
|
+
if (parsed.direction === 'BT') {
|
|
83
|
+
flipCanvasVertically(graph.canvas)
|
|
84
|
+
flipRoleCanvasVertically(graph.roleCanvas)
|
|
85
|
+
if (linkCanvas) flipLinkCanvasVertically(linkCanvas)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return canvasToString(graph.canvas, {
|
|
89
|
+
roleCanvas: graph.roleCanvas,
|
|
90
|
+
colorMode,
|
|
91
|
+
theme,
|
|
92
|
+
linkCanvas,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
} from './edge-cell-styles.ts'
|
|
41
41
|
import { drawBox } from './draw.ts'
|
|
42
42
|
import { getShapeDimensions } from './shapes/index.ts'
|
|
43
|
+
import { splitLines } from './multiline-utils.ts'
|
|
44
|
+
import { displayWidth } from './display-width.ts'
|
|
43
45
|
|
|
44
46
|
// `requireGridCoord` is defined in types.ts (a pure predicate over
|
|
45
47
|
// AsciiNode with no grid-state dependency) and re-exported here so the two
|
|
@@ -469,6 +471,27 @@ function calculateSubgraphBoundingBox(
|
|
|
469
471
|
sg.minY = minY - subgraphPadding - subgraphLabelSpace
|
|
470
472
|
sg.maxX = maxX + subgraphPadding
|
|
471
473
|
sg.maxY = maxY + subgraphPadding
|
|
474
|
+
|
|
475
|
+
// Widen the box, if necessary, so the subgraph's own cluster label fits
|
|
476
|
+
// without truncation. `drawSubgraphLabel` (draw-subgraphs.ts) writes the
|
|
477
|
+
// label into the interior columns `1..width-1` (width = maxX - minX), so
|
|
478
|
+
// the box needs `width >= labelWidth + 1` to avoid clipping the label
|
|
479
|
+
// against its own right border. Widening symmetrically (splitting any
|
|
480
|
+
// extra columns across both sides) keeps the child nodes visually
|
|
481
|
+
// centered inside the enlarged box rather than skewing it to one side.
|
|
482
|
+
const labelWidth = Math.max(
|
|
483
|
+
0,
|
|
484
|
+
...splitLines(sg.name).map((line) => displayWidth(line)),
|
|
485
|
+
)
|
|
486
|
+
const currentWidth = sg.maxX - sg.minX
|
|
487
|
+
const requiredWidth = labelWidth + 1
|
|
488
|
+
if (requiredWidth > currentWidth) {
|
|
489
|
+
const extra = requiredWidth - currentWidth
|
|
490
|
+
const extraLeft = Math.floor(extra / 2)
|
|
491
|
+
const extraRight = extra - extraLeft
|
|
492
|
+
sg.minX -= extraLeft
|
|
493
|
+
sg.maxX += extraRight
|
|
494
|
+
}
|
|
472
495
|
}
|
|
473
496
|
|
|
474
497
|
/** Ensure non-overlapping root subgraphs have minimum spacing. */
|
|
@@ -16,22 +16,10 @@
|
|
|
16
16
|
// const ascii = renderMermaidASCII('graph LR\n A --> B')
|
|
17
17
|
// ============================================================================
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
import { withDirectionOverride, detectDiagramType } from '@zombie-mermaid/core'
|
|
19
|
+
import { detectDiagramType } from '@zombie-mermaid/core'
|
|
21
20
|
import type { Direction, DiagramType } from '@zombie-mermaid/core'
|
|
22
|
-
import { convertToAsciiGraph } from './converter.ts'
|
|
23
|
-
import { createMapping } from './grid.ts'
|
|
24
|
-
import { drawGraph } from './draw.ts'
|
|
25
|
-
import {
|
|
26
|
-
canvasToString,
|
|
27
|
-
flipCanvasVertically,
|
|
28
|
-
flipRoleCanvasVertically,
|
|
29
|
-
} from './canvas.ts'
|
|
30
|
-
import { renderSequenceAscii } from './sequence.ts'
|
|
31
|
-
import { renderClassAscii } from './class-diagram.ts'
|
|
32
21
|
import { asciiRegistry } from './registry.ts'
|
|
33
22
|
import { addCoordsOverlay } from './coords.ts'
|
|
34
|
-
import { buildNodeLinkCanvas, flipLinkCanvasVertically } from './hyperlinks.ts'
|
|
35
23
|
import {
|
|
36
24
|
detectColorMode,
|
|
37
25
|
DEFAULT_ASCII_THEME,
|
|
@@ -142,7 +130,7 @@ export function renderMermaidASCII(
|
|
|
142
130
|
paddingX: options.paddingX ?? DEFAULT_PADDING_X,
|
|
143
131
|
paddingY: options.paddingY ?? DEFAULT_PADDING_Y,
|
|
144
132
|
boxBorderPadding: options.boxBorderPadding ?? DEFAULT_BOX_BORDER_PADDING,
|
|
145
|
-
graphDirection: 'TD', // default
|
|
133
|
+
graphDirection: 'TD', // default; renderFlowchartAscii overrides this for flowcharts/state diagrams
|
|
146
134
|
}
|
|
147
135
|
|
|
148
136
|
// Resolve color mode ('auto' or unset → detect environment, otherwise use specified mode)
|
|
@@ -156,76 +144,17 @@ export function renderMermaidASCII(
|
|
|
156
144
|
|
|
157
145
|
const diagramType: DiagramType = detectDiagramType(text)
|
|
158
146
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return options.showCoords ? addCoordsOverlay(result) : result
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
switch (diagramType) {
|
|
174
|
-
case 'sequence':
|
|
175
|
-
result = renderSequenceAscii(text, config, colorMode, theme)
|
|
176
|
-
break
|
|
177
|
-
|
|
178
|
-
case 'class':
|
|
179
|
-
result = renderClassAscii(text, config, colorMode, theme, {
|
|
180
|
-
hyperlinks: options.hyperlinks ?? false,
|
|
181
|
-
})
|
|
182
|
-
break
|
|
183
|
-
|
|
184
|
-
case 'flowchart':
|
|
185
|
-
default: {
|
|
186
|
-
// Flowchart + state diagram pipeline (original). `options.direction`
|
|
187
|
-
// replaces the parsed top-level direction before layout; see
|
|
188
|
-
// packages/core/src/direction-override.ts.
|
|
189
|
-
const parsed = withDirectionOverride(
|
|
190
|
-
parseMermaid(text),
|
|
191
|
-
options.direction,
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
// Normalize direction for grid layout.
|
|
195
|
-
// BT is laid out as TD then flipped vertically after drawing.
|
|
196
|
-
// RL is treated as LR (full RL support not yet implemented).
|
|
197
|
-
if (parsed.direction === 'LR' || parsed.direction === 'RL') {
|
|
198
|
-
config.graphDirection = 'LR'
|
|
199
|
-
} else {
|
|
200
|
-
config.graphDirection = 'TD'
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const graph = convertToAsciiGraph(parsed, config)
|
|
204
|
-
createMapping(graph)
|
|
205
|
-
drawGraph(graph)
|
|
206
|
-
|
|
207
|
-
// Opt-in OSC 8 hyperlinks: mark each `click`-linked node's label cells
|
|
208
|
-
// now, from the drawn node positions, before any flip below moves them.
|
|
209
|
-
const linkCanvas = options.hyperlinks
|
|
210
|
-
? buildNodeLinkCanvas(graph, parsed.interactions)
|
|
211
|
-
: undefined
|
|
212
|
-
|
|
213
|
-
// BT: flip the finished canvas vertically so the flow runs bottom→top.
|
|
214
|
-
// The grid layout ran as TD; flipping + character remapping produces BT.
|
|
215
|
-
if (parsed.direction === 'BT') {
|
|
216
|
-
flipCanvasVertically(graph.canvas)
|
|
217
|
-
flipRoleCanvasVertically(graph.roleCanvas)
|
|
218
|
-
if (linkCanvas) flipLinkCanvasVertically(linkCanvas)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
result = canvasToString(graph.canvas, {
|
|
222
|
-
roleCanvas: graph.roleCanvas,
|
|
223
|
-
colorMode,
|
|
224
|
-
theme,
|
|
225
|
-
linkCanvas,
|
|
226
|
-
})
|
|
227
|
-
}
|
|
228
|
-
}
|
|
147
|
+
// Registry dispatch (see src/ascii/registry.ts — issue #533 / #745):
|
|
148
|
+
// every diagram type, including 'flowchart', is registered there and
|
|
149
|
+
// dispatches to the exact same renderer calls each type's original
|
|
150
|
+
// switch case (or, for 'flowchart', inline sequence) used to make.
|
|
151
|
+
// `extras` carries every ASCII-only per-type option: `hyperlinks` (read
|
|
152
|
+
// by 'class' and 'flowchart') and `direction` (read by 'flowchart'
|
|
153
|
+
// only); both are ignored by every other registered type.
|
|
154
|
+
const result = asciiRegistry[diagramType](text, config, colorMode, theme, {
|
|
155
|
+
hyperlinks: options.hyperlinks ?? false,
|
|
156
|
+
direction: options.direction,
|
|
157
|
+
})
|
|
229
158
|
|
|
230
159
|
return options.showCoords ? addCoordsOverlay(result) : result
|
|
231
160
|
}
|
|
@@ -25,27 +25,31 @@
|
|
|
25
25
|
// no longer names anything under `src/ascii/`. Both directions are now
|
|
26
26
|
// one-way, and the set of registered types stays a single decision per
|
|
27
27
|
// renderer rather than a `switch` re-listed at each front door — the point of
|
|
28
|
-
// #533. Adding a type to one renderer's table without the other is
|
|
29
|
-
// expressible,
|
|
30
|
-
//
|
|
31
|
-
// has been adapted yet, not because the two must move in lockstep.
|
|
28
|
+
// #533. Adding a type to one renderer's table without the other is
|
|
29
|
+
// expressible in principle, though in practice 'xychart'/'er'/'sequence'/
|
|
30
|
+
// 'class'/'flowchart' now move together on both sides.
|
|
32
31
|
// ============================================================================
|
|
33
32
|
|
|
34
|
-
import type { DiagramType } from '@zombie-mermaid/core'
|
|
33
|
+
import type { DiagramType, Direction } from '@zombie-mermaid/core'
|
|
35
34
|
import type { AsciiConfig, AsciiTheme, ColorMode } from './types.ts'
|
|
36
35
|
import { renderXYChartAscii } from './xychart.ts'
|
|
37
36
|
import { renderErAscii } from './er-diagram.ts'
|
|
37
|
+
import { renderSequenceAscii } from './sequence.ts'
|
|
38
|
+
import { renderClassAscii } from './class-diagram.ts'
|
|
39
|
+
import { renderFlowchartAscii } from './flowchart.ts'
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
* Small, closed set of ASCII-only extras not every type needs —
|
|
41
|
-
*
|
|
42
|
-
* src/ascii/class-diagram.ts),
|
|
42
|
+
* Small, closed set of ASCII-only extras not every type needs — `class`
|
|
43
|
+
* reads `hyperlinks` (see `ClassAsciiOptions` in
|
|
44
|
+
* src/ascii/class-diagram.ts), `flowchart` reads both `direction` and
|
|
45
|
+
* `hyperlinks` (see `FlowchartAsciiExtras` in src/ascii/flowchart.ts). Kept
|
|
43
46
|
* as its own type rather than reusing `AsciiRenderOptions` from
|
|
44
47
|
* src/ascii/index.ts so this module stays a leaf of the ASCII tree: index.ts
|
|
45
48
|
* imports it, never the other way round.
|
|
46
49
|
*/
|
|
47
50
|
export interface AsciiRenderExtras {
|
|
48
51
|
hyperlinks?: boolean
|
|
52
|
+
direction?: Direction
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -65,13 +69,20 @@ export type AsciiRenderer = (
|
|
|
65
69
|
) => string
|
|
66
70
|
|
|
67
71
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
72
|
+
* Every diagram type `renderMermaidASCII` supports, dispatched through this
|
|
73
|
+
* table — see that function in src/ascii/index.ts, which has no fallback
|
|
74
|
+
* switch left now that 'flowchart' (the last holdout — see
|
|
75
|
+
* docs/decisions/diagram-type-registry-partial.md) is registered here too.
|
|
71
76
|
*/
|
|
72
|
-
export const asciiRegistry:
|
|
77
|
+
export const asciiRegistry: Record<DiagramType, AsciiRenderer> = {
|
|
73
78
|
xychart: (text, config, colorMode, theme) =>
|
|
74
79
|
renderXYChartAscii(text, config, colorMode, theme),
|
|
75
80
|
er: (text, config, colorMode, theme) =>
|
|
76
81
|
renderErAscii(text, config, colorMode, theme),
|
|
82
|
+
sequence: (text, config, colorMode, theme) =>
|
|
83
|
+
renderSequenceAscii(text, config, colorMode, theme),
|
|
84
|
+
class: (text, config, colorMode, theme, extras) =>
|
|
85
|
+
renderClassAscii(text, config, colorMode, theme, extras),
|
|
86
|
+
flowchart: (text, config, colorMode, theme, extras) =>
|
|
87
|
+
renderFlowchartAscii(text, config, colorMode, theme, extras),
|
|
77
88
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// Instead: actors → columns, messages → rows, all positioned linearly.
|
|
10
10
|
// ============================================================================
|
|
11
11
|
|
|
12
|
-
import { parseSequenceDiagram } from '
|
|
13
|
-
import type { Block } from '
|
|
12
|
+
import { parseSequenceDiagram } from '@zombie-mermaid/mermaid-parser'
|
|
13
|
+
import type { Block } from '@zombie-mermaid/mermaid-parser'
|
|
14
14
|
import type { AsciiConfig, CharRole, AsciiTheme, ColorMode } from './types.ts'
|
|
15
15
|
import {
|
|
16
16
|
mkCanvas,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
toDisplayCells,
|
|
29
29
|
WIDE_CHAR_PLACEHOLDER,
|
|
30
30
|
} from './display-width.ts'
|
|
31
|
-
import type { Message } from '
|
|
31
|
+
import type { Message } from '@zombie-mermaid/mermaid-parser'
|
|
32
32
|
import { DEFAULT_PADDING_X, DEFAULT_PADDING_Y, paddingOffset } from './types.ts'
|
|
33
33
|
|
|
34
34
|
// Width of a self-message's loop glyphs (├──┐ / ◀──┘), excluding the label.
|
|
@@ -256,6 +256,37 @@ export function renderSequenceAscii(
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
// Width (including border + padding, matching the box drawn in the
|
|
260
|
+
// vertical-layout pass below) of a note anchored to an actor. Computed up
|
|
261
|
+
// front, before lifeline x-positions are finalized, so `left`/`right`
|
|
262
|
+
// notes can participate in gap sizing the same way message labels already
|
|
263
|
+
// do (issue #953 case C) instead of only being checked against a layout
|
|
264
|
+
// that's already fixed.
|
|
265
|
+
function noteBoxWidth(note: (typeof diagram.notes)[number]): number {
|
|
266
|
+
const nLines = splitLines(note.text)
|
|
267
|
+
return Math.max(...nLines.map((l) => displayWidth(l))) + 2 + 2 * boxPad
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Per-actor widest 'left'/'right' note anchored to it. A `left` note on
|
|
271
|
+
// actor i draws into the gap to its left (between actor i-1 and i, or —
|
|
272
|
+
// for the leftmost actor — the canvas's own left margin); a `right` note
|
|
273
|
+
// draws into the gap to its right (between actor i and i+1). Both need to
|
|
274
|
+
// be reserved as part of layout, not clamped after the fact (issue #953
|
|
275
|
+
// cases A-C: notes previously only ever collided with whatever gap
|
|
276
|
+
// message-label sizing happened to leave).
|
|
277
|
+
const leftNoteWidth: number[] = new Array(diagram.actors.length).fill(0)
|
|
278
|
+
const rightNoteWidth: number[] = new Array(diagram.actors.length).fill(0)
|
|
279
|
+
for (const note of diagram.notes) {
|
|
280
|
+
if (note.position !== 'left' && note.position !== 'right') continue
|
|
281
|
+
const aIdx = actorIdx.get(note.actorIds[0]!) ?? 0
|
|
282
|
+
const w = noteBoxWidth(note)
|
|
283
|
+
if (note.position === 'left') {
|
|
284
|
+
leftNoteWidth[aIdx] = Math.max(leftNoteWidth[aIdx]!, w)
|
|
285
|
+
} else {
|
|
286
|
+
rightNoteWidth[aIdx] = Math.max(rightNoteWidth[aIdx]!, w)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
259
290
|
// Compute lifeline x-positions (greedy left-to-right).
|
|
260
291
|
// See paddingOffset's doc comment (types.ts) for why this is an offset
|
|
261
292
|
// from the paddingX default rather than the raw config value.
|
|
@@ -269,8 +300,19 @@ export function renderSequenceAscii(
|
|
|
269
300
|
// adjacent participants in different groups (or one grouped, one not)
|
|
270
301
|
// need room for the wall(s) between their boxes, plus a blank column on
|
|
271
302
|
// each side of every wall. Same-group neighbours have no wall between.
|
|
303
|
+
//
|
|
304
|
+
// A `left` note on the leftmost actor has no left neighbour to reserve a
|
|
305
|
+
// gap against — it draws into the canvas's own left margin instead, so
|
|
306
|
+
// that margin (the initial lifeline position itself) must be at least
|
|
307
|
+
// wide enough for it: `nx = llX[0] - nWidth - 1 >= 0` (see the note-x
|
|
308
|
+
// calculation below) requires `llX[0] >= nWidth + 1` (issue #953 case A —
|
|
309
|
+
// previously `nx` was clamped to 0 instead, forcing the note onto the
|
|
310
|
+
// lifeline it's supposed to sit beside).
|
|
272
311
|
const llX: number[] = [
|
|
273
|
-
|
|
312
|
+
Math.max(
|
|
313
|
+
halfBox[0]! + (boxSpanOf[0] === -1 ? 0 : BRACKET_GAP + 1),
|
|
314
|
+
leftNoteWidth[0]! + 1,
|
|
315
|
+
),
|
|
274
316
|
]
|
|
275
317
|
for (let i = 1; i < diagram.actors.length; i++) {
|
|
276
318
|
const prevSpan = boxSpanOf[i - 1]!
|
|
@@ -282,9 +324,16 @@ export function renderSequenceAscii(
|
|
|
282
324
|
// The base constraint already leaves two blank columns between boxes;
|
|
283
325
|
// each wall needs one column of its own plus one more blank beside it.
|
|
284
326
|
const wallExtra = walls === 0 ? 0 : walls * (BRACKET_GAP + 1) - 1
|
|
327
|
+
// A `right` note on actor i-1, or a `left` note on actor i, draws into
|
|
328
|
+
// this gap — reserve room for whichever is wider (issue #953 cases B/C)
|
|
329
|
+
// so a wide note doesn't overflow into the neighbouring lifeline. The
|
|
330
|
+
// `+2` mirrors adjMaxWidth's own margin below and matches the 1-column
|
|
331
|
+
// clearance the note-x calculation already leaves on the lifeline side.
|
|
332
|
+
const noteGapNeed = Math.max(rightNoteWidth[i - 1]!, leftNoteWidth[i]!) + 2
|
|
285
333
|
const gap = Math.max(
|
|
286
334
|
halfBox[i - 1]! + halfBox[i]! + 2 + wallExtra,
|
|
287
335
|
adjMaxWidth[i - 1]! + 2,
|
|
336
|
+
noteGapNeed,
|
|
288
337
|
minLifelineGap,
|
|
289
338
|
)
|
|
290
339
|
llX[i] = llX[i - 1]! + gap
|
|
@@ -435,8 +484,7 @@ export function renderSequenceAscii(
|
|
|
435
484
|
if (note.afterIndex !== -1) continue
|
|
436
485
|
curY += rowGap // gap before note
|
|
437
486
|
const nLines = splitLines(note.text)
|
|
438
|
-
const nWidth =
|
|
439
|
-
Math.max(...nLines.map((l) => displayWidth(l))) + 2 + 2 * boxPad
|
|
487
|
+
const nWidth = noteBoxWidth(note)
|
|
440
488
|
const nHeight = nLines.length + 2
|
|
441
489
|
|
|
442
490
|
const aIdx = actorIdx.get(note.actorIds[0]!) ?? 0
|
|
@@ -533,8 +581,7 @@ export function renderSequenceAscii(
|
|
|
533
581
|
curY += rowGap
|
|
534
582
|
const note = diagram.notes[n]!
|
|
535
583
|
const nLines = splitLines(note.text)
|
|
536
|
-
const nWidth =
|
|
537
|
-
Math.max(...nLines.map((l) => displayWidth(l))) + 2 + 2 * boxPad
|
|
584
|
+
const nWidth = noteBoxWidth(note)
|
|
538
585
|
const nHeight = nLines.length + 2
|
|
539
586
|
|
|
540
587
|
// Determine x position based on note.position
|
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
// Multi-series support: each series gets a distinct color from a palette.
|
|
12
12
|
// ============================================================================
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
parseXYChart,
|
|
16
|
+
getSeriesColor,
|
|
17
|
+
CHART_ACCENT_FALLBACK,
|
|
18
|
+
} from '@zombie-mermaid/mermaid-parser'
|
|
19
|
+
import type { XYChart } from '@zombie-mermaid/mermaid-parser'
|
|
16
20
|
import type {
|
|
17
21
|
AsciiConfig,
|
|
18
22
|
AsciiTheme,
|
|
@@ -23,7 +27,6 @@ import type {
|
|
|
23
27
|
} from './types.ts'
|
|
24
28
|
import { colorizeText } from './ansi.ts'
|
|
25
29
|
import { mkCanvas, mkRoleCanvas, write } from './canvas.ts'
|
|
26
|
-
import { getSeriesColor, CHART_ACCENT_FALLBACK } from '../xychart/colors.ts'
|
|
27
30
|
import { splitStatements } from '@zombie-mermaid/core'
|
|
28
31
|
|
|
29
32
|
// ============================================================================
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Craft Docs
|
|
4
|
+
Copyright (c) 2026 Dustin Fadler
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { NodeInteraction } from './types.ts'
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Shared `click` directive parsing and href sanitization.
|
|
5
|
+
//
|
|
6
|
+
// Every diagram type that recognizes Mermaid's `click` statement (flowchart/
|
|
7
|
+
// state via src/parser.ts, class diagrams via
|
|
8
|
+
// packages/mermaid-parser/src/class/parser.ts) shares this exact grammar
|
|
9
|
+
// and the same href-safety rules — pulled out here so
|
|
10
|
+
// neither copy can drift from the other. See
|
|
11
|
+
// docs/decisions/no-script-interactivity.md for the tier model this
|
|
12
|
+
// implements: an `href` becomes a real `<a>` link (tier 2), a tooltip
|
|
13
|
+
// becomes a `<title>` (tier 1), and a `call`/`callback` binding is recorded
|
|
14
|
+
// as data but never executed (tier 3 stays unimplemented, permanently).
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a `click <id> ...` statement and record the resulting
|
|
19
|
+
* {@link NodeInteraction} in `interactions`, keyed by whatever id scheme the
|
|
20
|
+
* caller's diagram type uses (flowchart/state node id, class diagram class
|
|
21
|
+
* id, ...).
|
|
22
|
+
*
|
|
23
|
+
* Supported forms:
|
|
24
|
+
* click id "url" ["tooltip"] [_blank|_self|_parent|_top]
|
|
25
|
+
* click id href "url" ["tooltip"] [_blank|_self|_parent|_top]
|
|
26
|
+
* click id call fn() ["tooltip"]
|
|
27
|
+
* click id callback fn() ["tooltip"]
|
|
28
|
+
*
|
|
29
|
+
* A line that doesn't match `click <id> ...` at all is a no-op — callers are
|
|
30
|
+
* expected to have already gated on a leading `click` keyword before calling
|
|
31
|
+
* this (see the `/^click\s+/i` checks at each call site) purely so this
|
|
32
|
+
* function's own regex can stay anchored to the id capture.
|
|
33
|
+
*/
|
|
34
|
+
export function applyClickStatement(
|
|
35
|
+
line: string,
|
|
36
|
+
interactions: Map<string, NodeInteraction>,
|
|
37
|
+
): void {
|
|
38
|
+
const match = line.match(/^click\s+([\w\p{L}-]+)\s+(.*)$/iu)
|
|
39
|
+
if (!match) return
|
|
40
|
+
|
|
41
|
+
const nodeId = match[1]!
|
|
42
|
+
let rest = match[2]!.trim()
|
|
43
|
+
|
|
44
|
+
const interaction: NodeInteraction = { ...interactions.get(nodeId) }
|
|
45
|
+
|
|
46
|
+
// `call fn()` / `callback fn()` — a script binding.
|
|
47
|
+
const callMatch = rest.match(/^(?:call|callback)\s+(.+?)\s*$/i)
|
|
48
|
+
if (callMatch) {
|
|
49
|
+
// A trailing quoted tooltip may follow the callback expression.
|
|
50
|
+
const withTooltip = callMatch[1]!.match(/^(.*?\))\s+"([^"]*)"\s*$/)
|
|
51
|
+
if (withTooltip) {
|
|
52
|
+
interaction.callback = withTooltip[1]!.trim()
|
|
53
|
+
interaction.tooltip = withTooltip[2]
|
|
54
|
+
} else {
|
|
55
|
+
interaction.callback = callMatch[1]!.trim()
|
|
56
|
+
}
|
|
57
|
+
interactions.set(nodeId, interaction)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Optional explicit `href` keyword.
|
|
62
|
+
rest = rest.replace(/^href\s+/i, '')
|
|
63
|
+
|
|
64
|
+
// Remaining tokens: "url" ["tooltip"] [_target]
|
|
65
|
+
const quoted = [...rest.matchAll(/"([^"]*)"/g)].map((m) => m[1]!)
|
|
66
|
+
if (quoted.length > 0) interaction.href = quoted[0]
|
|
67
|
+
if (quoted.length > 1) interaction.tooltip = quoted[1]
|
|
68
|
+
|
|
69
|
+
const targetMatch = rest.match(/(_blank|_self|_parent|_top)\s*$/i)
|
|
70
|
+
if (targetMatch) interaction.target = targetMatch[1]!.toLowerCase()
|
|
71
|
+
|
|
72
|
+
if (interaction.href !== undefined || interaction.tooltip !== undefined) {
|
|
73
|
+
interactions.set(nodeId, interaction)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Accept only link schemes that cannot execute script.
|
|
79
|
+
*
|
|
80
|
+
* A `click` target comes from diagram text, which may be untrusted. An
|
|
81
|
+
* `href` of `javascript:...` (or a `data:` URL containing markup) would turn
|
|
82
|
+
* a rendered diagram into an XSS vector for any page that inlines the SVG, so
|
|
83
|
+
* anything but http/https/mailto and same-document or relative references is
|
|
84
|
+
* dropped rather than emitted.
|
|
85
|
+
*
|
|
86
|
+
* C0 controls are rejected outright, before any other check. The URL parser
|
|
87
|
+
* strips tab and newline from *anywhere* in a URL, so `java\tscript:` reaches
|
|
88
|
+
* a browser as `javascript:` — while the scheme match below sees `java\t…`,
|
|
89
|
+
* finds no scheme, and waves it through as a relative reference. Splitting a
|
|
90
|
+
* blocked scheme with a control character is the whole bypass; there is no
|
|
91
|
+
* legitimate URL with a raw control in it, so the entire range goes.
|
|
92
|
+
*/
|
|
93
|
+
export function safeHref(href: string | undefined): string | undefined {
|
|
94
|
+
if (!href) return undefined
|
|
95
|
+
|
|
96
|
+
if (/[\x00-\x1F\x7F]/.test(href)) return undefined
|
|
97
|
+
|
|
98
|
+
const trimmed = href.trim()
|
|
99
|
+
// Relative, absolute-path, and fragment references carry no scheme.
|
|
100
|
+
if (/^[./#?]/.test(trimmed)) return trimmed
|
|
101
|
+
|
|
102
|
+
const scheme = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/)?.[1]
|
|
103
|
+
if (scheme === undefined) return trimmed
|
|
104
|
+
|
|
105
|
+
return /^(https?|mailto)$/i.test(scheme) ? trimmed : undefined
|
|
106
|
+
}
|