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>
| M | apps/ | 4 | ++-- |
| M | apps/ | 4 | ++-- |
| M | apps/ | 16 | ++++++++++++---- |
| M | docs/ | 2 | +- |
| M | renderers/ | 2 | +- |
| M | shared/ | 2 | +- |
| M | shared/ | 11 | ++++++++--- |
@@ -112,7 +112,7 @@ The [renderer catalog](https://diagram.zip/render/v1/catalog) reports every engi
112112 | Contract | Example identifier | Changes when |113113 | --- | --- | --- |114114 | Canonical schema | `1` | The canonical SVG structure changes incompatibly. |115-| Normalizer | `svg-normalizer-3` | Sanitization, annotation, or serialization behavior changes. |115+| Normalizer | `svg-normalizer-4` | Sanitization, annotation, or serialization behavior changes. |116116 | Profile | `neutral-svg-semantic-2` | Renderer recognition or role assignment changes. |117117 | Palette | `diagramzip-palette-2` | Materialized role colors change. |118118 | Materializer | `svg-materializer-4` | Appearance rules, padding, frame, or output assembly changes. |@@ -144,7 +144,7 @@ Normalized elements may also use `data-dz-fill`, `data-dz-stroke`, and `data-dz-
144144 ```xml145145 <svg146146 data-dz-schema="1"147- data-dz-normalizer="svg-normalizer-3"147+ data-dz-normalizer="svg-normalizer-4"148148 data-dz-engine="mermaid"149149 data-dz-profile="neutral-svg-semantic-2"150150 data-dz-palette="renderer"@@ -316,7 +316,7 @@ The [renderer catalog](https://diagram.zip/render/v1/catalog) reports every engi
316316 | Contract | Example identifier | Changes when |317317 | --- | --- | --- |318318 | Canonical schema | `1` | The canonical SVG structure changes incompatibly. |319-| Normalizer | `svg-normalizer-3` | Sanitization, annotation, or serialization behavior changes. |319+| Normalizer | `svg-normalizer-4` | Sanitization, annotation, or serialization behavior changes. |320320 | Profile | `neutral-svg-semantic-2` | Renderer recognition or role assignment changes. |321321 | Palette | `diagramzip-palette-2` | Materialized role colors change. |322322 | Materializer | `svg-materializer-4` | Appearance rules, padding, frame, or output assembly changes. |@@ -348,7 +348,7 @@ Normalized elements may also use `data-dz-fill`, `data-dz-stroke`, and `data-dz-
348348 ```xml349349 <svg350350 data-dz-schema="1"351- data-dz-normalizer="svg-normalizer-3"351+ data-dz-normalizer="svg-normalizer-4"352352 data-dz-engine="mermaid"353353 data-dz-profile="neutral-svg-semantic-2"354354 data-dz-palette="renderer"@@ -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>',@@ -134,7 +134,7 @@ Schema 1 uses the following root attributes:
134134 ```xml135135 <svg136136 data-dz-schema="1"137- data-dz-normalizer="svg-normalizer-3"137+ data-dz-normalizer="svg-normalizer-4"138138 data-dz-engine="example"139139 data-dz-profile="semantic-markup-2"140140 data-dz-palette="renderer"@@ -14,7 +14,7 @@ describe('engine catalog', () => {
1414 for (const engine of ENGINE_CATALOG) {1515 expect(engine.normalization).toMatchObject({1616 schema: '1',17- normalizer: 'svg-normalizer-3',17+ normalizer: 'svg-normalizer-4',1818 })1919 expect(engine.normalization.profile).not.toBe('safe-raw-1')2020 if (presentationOnly.has(engine.id)) {@@ -1,6 +1,6 @@
11 export const SVG_SCHEMA: '1'22 export const EDITABLE_SVG_SCHEMA: '1'3-export const NORMALIZER_BUILD: 'svg-normalizer-3'3+export const NORMALIZER_BUILD: 'svg-normalizer-4'44 export const MATERIALIZER_BUILD: 'svg-materializer-4'55 export const PALETTE_BUILD: 'diagramzip-palette-2'66 export const RAW_PROFILE: 'safe-raw-1'@@ -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 }