clone

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

fix: normalize Mermaid cluster surfaces

Dami committed 8b5cae6
Annotate neutral Mermaid cluster and edge-label backgrounds for themed SVGs, bump the normalizer identity, and update contract documentation.
commit
8b5cae6c1b3c3e8897211af0543edbabaae3d0b7
parent
d9483658a8694957c054906900cb4f19566a155e
author
Dami <dami.etoile@gmail.com>
7 files changed, +27 -14
Mapps/docs/docs/style/svg-normalization.md4++--
Mapps/docs/static/llms-full.txt4++--
Mapps/editor/test/svg-contract.test.js16++++++++++++----
Mdocs/svg-normalization.md2+-
Mrenderers/catalog/test/catalog.test.ts2+-
Mshared/svg/index.d.ts2+-
Mshared/svg/index.js11++++++++---
M apps/docs/docs/style/svg-normalization.md +2 -2
M apps/docs/static/llms-full.txt +2 -2
M apps/editor/test/svg-contract.test.js +12 -4
@@ -41,7 +41,7 @@ test('produces deterministic, idempotent raw canonical SVG', () => {
4141 4242 assert.equal(first, repeated)4343 assert.equal(first, normalizedAgain)44- assert.match(first, /data-dz-normalizer="svg-normalizer-3"/)44+ assert.match(first, /data-dz-normalizer="svg-normalizer-4"/)4545 assert.match(first, /data-dz-profile="safe-raw-1"/)4646 assert.match(first, /data-dz-palette="renderer"/)4747 assert.match(first, /data-dz-conformance="raw"/)
@@ -59,7 +59,7 @@ test('recomputes stale semantic annotations when upgrading the normalizer build'
5959 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>'6060 const upgraded = canonicalizeSvg(stale, metadata, 'dbml', 'dbml@1.0.31+graphviz@15.1.1')6161 62- assert.match(upgraded, /data-dz-normalizer="svg-normalizer-3"/)62+ assert.match(upgraded, /data-dz-normalizer="svg-normalizer-4"/)6363 assert.doesNotMatch(upgraded, /data-dz-owned=/)6464 assert.doesNotMatch(upgraded, /<ellipse[^>]*data-dz-/)6565 assert.match(upgraded, /data-renderer-id="kept" data-dz-fill="surface-1" data-dz-stroke="line"/)
@@ -83,7 +83,7 @@ test('exposes structured normalization failures to both callers', () => {
8383 )8484 })8585 86-const 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-3" 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>'86+const 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>'8787 8888 test('materializes explicit and automatic palettes without changing diagram geometry', () => {8989 const light = materializeSvg(semanticCanonical, 'light-transparent')
@@ -302,13 +302,21 @@ test('recognizes nested D2 and WaveDrom canvases while leaving masks renderer-de
302302 303303 test('normalizes CSS-driven Mermaid and BPMN neutral structures', () => {304304 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="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>',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>',306306 metadata, 'mermaid', 'mermaid@11.17.0',307307 )308308 assert.match(mermaid, /class="arrowMarkerPath"[^>]*data-dz-fill="line"[^>]*data-dz-stroke="line"/)309309 assert.match(mermaid, /id="diagramzip-sequence-arrowhead"[^>]*>.*data-dz-fill="line"[^>]*data-dz-stroke="line"/)310310 assert.match(mermaid, /class="flowchart-link"[^>]*data-dz-stroke="line"/)311311 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"/)313+ assert.match(mermaid, /class="edgeLabel"[^>]*>[\s\S]*?class="background"[^>]*data-dz-fill="surface-2"/)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/)312320 assert.match(mermaid, /class="nodeLabel"[^>]*data-dz-fill="ink"/)313321 const sequence = canonicalizeSvg(314322 '<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>',
M docs/svg-normalization.md +1 -1
M renderers/catalog/test/catalog.test.ts +1 -1
M shared/svg/index.d.ts +1 -1
M shared/svg/index.js +8 -3
@@ -11,7 +11,7 @@ const SAFE_DATA_FONT = /^data:(?:font\/(?:woff2?|opentype|truetype)|application\
1111 1212 export const SVG_SCHEMA = '1'1313 export const EDITABLE_SVG_SCHEMA = '1'14-export const NORMALIZER_BUILD = 'svg-normalizer-3'14+export const NORMALIZER_BUILD = 'svg-normalizer-4'1515 export const MATERIALIZER_BUILD = 'svg-materializer-4'1616 export const PALETTE_BUILD = 'diagramzip-palette-2'1717 export const RAW_PROFILE = 'safe-raw-1'
@@ -639,12 +639,13 @@ function applyWavedromProfile(root) {
639639 }640640 641641 function applyNeutralRendererDetails(root, engine) {642- const visit = (node, marker = '') => {642+ const visit = (node, marker = '', context = '') => {643643 if (node.type === 'text') return644644 const name = localName(node)645645 const classes = classNames(node)646646 const fill = ownPaint(node, 'fill')647647 const nextMarker = engine === 'mermaid' && name === 'marker' ? node.attributes.get('id') ?? '' : marker648+ const nextContext = classes.has('cluster') ? 'cluster' : classes.has('edgeLabel') ? 'edge-label' : classes.has('cluster-label') ? '' : context648649 if (engine === 'mermaid') {649650 if (classes.has('label-container')) {650651 node.attributes.set('data-dz-fill', 'surface-1')
@@ -668,6 +669,10 @@ function applyNeutralRendererDetails(root, engine) {
668669 node.attributes.set('data-dz-fill', 'line')669670 node.attributes.set('data-dz-stroke', 'line')670671 }672+ if (name === 'rect' && (nextContext === 'cluster' || (nextContext === 'edge-label' && classes.has('background')))673+ && neutralPaint(node, 'fill', new Set(['', 'none', 'white', '#fff', '#ffffff']))) {674+ node.attributes.set('data-dz-fill', 'surface-2')675+ }671676 }672677 if (engine === 'blockdiag' && SHAPE_ELEMENTS.has(name) && (fill === '#dbeafe' || fill === 'rgb(219,234,254)')) {673678 node.attributes.set('data-dz-fill', 'surface-2')
@@ -676,7 +681,7 @@ function applyNeutralRendererDetails(root, engine) {
676681 node.attributes.set('data-dz-fill', 'line')677682 node.attributes.set('data-dz-stroke', 'line')678683 }679- for (const child of node.children) visit(child, engine === 'mermaid' ? nextMarker : marker)684+ for (const child of node.children) visit(child, engine === 'mermaid' ? nextMarker : marker, engine === 'mermaid' ? nextContext : context)680685 }681686 visit(root)682687 }