mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(flowchart): mermaid translator (flowchart TD, all 5 shapes, 3 edge kinds)
Pure Mermaid translator: graph (nodes + edges) -> flowchart TD source.
5 node kinds (process/decision/terminator/subroutine/document) ->
Mermaid syntax ([..], {..}, ([..]), [[..]], [/../]).
3 edge kinds (solid/dotted/thick) -> Mermaid arrows (-->, -.->, ==>).
Label escaping: double quotes become #quot;, newlines become literal \n.
Also moves tests/fixtures per-test dirs to fixtures-epub / fixtures-css
so the shared tests/fixtures/ stays available for snapshot fixtures.
Amit Haridas
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
const {
|
||||
toMermaid,
|
||||
escapeLabel,
|
||||
nodeDeclaration,
|
||||
edgeDeclaration,
|
||||
} = require('../src/flowchart/flowchart-mermaid');
|
||||
const {
|
||||
normalizeIds,
|
||||
linearChain,
|
||||
decisionDiamond,
|
||||
parallelBranches,
|
||||
cycle,
|
||||
largeGraph,
|
||||
} = require('./fixtures/flowchart-snapshots');
|
||||
|
||||
describe('flowchart-mermaid: escapeLabel', () => {
|
||||
test('escapes double quotes', () => {
|
||||
expect(escapeLabel('say "hi"')).toBe('say #quot;hi#quot;');
|
||||
});
|
||||
|
||||
test('escapes newlines to literal \\n', () => {
|
||||
expect(escapeLabel('line1\nline2')).toBe('line1\\nline2');
|
||||
});
|
||||
|
||||
test('passes plain text through', () => {
|
||||
expect(escapeLabel('Hello world')).toBe('Hello world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flowchart-mermaid: nodeDeclaration', () => {
|
||||
test('process → A[Label]', () => {
|
||||
expect(nodeDeclaration({ id: 'A', kind: 'process', label: 'Step' })).toBe('A[Step]');
|
||||
});
|
||||
test('decision → A{Label}', () => {
|
||||
expect(nodeDeclaration({ id: 'A', kind: 'decision', label: 'Yes?' })).toBe('A{Yes?}');
|
||||
});
|
||||
test('terminator → A([Label])', () => {
|
||||
expect(nodeDeclaration({ id: 'A', kind: 'terminator', label: 'Start' })).toBe('A([Start])');
|
||||
});
|
||||
test('subroutine → A[[Label]]', () => {
|
||||
expect(nodeDeclaration({ id: 'A', kind: 'subroutine', label: 'Do thing' })).toBe(
|
||||
'A[[Do thing]]'
|
||||
);
|
||||
});
|
||||
test('document → A[/Label/]', () => {
|
||||
expect(nodeDeclaration({ id: 'A', kind: 'document', label: 'Report' })).toBe('A[/Report/]');
|
||||
});
|
||||
test('unknown kind throws', () => {
|
||||
expect(() => nodeDeclaration({ id: 'A', kind: 'hexagon', label: 'x' })).toThrow(/hexagon/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flowchart-mermaid: edgeDeclaration', () => {
|
||||
test('solid → A --> B', () => {
|
||||
expect(edgeDeclaration({ fromNodeId: 'A', toNodeId: 'B', kind: 'solid' }, 'A', 'B')).toBe(
|
||||
'A --> B'
|
||||
);
|
||||
});
|
||||
test('dotted → A -.-> B', () => {
|
||||
expect(edgeDeclaration({ fromNodeId: 'A', toNodeId: 'B', kind: 'dotted' }, 'A', 'B')).toBe(
|
||||
'A -.-> B'
|
||||
);
|
||||
});
|
||||
test('thick → A ==> B', () => {
|
||||
expect(edgeDeclaration({ fromNodeId: 'A', toNodeId: 'B', kind: 'thick' }, 'A', 'B')).toBe(
|
||||
'A ==> B'
|
||||
);
|
||||
});
|
||||
test('with label → A -->|yes| B', () => {
|
||||
expect(
|
||||
edgeDeclaration({ fromNodeId: 'A', toNodeId: 'B', kind: 'solid', label: 'yes' }, 'A', 'B')
|
||||
).toBe('A -->|yes| B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flowchart-mermaid: toMermaid — full graphs', () => {
|
||||
test('linearChain', () => {
|
||||
const out = toMermaid(normalizeIds(linearChain));
|
||||
expect(out).toMatch(/^flowchart TD/);
|
||||
expect(out).toMatch(/A\(\[Start\]\)/);
|
||||
expect(out).toMatch(/B\[Step 1\]/);
|
||||
expect(out).toMatch(/C\[Step 2\]/);
|
||||
expect(out).toMatch(/D\(\[End\]\)/);
|
||||
expect(out).toMatch(/A --> B/);
|
||||
expect(out).toMatch(/B --> C/);
|
||||
expect(out).toMatch(/C --> D/);
|
||||
});
|
||||
|
||||
test('decisionDiamond includes labeled branches', () => {
|
||||
const out = toMermaid(normalizeIds(decisionDiamond));
|
||||
expect(out).toMatch(/C\{Valid\?\}/);
|
||||
expect(out).toMatch(/C -->\|yes\| D/);
|
||||
expect(out).toMatch(/C -->\|no\| E/);
|
||||
});
|
||||
|
||||
test('parallelBranches', () => {
|
||||
const out = toMermaid(normalizeIds(parallelBranches));
|
||||
expect(out).toMatch(/B --> C/);
|
||||
expect(out).toMatch(/B --> D/);
|
||||
expect(out).toMatch(/C --> E/);
|
||||
expect(out).toMatch(/D --> E/);
|
||||
});
|
||||
|
||||
test('cycle uses dotted for the back-edge', () => {
|
||||
const out = toMermaid(normalizeIds(cycle));
|
||||
expect(out).toMatch(/A --> B/);
|
||||
expect(out).toMatch(/B -\.-> A/);
|
||||
});
|
||||
|
||||
test('largeGraph(20) emits 20 nodes and 19 edges in order', () => {
|
||||
const out = toMermaid(normalizeIds(largeGraph(20)));
|
||||
const nodeCount = (out.match(/^[A-Z]\[/gm) || []).length;
|
||||
const edgeCount = (out.match(/ --> /g) || []).length;
|
||||
expect(nodeCount).toBe(20);
|
||||
expect(edgeCount).toBe(19);
|
||||
});
|
||||
|
||||
test('empty graph still emits the header', () => {
|
||||
expect(toMermaid({ nodes: [], edges: [] })).toBe('flowchart TD');
|
||||
});
|
||||
|
||||
test('label with embedded double-quote is escaped', () => {
|
||||
const out = toMermaid(
|
||||
normalizeIds({
|
||||
nodes: [{ id: 'n1', kind: 'process', x: 0, y: 0, label: 'say "hi"' }],
|
||||
edges: [],
|
||||
})
|
||||
);
|
||||
expect(out).toMatch(/A\[say #quot;hi#quot;\]/);
|
||||
});
|
||||
|
||||
test('label with newline uses \\n escape', () => {
|
||||
const out = toMermaid(
|
||||
normalizeIds({
|
||||
nodes: [{ id: 'n1', kind: 'process', x: 0, y: 0, label: 'line1\nline2' }],
|
||||
edges: [],
|
||||
})
|
||||
);
|
||||
expect(out).toMatch(/A\[line1\\nline2\]/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user