feat(flowchart): 5 SVG shape templates (process/decision/terminator/subroutine/document)

Amit Haridas
This commit is contained in:
2026-09-14 21:25:38 +05:30
parent 7869be0ff9
commit 4a36b7db76
2 changed files with 132 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
/**
* SVG shape templates for the 5 supported Mermaid flowchart node kinds.
* Each `shapeSvg` returns ONE SVG element string — the canvas wraps it in a
* <g data-node-id="…"> alongside a <text> label.
*
* Pure module: no DOM, no globals, no side effects.
*
* @module flowchart-shapes
*/
'use strict';
const SHAPE_KINDS = ['process', 'decision', 'terminator', 'subroutine', 'document'];
const DEFAULT_WIDTH = 140;
const DEFAULT_HEIGHT = 60;
const LABEL_PADDING_X = 16;
const LABEL_PADDING_Y = 12;
function shapeSvg(kind, x, y, width, height) {
if (!SHAPE_KINDS.includes(kind)) {
throw new Error(`flowchart-shapes: unknown shape kind "${kind}"`);
}
switch (kind) {
case 'process':
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="4" ry="4" />`;
case 'terminator':
return `<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="${height / 2}" ry="${height / 2}" />`;
case 'subroutine': {
const inset = 4;
return (
`<rect x="${x}" y="${y}" width="${width}" height="${height}" rx="4" ry="4" />` +
`<rect x="${x + inset}" y="${y + inset}" width="${width - 2 * inset}" height="${height - 2 * inset}" rx="4" ry="4" />`
);
}
case 'decision': {
const cx = x + width / 2;
const cy = y + height / 2;
const left = `${x},${cy}`;
const top = `${cx},${y}`;
const right = `${x + width},${cy}`;
const bottom = `${cx},${y + height}`;
return `<polygon points="${left} ${top} ${right} ${bottom}" />`;
}
case 'document': {
// Parallelogram: top-right and bottom-right indented by ~20% of height.
const skew = Math.max(10, Math.round(height * 0.25));
const tl = `${x + skew},${y}`;
const tr = `${x + width},${y}`;
const br = `${x + width - skew},${y + height}`;
const bl = `${x},${y + height}`;
return `<polygon points="${tl} ${tr} ${br} ${bl}" />`;
}
default:
throw new Error(`flowchart-shapes: unknown shape kind "${kind}"`);
}
}
module.exports = {
shapeSvg,
SHAPE_KINDS,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
LABEL_PADDING_X,
LABEL_PADDING_Y,
};
+67
View File
@@ -0,0 +1,67 @@
/**
* @jest-environment node
*/
const {
shapeSvg,
SHAPE_KINDS,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
} = require('../src/flowchart/flowchart-shapes');
describe('flowchart-shapes: shapeSvg', () => {
test('process emits a <rect> at the given coordinates', () => {
const svg = shapeSvg('process', 10, 20, 100, 50);
expect(svg).toMatch(/<rect/);
expect(svg).toMatch(/x="10"/);
expect(svg).toMatch(/y="20"/);
expect(svg).toMatch(/width="100"/);
expect(svg).toMatch(/height="50"/);
});
test('decision emits a <polygon> diamond', () => {
const svg = shapeSvg('decision', 0, 0, 100, 60);
expect(svg).toMatch(/<polygon/);
// 4 points (diamond)
const match = /points="([^"]+)"/.exec(svg);
expect(match).not.toBeNull();
expect(match[1].split(/\s+/).filter(Boolean)).toHaveLength(4);
});
test('terminator emits a <rect> with rx (stadium)', () => {
const svg = shapeSvg('terminator', 0, 0, 120, 40);
expect(svg).toMatch(/<rect/);
expect(svg).toMatch(/rx="/);
});
test('subroutine emits two concentric <rect> elements (double border)', () => {
const svg = shapeSvg('subroutine', 0, 0, 100, 50);
const rects = svg.match(/<rect/g) || [];
expect(rects.length).toBeGreaterThanOrEqual(2);
});
test('document emits a <polygon> parallelogram', () => {
const svg = shapeSvg('document', 0, 0, 120, 60);
expect(svg).toMatch(/<polygon/);
const match = /points="([^"]+)"/.exec(svg);
expect(match[1].split(/\s+/).filter(Boolean)).toHaveLength(4);
});
test('unknown kind throws', () => {
expect(() => shapeSvg('hexagon', 0, 0, 100, 50)).toThrow(/hexagon/);
});
test('SHAPE_KINDS lists all 5 shapes', () => {
expect(SHAPE_KINDS.sort()).toEqual([
'decision',
'document',
'process',
'subroutine',
'terminator',
]);
});
test('DEFAULT_WIDTH and DEFAULT_HEIGHT are positive numbers', () => {
expect(DEFAULT_WIDTH).toBeGreaterThan(0);
expect(DEFAULT_HEIGHT).toBeGreaterThan(0);
});
});