mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(flowchart): 5 SVG shape templates (process/decision/terminator/subroutine/document)
Amit Haridas
This commit is contained in:
@@ -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,
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user