Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"d3-geo": "^3.1.1",
"lazyrepo": "0.0.0-alpha.27",
"lodash": "^4.17.21",
"mermaid": "11.12.2",
"mermaid": "11.15.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^4.10.38",
"radix-ui": "^1.4.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"dependencies": {
"@tldraw/tlschema": "workspace:*",
"@tldraw/utils": "workspace:*",
"mermaid": "11.12.2"
"mermaid": "11.15.0"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.2.1",
Expand Down
11 changes: 8 additions & 3 deletions packages/mermaid/src/flowchartDiagram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,18 @@ function buildHierarchy(subGraphs: FlowSubGraph[]) {

/** Parse flowchart-specific SVG layout data for use by {@link flowchartToBlueprint}. */
export function parseFlowchartLayout(root: Element): ParsedDiagramLayout {
// Mermaid 11.15 prefixes node and cluster dom ids with the diagram id (e.g.
// `mermaid-0-flowchart-s1-0` instead of `flowchart-s1-0`), so tolerate that
// optional `mermaid-<n>-` prefix when reading the clean id.
const nodes = parseNodesFromSvg(root, '.node', (domId) => {
const match = domId.match(/^flowchart-(.+)-\d+$/)
const match = domId.match(/^(?:mermaid-\d+-)?flowchart-(.+)-\d+$/)
return match ? match[1] : domId
})
const clusters = parseClustersFromSvg(root, '.cluster')
const clusters = parseClustersFromSvg(root, '.cluster', (domId) =>
domId.replace(/^mermaid-\d+-/, '')
)
const edges = parseAllEdgePointsFromSvg(root, (dataId) => {
const match = dataId.match(/^L_(.+)_([^_]+)_\d+$/)
const match = dataId.match(/(?:^|-)L_(.+)_([^_]+)_\d+$/)
return match ? { start: match[1], end: match[2] } : null
})
scaleLayout(nodes, clusters, edges, LAYOUT_SCALE)
Expand Down
71 changes: 71 additions & 0 deletions packages/mermaid/src/layoutParsing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { parseFlowchartLayout } from './flowchartDiagram'
import { parseStateDiagramLayout } from './stateDiagram'

function appendNode(root: Element, domId: string) {
const group = document.createElement('div')
group.classList.add('node')
group.setAttribute('id', domId)
group.setAttribute('transform', 'translate(100, 50)')
const rect = document.createElement('rect')
rect.setAttribute('width', '80')
rect.setAttribute('height', '40')
group.appendChild(rect)
root.appendChild(group)
}

function appendEdge(root: Element, dataId: string, points: [number, number][]) {
const path = document.createElement('path')
path.setAttribute('data-id', dataId)
path.setAttribute('id', `mermaid-0-${dataId}`)
path.setAttribute('data-points', btoa(JSON.stringify(points.map(([x, y]) => ({ x, y })))))
root.appendChild(path)
}

describe('parseFlowchartLayout', () => {
// Mermaid 11.15 prefixes node dom ids with the diagram id.
it('parses node ids from diagram-id-prefixed dom ids', () => {
const root = document.createElement('div')
appendNode(root, 'mermaid-0-flowchart-s1-0')
appendNode(root, 'mermaid-0-flowchart-s2-1')
appendEdge(root, 'L_s1_s2_0', [
[0, 0],
[100, 0],
])

const layout = parseFlowchartLayout(root)

expect([...layout.nodes.keys()].sort()).toEqual(['s1', 's2'])
expect(layout.edges.map((e) => [e.start, e.end])).toEqual([['s1', 's2']])
})

it('still parses legacy unprefixed dom ids', () => {
const root = document.createElement('div')
appendNode(root, 'flowchart-s1-0')
appendEdge(root, 'L_s1_s2_0', [
[0, 0],
[100, 0],
])

const layout = parseFlowchartLayout(root)

expect([...layout.nodes.keys()]).toEqual(['s1'])
expect(layout.edges.map((e) => [e.start, e.end])).toEqual([['s1', 's2']])
})
})

describe('parseStateDiagramLayout', () => {
it('parses node ids from diagram-id-prefixed dom ids', () => {
const root = document.createElement('div')
appendNode(root, 'mermaid-0-state-Still-2')
appendNode(root, 'mermaid-0-state-Moving-3')
appendEdge(root, 'edge0', [
[0, 0],
[100, 0],
])

const layout = parseStateDiagramLayout(root)

expect([...layout.nodes.keys()].sort()).toEqual(['Moving', 'Still'])
expect(layout.edges).toHaveLength(1)
})
})
2 changes: 1 addition & 1 deletion packages/mermaid/src/mindmapDiagram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export interface ParsedMindmapLayout {
}

function parseMindmapNodeId(domId: string): string {
const match = domId.match(/^node_(\d+)$/)
const match = domId.match(/^(?:mermaid-\d+-)?node_(\d+)$/)
return match ? match[1] : domId
}

Expand Down
19 changes: 19 additions & 0 deletions packages/mermaid/src/sequenceDiagram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ const LINETYPE = {
PAR_OVER_START: 32,
BIDIRECTIONAL_SOLID: 33,
BIDIRECTIONAL_DOTTED: 34,
SOLID_TOP: 41,
SOLID_BOTTOM: 42,
STICK_TOP: 43,
STICK_BOTTOM: 44,
SOLID_ARROW_TOP_REVERSE: 45,
SOLID_ARROW_BOTTOM_REVERSE: 46,
STICK_ARROW_TOP_REVERSE: 47,
STICK_ARROW_BOTTOM_REVERSE: 48,
SOLID_TOP_DOTTED: 51,
SOLID_BOTTOM_DOTTED: 52,
STICK_TOP_DOTTED: 53,
STICK_BOTTOM_DOTTED: 54,
SOLID_ARROW_TOP_REVERSE_DOTTED: 55,
SOLID_ARROW_BOTTOM_REVERSE_DOTTED: 56,
STICK_ARROW_TOP_REVERSE_DOTTED: 57,
STICK_ARROW_BOTTOM_REVERSE_DOTTED: 58,
CENTRAL_CONNECTION: 59,
CENTRAL_CONNECTION_REVERSE: 60,
CENTRAL_CONNECTION_DUAL: 61,
} as const satisfies SequenceDB['LINETYPE']

const PLACEMENT = {
Expand Down
13 changes: 10 additions & 3 deletions packages/mermaid/src/stateDiagram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,21 @@ const FRAME_TOP = 54

/** Parse state-diagram SVG layout data for use by {@link stateToBlueprint}. */
export function parseStateDiagramLayout(root: Element): ParsedDiagramLayout {
// Mermaid 11.15 prefixes node and cluster dom ids with the diagram id (e.g.
// `mermaid-0-state-Foo-1` instead of `state-Foo-1`), so tolerate that
// optional `mermaid-<n>-` prefix when reading the clean id.
const nodes = parseNodesFromSvg(
root,
'.node',
(domId) => domId.match(/^state-(.+)-\d+$/)?.[1] ?? domId
(domId) => domId.match(/^(?:mermaid-\d+-)?state-(.+)-\d+$/)?.[1] ?? domId
)
const clusters = parseClustersFromSvg(
root,
'.statediagram-cluster',
(domId) => domId.match(/^(?:mermaid-\d+-)?state-(.+)-\d+$/)?.[1] ?? domId
)
const clusters = parseClustersFromSvg(root, '.statediagram-cluster')
const edges = parseAllEdgePointsFromSvg(root, (dataId) =>
/^edge\d+$/.test(dataId) ? { start: '', end: '' } : null
/(?:^|-)edge\d+$/.test(dataId) ? { start: '', end: '' } : null
)
scaleLayout(nodes, clusters, edges, LAYOUT_SCALE)
return { nodes, clusters, edges }
Expand Down
58 changes: 58 additions & 0 deletions packages/mermaid/src/svgParsing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { parseFlowchartLayout } from './flowchartDiagram'
import { parseStateDiagramLayout } from './stateDiagram'

function svgFromString(markup: string): SVGSVGElement {
const container = document.createElement('div')
container.innerHTML = markup
return container.querySelector('svg') as unknown as SVGSVGElement
}

// Regression for the mermaid 11.15 upgrade: every rendered element id gained a
// `<svgId>-` prefix, which broke the node/cluster id parsers and produced empty
// diagrams (and therefore failed exports). The parsers must resolve bare ids
// across mermaid versions.
describe('layout parsing tolerates mermaid >= 11.15 prefixed ids', () => {
it('parses flowchart nodes and clusters', () => {
const svg = svgFromString(`
<svg id="mermaid-0">
<g class="cluster" id="mermaid-0-Frontend" transform="translate(0,0)">
<rect x="0" y="0" width="200" height="160" />
</g>
<g class="node" id="mermaid-0-flowchart-A-0" transform="translate(50,40)">
<rect width="80" height="40" />
</g>
</svg>
`)
const layout = parseFlowchartLayout(svg)
expect([...layout.nodes.keys()]).toEqual(['A'])
expect([...layout.clusters.keys()]).toEqual(['Frontend'])
})

it('parses state nodes and clusters', () => {
const svg = svgFromString(`
<svg id="mermaid-1">
<g class="statediagram-cluster" id="mermaid-1-state-Active-4" transform="translate(0,0)">
<rect x="0" y="0" width="240" height="180" />
</g>
<g class="node" id="mermaid-1-state-Idle-3" transform="translate(30,30)">
<rect width="60" height="30" />
</g>
</svg>
`)
const layout = parseStateDiagramLayout(svg)
expect([...layout.nodes.keys()]).toEqual(['Idle'])
expect([...layout.clusters.keys()]).toEqual(['Active'])
})

it('still parses bare ids from older mermaid versions', () => {
const svg = svgFromString(`
<svg>
<g class="node" id="flowchart-A-0" transform="translate(50,40)">
<rect width="80" height="40" />
</g>
</svg>
`)
const layout = parseFlowchartLayout(svg)
expect([...layout.nodes.keys()]).toEqual(['A'])
})
})
8 changes: 6 additions & 2 deletions packages/mermaid/src/svgParsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,14 @@ export function parseNodesFromSvg(
return out
}

export function parseClustersFromSvg(root: Element, selector: string): Map<string, ParsedCluster> {
export function parseClustersFromSvg(
root: Element,
selector: string,
idParser: NodeIdParser = (id) => id
): Map<string, ParsedCluster> {
const out = new Map<string, ParsedCluster>()
for (const groupEl of root.querySelectorAll(selector)) {
const id = groupEl.getAttribute('id') || ''
const id = idParser(groupEl.getAttribute('id') || '')
const rect = groupEl.querySelector('rect')
if (!rect) continue
const rx = parseFloat(rect.getAttribute('x') || '0')
Expand Down
Loading
Loading