- commit
- 5909228a5a8a54b9038a9853915d92647b0c0bbd
- parent
- 0fe5a564f260e752003bf40a4c7f64571e40bf68
- author
- Dami <dami.etoile@gmail.com>
@@ -73,8 +73,8 @@ function normalizeDiagram(value: unknown): Diagram {
7373 if (!isRecord(value.presentation)) {7474 throw new RequestError(400, 'invalid_request', 'diagram.presentation must be an object.')7575 }76- assertOnlyKeys(value.presentation, ['background', 'padding', 'frame'], 'diagram.presentation')77- const { background, padding, frame } = value.presentation76+ assertOnlyKeys(value.presentation, ['background', 'padding', 'frame', 'appearance'], 'diagram.presentation')77+ const { background, padding, frame, appearance = 'raw' } = value.presentation7878 if (typeof background !== 'string' || !backgroundPattern.test(background)) {7979 throw new RequestError(400, 'invalid_request', 'diagram.presentation.background is invalid.')8080 }@@ -84,12 +84,15 @@ function normalizeDiagram(value: unknown): Diagram {
8484 if (typeof frame !== 'boolean') {8585 throw new RequestError(400, 'invalid_request', 'diagram.presentation.frame is invalid.')8686 }87+ if (typeof appearance !== 'string' || !['raw', 'auto-transparent', 'light-transparent', 'dark-transparent', 'auto-framed', 'light-framed', 'dark-framed'].includes(appearance)) {88+ throw new RequestError(400, 'invalid_request', 'diagram.presentation.appearance is invalid.')89+ }8790 8891 return {8992 type: value.type,9093 source: value.source,9194 options,92- presentation: { background, padding: padding as number, frame },95+ presentation: { background, padding: padding as number, frame, appearance: appearance as Diagram['presentation']['appearance'] },9396 }9497 }9598 @@ -21,7 +21,7 @@ function payload(source = 'a -> b') {
2121 type: 'd2',2222 source,2323 options: {},24- presentation: { background: '', padding: 0, frame: false },24+ presentation: { background: '', padding: 0, frame: false, appearance: 'raw' },2525 },2626 metadata: { title: 'A to B', description: 'A small diagram.' },2727 }@@ -1,4 +1,5 @@
11 /*2+ Cache-Control: public, max-age=300, no-transform23 Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'34 X-Content-Type-Options: nosniff45 Referrer-Policy: strict-origin-when-cross-origin@@ -1,5 +1,6 @@
11 /*22 Access-Control-Allow-Origin: *3+ Cache-Control: public, max-age=300, no-transform34 Content-Security-Policy: default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; frame-src 'none'; base-uri 'none'; form-action 'none'45 X-Content-Type-Options: nosniff56 Referrer-Policy: no-referrer@@ -1,17 +1,31 @@
11 ---22 id: presentation33 title: General presentation settings4-description: Set the canvas background, padding, and frame for a diagram.zip diagram.4+description: Choose a shared appearance or keep the renderer canvas, padding, and frame.55 sidebar_position: 166 ---77 88 # General presentation settings99 10-These settings apply to every rendered diagram. Set them after you create valid source.10+Open **Details**, then use **Appearance** to choose how diagram.zip presents the SVG.11+12+## Appearance13+14+**Renderer default** keeps the safe renderer output. It also enables the canvas, padding, and frame controls.15+16+The shared appearances apply one Diagram.zip palette to supported renderer output:17+18+- **Match device** includes light and dark palettes in one SVG. The SVG selects a palette with `prefers-color-scheme`.19+- **Light** always uses the light palette.20+- **Dark** always uses the dark palette.21+- **Transparent** does not add an outer canvas.22+- **Framed** adds a matching canvas, standard padding, and a border.23+24+The editor disables appearances that the current renderer profile cannot support. Diagrams.net, Excalidraw, and TikZ currently support framed appearances only. Their authored paint remains unchanged inside the shared frame.1125 1226 ## Background1327 14-Set the canvas background to a color that supports your diagram. Use a light background for most documents. Check the contrast of labels and lines after you change it.28+Choose **Renderer default** before you set the background. Use a light background for most documents. Check the contrast of labels and lines after you change it.1529 1630 ## Padding1731 @@ -31,10 +45,9 @@ For example, [Mermaid styling](/style/types/mermaid/) uses Mermaid configuration
3145 3246 ## A safe order3347 34-1. Set the canvas background.35-2. Set padding.36-3. Choose the frame state.37-4. Apply type-specific styles.38-5. Check the preview at its target size.48+1. Choose a shared appearance when the renderer supports it.49+2. Use **Renderer default** when you need custom canvas controls.50+3. Apply type-specific styles.51+4. Check the preview at its target size.3952 4053 A **Renderer options** section appears only when the renderer adds options for that type.@@ -10,7 +10,7 @@ sidebar_label: ActDiag
1010 1111 ActDiag supports lane, action, edge, and diagram attributes.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: BlockDiag
1010 1111 BlockDiag supports diagram attributes and per-node or per-edge attributes.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: BPMN
1010 1111 BPMN appearance usually comes from BPMN diagram interchange data and renderer defaults.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Bytefield
1010 1111 Bytefield uses drawing-form properties for spans, labels, and layout.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: C4 PlantUML
1010 1111 C4 PlantUML supports layout macros, tags, legends, and PlantUML style controls.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: D2
1010 1111 D2 supports themes, shape properties, style blocks, classes, and sketch mode.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: DBML
1010 1111 DBML describes schema structure. It does not define visual presentation.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Diagrams.net
1010 1111 Diagrams.net stores geometry and style values on cells in the XML source.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Ditaa
1010 1111 Ditaa supports shape tags, color tags, dashed lines, and renderer options.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: ERD
1010 1111 ERD focuses on the data model. The renderer controls most visual values.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Excalidraw
1010 1111 Excalidraw stores the style of each element in the JSON source.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: GoAT
1010 1111 GoAT takes most geometry from the text. Renderer options control SVG colors and character handling.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Graphviz
1010 1111 DOT supports graph, node, and edge attributes.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Mermaid
1010 1111 Mermaid supports themes, frontmatter configuration, class definitions, and diagram-specific configuration.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Nomnoml
1010 1111 Nomnoml uses hash directives and custom classifier styles.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: NwDiag
1010 1111 NwDiag supports network and host attributes.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: PacketDiag
1010 1111 PacketDiag supports global layout values and field attributes.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Pikchr
1010 1111 Pikchr supports dimensions, colors, fills, line widths, and placement controls in the source.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: PlantUML
1010 1111 PlantUML supports themes, skin parameters, colors, sprites, and element-specific style rules.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: RackDiag
1010 1111 RackDiag supports rack and device attributes.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: SeqDiag
1010 1111 SeqDiag supports participant, message, and diagram attributes.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Structurizr
1010 1111 Structurizr supports workspace styles, themes, tags, and automatic layout.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Svgbob
1010 1111 Svgbob takes geometry from the text and supports renderer-level SVG settings.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Symbolator
1010 1111 Symbolator gets structure from hardware source. Renderer options control the title, scale, and output details.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: TikZ
1010 1111 TikZ supports colors, line forms, fills, node styles, scopes, and reusable style definitions.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: UMLet
1010 1111 UMLet stores position and style commands in the UXF XML source.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Vega
1010 1111 Vega controls presentation through scales, mark encodings, axes, legends, and configuration.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: Vega-Lite
1010 1111 Vega-Lite controls presentation through marks, encodings, scales, axes, legends, and configuration.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: WaveDrom
1010 1111 WaveDrom supports source configuration, signal attributes, and a limited set of bundled skins.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -10,7 +10,7 @@ sidebar_label: WireViz
1010 1111 WireViz uses YAML properties for connector, cable, wire, and output styles.1212 13-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.13+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.1414 1515 ## Source controls1616 @@ -1,6 +1,7 @@
11 import { readFile, readdir } from 'node:fs/promises'22 import { dirname, join } from 'node:path'33 import { fileURLToPath } from 'node:url'4+import { canonicalizeSvg, materializeSvg, supportedAppearances } from '../../diagramzip-svg/index.js'45 import { CLIENT_RENDERERS, httpRendererUrlFor } from '../src/components/DiagramExample/rendererRouting.mjs'56 67 const site = join(dirname(fileURLToPath(import.meta.url)), '..')@@ -43,6 +44,20 @@ async function check(name) {
4344 if (!response.ok || !body.includes('<svg')) {4445 throw new Error(`${example.engine}: HTTP ${response.status} ${body.replace(/\s+/g, ' ').slice(0, 240)}`)4546 }47+ const version = [48+ response.headers.get('X-Diagram-Engine-Version'),49+ response.headers.get('X-Renderer-Build'),50+ ].filter(Boolean).join(' ')51+ const canonical = canonicalizeSvg(body, {}, example.engine, version)52+ const conformance = canonical.match(/data-dz-conformance="([^"]+)"/)?.[1]53+ if (conformance !== 'presentation-only' && !/data-dz-(?:fill|stroke)=/.test(canonical)) {54+ throw new Error(`${example.engine}: normalization profile produced no semantic paint roles`)55+ }56+ const appearances = supportedAppearances(canonical)57+ const appearance = appearances.includes('auto-transparent')58+ ? 'auto-transparent'59+ : appearances.includes('auto-framed') ? 'auto-framed' : 'raw'60+ materializeSvg(canonical, appearance)4661 return example.engine4762 }4863 @@ -109,7 +109,7 @@ function stylePage(type) {
109109 ? `\n## Renderer options\n\n${options(type)}\n`110110 : ''111111 return frontmatter(type, 'style', `Style a ${type.label} diagram in diagram.zip.`) +112-`# Style ${type.label}\n\n${type.styleSummary}\n\nStart with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.\n\n## Source controls\n\n${lines(type.sourceStyle).map((item) => `- ${item}`).join('\n')}\n${rendererOptions}` +112+`# Style ${type.label}\n\n${type.styleSummary}\n\nStart with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.\n\n## Source controls\n\n${lines(type.sourceStyle).map((item) => `- ${item}`).join('\n')}\n${rendererOptions}` +113113 (limitations.length ? `## Limitations\n\n${limitations.map((item) => `- ${item}`).join('\n')}\n\n` : '') +114114 `## Related pages\n\n- [Create ${article} ${type.label} diagram](${syntaxPath(type.id)})\n- [General presentation settings](/style/presentation)\n- [Open ${type.label} in the editor](${editorLink(type.id)})\n`115115 }@@ -1,5 +1,5 @@
11 import React, {useCallback, useEffect, useRef, useState} from 'react';2-import {canonicalizeSvg, materializeSvg} from '../../../../diagramzip-svg/index.js';2+import {canonicalizeSvg, materializeSvg, supportedAppearances} from '../../../../diagramzip-svg/index.js';33 import {fitView, zoomView} from './viewMath.mjs';44 import {clientFrameUrlFor, httpRendererUrlFor} from './rendererRouting.mjs';55 import styles from './styles.module.css';@@ -54,7 +54,10 @@ class RendererFrame {
5454 if (!pending) return;5555 this.pending.delete(event.data.requestId);5656 pending.finish();57- if (event.data.ok && typeof event.data.svg === 'string') pending.resolve(event.data.svg);57+ if (event.data.ok && typeof event.data.svg === 'string') pending.resolve({58+ svg: event.data.svg,59+ version: typeof event.data.version === 'string' ? event.data.version : '',60+ });5861 else pending.reject(new Error(event.data.error || 'Client rendering failed.'));5962 }6063 @@ -171,8 +174,8 @@ export default function DiagramExample({engine, label, sourceUrl}) {
171174 const clientRenderer = clientRendererFor(engine);172175 let source;173176 if (clientRenderer) {174- source = await clientRenderer.render(engine, example.source, abortController.signal);175- source = canonicalizeSvg(source, {title: '', description: ''}, engine);177+ const clientResult = await clientRenderer.render(engine, example.source, abortController.signal);178+ source = canonicalizeSvg(clientResult.svg, {title: '', description: ''}, engine, clientResult.version);176179 } else {177180 const renderResponse = await fetch(httpRendererUrlFor(engine), {178181 method: 'POST',@@ -189,12 +192,11 @@ export default function DiagramExample({engine, label, sourceUrl}) {
189192 if (!renderResponse.ok) throw new Error(await responseError(renderResponse));190193 source = await renderResponse.text();191194 }192- try {193- source = materializeSvg(source, 'auto-transparent');194- } catch (error) {195- if (error?.code !== 'unsupported_appearance') throw error;196- source = materializeSvg(source, 'raw');197- }195+ const appearances = supportedAppearances(source);196+ const appearance = appearances.includes('auto-transparent')197+ ? 'auto-transparent'198+ : appearances.includes('auto-framed') ? 'auto-framed' : 'raw';199+ source = materializeSvg(source, appearance);198200 source = normalizeSvg(source);199201 const objectUrl = URL.createObjectURL(new Blob([source], {type: 'image/svg+xml'}));200202 if (objectUrlRef.current) URL.revokeObjectURL(objectUrlRef.current);@@ -60,7 +60,7 @@ sidebar_label: PlantUML
6060 6161 PlantUML supports themes, skin parameters, colors, sprites, and element-specific style rules.6262 63-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.63+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.6464 6565 ## Source controls6666 @@ -145,7 +145,7 @@ sidebar_label: Mermaid
145145 146146 Mermaid supports themes, frontmatter configuration, class definitions, and diagram-specific configuration.147147 148-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.148+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.149149 150150 ## Source controls151151 @@ -232,7 +232,7 @@ sidebar_label: Graphviz
232232 233233 DOT supports graph, node, and edge attributes.234234 235-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.235+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.236236 237237 ## Source controls238238 @@ -318,7 +318,7 @@ sidebar_label: D2
318318 319319 D2 supports themes, shape properties, style blocks, classes, and sketch mode.320320 321-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.321+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.322322 323323 ## Source controls324324 @@ -402,7 +402,7 @@ sidebar_label: C4 PlantUML
402402 403403 C4 PlantUML supports layout macros, tags, legends, and PlantUML style controls.404404 405-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.405+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.406406 407407 ## Source controls408408 @@ -483,7 +483,7 @@ sidebar_label: BlockDiag
483483 484484 BlockDiag supports diagram attributes and per-node or per-edge attributes.485485 486-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.486+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.487487 488488 ## Source controls489489 @@ -565,7 +565,7 @@ sidebar_label: SeqDiag
565565 566566 SeqDiag supports participant, message, and diagram attributes.567567 568-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.568+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.569569 570570 ## Source controls571571 @@ -652,7 +652,7 @@ sidebar_label: ActDiag
652652 653653 ActDiag supports lane, action, edge, and diagram attributes.654654 655-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.655+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.656656 657657 ## Source controls658658 @@ -736,7 +736,7 @@ sidebar_label: NwDiag
736736 737737 NwDiag supports network and host attributes.738738 739-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.739+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.740740 741741 ## Source controls742742 @@ -820,7 +820,7 @@ sidebar_label: PacketDiag
820820 821821 PacketDiag supports global layout values and field attributes.822822 823-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.823+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.824824 825825 ## Source controls826826 @@ -903,7 +903,7 @@ sidebar_label: RackDiag
903903 904904 RackDiag supports rack and device attributes.905905 906-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.906+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.907907 908908 ## Source controls909909 @@ -989,7 +989,7 @@ sidebar_label: BPMN
989989 990990 BPMN appearance usually comes from BPMN diagram interchange data and renderer defaults.991991 992-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.992+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.993993 994994 ## Source controls995995 @@ -1067,7 +1067,7 @@ sidebar_label: Bytefield
10671067 10681068 Bytefield uses drawing-form properties for spans, labels, and layout.10691069 1070-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1070+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.10711071 10721072 ## Source controls10731073 @@ -1151,7 +1151,7 @@ sidebar_label: DBML
11511151 11521152 DBML describes schema structure. It does not define visual presentation.11531153 1154-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1154+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.11551155 11561156 ## Source controls11571157 @@ -1232,7 +1232,7 @@ sidebar_label: Diagrams.net
12321232 12331233 Diagrams.net stores geometry and style values on cells in the XML source.12341234 1235-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1235+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.12361236 12371237 ## Source controls12381238 @@ -1304,7 +1304,7 @@ sidebar_label: Ditaa
13041304 13051305 Ditaa supports shape tags, color tags, dashed lines, and renderer options.13061306 1307-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1307+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.13081308 13091309 ## Source controls13101310 @@ -1396,7 +1396,7 @@ sidebar_label: ERD
13961396 13971397 ERD focuses on the data model. The renderer controls most visual values.13981398 1399-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1399+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.14001400 14011401 ## Source controls14021402 @@ -1475,7 +1475,7 @@ sidebar_label: Excalidraw
14751475 14761476 Excalidraw stores the style of each element in the JSON source.14771477 1478-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1478+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.14791479 14801480 ## Source controls14811481 @@ -1568,7 +1568,7 @@ sidebar_label: GoAT
15681568 15691569 GoAT takes most geometry from the text. Renderer options control SVG colors and character handling.15701570 1571-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1571+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.15721572 15731573 ## Source controls15741574 @@ -1653,7 +1653,7 @@ sidebar_label: Nomnoml
16531653 16541654 Nomnoml uses hash directives and custom classifier styles.16551655 1656-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1656+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.16571657 16581658 ## Source controls16591659 @@ -1729,7 +1729,7 @@ sidebar_label: Pikchr
17291729 17301730 Pikchr supports dimensions, colors, fills, line widths, and placement controls in the source.17311731 1732-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1732+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.17331733 17341734 ## Source controls17351735 @@ -1808,7 +1808,7 @@ sidebar_label: Structurizr
18081808 18091809 Structurizr supports workspace styles, themes, tags, and automatic layout.18101810 1811-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1811+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.18121812 18131813 ## Source controls18141814 @@ -1889,7 +1889,7 @@ sidebar_label: Svgbob
18891889 18901890 Svgbob takes geometry from the text and supports renderer-level SVG settings.18911891 1892-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1892+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.18931893 18941894 ## Source controls18951895 @@ -1976,7 +1976,7 @@ sidebar_label: Symbolator
19761976 19771977 Symbolator gets structure from hardware source. Renderer options control the title, scale, and output details.19781978 1979-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.1979+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.19801980 19811981 ## Source controls19821982 @@ -2062,7 +2062,7 @@ sidebar_label: TikZ
20622062 20632063 TikZ supports colors, line forms, fills, node styles, scopes, and reusable style definitions.20642064 2065-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.2065+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.20662066 20672067 ## Source controls20682068 @@ -2141,7 +2141,7 @@ sidebar_label: UMLet
21412141 21422142 UMLet stores position and style commands in the UXF XML source.21432143 2144-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.2144+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.21452145 21462146 ## Source controls21472147 @@ -2248,7 +2248,7 @@ sidebar_label: Vega
22482248 22492249 Vega controls presentation through scales, mark encodings, axes, legends, and configuration.22502250 2251-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.2251+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.22522252 22532253 ## Source controls22542254 @@ -2337,7 +2337,7 @@ sidebar_label: Vega-Lite
23372337 23382338 Vega-Lite controls presentation through marks, encodings, scales, axes, legends, and configuration.23392339 2340-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.2340+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.23412341 23422342 ## Source controls23432343 @@ -2421,7 +2421,7 @@ sidebar_label: WaveDrom
24212421 24222422 WaveDrom supports source configuration, signal attributes, and a limited set of bundled skins.24232423 2424-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.2424+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.24252425 24262426 ## Source controls24272427 @@ -2505,7 +2505,7 @@ sidebar_label: WireViz
25052505 25062506 WireViz uses YAML properties for connector, cable, wire, and output styles.25072507 2508-Start with the [general presentation settings](/style/presentation). They control the canvas background, padding, and frame for every diagram.2508+Start with the [general presentation settings](/style/presentation). Choose a shared appearance, or use the renderer defaults with custom canvas controls.25092509 25102510 ## Source controls25112511 @@ -9,15 +9,24 @@ describe('engine catalog', () => {
99 expect(ENGINE_CATALOG.map(engine => engine.id)).toEqual(ENGINE_IDS)1010 })1111 12- it('reports raw-only normalization until an engine profile earns themed support', () => {13- for (const engine of ENGINE_CATALOG.filter(entry => entry.id !== 'graphviz')) {12+ it('reports an honest capability for every pinned renderer contract', () => {13+ const presentationOnly = new Set(['diagramsnet', 'excalidraw', 'tikz'])14+ for (const engine of ENGINE_CATALOG) {1415 expect(engine.normalization).toMatchObject({1516 schema: '1',1617 normalizer: 'svg-normalizer-1',17- profile: 'safe-raw-1',18- conformance: 'raw',19- appearances: ['raw'],2018 })19+ expect(engine.normalization.profile).not.toBe('safe-raw-1')20+ if (presentationOnly.has(engine.id)) {21+ expect(engine.normalization).toMatchObject({22+ profile: 'authored-svg-presentation-1',23+ conformance: 'presentation-only',24+ appearances: ['raw', 'auto-framed', 'light-framed', 'dark-framed'],25+ })26+ } else {27+ expect(['semantic', 'adaptive']).toContain(engine.normalization.conformance)28+ expect(engine.normalization.appearances).toEqual(expect.arrayContaining(['raw', 'auto-transparent', 'dark-framed']))29+ }2130 }2231 expect(ENGINE_CATALOG.find(entry => entry.id === 'graphviz')?.normalization).toMatchObject({2332 profile: 'graphviz-15-semantic-1',@@ -30,20 +30,127 @@ export const RAW_NORMALIZATION = Object.freeze({
3030 conformance: 'raw',3131 appearances: Object.freeze(['raw']),3232 })33-const THEMED_APPEARANCES = Object.freeze(APPEARANCES.filter(appearance => appearance !== 'raw'))34-const GRAPHVIZ_NORMALIZATION = Object.freeze({35- schema: SVG_SCHEMA,36- normalizer: NORMALIZER_BUILD,37- profile: 'graphviz-15-semantic-1',38- palette: PALETTE_BUILD,39- conformance: 'semantic',40- appearances: APPEARANCES,41- limitations: Object.freeze(['Authored non-neutral colors are preserved and may not adapt between palettes.']),33+const FRAMED_APPEARANCES = Object.freeze(['raw', 'auto-framed', 'light-framed', 'dark-framed'])34+35+function capability(profile, conformance, limitations, appearances = APPEARANCES) {36+ return Object.freeze({37+ schema: SVG_SCHEMA,38+ normalizer: NORMALIZER_BUILD,39+ profile,40+ palette: PALETTE_BUILD,41+ conformance,42+ appearances,43+ limitations: Object.freeze(limitations),44+ })45+}46+47+const GRAPHVIZ_NORMALIZATION = capability(48+ 'graphviz-15-semantic-1',49+ 'semantic',50+ ['Authored non-neutral colors are preserved and may not adapt between palettes.'],51+)52+const D2_NORMALIZATION = capability(53+ 'd2-0.7-semantic-1',54+ 'semantic',55+ ['Authored D2 colors are preserved; the pinned neutral and primary theme roles adapt.'],56+)57+const PLANTUML_NORMALIZATION = capability(58+ 'plantuml-2026-semantic-1',59+ 'semantic',60+ ['Pinned neutral PlantUML paint adapts; explicit themes and authored colors remain renderer-defined.'],61+)62+const SVGBOB_NORMALIZATION = capability(63+ 'svgbob-0.7-semantic-1',64+ 'semantic',65+ ['Line art, labels, and ordinary surfaces adapt; source-specific non-neutral paint is preserved.'],66+)67+const NEUTRAL_SVG_NORMALIZATION = capability(68+ 'neutral-svg-semantic-1',69+ 'adaptive',70+ ['Neutral paint adapts; authored non-neutral fills, shadows, and data colors are preserved.'],71+)72+const DETAILED_SVG_NORMALIZATION = capability(73+ 'structured-svg-semantic-1',74+ 'adaptive',75+ ['Stable structural roles adapt; authored categorical and semantic colors are preserved.'],76+)77+const PRESENTATION_NORMALIZATION = capability(78+ 'authored-svg-presentation-1',79+ 'presentation-only',80+ ['The outer canvas and frame adapt. Paint inside the authored SVG remains unchanged.'],81+ FRAMED_APPEARANCES,82+)83+84+const PROFILE_BY_ENGINE = Object.freeze({85+ graphviz: GRAPHVIZ_NORMALIZATION,86+ dbml: GRAPHVIZ_NORMALIZATION,87+ erd: GRAPHVIZ_NORMALIZATION,88+ wireviz: GRAPHVIZ_NORMALIZATION,89+ d2: D2_NORMALIZATION,90+ plantuml: PLANTUML_NORMALIZATION,91+ c4plantuml: PLANTUML_NORMALIZATION,92+ structurizr: PLANTUML_NORMALIZATION,93+ svgbob: SVGBOB_NORMALIZATION,94+ ditaa: SVGBOB_NORMALIZATION,95+ blockdiag: NEUTRAL_SVG_NORMALIZATION,96+ seqdiag: NEUTRAL_SVG_NORMALIZATION,97+ actdiag: NEUTRAL_SVG_NORMALIZATION,98+ nwdiag: NEUTRAL_SVG_NORMALIZATION,99+ packetdiag: NEUTRAL_SVG_NORMALIZATION,100+ rackdiag: NEUTRAL_SVG_NORMALIZATION,101+ bytefield: NEUTRAL_SVG_NORMALIZATION,102+ mermaid: NEUTRAL_SVG_NORMALIZATION,103+ bpmn: NEUTRAL_SVG_NORMALIZATION,104+ nomnoml: NEUTRAL_SVG_NORMALIZATION,105+ pikchr: NEUTRAL_SVG_NORMALIZATION,106+ symbolator: NEUTRAL_SVG_NORMALIZATION,107+ umlet: NEUTRAL_SVG_NORMALIZATION,108+ goat: DETAILED_SVG_NORMALIZATION,109+ vega: DETAILED_SVG_NORMALIZATION,110+ vegalite: DETAILED_SVG_NORMALIZATION,111+ wavedrom: DETAILED_SVG_NORMALIZATION,112+ diagramsnet: PRESENTATION_NORMALIZATION,113+ excalidraw: PRESENTATION_NORMALIZATION,114+ tikz: PRESENTATION_NORMALIZATION,115+})116+117+const VERSION_PATTERNS = Object.freeze({118+ graphviz: /graphviz@15\.1\.1/,119+ dbml: /(?:dbml@1\.0\.31|graphviz@15\.1\.1)/,120+ erd: /(?:erd@0\.2\.1\.0|graphviz@15\.1\.1)/,121+ wireviz: /(?:wireviz@)?0\.3\.2/,122+ d2: /d2@0\.7\.1/,123+ plantuml: /plantuml@1\.2026\.6/,124+ c4plantuml: /(?:c4plantuml@2\.7\.0|plantuml@1\.2026\.6)/,125+ structurizr: /structurizr@6\.2\.2/,126+ svgbob: /svgbob@0\.7\.6/,127+ ditaa: /(?:ditaa-ascii|svgbob@0\.7\.6)/,128+ blockdiag: /(?:blockdiag@)?3\.4\.2/,129+ seqdiag: /(?:seqdiag@)?3\.0\.0/,130+ actdiag: /(?:actdiag@)?3\.0\.0/,131+ nwdiag: /(?:nwdiag@)?3\.0\.0/,132+ packetdiag: /(?:packetdiag@)?3\.0\.0/,133+ rackdiag: /(?:rackdiag@)?3\.0\.0/,134+ bytefield: /bytefield-svg@1\.11\.0/,135+ mermaid: /mermaid@11\.17\.0/,136+ bpmn: /bpmn-js@18\.25\.1/,137+ nomnoml: /nomnoml@1\.7\.0/,138+ pikchr: /pikchr@85e65b9686/,139+ symbolator: /(?:symbolator@)?1\.2\.2/,140+ umlet: /diagramzip-umlet-svg@1/,141+ goat: /goat@0\.5\.1/,142+ vega: /vega@6\.3\.1/,143+ vegalite: /vega-lite@6\.4\.3/,144+ wavedrom: /wavedrom@3\.6\.2/,145+ diagramsnet: /diagrams\.net@29\.6\.1/,146+ excalidraw: /@excalidraw\/excalidraw@0\.18\.1/,147+ tikz: /@planktimerr\/tikzjax@1\.0\.63/,42148 })43149 44150 export function normalizationFor(engine, rendererVersion = '') {45- if (engine === 'graphviz' && rendererVersion.startsWith('graphviz@15.1.1')) return GRAPHVIZ_NORMALIZATION46- return RAW_NORMALIZATION151+ const profile = PROFILE_BY_ENGINE[engine]152+ const pattern = VERSION_PATTERNS[engine]153+ return profile && pattern?.test(rendererVersion) ? profile : RAW_NORMALIZATION47154 }48155 49156 export class SvgNormalizationError extends Error {@@ -84,7 +191,7 @@ function safeAttribute(name, value) {
84191 }85192 86193 function escapeText(value) {87- return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')194+ return String(value ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')88195 }89196 90197 function escapeAttribute(value) {@@ -93,7 +200,10 @@ function escapeAttribute(value) {
93200 94201 function serialize(node) {95202 if (node.type === 'text') return escapeText(node.value)96- const attributes = [...node.attributes].map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join('')203+ let attributes = ''204+ node.attributes.forEach((value, name) => {205+ attributes += ` ${name}="${escapeAttribute(value)}"`206+ })97207 return `<${node.name}${attributes}>${node.children.map(serialize).join('')}</${node.name}>`98208 }99209 @@ -101,6 +211,12 @@ function localName(element) {
101211 return element.name.toLowerCase().split(':').at(-1) ?? element.name.toLowerCase()102212 }103213 214+function qualifiedName(node) {215+ if (typeof node.name === 'string' && node.name !== '') return node.name216+ if (typeof node.prefix === 'string' && node.prefix !== '') return `${node.prefix}:${node.local}`217+ return node.local218+}219+104220 function numericDimension(value) {105221 if (value === undefined || !/^-?\d+(?:\.\d+)?(?:px)?$/.test(value.trim())) return null106222 const parsed = Number.parseFloat(value)@@ -205,8 +321,156 @@ function applyGraphvizProfile(root) {
205321 visit(root)206322 }207323 208-function applyProfile(root, capability) {324+const BLACK_PAINT = new Set(['black', '#000', '#000000', '#111', '#111111', '#111827', '#181818', '#33322e', 'rgb(0,0,0)'])325+const WHITE_PAINT = new Set(['white', '#fff', '#ffffff', '#e2e2f0', '#e8e8e8', '#eee8d5', '#f1f1f1', '#f7f8fe', 'rgb(255,255,255)'])326+const MUTED_PAINT = new Set(['gray', 'grey', '#666', '#666666', '#7f7f7f', '#888', '#888888', '#aaa', '#aaaaaa'])327+const SHAPE_ELEMENTS = new Set(['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect'])328+const TEXT_ELEMENTS = new Set(['text', 'tspan', 'div', 'span', 'p', 'b', 'strong', 'i', 'em', 'small', 'sub', 'sup', 'code', 'li'])329+330+function normalizedPaint(value) {331+ return (value ?? '').replaceAll(' ', '').toLowerCase()332+}333+334+function ownPaint(element, property) {335+ return normalizedPaint(element.attributes.get(property) ?? styleValue(element, property) ?? '')336+}337+338+function classNames(element) {339+ return new Set((element.attributes.get('class') ?? '').split(/\s+/).filter(Boolean))340+}341+342+function filteredShadow(element) {343+ const style = element.attributes.get('style') ?? ''344+ return /(?:^|;)\s*filter\s*:/i.test(style) && /(?:^|;)\s*opacity\s*:/i.test(style)345+}346+347+function applyNeutralSvgProfile(root, engine, extraSurfacePaint = []) {348+ const bounds = presentationBounds(root)349+ const surfaces = new Set([...WHITE_PAINT, ...extraSurfacePaint.map(normalizedPaint)])350+ const visit = (node, inheritedFill = '', inheritedStroke = '') => {351+ if (node.type === 'text') return352+ const name = localName(node)353+ const fill = ownPaint(node, 'fill') || inheritedFill354+ const stroke = ownPaint(node, 'stroke') || inheritedStroke355+ const backdrop = rendererBackdrop(node, bounds, engine)356+ if (backdrop) node.attributes.set('data-dz-role', 'canvas')357+ if (TEXT_ELEMENTS.has(name)) {358+ if (BLACK_PAINT.has(fill) || fill === '') node.attributes.set('data-dz-fill', 'ink')359+ else if (MUTED_PAINT.has(fill)) node.attributes.set('data-dz-fill', 'ink-muted')360+ } else if (SHAPE_ELEMENTS.has(name) && !backdrop) {361+ if (surfaces.has(fill)) node.attributes.set('data-dz-fill', 'surface-1')362+ else if (BLACK_PAINT.has(fill) && !filteredShadow(node)) node.attributes.set('data-dz-fill', 'line')363+ if (BLACK_PAINT.has(stroke)) node.attributes.set('data-dz-stroke', 'line')364+ else if (MUTED_PAINT.has(stroke)) node.attributes.set('data-dz-stroke', 'line-muted')365+ }366+ for (const child of node.children) visit(child, fill, stroke)367+ }368+ visit(root)369+}370+371+function applyD2Profile(root) {372+ applyNeutralSvgProfile(root, 'd2', ['#f7f8fe'])373+ const visit = node => {374+ if (node.type === 'text') return375+ const classes = classNames(node)376+ const name = localName(node)377+ if (classes.has('fill-N1') && TEXT_ELEMENTS.has(name)) node.attributes.set('data-dz-fill', 'ink')378+ if (classes.has('fill-N2') && TEXT_ELEMENTS.has(name)) node.attributes.set('data-dz-fill', 'ink-muted')379+ if (classes.has('fill-B6') && SHAPE_ELEMENTS.has(name)) node.attributes.set('data-dz-fill', 'surface-1')380+ if (classes.has('fill-B1')) node.attributes.set('data-dz-fill', 'accent-1')381+ if (classes.has('stroke-B1') || classes.has('connection')) node.attributes.set('data-dz-stroke', 'accent-1')382+ for (const child of node.children) visit(child)383+ }384+ visit(root)385+}386+387+function applySvgbobProfile(root) {388+ applyNeutralSvgProfile(root, 'svgbob')389+ const visit = node => {390+ if (node.type === 'text') return391+ const classes = classNames(node)392+ const name = localName(node)393+ if (classes.has('backdrop')) node.attributes.set('data-dz-role', 'canvas')394+ if (TEXT_ELEMENTS.has(name)) node.attributes.set('data-dz-fill', 'ink')395+ if (classes.has('solid')) node.attributes.set('data-dz-stroke', 'line')396+ if (classes.has('nofill') || classes.has('bg_filled')) node.attributes.set('data-dz-fill', 'surface-1')397+ if (classes.has('filled')) node.attributes.set('data-dz-fill', 'line')398+ for (const child of node.children) visit(child)399+ }400+ visit(root)401+}402+403+function applyPlantumlProfile(root) {404+ applyNeutralSvgProfile(root, 'plantuml', ['#e2e2f0', '#f1f1f1'])405+}406+407+function applyGoatProfile(root) {408+ applyNeutralSvgProfile(root, 'goat')409+ const visit = node => {410+ if (node.type === 'text') return411+ const classes = classNames(node)412+ const name = localName(node)413+ if (TEXT_ELEMENTS.has(name)) node.attributes.set('data-dz-fill', 'ink')414+ if (classes.has('path')) node.attributes.set('data-dz-stroke', 'line')415+ if (classes.has('arrowhead') || classes.has('filled')) node.attributes.set('data-dz-fill', 'line')416+ if (classes.has('hollow')) node.attributes.set('data-dz-fill', 'surface-1')417+ for (const child of node.children) visit(child)418+ }419+ visit(root)420+}421+422+function applyVegaProfile(root) {423+ applyNeutralSvgProfile(root, 'vega')424+ const visit = (node, context = '') => {425+ if (node.type === 'text') return426+ const classes = classNames(node)427+ const nextContext = [...classes].find(value => value.startsWith('role-')) ?? context428+ const name = localName(node)429+ if (classes.has('background') && rendererBackdrop(node, presentationBounds(root), 'vega')) node.attributes.set('data-dz-role', 'canvas')430+ if (nextContext === 'role-axis-label' || nextContext === 'role-legend-label' || nextContext === 'role-title') {431+ if (TEXT_ELEMENTS.has(name)) node.attributes.set('data-dz-fill', 'ink')432+ }433+ if (nextContext === 'role-axis-tick' || nextContext === 'role-axis-domain' || nextContext === 'role-grid') {434+ if (SHAPE_ELEMENTS.has(name)) node.attributes.set('data-dz-stroke', 'line-muted')435+ }436+ for (const child of node.children) visit(child, nextContext)437+ }438+ visit(root)439+}440+441+function applyWavedromProfile(root) {442+ const accentClasses = new Map([443+ ['s8', 'accent-3'], ['s9', 'accent-3'], ['s10', 'accent-1'], ['s11', 'accent-1'],444+ ['s12', 'accent-2'], ['s13', 'accent-2'], ['s14', 'accent-3'], ['s15', 'accent-1'],445+ ])446+ const visit = node => {447+ if (node.type === 'text') return448+ const classes = classNames(node)449+ const name = localName(node)450+ if (TEXT_ELEMENTS.has(name) && ![...classes].some(value => ['muted', 'warning', 'error', 'info', 'success'].includes(value))) {451+ node.attributes.set('data-dz-fill', 'ink')452+ }453+ if ([...classes].some(value => ['s1', 's2', 's3', 's4'].includes(value))) node.attributes.set('data-dz-stroke', 'line')454+ if (classes.has('s5') || classes.has('s7')) node.attributes.set('data-dz-fill', 'surface-1')455+ if (classes.has('s6')) node.attributes.set('data-dz-fill', 'line')456+ for (const [className, role] of accentClasses) if (classes.has(className)) node.attributes.set('data-dz-fill', role)457+ if (classes.has('s16')) node.attributes.set('data-dz-stroke', 'accent-1')458+ for (const child of node.children) visit(child)459+ }460+ visit(root)461+}462+463+function applyProfile(root, capability, engine) {209464 if (capability.profile === 'graphviz-15-semantic-1') applyGraphvizProfile(root)465+ else if (capability.profile === 'd2-0.7-semantic-1') applyD2Profile(root)466+ else if (capability.profile === 'plantuml-2026-semantic-1') applyPlantumlProfile(root)467+ else if (capability.profile === 'svgbob-0.7-semantic-1') applySvgbobProfile(root)468+ else if (capability.profile === 'neutral-svg-semantic-1') applyNeutralSvgProfile(root, engine)469+ else if (capability.profile === 'structured-svg-semantic-1') {470+ if (engine === 'goat') applyGoatProfile(root)471+ else if (engine === 'vega' || engine === 'vegalite') applyVegaProfile(root)472+ else if (engine === 'wavedrom') applyWavedromProfile(root)473+ }210474 }211475 212476 function applyRootBackgroundStyle(root, background) {@@ -283,9 +547,11 @@ function parseSafeSvg(source) {
283547 skippedDepth++284548 return285549 }286- const next = element(tag.name)287- for (const attribute of Object.values(tag.attributes)) {288- if (safeAttribute(attribute.name, attribute.value)) next.attributes.set(attribute.name, attribute.value)550+ const next = element(qualifiedName(tag))551+ for (const [attributeKey, attribute] of Object.entries(tag.attributes)) {552+ const name = typeof attribute === 'string' ? attributeKey : qualifiedName(attribute) ?? attributeKey553+ const value = typeof attribute === 'string' ? attribute : attribute.value554+ if (safeAttribute(name, value)) next.attributes.set(name, value)289555 }290556 const parent = stack.at(-1)291557 if (parent) parent.children.push(next)@@ -330,15 +596,16 @@ export function sanitizeAndDecorateSvg(source, metadata, presentation, engine, r
330596 export function canonicalizeSvg(source, metadata, engine, rendererVersion = '') {331597 const root = parseSafeSvg(source)332598 const capability = normalizationFor(engine, rendererVersion)333- applyProfile(root, capability)599+ applyProfile(root, capability, engine)334600 root.attributes.set('data-dz-schema', SVG_SCHEMA)335601 root.attributes.set('data-dz-normalizer', NORMALIZER_BUILD)336602 root.attributes.set('data-dz-profile', capability.profile)337- root.attributes.set('data-dz-palette', capability.palette)603+ root.attributes.set('data-dz-palette', 'renderer')338604 root.attributes.set('data-dz-engine', engine)339605 root.attributes.set('data-dz-appearance', 'raw')340606 root.attributes.set('data-dz-conformance', capability.conformance)341- if (capability !== RAW_NORMALIZATION) root.attributes.set('data-dz-appearances', THEMED_APPEARANCES.join(' '))607+ const appearances = capability.appearances.filter(appearance => appearance !== 'raw')608+ if (appearances.length > 0) root.attributes.set('data-dz-appearances', appearances.join(' '))342609 const bounds = presentationBounds(root)343610 if (bounds !== null) root.attributes.set('data-dz-bounds', bounds.join(' '))344611 addMetadata(root, metadata)@@ -436,3 +703,14 @@ export function materializeSvg(canonical, appearance) {
436703 }437704 return serialize(root)438705 }706+707+export function supportedAppearances(canonical) {708+ const root = parseSafeSvg(canonical)709+ if (root.attributes.get('data-dz-schema') !== SVG_SCHEMA) {710+ throw new SvgNormalizationError(422, 'not_canonical', 'SVG does not use the current Diagram.zip canonical schema.')711+ }712+ return Object.freeze([713+ 'raw',714+ ...(root.attributes.get('data-dz-appearances') ?? '').split(/\s+/).filter(appearance => APPEARANCES.includes(appearance)),715+ ])716+}@@ -110,7 +110,7 @@ Content-Type: application/json
110110 "type": "d2",111111 "source": "a -> b",112112 "options": {},113- "presentation": { "background": "", "padding": 0, "frame": false }113+ "presentation": { "background": "", "padding": 0, "frame": false, "appearance": "raw" }114114 },115115 "metadata": { "title": "A to B", "description": "" }116116 }@@ -181,6 +181,11 @@ The API materializes the requested appearance from the canonical R2 object. It
181181 does not store a second SVG. The appearance has its own ETag. Locked renders182182 cannot use this server-side path because the API cannot decrypt their SVG.183183 184+The saved presentation records the selected appearance. The editor places the185+same appearance on stable SVG and Markdown links. `raw` uses the renderer canvas186+controls. A shared appearance ignores those legacy controls and materializes its187+own palette and, for framed appearances, its own canvas.188+184189 ## Editor and share UX185190 186191 Open drafts continue to autosave locally and render after the existing debounce.@@ -162,6 +162,19 @@ document.querySelector('#app').innerHTML = `
162162 </label>163163 <fieldset class="presentation-fields">164164 <legend>Presentation</legend>165+ <label class="appearance-field">166+ <span>Appearance</span>167+ <select id="diagram-appearance">168+ <option value="raw">Renderer default</option>169+ <option value="auto-transparent">Match device · transparent</option>170+ <option value="light-transparent">Light · transparent</option>171+ <option value="dark-transparent">Dark · transparent</option>172+ <option value="auto-framed">Match device · framed</option>173+ <option value="light-framed">Light · framed</option>174+ <option value="dark-framed">Dark · framed</option>175+ </select>176+ <small id="appearance-note">Renderer default keeps the renderer's paint and enables the canvas controls below.</small>177+ </label>165178 <label>166179 <span>Canvas</span>167180 <select id="diagram-background">@@ -252,6 +265,7 @@ document.querySelector('#app').innerHTML = `
252265 const typePicker = document.querySelector('#diagram-type')253266 const titleInput = document.querySelector('#diagram-title')254267 const descriptionInput = document.querySelector('#diagram-description')268+const appearanceInput = document.querySelector('#diagram-appearance')255269 const backgroundInput = document.querySelector('#diagram-background')256270 const paddingInput = document.querySelector('#diagram-padding')257271 const frameInput = document.querySelector('#diagram-frame')@@ -265,9 +279,11 @@ const initialPresentation = normalizePresentation(initialState.presentation)
265279 typePicker.value = initialState.type266280 titleInput.value = initialMetadata.title267281 descriptionInput.value = initialMetadata.description282+appearanceInput.value = initialPresentation.appearance268283 backgroundInput.value = initialPresentation.background269284 paddingInput.value = String(initialPresentation.padding)270285 frameInput.checked = initialPresentation.frame286+syncPresentationControls()271287 updateDocumentMetadata()272288 activeType = initialState.type273289 typeDrafts.set(initialState.type, initialState)@@ -287,6 +303,7 @@ const preview = new PreviewController({
287303 minimap: document.querySelector('#minimap'),288304 minimapImage: document.querySelector('#minimap-image'),289305 minimapViewport: document.querySelector('#minimap-viewport'),306+ onAppearances: updateAppearanceOptions,290307 })291308 292309 typePicker.addEventListener('change', () => {@@ -299,8 +316,9 @@ document.querySelector('#details').addEventListener('click', () => {
299316 document.querySelector('#details-dialog').showModal()300317 descriptionInput.focus()301318 })302-for (const input of [titleInput, descriptionInput, backgroundInput, paddingInput, frameInput]) {319+for (const input of [titleInput, descriptionInput, appearanceInput, backgroundInput, paddingInput, frameInput]) {303320 input.addEventListener('input', () => {321+ syncPresentationControls()304322 updateDocumentMetadata()305323 clearTimeout(commitDetails.timeout)306324 commitDetails.timeout = setTimeout(commitDetails, 220)@@ -372,6 +390,7 @@ function currentState() {
372390 options: {},373391 meta: normalizeMetadata({ title: titleInput.value, description: descriptionInput.value }),374392 presentation: normalizePresentation({393+ appearance: appearanceInput.value,375394 background: backgroundInput.value,376395 padding,377396 frame: frameInput.checked,@@ -379,6 +398,25 @@ function currentState() {
379398 }380399 }381400 401+function syncPresentationControls() {402+ const usesRendererPresentation = appearanceInput.value === 'raw'403+ backgroundInput.disabled = !usesRendererPresentation404+ paddingInput.disabled = !usesRendererPresentation405+ frameInput.disabled = !usesRendererPresentation406+ document.querySelector('#appearance-note').textContent = usesRendererPresentation407+ ? "Renderer default keeps the renderer's paint and enables the canvas controls below."408+ : 'This appearance uses the shared Diagram.zip palette. Canvas, padding, and frame come from that appearance.'409+}410+411+function updateAppearanceOptions(appearances, appliedAppearance) {412+ const supported = new Set(appearances)413+ for (const option of appearanceInput.options) option.disabled = !supported.has(option.value)414+ if (appearanceInput.value === appliedAppearance) return415+ appearanceInput.value = appliedAppearance416+ syncPresentationControls()417+ queueMicrotask(commitDetails)418+}419+382420 function scheduleUpdate(delay = 1200) {383421 clearTimeout(renderTimer)384422 renderTimer = setTimeout(commitState, delay)@@ -448,9 +486,11 @@ function applyState(state, commit = true) {
448486 typePicker.value = state.type449487 titleInput.value = meta.title450488 descriptionInput.value = meta.description489+ appearanceInput.value = presentation.appearance451490 backgroundInput.value = presentation.background452491 paddingInput.value = String(presentation.padding)453492 frameInput.checked = presentation.frame493+ syncPresentationControls()454494 updateDocumentMetadata()455495 sourceEditor.setDocument({ source: state.source, diagramType: state.type })456496 if (commit) commitState()@@ -471,7 +511,8 @@ function populateShareDialog() {
471511 ? editAliasUrl(location.origin, remote.aliasId, remote.writeCapability)472512 : ''473513 const savedState = savedLocked ? null : remote.savedState474- const svgLink = savedState ? stableRenderUrl(location.origin, remote.aliasId, 'svg') : ''514+ const savedAppearance = savedState ? normalizePresentation(savedState.presentation).appearance : 'raw'515+ const svgLink = savedState ? stableRenderUrl(location.origin, remote.aliasId, 'svg', savedAppearance) : ''475516 const imageLink = document.querySelector('#image-link')476517 const markdownLink = document.querySelector('#markdown-link')477518 const markdownTitle = savedState?.meta.title.trim().replace(/[\[\]\r\n]/g, ' ') || 'Diagram'@@ -1,5 +1,5 @@
11 import { clientAdapterFor } from './client-renderers.js'2-import { canonicalizeSvg, materializePresentation } from './client-svg.js'2+import { canonicalizeSvg, materializePresentation, materializeSvg, supportedAppearances } from './client-svg.js'33 import { httpRendererUnitFor } from './renderer-units.js'44 55 const STATUS_LABELS = {@@ -14,13 +14,14 @@ function clamp(value, minimum, maximum) {
1414 }1515 1616 export class PreviewController {17- constructor({ stage, image, status, minimap, minimapImage, minimapViewport }) {17+ constructor({ stage, image, status, minimap, minimapImage, minimapViewport, onAppearances = () => {} }) {1818 this.stage = stage1919 this.image = image2020 this.status = status2121 this.minimap = minimap2222 this.minimapImage = minimapImage2323 this.minimapViewport = minimapViewport24+ this.onAppearances = onAppearances2425 this.scale = 12526 this.x = 02627 this.y = 0@@ -109,7 +110,14 @@ export class PreviewController {
109110 }110111 if (requestNumber !== this.requestNumber) return111112 const canonical = this.normalizedSvgBlob(rendered.body)112- const displayed = this.normalizedSvgBlob(materializePresentation(rendered.body, presentation))113+ const appearances = supportedAppearances(rendered.body)114+ const requestedAppearance = presentation.appearance ?? 'raw'115+ const appearance = appearances.includes(requestedAppearance) ? requestedAppearance : 'raw'116+ this.onAppearances(appearances, appearance)117+ const displayedSvg = appearance === 'raw'118+ ? materializePresentation(rendered.body, presentation)119+ : materializeSvg(rendered.body, appearance)120+ const displayed = this.normalizedSvgBlob(displayedSvg)113121 this.latestRenderKey = renderKey114122 this.latestSvgBlob = canonical.blob115123 this.latestRendererIdentity = rendered.identity@@ -140,7 +148,11 @@ export class PreviewController {
140148 format: 'svg',141149 options,142150 metadata: meta,143- presentation,151+ presentation: {152+ background: presentation.background ?? '',153+ padding: presentation.padding ?? 0,154+ frame: presentation.frame ?? false,155+ },144156 }, signal)145157 return this.renderResponse(response, type)146158 }@@ -1,4 +1,5 @@
11 import { deflate, inflate } from 'pako'2+import { APPEARANCES } from './client-svg.js'23 34 // Leave room below Cloudflare's 16 KB request URL ceiling.45 export const MAX_IMAGE_URL_LENGTH = 15_000@@ -73,13 +74,14 @@ export function normalizePresentation(presentation = {}) {
7374 const background = presentation.background ?? ''7475 const padding = presentation.padding ?? 07576 const frame = presentation.frame ?? false77+ const appearance = presentation.appearance ?? 'raw'7678 if (typeof background !== 'string' || (background && !/^#[0-9a-f]{6}$/i.test(background))) {7779 throw new Error('Invalid diagram presentation.')7880 }79- if (!Number.isInteger(padding) || padding < 0 || padding > 256 || typeof frame !== 'boolean') {81+ if (!Number.isInteger(padding) || padding < 0 || padding > 256 || typeof frame !== 'boolean' || !APPEARANCES.includes(appearance)) {8082 throw new Error('Invalid diagram presentation.')8183 }82- return { background, padding, frame }84+ return { background, padding, frame, appearance }8385 }8486 8587 function hasMetadata(meta) {@@ -87,7 +89,7 @@ function hasMetadata(meta) {
8789 }8890 8991 function hasPresentation(presentation) {90- return Boolean(presentation.background || presentation.padding || presentation.frame)92+ return Boolean(presentation.background || presentation.padding || presentation.frame || presentation.appearance !== 'raw')9193 }9294 9395 export function imageUrl(origin, { type, source, options = {}, meta = {}, presentation = {} }) {@@ -487,6 +487,8 @@ dialog::backdrop {
487487 border-radius: 12px;488488 }489489 .presentation-fields legend { padding: 0 6px; font-weight: 750; }490+.presentation-fields .appearance-field { grid-column: 1 / -1; }491+.presentation-fields .appearance-field small { display: block; margin-top: 7px; color: var(--muted); font-size: 12px; line-height: 1.4; }490492 .presentation-fields label > span { display: block; margin-bottom: 7px; font-weight: 700; }491493 .presentation-fields select,492494 .presentation-fields input[type="number"] {@@ -509,6 +511,7 @@ dialog::backdrop {
509511 }510512 .presentation-fields .checkbox-field input { width: 18px; height: 18px; accent-color: var(--ink); }511513 .presentation-fields .checkbox-field span { display: inline; margin: 0; }514+.presentation-fields :disabled { cursor: not-allowed; opacity: .5; }512515 513516 .privacy-fields {514517 display: flex;@@ -15,7 +15,7 @@ const state = {
1515 source: 'a -> b',1616 options: { theme: 200 },1717 meta: { title: 'Private map', description: 'Secret topology.' },18- presentation: { background: '#ffffff', padding: 16, frame: true },18+ presentation: { background: '#ffffff', padding: 16, frame: true, appearance: 'raw' },1919 }2020 2121 test('encrypts and unlocks a diagram with no plaintext in the payload', async () => {@@ -22,9 +22,10 @@ test('round trips Unicode text', () => {
2222 })2323 2424 test('validates structured presentation values', () => {25- assert.deepEqual(normalizePresentation(), { background: '', padding: 0, frame: false })25+ assert.deepEqual(normalizePresentation(), { background: '', padding: 0, frame: false, appearance: 'raw' })2626 assert.throws(() => normalizePresentation({ background: 'red' }), /presentation/)2727 assert.throws(() => normalizePresentation({ padding: 257 }), /presentation/)28+ assert.throws(() => normalizePresentation({ appearance: 'sepia' }), /presentation/)2829 })2930 3031 test('validates metadata values', () => {@@ -60,7 +61,18 @@ test('carries presentation in the static SVG payload', () => {
6061 presentation: { background: '#f4f4f4', padding: 24, frame: true },6162 }))6263 assert.deepEqual(JSON.parse(decodeText(url.searchParams.get('dz'))), {63- presentation: { background: '#f4f4f4', padding: 24, frame: true },64+ presentation: { background: '#f4f4f4', padding: 24, frame: true, appearance: 'raw' },65+ })66+})67+68+test('carries a shared SVG appearance in presentation state', () => {69+ const url = new URL(imageUrl('https://diagram.zip', {70+ type: 'd2',71+ source: 'a -> b',72+ presentation: { appearance: 'auto-transparent' },73+ }))74+ assert.deepEqual(JSON.parse(decodeText(url.searchParams.get('dz'))), {75+ presentation: { background: '', padding: 0, frame: false, appearance: 'auto-transparent' },6476 })6577 })6678 @@ -7,6 +7,8 @@ import {
77 canonicalizeSvg,88 materializePresentation,99 materializeSvg,10+ supportedAppearances,11+ normalizationFor,1012 sanitizeAndDecorateSvg,1113 } from '../../diagramzip-svg/index.js'1214 @@ -27,6 +29,11 @@ test('freezes namespaced appearance and raw capability contracts', () => {
2729 assert.deepEqual(RAW_NORMALIZATION.appearances, ['raw'])2830 })2931 32+test('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+})36+3037 test('produces deterministic, idempotent raw canonical SVG', () => {3138 const first = sanitizeAndDecorateSvg(source, metadata, presentation, 'pikchr')3239 const repeated = sanitizeAndDecorateSvg(source, metadata, presentation, 'pikchr')@@ -77,6 +84,18 @@ test('materializes explicit and automatic palettes without changing diagram geom
7784 assert.match(automatic, /@media\(prefers-color-scheme:dark\)/)7885 })7986 87+test('reports the appearances declared by the canonical profile', () => {88+ assert.deepEqual(supportedAppearances(semanticCanonical), [89+ 'raw',90+ 'auto-transparent',91+ 'light-transparent',92+ 'dark-transparent',93+ 'auto-framed',94+ 'light-framed',95+ 'dark-framed',96+ ])97+})98+8099 test('materializes a bounded frame and remains idempotent', () => {81100 const framed = materializeSvg(semanticCanonical, 'dark-framed')82101 const repeated = materializeSvg(framed, 'dark-framed')@@ -110,3 +129,49 @@ test('applies the pinned GraphViz semantic profile without recoloring authored p
110129 assert.match(dark, /fill="red" stroke="red"/)111130 assert.doesNotMatch(dark, /fill="red" stroke="red"[^>]*data-dz-/)112131 })132+133+test('applies D2, PlantUML, Svgbob, and neutral SVG family roles', () => {134+ const fixtures = [135+ {136+ engine: 'd2', version: 'd2@0.7.1/dagre-wasm-1', profile: 'd2-0.7-semantic-1',137+ 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>',138+ roles: ['data-dz-role="canvas"', 'data-dz-fill="surface-1"', 'data-dz-stroke="accent-1"', 'data-dz-fill="ink"'],139+ },140+ {141+ engine: 'plantuml', version: 'plantuml@1.2026.6/edge-wasm-1', profile: 'plantuml-2026-semantic-1',142+ 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>',143+ roles: ['data-dz-fill="surface-1"', 'data-dz-stroke="line"', 'data-dz-fill="ink"'],144+ },145+ {146+ engine: 'svgbob', version: 'svgbob@0.7.6/edge-wasm-1', profile: 'svgbob-0.7-semantic-1',147+ 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>',148+ roles: ['data-dz-role="canvas"', 'data-dz-stroke="line"', 'data-dz-fill="surface-1"', 'data-dz-fill="ink"'],149+ },150+ {151+ engine: 'blockdiag', version: 'blockdiag@3.4.2/python-worker-1', profile: 'neutral-svg-semantic-1',152+ 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>',153+ roles: ['data-dz-fill="surface-1"', 'data-dz-stroke="line"', 'data-dz-fill="ink"'],154+ },155+ ]156+157+ for (const fixture of fixtures) {158+ const canonical = canonicalizeSvg(fixture.source, metadata, fixture.engine, fixture.version)159+ assert.match(canonical, new RegExp(`data-dz-profile="${fixture.profile}"`))160+ for (const role of fixture.roles) assert.ok(canonical.includes(role), `${fixture.engine} missing ${role}`)161+ assert.match(materializeSvg(canonical, 'dark-transparent'), /data-dz-appearance="dark-transparent"/)162+ }163+})164+165+test('keeps authored SVG paint intact while offering an outer frame', () => {166+ const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><rect width="10" height="10" fill="hotpink"></rect></svg>'167+ const canonical = canonicalizeSvg(source, metadata, 'tikz', '@planktimerr/tikzjax@1.0.63/client-unit-1')168+ const framed = materializeSvg(canonical, 'auto-framed')169+170+ assert.match(canonical, /data-dz-conformance="presentation-only"/)171+ assert.match(canonical, /data-dz-appearances="auto-framed light-framed dark-framed"/)172+ assert.match(framed, /fill="hotpink"/)173+ assert.throws(174+ () => materializeSvg(canonical, 'dark-transparent'),175+ error => error instanceof SvgNormalizationError && error.code === 'unsupported_appearance',176+ )177+})@@ -183,10 +183,27 @@ to browser and Worker renderers. Every catalog entry reports its active
183183 normalization capability. An unrecognized renderer contract remains on184184 `safe-raw-1` and supports only `raw`.185185 186-GraphViz `15.1.1` currently uses `graphviz-15-semantic-1`. The profile relies on187-GraphViz's stable `graph`, `node`, `edge`, and `cluster` groups. It adapts neutral188-surfaces, text, connectors, and arrowheads. Explicit non-neutral authored colors189-remain unchanged and are reported as a profile limitation.186+The current catalog uses these profile families:187+188+| Profile | Conformance | Engines | Supported appearances |189+| --- | --- | --- | --- |190+| `graphviz-15-semantic-1` | Semantic | Graphviz, DBML, ERD, WireViz | All |191+| `d2-0.7-semantic-1` | Semantic | D2 | All |192+| `plantuml-2026-semantic-1` | Semantic | PlantUML, C4 PlantUML, Structurizr | All |193+| `svgbob-0.7-semantic-1` | Semantic | Svgbob, Ditaa | All |194+| `neutral-svg-semantic-1` | Adaptive | BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag, RackDiag, Bytefield, Mermaid, BPMN, Nomnoml, Pikchr, Symbolator, UMLet | All |195+| `structured-svg-semantic-1` | Adaptive | GoAT, Vega, Vega-Lite, WaveDrom | All |196+| `authored-svg-presentation-1` | Presentation only | Diagrams.net, Excalidraw, TikZ | Raw and framed |197+198+Semantic profiles rely on pinned renderer structures and stable class or group199+meaning. Adaptive profiles recognize neutral paint and selected structural200+roles. They preserve authored data colors and other non-neutral paint.201+Presentation-only profiles do not claim that the authored drawing adapts. They202+add only the shared outer canvas and frame.203+204+Profile selection also checks the pinned renderer version. An engine upgrade205+does not inherit a profile until its output passes the catalog audit. Unknown206+builds fall back to `safe-raw-1`.190207 191208 ## Semantic visual roles192209