From e4454e0f5d3156c703a4794c77453f6ec76e5ffc Mon Sep 17 00:00:00 2001 From: Vladimir Urushev Date: Tue, 28 Apr 2026 02:40:00 +0200 Subject: [PATCH] fix: sequence diagram support via mermaid/svgdom upgrade --- .github/workflows/publish.yml | 3 -- CHANGELOG.md | 4 +-- src/mermaid/render.ts | 6 ++-- src/mermaid/style-polyfill.ts | 64 ----------------------------------- test/mermaid-elk.test.ts | 8 +---- 5 files changed, 6 insertions(+), 79 deletions(-) delete mode 100644 src/mermaid/style-polyfill.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fc5c4a6..0f4d747 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,9 +22,6 @@ jobs: node-version: 22 registry-url: 'https://registry.npmjs.org' - - name: Ensure npm with OIDC support - run: npm install -g npm@latest - - name: Install dependencies run: npm install diff --git a/CHANGELOG.md b/CHANGELOG.md index 7247b03..4a310e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ Project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.3.1] - 2026-04-28 -### Fixed +### Added -- Sequence diagram rendering (polyfill CSSStyleDeclaration for svgdom) +- Sequence diagram support (mermaid 11.14, svgdom 0.1.23) ### Changed diff --git a/src/mermaid/render.ts b/src/mermaid/render.ts index 5a87c62..e904a57 100644 --- a/src/mermaid/render.ts +++ b/src/mermaid/render.ts @@ -1,6 +1,5 @@ import { createHash } from 'node:crypto' import { Resvg } from '@resvg/resvg-js' -import { patchSvgdomStyle } from './style-polyfill.js' // Patch JSON.stringify to handle circular references (elkjs debug logs) const originalStringify = JSON.stringify @@ -50,8 +49,6 @@ async function loadMermaid() { win.setInterval = setInterval win.clearInterval = clearInterval win.console = console - - patchSvgdomStyle(win) } // Load and register ELK layout engine @@ -117,6 +114,9 @@ export async function renderMermaid(code: string): Promise { svg = svg.replace(/style="[^"]*max-width:[^;]*;?\s*"/, '') } + // resvg doesn't support orient="auto-start-reverse" on markers + svg = svg.replace(/orient="auto-start-reverse"/g, 'orient="auto"') + // Convert SVG to PNG with 4x scale for crisp images const resvg = new Resvg(svg, { fitTo: { diff --git a/src/mermaid/style-polyfill.ts b/src/mermaid/style-polyfill.ts deleted file mode 100644 index 39083f4..0000000 --- a/src/mermaid/style-polyfill.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Polyfill CSSStyleDeclaration for svgdom elements. - * - * svgdom's element.style returns the element itself (circular ref), not a CSSStyleDeclaration. - * D3 (bundled in mermaid) calls element.style.removeProperty() during sequence diagram rendering. - * This shim provides minimal CSSStyleDeclaration methods that parse/manipulate the style attribute. - */ - -interface StyleShim { - getPropertyValue(name: string): string - setProperty(name: string, value: string): void - removeProperty(name: string): string -} - -interface PatchableElement { - getAttribute(name: string): string | null - setAttribute(name: string, value: string): void - _styleShim?: StyleShim -} - -function createStyleShim(element: PatchableElement): StyleShim { - return { - getPropertyValue(name: string): string { - const style = element.getAttribute('style') || '' - const match = style.match(new RegExp(`${name}\\s*:\\s*([^;]+)`)) - return match ? match[1].trim() : '' - }, - setProperty(name: string, value: string): void { - const style = element.getAttribute('style') || '' - const regex = new RegExp(`${name}\\s*:[^;]+;?\\s*`) - const cleaned = style.replace(regex, '') - const separator = cleaned && !cleaned.endsWith(';') ? '; ' : '' - element.setAttribute('style', `${cleaned}${separator}${name}: ${value};`.trim()) - }, - removeProperty(name: string): string { - const style = element.getAttribute('style') || '' - const regex = new RegExp(`${name}\\s*:[^;]+;?\\s*`, 'g') - const oldValue = this.getPropertyValue(name) - element.setAttribute('style', style.replace(regex, '').trim()) - return oldValue - }, - } -} - -/** - * Patch SVGElement.prototype.style to return a CSSStyleDeclaration shim. - * Must be called after svgdom creates the window but before mermaid renders. - */ -export function patchSvgdomStyle(win: Record): void { - // biome-ignore lint/suspicious/noExplicitAny: accessing svgdom's SVGElement - const SVGElement = (win as any).SVGElement - if (!SVGElement) return - - Object.defineProperty(SVGElement.prototype, 'style', { - get(this: PatchableElement) { - if (!this._styleShim) { - this._styleShim = createStyleShim(this) - } - return this._styleShim - }, - set() {}, - configurable: true, - }) -} diff --git a/test/mermaid-elk.test.ts b/test/mermaid-elk.test.ts index 9306cda..fbc7ad7 100644 --- a/test/mermaid-elk.test.ts +++ b/test/mermaid-elk.test.ts @@ -87,10 +87,7 @@ flowchart TB expect(filename).toMatch(/^mermaid-[a-f0-9]{12}\.png$/) }) - it('renders sequence diagrams - regression test for CSSStyleDeclaration polyfill', async () => { - // D3 (bundled in mermaid) calls element.style.removeProperty() during sequence rendering - // svgdom doesn't implement CSSStyleDeclaration, so we polyfill it - // Without the polyfill: TypeError: this.style.removeProperty is not a function + it('renders sequence diagrams', async () => { const sequenceDiagram = ` sequenceDiagram participant A as Service A @@ -100,13 +97,10 @@ sequenceDiagram ` const png = await renderMermaid(sequenceDiagram) - // Check PNG magic bytes expect(png[0]).toBe(0x89) expect(png[1]).toBe(0x50) expect(png[2]).toBe(0x4e) expect(png[3]).toBe(0x47) - - // Sequence diagram should render to reasonable size expect(png.length).toBeGreaterThan(5000) })