clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / apps / editor / test / svg-contract.test.js
svg-contract.test.js 28.2K
1import assert from 'node:assert/strict'2import test from 'node:test'3import {4 APPEARANCES,5 RAW_NORMALIZATION,6 SvgNormalizationError,7 canonicalizeSvg,8 materializePresentation,9 materializeSvg,10 supportedAppearances,11 normalizationFor,12 sanitizeAndDecorateSvg,13} from '../../../shared/svg/index.js'1415const metadata = { title: '', description: '' }16const presentation = { background: '', padding: 0, frame: false }17const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><path d="M0 0L10 10" stroke="black"></path></svg>'1819test('freezes namespaced appearance and raw capability contracts', () => {20 assert.deepEqual(APPEARANCES, [21 'raw',22 'auto-transparent',23 'light-transparent',24 'dark-transparent',25 'auto-framed',26 'light-framed',27 'dark-framed',28 ])29 assert.deepEqual(RAW_NORMALIZATION.appearances, ['raw'])30})3132test('falls back to safe raw for an unknown renderer build', () => {33 assert.equal(normalizationFor('d2', 'd2@future').profile, 'safe-raw-1')34 assert.equal(normalizationFor('unknown', 'unknown@1').profile, 'safe-raw-1')35})3637test('produces deterministic, idempotent raw canonical SVG', () => {38 const first = sanitizeAndDecorateSvg(source, metadata, presentation, 'pikchr')39 const repeated = sanitizeAndDecorateSvg(source, metadata, presentation, 'pikchr')40 const normalizedAgain = sanitizeAndDecorateSvg(first, metadata, presentation, 'pikchr')4142 assert.equal(first, repeated)43 assert.equal(first, normalizedAgain)44 assert.match(first, /data-dz-normalizer="svg-normalizer-4"/)45 assert.match(first, /data-dz-profile="safe-raw-1"/)46 assert.match(first, /data-dz-palette="renderer"/)47 assert.match(first, /data-dz-conformance="raw"/)48 assert.doesNotMatch(first, /data-theme=/)49})5051test('does not duplicate root metadata when canonical output crosses another consumer', () => {52 const once = canonicalizeSvg(source, { title: 'A', description: 'B' }, 'pikchr')53 const twice = canonicalizeSvg(once, { title: 'A', description: 'B' }, 'pikchr')54 assert.equal((twice.match(/<title>/g) ?? []).length, 1)55 assert.equal((twice.match(/<desc>/g) ?? []).length, 1)56})5758test('recomputes stale semantic annotations when upgrading the normalizer build', () => {59 const stale = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 50" data-dz-schema="1" data-dz-normalizer="svg-normalizer-1" data-dz-profile="graphviz-15-semantic-1" data-dz-materializer="svg-materializer-1"><style data-dz-owned="materializer">stale</style><rect data-dz-owned="materializer" data-dz-role="canvas" width="100" height="50"></rect><rect data-dz-owned="normalizer" data-dz-role="object-surface" data-dz-fill="surface-1" width="10" height="10"></rect><g class="node"><ellipse fill="none" stroke="black" stroke-width="0" cx="50" cy="25" rx="40" ry="20" data-dz-fill="surface-1"></ellipse><rect x="20" y="10" width="60" height="30" fill="white" stroke="black" data-renderer-id="kept" data-dz-fill="surface-3"></rect></g></svg>'60 const upgraded = canonicalizeSvg(stale, metadata, 'dbml', 'dbml@1.0.31+graphviz@15.1.1')6162 assert.match(upgraded, /data-dz-normalizer="svg-normalizer-4"/)63 assert.doesNotMatch(upgraded, /data-dz-owned=/)64 assert.doesNotMatch(upgraded, /<ellipse[^>]*data-dz-/)65 assert.match(upgraded, /data-renderer-id="kept" data-dz-fill="surface-1" data-dz-stroke="line"/)66})6768test('keeps legacy editor presentation outside the canonical render', () => {69 const canonical = canonicalizeSvg(source, metadata, 'pikchr')70 const displayed = materializePresentation(canonical, { background: '#ffffff', padding: 8, frame: true })7172 assert.match(canonical, /data-dz-bounds="0 0 10 10"/)73 assert.doesNotMatch(canonical, /fill="#ffffff"/)74 assert.doesNotMatch(canonical, /viewBox="-8 -8 26 26"/)75 assert.match(displayed, /fill="#ffffff"/)76 assert.match(displayed, /viewBox="-8 -8 26 26"/)77})7879test('exposes structured normalization failures to both callers', () => {80 assert.throws(81 () => sanitizeAndDecorateSvg('<svg><style>@import "https://example.com/x";</style></svg>', metadata, presentation, 'pikchr'),82 error => error instanceof SvgNormalizationError && error.code === 'unsafe_svg' && error.status === 422,83 )84})8586const semanticCanonical = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10" width="10" height="10" data-dz-schema="1" data-dz-normalizer="svg-normalizer-4" data-dz-profile="fixture-semantic-1" data-dz-palette="renderer" data-dz-engine="fixture" data-dz-appearance="raw" data-dz-conformance="semantic" data-dz-bounds="0 0 10 10" data-dz-appearances="auto-transparent light-transparent dark-transparent auto-framed light-framed dark-framed"><rect x="1" y="1" width="8" height="8" fill="white" stroke="black" data-dz-fill="surface-1" data-dz-stroke="line"></rect><text x="5" y="6" fill="black" data-dz-fill="ink">A</text></svg>'8788test('materializes explicit and automatic palettes without changing diagram geometry', () => {89 const light = materializeSvg(semanticCanonical, 'light-transparent')90 const dark = materializeSvg(semanticCanonical, 'dark-transparent')91 const automatic = materializeSvg(semanticCanonical, 'auto-transparent')9293 for (const output of [light, dark, automatic]) {94 assert.match(output, /data-dz-materializer="svg-materializer-4"/)95 assert.match(output, /data-dz-palette="diagramzip-palette-2"/)96 assert.match(output, /<rect x="1" y="1" width="8" height="8"/)97 assert.doesNotMatch(output, /<(?:rect|path|polygon)[^>]*data-dz-role="canvas"/)98 }99 for (const output of [light, dark, automatic]) {100 assert.ok(output.includes("data-dz-appearance^=\"light-\"") && output.includes("--dz-ink:#0f172a"))101 assert.ok(output.includes("data-dz-appearance^=\"dark-\"") && output.includes("--dz-ink:#f8fafc"))102 }103 assert.match(automatic, /@media\(prefers-color-scheme:dark\)/)104})105106test('reports the appearances declared by the canonical profile', () => {107 assert.deepEqual(supportedAppearances(semanticCanonical), [108 'raw',109 'auto-transparent',110 'light-transparent',111 'dark-transparent',112 'auto-framed',113 'light-framed',114 'dark-framed',115 ])116})117118test('materializes a bounded frame and remains idempotent', () => {119 const framed = materializeSvg(semanticCanonical, 'dark-framed')120 const repeated = materializeSvg(framed, 'dark-framed')121122 assert.equal(framed, repeated)123 assert.match(framed, /viewBox="-24 -24 58 58"/)124 assert.match(framed, /data-dz-role="canvas"/)125 assert.match(framed, /data-dz-role="frame"/)126 assert.match(framed, /vector-effect="non-scaling-stroke"/)127})128129test('fails closed when a raw-only profile requests a themed appearance', () => {130 const raw = sanitizeAndDecorateSvg(source, metadata, presentation, 'pikchr')131 assert.throws(132 () => materializeSvg(raw, 'dark-framed'),133 error => error instanceof SvgNormalizationError && error.code === 'unsupported_appearance',134 )135})136137test('applies the pinned GraphViz semantic profile without recoloring authored paint', () => {138 const graphviz = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 50"><g class="graph"><polygon fill="white" stroke="none" points="0,0 100,0 100,50 0,50"></polygon><g class="node"><ellipse fill="none" stroke="black" cx="25" cy="25" rx="20" ry="10"></ellipse><text fill="black">A</text></g><g class="node"><ellipse fill="red" stroke="red" cx="75" cy="25" rx="20" ry="10"></ellipse><text fill="white">B</text></g><g class="edge"><path fill="none" stroke="black" d="M45 25L55 25"></path><polygon fill="black" stroke="black" points="55,25 50,20 50,30"></polygon></g></g></svg>'139 const canonical = sanitizeAndDecorateSvg(graphviz, metadata, presentation, 'graphviz', 'graphviz@15.1.1')140 const dark = materializeSvg(canonical, 'dark-transparent')141142 assert.match(canonical, /data-dz-profile="graphviz-15-semantic-2"/)143 assert.match(canonical, /data-dz-conformance="semantic"/)144 assert.match(canonical, /data-dz-role="canvas"/)145 assert.match(canonical, /data-dz-fill="surface-1"/)146 assert.match(canonical, /data-dz-stroke="line"/)147 assert.match(dark, /data-dz-role="canvas"\]:not/)148 assert.match(dark, /fill="red" stroke="red"/)149 assert.doesNotMatch(dark, /fill="red" stroke="red"[^>]*data-dz-/)150})151152test('applies D2, PlantUML, Svgbob, and neutral SVG family roles', () => {153 const fixtures = [154 {155 engine: 'd2', version: 'd2@0.7.1/dagre-wasm-1', profile: 'd2-0.7-semantic-2',156 source: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="0" y="0" width="20" height="20" fill="#FFFFFF"></rect><rect class="shape fill-B6 stroke-B1" x="2" y="2" width="16" height="16" fill="#f7f8fe" stroke="#0d32b2"></rect><text class="fill-N1" fill="#0a0f25">A</text></svg>',157 roles: ['data-dz-role="canvas"', 'data-dz-fill="surface-1"', 'data-dz-stroke="accent-1"', 'data-dz-fill="ink"'],158 },159 {160 engine: 'plantuml', version: 'plantuml@1.2026.6/edge-wasm-1', profile: 'plantuml-2026-semantic-2',161 source: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="2" y="2" width="16" height="16" fill="#E2E2F0" stroke="#181818"></rect><text fill="#000000">A</text></svg>',162 roles: ['data-dz-fill="surface-1"', 'data-dz-stroke="line"', 'data-dz-fill="ink"'],163 },164 {165 engine: 'svgbob', version: 'svgbob@0.7.6/edge-wasm-1', profile: 'svgbob-0.7-semantic-2',166 source: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><style>.solid{stroke:black}.nofill{fill:white}</style><rect class="backdrop" x="0" y="0" width="20" height="20" fill="white"></rect><rect class="solid nofill" x="2" y="2" width="16" height="16"></rect><text>A</text></svg>',167 roles: ['data-dz-role="canvas"', 'data-dz-stroke="line"', 'data-dz-fill="surface-1"', 'data-dz-fill="ink"'],168 },169 {170 engine: 'blockdiag', version: 'blockdiag@3.4.2/python-worker-1', profile: 'neutral-svg-semantic-2',171 source: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect x="2" y="2" width="16" height="16" fill="rgb(255,255,255)" stroke="rgb(0,0,0)"></rect><text fill="rgb(0,0,0)">A</text></svg>',172 roles: ['data-dz-fill="surface-1"', 'data-dz-stroke="line"', 'data-dz-fill="ink"'],173 },174 ]175176 for (const fixture of fixtures) {177 const canonical = canonicalizeSvg(fixture.source, metadata, fixture.engine, fixture.version)178 assert.match(canonical, new RegExp(`data-dz-profile="${fixture.profile}"`))179 for (const role of fixture.roles) assert.ok(canonical.includes(role), `${fixture.engine} missing ${role}`)180 assert.match(materializeSvg(canonical, 'dark-transparent'), /data-dz-appearance="dark-transparent"/)181 }182})183184test('normalizes squaring voltage bands, wires, and labels with dedicated palettes', () => {185 const squaring = canonicalizeSvg(186 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40"><rect class="squaring-canvas" x="0" y="0" width="80" height="40" fill="#ffffff"></rect><g class="squaring-square" data-side="2"><rect class="squaring-square-fill" data-band="7" x="4" y="4" width="20" height="20" fill="#f87171" stroke="#1e293b"></rect><text class="squaring-square-label" x="14" y="14" fill="#0f172a">2</text></g><path class="squaring-wire-path" d="M0 0L1 1" fill="none" stroke="#1e293b"></path><circle class="squaring-node-dot" data-band="0" cx="1" cy="1" r="5" fill="#2563eb"></circle><rect class="squaring-block" x="1" y="1" width="2" height="2" fill="none" stroke="#db2777"></rect><text class="squaring-summary" fill="#666666">Order 3</text></svg>',187 metadata, 'squaring', 'diagramzip-squaring@3',188 )189190 assert.match(squaring, /data-dz-profile="squaring-semantic-1"/)191 assert.match(squaring, /class="squaring-canvas"[^>]*data-dz-role="canvas"/)192 assert.match(squaring, /class="squaring-square-fill"[^>]*data-dz-fill="squaring-square-7"[^>]*data-dz-stroke="squaring-edge"/)193 assert.match(squaring, /class="squaring-square-label"[^>]*data-dz-fill="squaring-label"/)194 assert.match(squaring, /class="squaring-wire-path"[^>]*data-dz-stroke="squaring-wire"/)195 assert.match(squaring, /class="squaring-node-dot"[^>]*data-dz-fill="squaring-node-0"/)196 assert.match(squaring, /class="squaring-block"[^>]*data-dz-stroke="squaring-block"/)197 assert.match(squaring, /class="squaring-summary"[^>]*data-dz-fill="ink-muted"/)198 const dark = materializeSvg(squaring, 'dark-transparent')199 assert.match(dark, /:root\[data-dz-profile="squaring-semantic-1"\]\[data-dz-appearance\^="dark-"\]\{[^}]*--dz-sq-square-7:#dc2626/)200 assert.match(dark, /--dz-sq-wire:#a5b4fc/)201 assert.match(dark, /\[data-dz-fill="squaring-square-7"\]\{fill:var\(--dz-sq-square-7\)!important\}/)202})203204test('normalizes TRN table structure and relationship zones', () => {205 const trn = canonicalizeSvg(206 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 40"><rect class="trn-canvas" width="80" height="40" fill="#fbf8dc"></rect><text class="trn-title" fill="#111827">Bread</text><rect class="trn-table-surface" y="10" width="80" height="30" fill="white"></rect><path class="trn-zone-shape trn-branch-4" fill="white" stroke="#65a268"></path><text class="trn-operation-label" fill="#111827">mix</text></svg>',207 metadata, 'trn', 'diagramzip-trn@16',208 )209210 assert.match(trn, /data-dz-profile="trn-semantic-4"/)211 assert.match(trn, /class="trn-zone-shape trn-branch-4"[^>]*data-dz-fill="accent-4"[^>]*data-dz-stroke="line-muted"/)212 assert.match(trn, /class="trn-operation-label"[^>]*data-dz-fill="on-accent"/)213 const light = materializeSvg(trn, 'light-transparent')214 assert.match(light, /:root\[data-dz-profile="trn-semantic-4"\]\[data-dz-appearance\^="light-"\]/)215 const dark = materializeSvg(trn, 'dark-transparent')216 assert.match(dark, /data-dz-appearance="dark-transparent"/)217 assert.match(dark, /--dz-accent-4:#34d399/)218 assert.match(dark, /:root\[data-dz-appearance\^="dark-"\]\{[^}]*--dz-accent-4:#34d399/)219})220221test('themes every Svgbob stroke without treating open curves as object surfaces', () => {222 const canonical = canonicalizeSvg(223 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 20" class="svgbob"><path id="curve" d="M 8,2 A 8,8 0,0,0 8,18" class="nofill"></path><path id="closed" d="M20 2L30 2L30 18L20 18Z" class="nofill"></path><line id="plain" x1="2" y1="10" x2="6" y2="10"></line><rect id="box" x="32" y="2" width="6" height="16" class="solid nofill"></rect></svg>',224 metadata, 'svgbob', 'svgbob@0.7.6/edge-wasm-1',225 )226227 const curve = canonical.match(/<path id="curve"[^>]*>/)?.[0] ?? ''228 const closed = canonical.match(/<path id="closed"[^>]*>/)?.[0] ?? ''229 const plain = canonical.match(/<line id="plain"[^>]*>/)?.[0] ?? ''230 const box = canonical.match(/<rect id="box"[^>]*>/)?.[0] ?? ''231 assert.match(curve, /data-dz-stroke="line"/)232 assert.doesNotMatch(curve, /data-dz-fill=/)233 assert.match(closed, /data-dz-stroke="line"/)234 assert.match(closed, /data-dz-fill="surface-1"/)235 assert.match(plain, /data-dz-stroke="line"/)236 assert.match(box, /data-dz-stroke="line"/)237 assert.match(box, /data-dz-fill="surface-1"/)238})239240test('adapts neutral authored paint while preserving non-neutral paint and transparent support', () => {241 const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><rect width="10" height="10" fill="white"></rect><path d="M1 1L9 9" stroke="hotpink"></path><text x="2" y="6" fill="black">A</text></svg>'242 const canonical = canonicalizeSvg(source, metadata, 'tikz', '@planktimerr/tikzjax@1.0.63/client-unit-1')243 const framed = materializeSvg(canonical, 'auto-framed')244 const transparent = materializeSvg(canonical, 'dark-transparent')245246 assert.match(canonical, /data-dz-profile="authored-neutral-semantic-1"/)247 assert.match(canonical, /data-dz-conformance="adaptive"/)248 assert.match(canonical, /data-dz-role="canvas"/)249 assert.match(canonical, /stroke="hotpink"/)250 assert.doesNotMatch(canonical, /stroke="hotpink"[^>]*data-dz-/)251 assert.match(canonical, /data-dz-fill="ink"/)252 assert.match(framed, /data-dz-role="frame"/)253 assert.match(transparent, /data-dz-appearance="dark-transparent"/)254})255256test('keeps GraphViz canvases and arrowheads coherent without consuming WireViz wire colors', () => {257 const graphviz = canonicalizeSvg(258 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 20"><g class="graph"><polygon fill="white" stroke="none" points="-4,-4 44,-4 44,24 -4,24"></polygon><g class="edge"><path fill="none" stroke="black" d="M2 10L30 10"></path><polygon fill="black" stroke="black" points="30,10 25,7 25,13"></polygon></g></g></svg>',259 metadata, 'graphviz', 'graphviz@15.1.1',260 )261 assert.match(graphviz, /data-dz-role="canvas"/)262 assert.match(graphviz, /<polygon fill="black" stroke="black"[^>]*data-dz-stroke="line"[^>]*data-dz-fill="line"/)263264 const wireviz = canonicalizeSvg(265 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 30"><g class="graph"><polygon fill="white" stroke="none" points="0,0 100,0 100,30 0,30"></polygon><g class="node"><rect x="2" y="2" width="96" height="26" fill="white" stroke="black"></rect><path d="M2 10L98 10" fill="none" stroke="#ef4444"></path><path d="M2 14L98 14" fill="none" stroke="white"></path></g></g></svg>',266 metadata, 'wireviz', '0.3.2',267 )268 assert.match(wireviz, /stroke="#ef4444"/)269 assert.doesNotMatch(wireviz, /stroke="#ef4444"[^>]*data-dz-/)270 assert.match(wireviz, /stroke="white"/)271 assert.doesNotMatch(wireviz, /stroke="white"[^>]*data-dz-/)272})273274test('maps DBML table paint without turning invisible bounds into oval surfaces', () => {275 const canonical = canonicalizeSvg(276 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80"><g class="graph"><g class="node"><ellipse fill="none" stroke="black" stroke-width="0" cx="60" cy="40" rx="58" ry="38"></ellipse><polygon fill="#1d71b8" stroke="none" points="10,10 110,10 110,35 10,35"></polygon><text x="20" y="28" fill="#ffffff">users</text><polygon fill="#e7e2dd" stroke="none" points="10,35 110,35 110,70 10,70"></polygon><text x="20" y="58" fill="#29235c">id integer</text></g><g class="edge"><path fill="none" stroke="#29235c" d="M1 1L8 8"></path><polygon fill="#29235c" stroke="#29235c" points="8,8 4,7 7,4"></polygon></g></g></svg>',277 metadata, 'dbml', 'dbml@1.0.31+graphviz@15.1.1',278 )279 assert.match(canonical, /fill="#1d71b8"[^>]*data-dz-fill="accent-1"/)280 assert.match(canonical, /fill="#ffffff"[^>]*data-dz-fill="on-accent"/)281 assert.match(canonical, /fill="#e7e2dd"[^>]*data-dz-fill="surface-1"/)282 assert.match(canonical, /fill="#29235c"[^>]*data-dz-fill="ink"/)283 assert.doesNotMatch(canonical, /<ellipse[^>]*data-dz-/)284})285286test('recognizes nested D2 and WaveDrom canvases while leaving masks renderer-defined', () => {287 const d2 = canonicalizeSvg(288 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><svg viewBox="-10 -10 40 40"><rect x="-10" y="-10" width="40" height="40" fill="#FFFFFF" class="fill-N7" stroke-width="0"></rect><mask id="m"><rect x="-10" y="-10" width="40" height="40" fill="white"></rect><rect x="1" y="1" width="8" height="8" fill="black"></rect></mask><rect class="shape fill-B6" x="2" y="2" width="16" height="16" fill="#f7f8fe"></rect></svg></svg>',289 metadata, 'd2', 'd2@0.7.1/dagre-wasm-1',290 )291 assert.equal((d2.match(/data-dz-role="canvas"/g) ?? []).length, 1)292 assert.doesNotMatch(d2.match(/<mask[\s\S]*?<\/mask>/)?.[0] ?? '', /data-dz-(?:fill|stroke|role)/)293294 const wavedrom = canonicalizeSvg(295 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 20"><rect width="40" height="20" style="stroke:none;fill:white"></rect><text x="5" y="12" class="info">clock</text><path class="s7" d="M0 0L20 0L20 20L0 20Z"></path></svg>',296 metadata, 'wavedrom', 'wavedrom@3.6.2',297 )298 assert.match(wavedrom, /data-dz-role="canvas"/)299 assert.match(wavedrom, /class="info"[^>]*data-dz-fill="ink-muted"/)300 assert.match(wavedrom, /class="s7"[^>]*data-dz-fill="surface-1"/)301})302303test('normalizes CSS-driven Mermaid and BPMN neutral structures', () => {304 const mermaid = canonicalizeSvg(305 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 40"><defs><marker id="diagramzip-sequence-arrowhead"><path class="arrowMarkerPath" d="M0 0L5 2L0 4Z"></path></marker></defs><path class="flowchart-link" d="M20 20L80 20"></path><g class="clusters"><g class="cluster"><rect style="" x="1" y="1" width="98" height="38"></rect><g class="cluster-label"><rect class="background" style=""></rect></g></g></g><g class="edgeLabels"><g class="edgeLabel"><g class="label"><g><rect class="background" style="" x="20" y="10" width="20" height="10"></rect></g></g></g></g><g class="node default"><rect class="basic label-container" x="2" y="2" width="36" height="24"></rect><foreignObject><div xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Idea</span></div></foreignObject></g></svg>',306 metadata, 'mermaid', 'mermaid@11.17.0',307 )308 assert.match(mermaid, /class="arrowMarkerPath"[^>]*data-dz-fill="line"[^>]*data-dz-stroke="line"/)309 assert.match(mermaid, /id="diagramzip-sequence-arrowhead"[^>]*>.*data-dz-fill="line"[^>]*data-dz-stroke="line"/)310 assert.match(mermaid, /class="flowchart-link"[^>]*data-dz-stroke="line"/)311 assert.match(mermaid, /class="basic label-container"[^>]*data-dz-fill="surface-1"[^>]*data-dz-stroke="line"/)312 assert.match(mermaid, /class="cluster"[^>]*>[\s\S]*?<rect style=""[^>]*data-dz-fill="surface-2"[^>]*data-dz-stroke="line-muted"/)313 assert.match(mermaid, /class="edgeLabel"[^>]*>[\s\S]*?class="background"[^>]*data-dz-fill="surface-1"/)314 assert.doesNotMatch(mermaid.match(/class="cluster-label"[\s\S]*?<rect[^>]*>/)?.[0] ?? '', /data-dz-fill/)315 const authoredCluster = canonicalizeSvg(316 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><g class="cluster"><rect fill="#ffffde" x="1" y="1" width="18" height="18"></rect></g></svg>',317 metadata, 'mermaid', 'mermaid@11.17.0',318 )319 assert.doesNotMatch(authoredCluster, /fill="#ffffde"[^>]*data-dz-fill/)320 assert.match(mermaid, /class="nodeLabel"[^>]*data-dz-fill="ink"/)321 const sequence = canonicalizeSvg(322 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 40"><rect class="actor actor-top" x="2" y="2" width="36" height="24" fill="#eaeaea"></rect><text class="actor actor-box"><tspan>Owner</tspan></text><line class="actor-line" x1="20" y1="26" x2="20" y2="40"></line><line class="messageLine0" x1="20" y1="30" x2="80" y2="30"></line><rect class="labelBox" x="60" y="2" width="36" height="24"></rect></svg>',323 metadata, 'mermaid', 'mermaid@11.17.0',324 )325 assert.match(sequence, /class="actor actor-top"[^>]*data-dz-fill="surface-1"[^>]*data-dz-stroke="line"/)326 assert.match(sequence, /class="actor-line"[^>]*data-dz-stroke="line-muted"/)327 assert.match(sequence, /class="messageLine0"[^>]*data-dz-stroke="line"/)328 assert.match(sequence, /class="labelBox"[^>]*data-dz-fill="surface-1"[^>]*data-dz-stroke="line"/)329330 const bpmn = canonicalizeSvg(331 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 40"><rect x="2" y="2" width="70" height="30" style="stroke:rgb(34,36,42);stroke-width:2px;fill:white"></rect><text class="djs-label" x="10" y="20" style="fill:rgb(34,36,42)"><tspan x="10" y="20">Task</tspan></text><rect class="djs-hit" x="0" y="0" width="100" height="40" style="fill:none;stroke-opacity:0;stroke:white"></rect></svg>',332 metadata, 'bpmn', 'bpmn-js@18.25.1',333 )334 assert.match(bpmn, /data-dz-fill="surface-1"/)335 assert.match(bpmn, /data-dz-stroke="line"/)336 assert.equal((bpmn.match(/data-dz-fill="ink"/g) ?? []).length, 2)337 assert.doesNotMatch(bpmn.match(/<rect class="djs-hit"[^>]*>/)?.[0] ?? '', /data-dz-/)338 assert.equal(normalizationFor('bpmn', 'diagramzip-bpmn-svg@1').profile, 'neutral-svg-semantic-2')339})340341test('creates addressable surfaces for closed Pikchr and line-built Svgbob boxes', () => {342 const pikchr = canonicalizeSvg(343 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 20"><path d="M1 1L19 1L19 19L1 19Z" style="fill:none;stroke:rgb(0,0,0)"></path><text x="5" y="12" fill="black">A</text></svg>',344 metadata, 'pikchr', 'pikchr@85e65b9686/edge-wasm-1',345 )346 assert.match(pikchr, /<path[^>]*data-dz-fill="surface-1"/)347348 const svgbob = canonicalizeSvg(349 '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="30" class="svgbob"><g><line x1="4" y1="4" x2="24" y2="4" class="solid"></line><line x1="4" y1="4" x2="4" y2="24" class="solid"></line><line x1="24" y1="4" x2="24" y2="24" class="solid"></line><line x1="4" y1="24" x2="24" y2="24" class="solid"></line></g></svg>',350 metadata, 'svgbob', 'svgbob@0.7.6/edge-wasm-1',351 )352 assert.match(svgbob, /data-dz-owned="normalizer" data-dz-role="object-surface" data-dz-fill="surface-1"/)353 assert.equal((svgbob.match(/data-dz-role="object-surface"/g) ?? []).length, 1)354 assert.equal(canonicalizeSvg(svgbob, metadata, 'svgbob', 'svgbob@0.7.6/edge-wasm-1'), svgbob)355356 const labeled = canonicalizeSvg(357 '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="30" class="svgbob"><text x="8" y="16">User</text><g><line x1="4" y1="4" x2="24" y2="4" class="solid"></line><line x1="4" y1="4" x2="4" y2="24" class="solid"></line><line x1="24" y1="4" x2="24" y2="24" class="solid"></line><line x1="4" y1="24" x2="24" y2="24" class="solid"></line></g></svg>',358 metadata, 'svgbob', 'svgbob@0.7.6/edge-wasm-1',359 )360 assert.ok(labeled.indexOf('data-dz-role="object-surface"') < labeled.indexOf('>User</text>'))361})362363test('keeps hollow semantic markers unpainted in themed appearances', () => {364 const plantuml = canonicalizeSvg(365 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 80"><g><circle fill="#E2E2F0" stroke="#181818" r="8" cx="50" cy="10"></circle><path fill="#00000000" stroke="#181818" d="M50,18 L50,45 M37,26 L63,26 M50,45 L37,60 M50,45 L63,60"></path></g></svg>',366 metadata, 'plantuml', 'plantuml@1.2026.6/edge-wasm-1',367 )368 const goat = canonicalizeSvg(369 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><circle class="hollow" cx="10" cy="10" r="6"></circle></svg>',370 metadata, 'goat', 'goat@0.5.1/edge-wasm-1',371 )372 for (const canonical of [plantuml, goat]) {373 assert.match(canonical, /data-dz-fill="none"/)374 assert.match(materializeSvg(canonical, 'dark-transparent'), /\[data-dz-fill="none"\]\{fill:none!important\}/)375 }376})377378test('preserves WaveDrom authored categorical colors', () => {379 const canonical = canonicalizeSvg(380 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 20"><style>.s8{fill:#ffe0b9}.s9{fill:#ffc0a0}</style><path class="s8" d="M0 0H20V20H0Z"></path><path class="s9" d="M20 0H40V20H20Z"></path></svg>',381 metadata, 'wavedrom', 'wavedrom@3.6.2',382 )383 assert.doesNotMatch(canonical.match(/<path class="s8"[^>]*>/)?.[0] ?? '', /data-dz-fill/)384 assert.doesNotMatch(canonical.match(/<path class="s9"[^>]*>/)?.[0] ?? '', /data-dz-fill/)385 const themed = materializeSvg(canonical, 'dark-transparent')386 assert.match(themed, /\.s8\{fill:#ffe0b9\}/)387 assert.match(themed, /\.s9\{fill:#ffc0a0\}/)388})389390test('resolves text against normalized light surfaces and C4 accents', () => {391 const blockdiag = canonicalizeSvg(392 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 40"><rect x="5" y="5" width="90" height="30" fill="rgb(219,234,254)" stroke="rgb(0,0,0)"></rect><text x="20" y="25" fill="rgb(0,0,0)">Agent</text></svg>',393 metadata, 'blockdiag', 'blockdiag@3.4.2/python-worker-1',394 )395 assert.match(blockdiag, /data-dz-fill="surface-2"/)396 assert.match(blockdiag, /<text[^>]*data-dz-fill="ink"/)397398 const c4 = canonicalizeSvg(399 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 50"><g><rect x="5" y="5" width="90" height="40" fill="#08427B" stroke="#181818"></rect><text x="20" y="30" fill="#000000">Person</text></g></svg>',400 metadata, 'c4plantuml', 'plantuml@1.2026.6/edge-wasm-1',401 )402 assert.match(c4, /data-dz-fill="accent-1"/)403 assert.match(c4, /<text[^>]*data-dz-fill="on-accent"/)404})405406test('transparent appearances survive adversarial light and dark substrates without a renderer canvas', () => {407 const fixtures = [408 canonicalizeSvg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect width="20" height="20" fill="white"></rect><path d="M2 2L18 18" stroke="black"></path></svg>', metadata, 'excalidraw', '@excalidraw/excalidraw@0.18.1'),409 canonicalizeSvg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><rect class="backdrop" width="20" height="20" fill="white"></rect><rect class="solid nofill" x="2" y="2" width="16" height="16"></rect><text x="5" y="12">A</text></svg>', metadata, 'ditaa', 'ditaa-ascii-via-svgbob@0.7.6/edge-wasm-1'),410 ]411 for (const canonical of fixtures) {412 for (const appearance of ['light-transparent', 'dark-transparent']) {413 const output = materializeSvg(canonical, appearance)414 assert.match(output, /\[data-dz-role="canvas"\]:not\(\[data-dz-owned="materializer"\]\)\{display:none!important\}/)415 assert.doesNotMatch(output, /<(?:rect|path|polygon)[^>]*data-dz-owned="materializer"[^>]*data-dz-role="canvas"/)416 }417 }418})