- commit
- 9366112e06dec4579491c33f09ea1b85f356937d
- parent
- 3bee574583cf31bc998399c3c22e2f3857677bc0
- author
- Dami <dami.etoile@gmail.com>
@@ -35,6 +35,7 @@ runtime WebAssembly instantiation; the Worker target needs a direct precompiled
3535 module adapter.3636 3737 All returned SVG passes through the same sanitizer. Scripts, event handlers,38-external resources, and embedded HTML (`foreignObject`) are removed. This is an39-intentional security constraint and may reduce label fidelity for an engine that40-emits HTML-backed SVG; any observed engine-specific loss belongs in the catalog.38+external resources, and active embedded HTML are removed. Mermaid's39+`foreignObject` labels retain only a narrow formatting allowlist, with links and40+resource-loading elements stripped. Any observed engine-specific loss belongs41+in the catalog.@@ -1,12 +1,12 @@
11 {22 "name": "diagramzip-render",3- "version": "0.2.0",3+ "version": "0.2.1",44 "lockfileVersion": 3,55 "requires": true,66 "packages": {77 "": {88 "name": "diagramzip-render",9- "version": "0.2.0",9+ "version": "0.2.1",1010 "dependencies": {1111 "bytefield-svg": "1.11.0",1212 "json5": "2.2.3",@@ -37,10 +37,20 @@ const fixtures = {
3737 wireviz: 'wireviz.yaml',3838 }3939 40+const expectedLabels = {41+ mermaid: 'How to contribute?',42+ d2: 'dogs',43+ ditaa: 'Kroki',44+ symbolator: 'Clocking',45+ tikz: 'Hydrogen',46+}47+48+const canvasEngines = new Set(['graphviz', 'd2', 'ditaa', 'svgbob', 'symbolator'])49+4050 async function render([engine, filename]) {4151 const source = await readFile(resolve(fixtureDirectory, filename), 'utf8')42- const presentation = engine === 'svgbob'43- ? { background: '#f3f3f3', padding: 256, frame: true }52+ const presentation = canvasEngines.has(engine)53+ ? { background: '#f3f3f3', padding: engine === 'svgbob' ? 256 : 24, frame: true }4454 : { background: '', padding: 0, frame: false }4555 const response = await fetch(endpoint, {4656 method: 'POST',@@ -58,6 +68,26 @@ async function render([engine, filename]) {
5868 if (!response.ok) throw new Error(`${engine}: HTTP ${response.status}: ${body.slice(0, 240)}`)5969 if (!/<svg[\s>]/i.test(body)) throw new Error(`${engine}: response is not SVG`)6070 if (!/\bviewBox\s*=|\bwidth\s*=.*\bheight\s*=/is.test(body)) throw new Error(`${engine}: SVG has no usable dimensions`)71+ if (expectedLabels[engine] && !body.includes(expectedLabels[engine])) {72+ throw new Error(`${engine}: expected label was removed from the SVG`)73+ }74+ if (engine === 'tikz' && !body.includes('data:application/x-font-woff;base64')) {75+ throw new Error('tikz: embedded font stylesheet was removed from the SVG')76+ }77+ if (engine === 'd2' && (!body.includes('@keyframes dashdraw') || !body.includes('animation: dashdraw'))) {78+ throw new Error('d2: animated connection stylesheet was removed from the SVG')79+ }80+ if (engine === 'symbolator' && !body.includes('.fnt1')) {81+ throw new Error('symbolator: embedded stylesheet was removed from the SVG')82+ }83+ if (canvasEngines.has(engine)) {84+ if (!body.includes('background-color:#f3f3f3') || !body.includes('fill="#f3f3f3"')) {85+ throw new Error(`${engine}: selected canvas background was not applied`)86+ }87+ if (/class="backdrop"|<rect[^>]*width="100%"[^>]*height="100%"[^>]*fill="white"|<polygon fill="white" stroke="none"/i.test(body)) {88+ throw new Error(`${engine}: renderer backdrop still masks the selected canvas`)89+ }90+ }6191 if (engine === 'svgbob') {6292 const root = body.match(/<svg\b[^>]*>/i)?.[0] ?? ''6393 if (!/viewBox="-256 -256 /.test(root) || !/width="\d+(?:\.\d+)?"/.test(root) || !/height="\d+(?:\.\d+)?"/.test(root)) {@@ -59,6 +59,7 @@ const versions: Partial<Record<EngineId, string>> = {
5959 }6060 6161 const losses: Partial<Record<EngineId, readonly string[]>> = {62+ mermaid: ['External links and resource-loading elements are removed; XHTML labels retain only safe text formatting.'],6263 graphviz: [6364 'The published Viz.js wrapper performs runtime WebAssembly instantiation, which Workers prohibit; GraphViz remains on the origin until a precompiled module adapter is built.',6465 'External links and resources are rejected during SVG sanitization.',@@ -66,6 +67,7 @@ const losses: Partial<Record<EngineId, readonly string[]>> = {
6667 bytefield: ['Only SVG is in the v2 rendering contract.'],6768 dbml: ['The upstream package mixes ESM files into a CommonJS package and remains on the origin until it can be safely repackaged.'],6869 nomnoml: ['Only SVG is in the v2 rendering contract.'],70+ symbolator: ['External links are removed during SVG sanitization.'],6971 vega: ['URL-backed data and images are rejected; data must be embedded as values.', 'Only SVG is in the v2 rendering contract.'],7072 vegalite: ['URL-backed data and images are rejected; data must be embedded as values.', 'Only SVG is in the v2 rendering contract.'],7173 wavedrom: ['Only the six bundled Kroki skins are accepted.', 'Only SVG is in the v2 rendering contract.'],@@ -66,7 +66,7 @@ async function render(request: RenderRequest, env: Env, signal: AbortSignal): Pr
6666 rendered = await adapter.render(request, signal)6767 }6868 if (signal.aborted) throw signal.reason69- const body = sanitizeAndDecorateSvg(rendered.body, request.metadata, request.presentation)69+ const body = sanitizeAndDecorateSvg(rendered.body, request.metadata, request.presentation, request.engine)7070 return new Response(body, {7171 headers: {7272 'Access-Control-Allow-Origin': '*',@@ -1,6 +1,6 @@
11 import { SaxesParser, type SaxesTagNS } from 'saxes'22 import { RenderError } from './errors'3-import type { RenderMetadata, RenderPresentation } from './types'3+import type { EngineId, RenderMetadata, RenderPresentation } from './types'44 55 type SvgNode = SvgElement | { type: 'text'; value: string }66 @@ -12,9 +12,12 @@ interface SvgElement {
1212 }1313 1414 const MAX_SVG_LENGTH = 4_194_30415-const BLOCKED_ELEMENTS = new Set(['script', 'foreignobject', 'iframe', 'object', 'embed', 'audio', 'video'])15+const BLOCKED_ELEMENTS = new Set(['script', 'iframe', 'object', 'embed', 'audio', 'video'])16+const FOREIGN_OBJECT_ELEMENTS = new Set([17+ 'foreignobject', 'div', 'span', 'p', 'br', 'b', 'strong', 'i', 'em', 'small', 'sub', 'sup', 'code', 'ul', 'ol', 'li', 'a',18+])1619 const SAFE_DATA_IMAGE = /^data:image\/(?:png|jpeg|gif|webp);base64,[a-z0-9+/=\s]+$/i17-const SAFE_DATA_FONT = /^data:font\/(?:woff2?|opentype|truetype);base64,[a-z0-9+/=\s]+$/i20+const SAFE_DATA_FONT = /^data:(?:font\/(?:woff2?|opentype|truetype)|application\/(?:x-)?font-woff2?);base64,[a-z0-9+/=\s]+$/i1821 1922 function safeCssReference(value: string): boolean {2023 const normalized = value.trim()@@ -65,6 +68,10 @@ function serialize(node: SvgNode): string {
6568 return `<${node.name}${attributes}>${node.children.map(serialize).join('')}</${node.name}>`6669 }6770 71+function localName(element: SvgElement): string {72+ return element.name.toLowerCase().split(':').at(-1) ?? element.name.toLowerCase()73+}74+6875 function numericDimension(value: string | undefined): number | null {6976 if (value === undefined || !/^-?\d+(?:\.\d+)?(?:px)?$/.test(value.trim())) return null7077 const parsed = Number.parseFloat(value)@@ -81,8 +88,81 @@ function presentationBounds(root: SvgElement): [number, number, number, number]
8188 return width !== null && height !== null ? [0, 0, width, height] : null8289 }8390 84-function decorate(root: SvgElement, metadata: RenderMetadata, presentation: RenderPresentation): void {91+function styleValue(element: SvgElement, property: string): string | null {92+ const style = element.attributes.get('style')93+ if (!style) return null94+ for (const declaration of style.split(';')) {95+ const separator = declaration.indexOf(':')96+ if (separator < 0) continue97+ if (declaration.slice(0, separator).trim().toLowerCase() === property) {98+ return declaration.slice(separator + 1).trim()99+ }100+ }101+ return null102+}103+104+function whiteFill(element: SvgElement): boolean {105+ const fill = (element.attributes.get('fill') ?? styleValue(element, 'fill') ?? '').replaceAll(' ', '').toLowerCase()106+ return fill === 'white' || fill === '#fff' || fill === '#ffffff' || fill === 'rgb(255,255,255)'107+}108+109+function numericCoordinate(value: string | undefined): number | null {110+ if (value === undefined || !/^-?\d+(?:\.\d+)?$/.test(value.trim())) return null111+ const number = Number(value)112+ return Number.isFinite(number) ? number : null113+}114+115+function rectCoversBounds(element: SvgElement, bounds: [number, number, number, number]): boolean {116+ const widthValue = element.attributes.get('width')?.trim()117+ const heightValue = element.attributes.get('height')?.trim()118+ if (widthValue === '100%' && heightValue === '100%') return true119+ const x = numericCoordinate(element.attributes.get('x') ?? '0')120+ const y = numericCoordinate(element.attributes.get('y') ?? '0')121+ const width = numericCoordinate(widthValue)122+ const height = numericCoordinate(heightValue)123+ if (x === null || y === null || width === null || height === null) return false124+ const [boundX, boundY, boundWidth, boundHeight] = bounds125+ const tolerance = 0.01126+ return x <= boundX + tolerance127+ && y <= boundY + tolerance128+ && x + width >= boundX + boundWidth - tolerance129+ && y + height >= boundY + boundHeight - tolerance130+}131+132+function rendererBackdrop(element: SvgElement, bounds: [number, number, number, number] | null, engine: EngineId): boolean {133+ const name = localName(element)134+ const classes = new Set((element.attributes.get('class') ?? '').toLowerCase().split(/\s+/))135+ if (name === 'rect' && classes.has('backdrop')) return true136+ if (name === 'rect' && bounds !== null && whiteFill(element) && rectCoversBounds(element, bounds)) return true137+ return engine === 'graphviz'138+ && name === 'polygon'139+ && whiteFill(element)140+ && (element.attributes.get('stroke') ?? '').toLowerCase() === 'none'141+}142+143+function removeRendererBackdrops(element: SvgElement, inheritedBounds: [number, number, number, number] | null, engine: EngineId): void {144+ const bounds = localName(element) === 'svg' ? presentationBounds(element) ?? inheritedBounds : inheritedBounds145+ element.children = element.children.filter(child => {146+ if (child.type === 'text') return true147+ if (rendererBackdrop(child, bounds, engine)) return false148+ removeRendererBackdrops(child, bounds, engine)149+ return true150+ })151+}152+153+function applyRootBackgroundStyle(root: SvgElement, background: string): void {154+ const declarations = (root.attributes.get('style') ?? '')155+ .split(';')156+ .map(value => value.trim())157+ .filter(Boolean)158+ .filter(value => !/^background(?:-color)?\s*:/i.test(value))159+ declarations.push(`background-color:${background}`)160+ root.attributes.set('style', `${declarations.join(';')};`)161+}162+163+function decorate(root: SvgElement, metadata: RenderMetadata, presentation: RenderPresentation, engine: EngineId): void {85164 const additions: SvgNode[] = []165+ let frame: SvgElement | null = null86166 if (metadata.title) additions.push({ type: 'element', name: 'title', attributes: new Map(), children: [{ type: 'text', value: metadata.title }] })87167 if (metadata.description) additions.push({ type: 'element', name: 'desc', attributes: new Map(), children: [{ type: 'text', value: metadata.description }] })88168 @@ -93,30 +173,47 @@ function decorate(root: SvgElement, metadata: RenderMetadata, presentation: Rend
93173 if (bounds !== null) {94174 const [x, y, width, height] = bounds95175 const padding = presentation.padding96- const next = [x - padding, y - padding, width + padding * 2, height + padding * 2]176+ const next: [number, number, number, number] = [x - padding, y - padding, width + padding * 2, height + padding * 2]97177 root.attributes.set('viewBox', next.join(' '))98178 if (root.attributes.has('width')) root.attributes.set('width', String(next[2]))99179 if (root.attributes.has('height')) root.attributes.set('height', String(next[3]))100- if (presentation.background || presentation.frame) {180+ if (presentation.background) {101181 const attributes = new Map([102182 ['x', String(next[0])],103183 ['y', String(next[1])],104184 ['width', String(next[2])],105185 ['height', String(next[3])],106- ['fill', presentation.background || 'none'],186+ ['fill', presentation.background],107187 ])108- if (presentation.frame) {109- attributes.set('stroke', '#000000')110- attributes.set('stroke-width', '1')111- attributes.set('vector-effect', 'non-scaling-stroke')112- }113188 additions.push({ type: 'element', name: 'rect', attributes, children: [] })114189 }190+ if (presentation.frame) {191+ frame = {192+ type: 'element',193+ name: 'rect',194+ attributes: new Map([195+ ['x', String(next[0] + 0.5)],196+ ['y', String(next[1] + 0.5)],197+ ['width', String(Math.max(0, next[2] - 1))],198+ ['height', String(Math.max(0, next[3] - 1))],199+ ['fill', 'none'],200+ ['stroke', '#000000'],201+ ['stroke-width', '1'],202+ ['vector-effect', 'non-scaling-stroke'],203+ ]),204+ children: [],205+ }206+ }207+ }208+ if (presentation.background) {209+ removeRendererBackdrops(root, bounds, engine)210+ applyRootBackgroundStyle(root, presentation.background)115211 }116212 root.children.unshift(...additions)213+ if (frame) root.children.push(frame)117214 }118215 119-export function sanitizeAndDecorateSvg(source: string, metadata: RenderMetadata, presentation: RenderPresentation): string {216+export function sanitizeAndDecorateSvg(source: string, metadata: RenderMetadata, presentation: RenderPresentation, engine: EngineId): string {120217 if (source.length > MAX_SVG_LENGTH) throw new RenderError(413, 'render_too_large', 'Rendered SVG is too large.')121218 let root: SvgElement | null = null122219 const stack: SvgElement[] = []@@ -130,8 +227,9 @@ export function sanitizeAndDecorateSvg(source: string, metadata: RenderMetadata,
130227 if (value.includes('[')) parseError = new Error('Internal DOCTYPE subsets are not allowed')131228 })132229 parser.on('opentag', (tag: SaxesTagNS) => {133- const localName = tag.local.toLowerCase()134- if (skippedDepth > 0 || BLOCKED_ELEMENTS.has(localName)) {230+ const tagLocalName = tag.local.toLowerCase()231+ const insideForeignObject = tagLocalName === 'foreignobject' || stack.some(element => localName(element) === 'foreignobject')232+ if (skippedDepth > 0 || BLOCKED_ELEMENTS.has(tagLocalName) || (insideForeignObject && !FOREIGN_OBJECT_ELEMENTS.has(tagLocalName))) {135233 skippedDepth++136234 return137235 }@@ -146,18 +244,20 @@ export function sanitizeAndDecorateSvg(source: string, metadata: RenderMetadata,
146244 else parseError = new Error('Multiple root elements')147245 stack.push(element)148246 })149- parser.on('text', value => {247+ const appendText = (value: string) => {150248 if (skippedDepth > 0 || value === '') return151249 const parent = stack.at(-1)152250 if (parent) {153- if (parent.name.toLowerCase() === 'style' && unsafeCss(value)) {251+ if (localName(parent) === 'style' && unsafeCss(value)) {154252 throw new RenderError(422, 'unsafe_svg', 'Renderer output contains unsafe CSS.')155253 }156254 parent.children.push({ type: 'text', value })157255 } else if (value.trim() !== '') {158256 parseError = new Error('Text outside root element')159257 }160- })258+ }259+ parser.on('text', appendText)260+ parser.on('cdata', appendText)161261 parser.on('closetag', () => {162262 if (skippedDepth > 0) {163263 skippedDepth--@@ -180,6 +280,6 @@ export function sanitizeAndDecorateSvg(source: string, metadata: RenderMetadata,
180280 }181281 const parsedRoot = root as SvgElement182282 if (parsedRoot.name.toLowerCase() !== 'svg') throw new RenderError(422, 'invalid_svg', 'Renderer returned invalid SVG.')183- decorate(parsedRoot, metadata, presentation)283+ decorate(parsedRoot, metadata, presentation, engine)184284 return serialize(parsedRoot)185285 }@@ -4,19 +4,68 @@ import { sanitizeAndDecorateSvg } from '../src/svg'
44 describe('SVG normalization', () => {55 it('removes active content and external links', () => {66 const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 20" onload="alert(1)"><script>alert(1)</script><a href="https://example.com"><text>x</text></a></svg>'7- const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '', padding: 0, frame: false })7+ const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '', padding: 0, frame: false }, 'pikchr')88 expect(result).not.toContain('onload')99 expect(result).not.toContain('<script')1010 expect(result).not.toContain('https://example.com')1111 expect(result).toContain('<text>x</text>')1212 })1313 14+ it('preserves CDATA labels and styles used by Ditaa, Symbolator, and TikZ', () => {15+ const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><style><![CDATA[.label { font-family: Helvetica; } @keyframes dashdraw { from { stroke-dashoffset: 10; } } .animated { animation: dashdraw 2s linear infinite; }]]></style><text class="label"><![CDATA[Alice Agent]]></text></svg>'16+ const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '', padding: 0, frame: false }, 'ditaa')17+ expect(result).toContain('.label { font-family: Helvetica; }')18+ expect(result).toContain('@keyframes dashdraw')19+ expect(result).toContain('animation: dashdraw')20+ expect(result).toContain('<text class="label">Alice Agent</text>')21+ })22+23+ it('preserves a narrow safe subset of Mermaid XHTML labels', () => {24+ const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><foreignObject width="100" height="20"><div xmlns="http://www.w3.org/1999/xhtml" style="text-align:center"><span><strong>Alice</strong> Agent</span><script>alert(1)</script><img src="https://example.com/x"></img></div></foreignObject></svg>'25+ const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '', padding: 0, frame: false }, 'mermaid')26+ expect(result).toContain('<foreignObject')27+ expect(result).toContain('<strong>Alice</strong> Agent')28+ expect(result).not.toContain('<script')29+ expect(result).not.toContain('<img')30+ expect(result).not.toContain('https://example.com')31+ })32+33+ it('replaces renderer canvas backdrops without removing internal white shapes', () => {34+ const source = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 120 80"><rect x="-10" y="-10" width="100%" height="100%" fill="white"></rect><g><rect x="0" y="0" width="40" height="20" fill="white" stroke="black"></rect></g></svg>'35+ const result = sanitizeAndDecorateSvg(source, { title: '', description: '' }, { background: '#f4f4f4', padding: 0, frame: false }, 'symbolator')36+ expect(result).not.toContain('width="100%"')37+ expect(result).toContain('<rect x="0" y="0" width="40" height="20" fill="white" stroke="black"></rect>')38+ expect(result).toContain('background-color:#f4f4f4')39+ expect(result).toContain('fill="#f4f4f4"')40+ })41+42+ it('removes nested D2, Svgbob, and GraphViz canvas backdrops', () => {43+ const d2 = sanitizeAndDecorateSvg(44+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80"><svg viewBox="-10 -10 120 80"><rect class="backdrop" x="-10" y="-10" width="120" height="80" fill="#FFFFFF"></rect><rect x="0" y="0" width="20" height="20" fill="#FFFFFF" stroke="#000000"></rect></svg></svg>',45+ { title: '', description: '' },46+ { background: '#f4f4f4', padding: 0, frame: false },47+ 'd2',48+ )49+ expect(d2).not.toContain('class="backdrop"')50+ expect(d2).toContain('width="20" height="20" fill="#FFFFFF" stroke="#000000"')51+52+ const graphviz = sanitizeAndDecorateSvg(53+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><g><polygon fill="white" stroke="none" points="0,0 20,0 20,20 0,20"></polygon><polygon fill="white" stroke="black" points="2,2 4,2 4,4 2,4"></polygon></g></svg>',54+ { title: '', description: '' },55+ { background: '#f4f4f4', padding: 0, frame: false },56+ 'graphviz',57+ )58+ expect(graphviz).not.toContain('fill="white" stroke="none"')59+ expect(graphviz).toContain('fill="white" stroke="black"')60+ })61+1462 it('applies metadata and presentation after sanitization', () => {1563 const source = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="20" viewBox="0 0 10 20"><path d="M0 0"/></svg>'1664 const result = sanitizeAndDecorateSvg(1765 source,1866 { title: 'A & B', description: 'Diagram <description>' },1967 { background: '#ffffff', padding: 4, frame: true },68+ 'pikchr',2069 )2170 expect(result).toContain('viewBox="-4 -4 18 28"')2271 expect(result).toContain('width="18"')@@ -25,6 +74,7 @@ describe('SVG normalization', () => {
2574 expect(result).toContain('<desc>Diagram <description></desc>')2675 expect(result).toContain('fill="#ffffff"')2776 expect(result).toContain('vector-effect="non-scaling-stroke"')77+ expect(result).toContain('x="-3.5" y="-3.5" width="17" height="27" fill="none"')2878 })2979 3080 it('rejects unsafe stylesheet URLs', () => {@@ -32,6 +82,7 @@ describe('SVG normalization', () => {
3282 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><style>rect{fill:url(https://example.com/a)}</style></svg>',3383 { title: '', description: '' },3484 { background: '', padding: 0, frame: false },85+ 'pikchr',3586 )).toThrow('unsafe CSS')3687 })3788 })@@ -1,9 +1,9 @@
11 /* eslint-disable */2-// Generated by Wrangler by running `wrangler types` (hash: 4f6a3670675e6279bde017f15866165d)2+// Generated by Wrangler by running `wrangler types` (hash: 97cd115c298051e27ff7da107a96b48f)33 // Runtime types generated with workerd@1.20260820.1 2026-08-20 nodejs_compat44 interface __BaseEnv_Env {55 ORIGIN_URL: "https://diagram-zip.fly.dev";6- RENDERER_BUILD: "diagramzip-render-0.2.0";6+ RENDERER_BUILD: "diagramzip-render-0.2.1";77 }88 declare namespace Cloudflare {99 interface GlobalProps {@@ -5,6 +5,7 @@ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
55 import { tags } from '@lezer/highlight'66 import { diagramTypes, isKnownDiagramType, languageFor } from './diagram-types.js'77 import { exampleStateFor } from './examples.js'8+import { stateForTypeChange } from './type-drafts.js'89 import {910 DEFAULT_DIAGRAM_TYPE,1011 normalizeMetadata,@@ -274,9 +275,8 @@ const preview = new PreviewController({
274275 })275276 276277 typePicker.addEventListener('change', () => {277- typeDrafts.set(activeType, currentState())278278 const type = typePicker.value279- applyState(typeDrafts.get(type) ?? exampleStateFor(type))279+ applyState(stateForTypeChange(typeDrafts, activeType, type, currentState(), exampleStateFor))280280 })281281 282282 document.querySelector('#details').addEventListener('click', () => {@@ -0,0 +1,34 @@
1+import assert from 'node:assert/strict'2+import test from 'node:test'3+import { stateForTypeChange } from '../src/type-drafts.js'4+5+test('keeps draft source and selector type paired across engine switches', () => {6+ const drafts = new Map()7+ const example = type => ({ type, source: `${type} example` })8+9+ const graphviz = stateForTypeChange(10+ drafts,11+ 'mermaid',12+ 'graphviz',13+ { type: 'graphviz', source: 'edited mermaid' },14+ example,15+ )16+ const d2 = stateForTypeChange(17+ drafts,18+ 'graphviz',19+ 'd2',20+ { ...graphviz, type: 'd2' },21+ example,22+ )23+ const restoredGraphviz = stateForTypeChange(24+ drafts,25+ 'd2',26+ 'graphviz',27+ { ...d2, type: 'graphviz' },28+ example,29+ )30+31+ assert.deepEqual(drafts.get('mermaid'), { type: 'mermaid', source: 'edited mermaid' })32+ assert.deepEqual(drafts.get('d2'), { type: 'd2', source: 'd2 example' })33+ assert.deepEqual(restoredGraphviz, { type: 'graphviz', source: 'graphviz example' })34+})@@ -535,6 +535,8 @@ The first coverage plane is implemented in `diagramzip-render`. Its catalog cont
535535 536536 The first feasibility findings are already reflected in routing and the loss ledger: DBML's package cannot currently be imported cleanly in the Worker runtime, and Viz.js attempts runtime WebAssembly compilation. Both remain fully covered through the origin rather than blocking the migration. The current dry-run bundle is approximately 4.4 MB uncompressed and 0.85 MB compressed.537537 538+The SVG compatibility boundary preserves CDATA-backed text and embedded font styles used by Ditaa, Symbolator, diagrams.net, and TikZ. Mermaid retains a narrow allowlist of XHTML label formatting inside `foreignObject`, while scripts, event handlers, external links, and resource-loading elements remain blocked. When a user selects a canvas color, known full-size white renderer backdrops are removed before the chosen background is applied; internal white diagram shapes are left intact.539+538540 ## Primary implementation references539541 540542 - [Cloudflare Workers TypeScript](https://developers.cloudflare.com/workers/languages/typescript/)@@ -1640,7 +1640,7 @@ Rel(bob, tandem, "Completes tasks")
16401640 +--------+--------+16411641 | Shared Tasks |16421642 | {s} |1643- +-----------------+`,d2:Sx,nwdiag:kx,packetdiag:Ax,rackdiag:Mx,bpmn:bx.replaceAll(`Examine Situation`,`Bob Agent tests Tandem`).replaceAll(`Things OK?`,`Tests pass?`).replaceAll(`Notification Sent`,`Alice and Bob share tasks`).replaceAll(`Error Propagated`,`Alice Agent revises flow`).replaceAll(`Klaus`,`Alice`).replaceAll(`Walter`,`Bob`),bytefield:xx,dbml:Cx,diagramsnet:wx,erd:Tx,excalidraw:Ex,goat:Dx,nomnoml:Ox,pikchr:jx,svgbob:Nx,symbolator:Px,tikz:Fx,umlet:Ix,vega:Lx,vegalite:Rx,wavedrom:zx,wireviz:Bx},lC={plantuml:[`Tandem task sync`,`Alice and Bob coordinate shared tasks through their agents.`],mermaid:[`Shared task flow`,`A compact view of ideas moving through the Tandem team.`],graphviz:[`Tandem collaboration graph`,`People and agents converge on one synchronized task list.`],d2:[`Connection styles`,`D2 connection, arrowhead, shape, and animation syntax.`],c4plantuml:[`Tandem system context`,`The people and software systems surrounding Tandem.`],blockdiag:[`Tandem block flow`,`A block-oriented view of the shared task workflow.`],seqdiag:[`Tandem task sequence`,`A task moves from Alice to Bob and back through Tandem.`],actdiag:[`Tandem delivery activities`,`Work crosses Alice, Alice Agent, and Bob Agent lanes.`],nwdiag:[`Network layout`,`A sample network topology and its connected address ranges.`],packetdiag:[`TCP packet structure`,`The bit-level layout of a TCP packet header.`],rackdiag:[`Rack layout`,`A sample equipment rack rendered from rackdiag syntax.`],bpmn:[`Agent-assisted review`,`A BPMN review flow that branches on test results.`],bytefield:[`Byte field layout`,`A compact byte-oriented protocol field example.`],dbml:[`Database schema`,`Tables and relationships expressed with DBML.`],diagramsnet:[`Venn diagram`,`An overlapping-set example imported from diagrams.net.`],ditaa:[`Tandem ASCII architecture`,`An ASCII sketch becomes a rendered system diagram.`],erd:[`Data model`,`Entities and relationships in a small relational schema.`],excalidraw:[`Venn sketch`,`A hand-drawn overlapping-set diagram.`],goat:[`Component shapes`,`GoAT exercises boxes, connectors, curves, and symbols.`],nomnoml:[`Pirate class diagram`,`A playful class diagram demonstrating Nomnoml syntax.`],pikchr:[`Cardinal points`,`A diamond marks north, east, south, and west.`],structurizr:[`Tandem landscape`,`A software landscape linking people, agents, and Tandem.`],svgbob:[`Cloud network`,`An ASCII cloud connects clients and services.`],symbolator:[`Hardware component`,`A component symbol generated from interface declarations.`],tikz:[`Periodic table`,`A color-coded periodic table rendered with TikZ.`],umlet:[`UML example`,`A UMLet document rendered directly from its XML form.`],vega:[`Bar chart`,`A declarative bar chart expressed with Vega.`],vegalite:[`Discretizing scale`,`A Vega-Lite example comparing discretized values.`],wavedrom:[`Timing diagram`,`Digital signals and timing relationships rendered by WaveDrom.`],wireviz:[`Wiring harness`,`Connections, cables, and pins described with WireViz.`]};function uC(e){return cC[e]??aC}function dC(e){let t=px.findIndex(t=>t.id===e),[n,r]=lC[e]??[`Diagram example`,`A diagram.zip catalog example.`];return{type:e,source:uC(e),options:{},...Hx(Math.max(t,0),{title:n,description:r})}}var fC=`/api/v1`,pC=/^[A-Za-z0-9_-]{16}$/,mC=/^[A-Za-z0-9_-]{43}$/;function hC(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``)}function gC(){return hC(crypto.getRandomValues(new Uint8Array(32)))}function _C(e){return e.match(/^\/d\/([A-Za-z0-9_-]{16})\/?$/)?.[1]??null}function vC(e,t){if(!pC.test(t))throw Error(`Invalid diagram alias.`);return new URL(`/d/${t}`,e).toString()}function yC(e,t,n){if(!mC.test(n))throw Error(`Invalid write capability.`);let r=new URL(vC(e,t));return r.hash=`w=${n}`,r.toString()}function bC(e,t,n=`svg`){if(!pC.test(t))throw Error(`Invalid diagram alias.`);if(n!==`svg`&&n!==`png`)throw Error(`Invalid render format.`);return new URL(`${fC}/aliases/${t}/renders/${n}`,e).toString()}function xC(e){return e.match(/^#w=([A-Za-z0-9_-]{43})$/)?.[1]??null}var SC=class extends Error{constructor(e,{status:t,code:n}={}){super(e),this.status=t,this.code=n}};function CC(e){if(!e||typeof e!=`object`||typeof e.type!=`string`||typeof e.source!=`string`)throw Error(`Invalid diagram state.`);return{mode:`open`,diagram:{type:e.type,source:e.source,options:e.options??{},presentation:sC(e.presentation)},metadata:oC(e.meta)}}function wC(e){if(!e||typeof e!=`object`||!pC.test(e.aliasId)||!mC.test(e.contentId)||!mC.test(e.renderId)||!Number.isSafeInteger(e.revision)||e.mode!==`open`&&e.mode!==`locked`)throw new SC(`The persistence service returned an invalid response.`,{code:`invalid_response`});let t={aliasId:e.aliasId,contentId:e.contentId,renderId:e.renderId,revision:e.revision,mode:e.mode,createdAt:e.createdAt,updatedAt:e.updatedAt};if(e.mode===`locked`){if(!e.encryptedContent||!e.encryptedMetadata||!e.keyEnvelope)throw new SC(`The persistence service returned an invalid locked diagram.`,{code:`invalid_response`});return{...t,encryptedContent:e.encryptedContent,encryptedMetadata:e.encryptedMetadata,keyEnvelope:e.keyEnvelope}}if(!e.diagram||typeof e.diagram!=`object`)throw new SC(`The persistence service returned an invalid open diagram.`,{code:`invalid_response`});return{...t,state:{type:e.diagram.type,source:e.diagram.source,options:e.diagram.options??{},presentation:sC(e.diagram.presentation),meta:oC(e.metadata)}}}var TC=class{constructor({fetchImpl:e=globalThis.fetch,origin:t=globalThis.location?.origin??`http://localhost`,apiPrefix:n=fC}={}){this.fetch=(...t)=>e(...t),this.origin=t,this.apiPrefix=n.replace(/\/$/,``)}url(e=``){return new URL(`${this.apiPrefix}/aliases${e}`,this.origin)}async request(e,t){let n=await this.fetch(e,t),r=await n.json().catch(()=>null);if(!n.ok)throw new SC(r?.error?.message||`Persistence request failed with HTTP ${n.status}.`,{status:n.status,code:r?.error?.code});return wC(r)}async errorFor(e){let t=await e.json().catch(()=>null);return new SC(t?.error?.message||`Persistence request failed with HTTP ${e.status}.`,{status:e.status,code:t?.error?.code})}async createPayload(e){let t=gC();return{...await this.request(this.url(),{method:`POST`,headers:{Authorization:`Bearer ${t}`,"Content-Type":`application/json`},body:JSON.stringify(e)}),writeCapability:t}}createAlias(e){return this.createPayload(CC(e))}createLockedAlias(e){if(e?.mode!==`locked`)throw Error(`Invalid locked diagram payload.`);return this.createPayload(e)}getAlias(e){if(!pC.test(e))throw Error(`Invalid diagram alias.`);return this.request(this.url(`/${e}`),{headers:{Accept:`application/json`}})}updatePayload(e,t,n,r){if(!pC.test(e))throw Error(`Invalid diagram alias.`);if(!Number.isSafeInteger(n)||n<1)throw Error(`Invalid alias revision.`);if(!mC.test(r))throw Error(`Invalid write capability.`);return this.request(this.url(`/${e}`),{method:`PUT`,headers:{Accept:`application/json`,Authorization:`Bearer ${r}`,"Content-Type":`application/json`,"If-Match":`"${n}"`},body:JSON.stringify(t)})}updateAlias(e,t,n,r){return this.updatePayload(e,CC(t),n,r)}updateLockedAlias(e,t,n,r){if(t?.mode!==`locked`)throw Error(`Invalid locked diagram payload.`);return this.updatePayload(e,t,n,r)}async uploadRender({aliasId:e,renderId:t,revision:n,writeCapability:r,format:i,mode:a,render:o}){if(!pC.test(e))throw Error(`Invalid diagram alias.`);if(!mC.test(t))throw Error(`Invalid render ID.`);if(!Number.isSafeInteger(n)||n<1)throw Error(`Invalid alias revision.`);if(!mC.test(r))throw Error(`Invalid write capability.`);if(i!==`svg`&&i!==`png`)throw Error(`Invalid render format.`);if(a!==`open`&&a!==`locked`)throw Error(`Invalid diagram mode.`);let s=a===`locked`?`application/json`:i===`svg`?`image/svg+xml`:`image/png`,c=a===`locked`?JSON.stringify(o):o,l=await this.fetch(this.url(`/${e}/renders/${i}`),{method:`PUT`,headers:{Authorization:`Bearer ${r}`,"Content-Type":s,"If-Match":`"${n}"`,"X-Render-Id":t},body:c});if(!l.ok)throw await this.errorFor(l)}async getEncryptedRender(e,t){if(!pC.test(e))throw Error(`Invalid diagram alias.`);if(t!==`svg`&&t!==`png`)throw Error(`Invalid render format.`);let n=this.url(`/${e}/renders/${t}`);n.searchParams.set(`encrypted`,`1`);let r=await this.fetch(n,{headers:{Accept:`application/json`}});if(!r.ok)throw await this.errorFor(r);return r.json()}},EC=6e5,DC=new TextEncoder,OC=new TextDecoder,kC=DC.encode(`diagram.zip:content:v1`),AC=DC.encode(`diagram.zip:metadata:v1`),jC=DC.encode(`diagram.zip:bundle-key:v1`);function MC(e){if(e!==`svg`&&e!==`png`)throw Error(`Invalid render format.`);return DC.encode(`diagram.zip:render:${e}:v1`)}function NC(e){let t=``,n=32768;for(let r=0;r<e.length;r+=n)t+=String.fromCharCode(...e.subarray(r,r+n));return btoa(t).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``)}function PC(e){if(typeof e!=`string`||!/^[A-Za-z0-9_-]+$/.test(e))throw Error(`Invalid encrypted diagram.`);let t=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=t+`=`.repeat((4-t.length%4)%4),r=atob(n),i=Uint8Array.from(r,e=>e.charCodeAt(0));if(NC(i)!==e)throw Error(`Invalid encrypted diagram.`);return i}function FC(e){return crypto.getRandomValues(new Uint8Array(e))}async function IC(e,t){if(!(e instanceof Uint8Array)||e.byteLength!==32)throw Error(`Invalid diagram key.`);return crypto.subtle.importKey(`raw`,e,{name:`AES-GCM`},!1,t)}async function LC(e,{iterations:t,salt:n}){if(typeof e!=`string`)throw Error(`Password is required.`);let r=await crypto.subtle.importKey(`raw`,DC.encode(e),`PBKDF2`,!1,[`deriveKey`]);return crypto.subtle.deriveKey({name:`PBKDF2`,hash:`SHA-256`,iterations:t,salt:n},r,{name:`AES-GCM`,length:256},!1,[`encrypt`,`decrypt`])}async function RC(e,t,n){let r=FC(12),i=await crypto.subtle.encrypt({name:`AES-GCM`,iv:r,additionalData:n,tagLength:128},t,e);return{v:1,alg:`A256GCM`,iv:NC(r),ciphertext:NC(new Uint8Array(i))}}async function zC(e,t,n){if(!e||e.v!==1||e.alg!==`A256GCM`)throw Error(`Invalid encrypted diagram.`);let r=PC(e.iv);if(r.byteLength!==12)throw Error(`Invalid encrypted diagram.`);let i=PC(e.ciphertext);if(i.byteLength<16)throw Error(`Invalid encrypted diagram.`);return new Uint8Array(await crypto.subtle.decrypt({name:`AES-GCM`,iv:r,additionalData:n,tagLength:128},t,i))}async function BC(e,t,n){return RC(DC.encode(JSON.stringify(e)),t,n)}async function VC(e,t,n){return JSON.parse(OC.decode(await zC(e,t,n)))}function HC(e,t){if(!e||typeof e!=`object`||!yx(e.type)||typeof e.source!=`string`||!e.options||typeof e.options!=`object`||Array.isArray(e.options))throw Error(`Invalid encrypted diagram.`);return{type:e.type,source:e.source,options:e.options,presentation:sC(e.presentation),meta:oC(t)}}async function UC(e,t){if(typeof t!=`string`||t.length<8)throw Error(`Password must be at least 8 characters.`);let n=FC(16),r={name:`PBKDF2`,hash:`SHA-256`,iterations:EC,salt:NC(n)};return{v:1,kdf:r,wrap:await RC(e,await LC(t,{iterations:r.iterations,salt:n}),jC)}}async function WC(e,t){if(!e||e.v!==1||e.kdf?.name!==`PBKDF2`||e.kdf.hash!==`SHA-256`||!Number.isInteger(e.kdf.iterations)||e.kdf.iterations<1e5||e.kdf.iterations>5e6)throw Error(`Invalid encrypted diagram.`);let n=PC(e.kdf.salt);if(n.byteLength!==16)throw Error(`Invalid encrypted diagram.`);let r=await LC(t,{iterations:e.kdf.iterations,salt:n}),i=await zC(e.wrap,r,jC);if(i.byteLength!==32)throw Error(`Invalid encrypted diagram.`);return i}async function GC(e,t,n){let r=HC({type:e.type,source:e.source,options:e.options??{},presentation:e.presentation},e.meta),i=await IC(t,[`encrypt`]);return{mode:`locked`,encryptedContent:await BC({type:r.type,source:r.source,options:r.options,presentation:r.presentation},i,kC),encryptedMetadata:await BC(r.meta,i,AC),keyEnvelope:n}}async function KC(e,t){let n=FC(32);return{bundleKey:n,payload:await GC(e,n,await UC(n,t))}}async function qC(e,t){if(!e||e.mode!==`locked`)throw Error(`Invalid encrypted diagram.`);try{let n=await WC(e.keyEnvelope,t),r=await IC(n,[`decrypt`]),[i,a]=await Promise.all([VC(e.encryptedContent,r,kC),VC(e.encryptedMetadata,r,AC)]);return{bundleKey:n,state:HC(i,a)}}catch{throw Error(`Wrong password or damaged diagram.`)}}async function JC(e,t,n){if(!(e instanceof Blob))throw Error(`Invalid rendered image.`);let r=await IC(t,[`encrypt`]);return RC(new Uint8Array(await e.arrayBuffer()),r,MC(n))}function YC(e,t,n){return Math.min(Math.max(e,t),n)}var XC=class{constructor({stage:e,image:t,status:n,minimap:r,minimapImage:i,minimapViewport:a}){this.stage=e,this.image=t,this.status=n,this.minimap=r,this.minimapImage=i,this.minimapViewport=a,this.scale=1,this.x=0,this.y=0,this.objectUrl=null,this.abortController=null,this.requestNumber=0,this.pendingRender=null,this.renderLoop=null,this.latestRenderKey=null,this.latestSvgBlob=null,this.imageFallbackUrl=null,this.activeImageUrl=null,this.drag=null,this.image.addEventListener(`load`,()=>{this.image.src===this.activeImageUrl&&(this.imageFallbackUrl=null,this.setStatus(`Rendered`,`ready`),this.fit())}),this.image.addEventListener(`error`,()=>this.retryBlockedImage()),this.stage.addEventListener(`pointerdown`,e=>this.startPan(e)),this.stage.addEventListener(`pointermove`,e=>this.pan(e)),this.stage.addEventListener(`pointerup`,e=>this.endPan(e)),this.stage.addEventListener(`pointercancel`,e=>this.endPan(e)),this.stage.addEventListener(`wheel`,e=>this.wheelZoom(e),{passive:!1}),this.minimap.addEventListener(`pointerdown`,e=>this.moveFromMinimap(e)),new ResizeObserver(()=>this.updateTransform()).observe(this.stage)}render({type:e,source:t,options:n={},meta:r={},presentation:i={}}){if(!t.trim()){this.requestNumber++,this.pendingRender=null,this.abortController?.abort(),this.setStatus(`Write something to render.`,`idle`);return}let a=JSON.stringify({type:e,source:t,options:n,meta:r,presentation:i});return this.latestRenderKey===a&&this.latestSvgBlob?Promise.resolve():(this.pendingRender={type:e,source:t,options:n,meta:r,presentation:i,renderKey:a,requestNumber:++this.requestNumber},this.abortController?.abort(),this.renderLoop||=this.drainRenderQueue(),this.renderLoop)}async drainRenderQueue(){try{for(;this.pendingRender;){let e=this.pendingRender;this.pendingRender=null,await this.performRender(e)}}finally{this.renderLoop=null,this.pendingRender&&(this.renderLoop=this.drainRenderQueue())}}async performRender({type:e,source:t,options:n,meta:r,presentation:i,renderKey:a,requestNumber:o}){let s=new AbortController;this.abortController=s,this.setStatus(`Rendering…`,`loading`),this.stage.style.setProperty(`--render-background`,`var(--preview-bg)`);try{let c=await fetch(`/render/v1/svg`,{method:`POST`,headers:{Accept:`image/svg+xml`,"Content-Type":`application/json`},body:JSON.stringify({engine:e,source:t,format:`svg`,options:n,metadata:r,presentation:i}),signal:s.signal});if(o!==this.requestNumber)return;if(!c.ok)throw Error(await this.errorMessage(c));this.status.dataset.cache=c.headers.get(`X-Diagram-Cache`)?.toLowerCase()??`browser`;let{blob:l,background:u}=this.normalizedSvgBlob(await c.text());this.latestRenderKey=a,this.latestSvgBlob=l,this.imageFallbackUrl=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(await l.text())}`,this.stage.style.setProperty(`--render-background`,u);let d=URL.createObjectURL(l),f=this.objectUrl;this.objectUrl=d,this.activeImageUrl=d,this.image.src=d,this.minimapImage.src=d,this.image.hidden=!1,this.setStatus(`Loading image…`,`loading`),f&&URL.revokeObjectURL(f)}catch(e){if(e.name===`AbortError`)return;this.setStatus(e.message||`Could not render this diagram.`,`error`)}finally{this.abortController===s&&(this.abortController=null)}}retryBlockedImage(){if(this.image.src===this.activeImageUrl){if(this.imageFallbackUrl&&this.activeImageUrl.startsWith(`blob:`)){let e=this.imageFallbackUrl;this.imageFallbackUrl=null,this.objectUrl&&URL.revokeObjectURL(this.objectUrl),this.objectUrl=null,this.activeImageUrl=e,this.image.src=e,this.minimapImage.src=e;return}this.setStatus(`The rendered image was blocked or invalid.`,`error`)}}svgBlobFor(e){let{type:t,source:n,options:r={},meta:i={},presentation:a={}}=e,o=JSON.stringify({type:t,source:n,options:r,meta:i,presentation:a});return this.latestRenderKey===o?this.latestSvgBlob:null}async errorMessage(e){let t=await e.text();if(e.headers.get(`Content-Type`)?.includes(`application/json`))try{let e=JSON.parse(t);if(typeof e?.error?.message==`string`)return e.error.message}catch{}return new DOMParser().parseFromString(t,`text/html`).body.textContent.trim().replace(/\s+/g,` `)||`Render failed with HTTP ${e.status}.`}normalizedSvgBlob(e){let t=new DOMParser().parseFromString(e,`image/svg+xml`).documentElement,n=t.getAttribute(`viewBox`)?.trim().split(/[\s,]+/).map(Number);t.nodeName===`svg`&&n?.length===4&&n.every(Number.isFinite)&&(t.hasAttribute(`width`)||t.setAttribute(`width`,String(n[2])),t.hasAttribute(`height`)||t.setAttribute(`height`,String(n[3])),e=new XMLSerializer().serializeToString(t));let r=t.style.backgroundColor||t.style.background,i=r&&r!==`transparent`?r:`#ffffff`;return{blob:new Blob([e],{type:`image/svg+xml`}),background:i}}setStatus(e,t){this.status.textContent=e,this.status.dataset.state=t}zoom(e){if(this.image.hidden)return;let t=this.stage.getBoundingClientRect();this.zoomAt(e,t.width/2,t.height/2)}zoomAt(e,t,n){let r=YC(this.scale*e,.1,8),i=(t-this.x)/this.scale,a=(n-this.y)/this.scale;this.x=t-i*r,this.y=n-a*r,this.scale=r,this.updateTransform()}wheelZoom(e){if(this.image.hidden||e.target.closest(`.preview-toolbar, .minimap`))return;e.preventDefault();let t=this.stage.getBoundingClientRect(),n=e.deltaY*(e.deltaMode===WheelEvent.DOM_DELTA_LINE?16:e.deltaMode===WheelEvent.DOM_DELTA_PAGE?t.height:1);this.zoomAt(Math.exp(-YC(n,-240,240)*.002),e.clientX-t.left,e.clientY-t.top)}oneToOne(){if(this.image.hidden)return;let e=this.stage.getBoundingClientRect();this.scale=1,this.x=(e.width-this.image.naturalWidth)/2,this.y=(e.height-this.image.naturalHeight)/2,this.updateTransform()}fit(){if(!this.image.naturalWidth||!this.image.naturalHeight)return;let e=this.stage.getBoundingClientRect(),t=Math.min(64,e.width*.12);this.scale=YC(Math.min((e.width-t*2)/this.image.naturalWidth,(e.height-t*2)/this.image.naturalHeight),.1,2),this.x=(e.width-this.image.naturalWidth*this.scale)/2,this.y=(e.height-this.image.naturalHeight*this.scale)/2,this.updateTransform()}updateTransform(){this.image.style.transform=`translate(${this.x}px, ${this.y}px) scale(${this.scale})`,this.updateMinimap()}startPan(e){this.image.hidden||e.button!==0||e.target.closest(`.preview-toolbar, .minimap`)||(this.drag={pointerId:e.pointerId,x:e.clientX,y:e.clientY,originX:this.x,originY:this.y},this.stage.setPointerCapture(e.pointerId),this.stage.dataset.dragging=`true`)}pan(e){!this.drag||e.pointerId!==this.drag.pointerId||(this.x=this.drag.originX+e.clientX-this.drag.x,this.y=this.drag.originY+e.clientY-this.drag.y,this.updateTransform())}endPan(e){!this.drag||e.pointerId!==this.drag.pointerId||(this.drag=null,delete this.stage.dataset.dragging)}moveFromMinimap(e){if(!this.image.naturalWidth||!this.image.naturalHeight)return;let t=this.minimap.getBoundingClientRect(),n=Math.min(t.width/this.image.naturalWidth,t.height/this.image.naturalHeight),r=(t.width-this.image.naturalWidth*n)/2,i=(t.height-this.image.naturalHeight*n)/2,a=(e.clientX-t.left-r)/n,o=(e.clientY-t.top-i)/n,s=this.stage.getBoundingClientRect();this.x=s.width/2-a*this.scale,this.y=s.height/2-o*this.scale,this.updateTransform()}updateMinimap(){if(!this.image.naturalWidth||!this.image.naturalHeight)return;let e=this.stage.getBoundingClientRect(),t=this.image.naturalWidth*this.scale,n=this.image.naturalHeight*this.scale,r=this.x>=0&&this.y>=0&&this.x+t<=e.width&&this.y+n<=e.height;if(this.minimap.hidden=r,r)return;let i=this.minimap.getBoundingClientRect(),a=Math.min(i.width/this.image.naturalWidth,i.height/this.image.naturalHeight),o=(i.width-this.image.naturalWidth*a)/2,s=(i.height-this.image.naturalHeight*a)/2,c=YC(-this.x/this.scale,0,this.image.naturalWidth),l=YC(-this.y/this.scale,0,this.image.naturalHeight),u=YC((e.width-this.x)/this.scale,0,this.image.naturalWidth),d=YC((e.height-this.y)/this.scale,0,this.image.naturalHeight);this.minimapViewport.style.left=`${o+c*a}px`,this.minimapViewport.style.top=`${s+l*a}px`,this.minimapViewport.style.width=`${Math.max(4,(u-c)*a)}px`,this.minimapViewport.style.height=`${Math.max(4,(d-l)*a)}px`}},ZC=`diagram.zip:draft:local:v2`,QC=`diagram.zip:draft:alias:v1:`,$C=`diagram.zip:write:v1:`,ew=new Oe,tw=new TC,nw,rw=!1,iw,aw=!1,ow=``,Q={aliasId:null,contentId:null,renderId:null,revision:null,mode:`open`,savedMode:null,writeCapability:null,savedState:null,savedSnapshot:null,bundleKey:null,keyEnvelope:null,encryptedContent:null,encryptedMetadata:null,keyEnvelopeDirty:!1,dirty:!0},sw=new Map;document.querySelector(`#app`).innerHTML=`1643+ +-----------------+`,d2:Sx,nwdiag:kx,packetdiag:Ax,rackdiag:Mx,bpmn:bx.replaceAll(`Examine Situation`,`Bob Agent tests Tandem`).replaceAll(`Things OK?`,`Tests pass?`).replaceAll(`Notification Sent`,`Alice and Bob share tasks`).replaceAll(`Error Propagated`,`Alice Agent revises flow`).replaceAll(`Klaus`,`Alice`).replaceAll(`Walter`,`Bob`),bytefield:xx,dbml:Cx,diagramsnet:wx,erd:Tx,excalidraw:Ex,goat:Dx,nomnoml:Ox,pikchr:jx,svgbob:Nx,symbolator:Px,tikz:Fx,umlet:Ix,vega:Lx,vegalite:Rx,wavedrom:zx,wireviz:Bx},lC={plantuml:[`Tandem task sync`,`Alice and Bob coordinate shared tasks through their agents.`],mermaid:[`Shared task flow`,`A compact view of ideas moving through the Tandem team.`],graphviz:[`Tandem collaboration graph`,`People and agents converge on one synchronized task list.`],d2:[`Connection styles`,`D2 connection, arrowhead, shape, and animation syntax.`],c4plantuml:[`Tandem system context`,`The people and software systems surrounding Tandem.`],blockdiag:[`Tandem block flow`,`A block-oriented view of the shared task workflow.`],seqdiag:[`Tandem task sequence`,`A task moves from Alice to Bob and back through Tandem.`],actdiag:[`Tandem delivery activities`,`Work crosses Alice, Alice Agent, and Bob Agent lanes.`],nwdiag:[`Network layout`,`A sample network topology and its connected address ranges.`],packetdiag:[`TCP packet structure`,`The bit-level layout of a TCP packet header.`],rackdiag:[`Rack layout`,`A sample equipment rack rendered from rackdiag syntax.`],bpmn:[`Agent-assisted review`,`A BPMN review flow that branches on test results.`],bytefield:[`Byte field layout`,`A compact byte-oriented protocol field example.`],dbml:[`Database schema`,`Tables and relationships expressed with DBML.`],diagramsnet:[`Venn diagram`,`An overlapping-set example imported from diagrams.net.`],ditaa:[`Tandem ASCII architecture`,`An ASCII sketch becomes a rendered system diagram.`],erd:[`Data model`,`Entities and relationships in a small relational schema.`],excalidraw:[`Venn sketch`,`A hand-drawn overlapping-set diagram.`],goat:[`Component shapes`,`GoAT exercises boxes, connectors, curves, and symbols.`],nomnoml:[`Pirate class diagram`,`A playful class diagram demonstrating Nomnoml syntax.`],pikchr:[`Cardinal points`,`A diamond marks north, east, south, and west.`],structurizr:[`Tandem landscape`,`A software landscape linking people, agents, and Tandem.`],svgbob:[`Cloud network`,`An ASCII cloud connects clients and services.`],symbolator:[`Hardware component`,`A component symbol generated from interface declarations.`],tikz:[`Periodic table`,`A color-coded periodic table rendered with TikZ.`],umlet:[`UML example`,`A UMLet document rendered directly from its XML form.`],vega:[`Bar chart`,`A declarative bar chart expressed with Vega.`],vegalite:[`Discretizing scale`,`A Vega-Lite example comparing discretized values.`],wavedrom:[`Timing diagram`,`Digital signals and timing relationships rendered by WaveDrom.`],wireviz:[`Wiring harness`,`Connections, cables, and pins described with WireViz.`]};function uC(e){return cC[e]??aC}function dC(e){let t=px.findIndex(t=>t.id===e),[n,r]=lC[e]??[`Diagram example`,`A diagram.zip catalog example.`];return{type:e,source:uC(e),options:{},...Hx(Math.max(t,0),{title:n,description:r})}}function fC(e,t,n,r,i){return e.set(t,{...r,type:t}),e.get(n)??i(n)}var pC=`/api/v1`,mC=/^[A-Za-z0-9_-]{16}$/,hC=/^[A-Za-z0-9_-]{43}$/;function gC(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``)}function _C(){return gC(crypto.getRandomValues(new Uint8Array(32)))}function vC(e){return e.match(/^\/d\/([A-Za-z0-9_-]{16})\/?$/)?.[1]??null}function yC(e,t){if(!mC.test(t))throw Error(`Invalid diagram alias.`);return new URL(`/d/${t}`,e).toString()}function bC(e,t,n){if(!hC.test(n))throw Error(`Invalid write capability.`);let r=new URL(yC(e,t));return r.hash=`w=${n}`,r.toString()}function xC(e,t,n=`svg`){if(!mC.test(t))throw Error(`Invalid diagram alias.`);if(n!==`svg`&&n!==`png`)throw Error(`Invalid render format.`);return new URL(`${pC}/aliases/${t}/renders/${n}`,e).toString()}function SC(e){return e.match(/^#w=([A-Za-z0-9_-]{43})$/)?.[1]??null}var CC=class extends Error{constructor(e,{status:t,code:n}={}){super(e),this.status=t,this.code=n}};function wC(e){if(!e||typeof e!=`object`||typeof e.type!=`string`||typeof e.source!=`string`)throw Error(`Invalid diagram state.`);return{mode:`open`,diagram:{type:e.type,source:e.source,options:e.options??{},presentation:sC(e.presentation)},metadata:oC(e.meta)}}function TC(e){if(!e||typeof e!=`object`||!mC.test(e.aliasId)||!hC.test(e.contentId)||!hC.test(e.renderId)||!Number.isSafeInteger(e.revision)||e.mode!==`open`&&e.mode!==`locked`)throw new CC(`The persistence service returned an invalid response.`,{code:`invalid_response`});let t={aliasId:e.aliasId,contentId:e.contentId,renderId:e.renderId,revision:e.revision,mode:e.mode,createdAt:e.createdAt,updatedAt:e.updatedAt};if(e.mode===`locked`){if(!e.encryptedContent||!e.encryptedMetadata||!e.keyEnvelope)throw new CC(`The persistence service returned an invalid locked diagram.`,{code:`invalid_response`});return{...t,encryptedContent:e.encryptedContent,encryptedMetadata:e.encryptedMetadata,keyEnvelope:e.keyEnvelope}}if(!e.diagram||typeof e.diagram!=`object`)throw new CC(`The persistence service returned an invalid open diagram.`,{code:`invalid_response`});return{...t,state:{type:e.diagram.type,source:e.diagram.source,options:e.diagram.options??{},presentation:sC(e.diagram.presentation),meta:oC(e.metadata)}}}var EC=class{constructor({fetchImpl:e=globalThis.fetch,origin:t=globalThis.location?.origin??`http://localhost`,apiPrefix:n=pC}={}){this.fetch=(...t)=>e(...t),this.origin=t,this.apiPrefix=n.replace(/\/$/,``)}url(e=``){return new URL(`${this.apiPrefix}/aliases${e}`,this.origin)}async request(e,t){let n=await this.fetch(e,t),r=await n.json().catch(()=>null);if(!n.ok)throw new CC(r?.error?.message||`Persistence request failed with HTTP ${n.status}.`,{status:n.status,code:r?.error?.code});return TC(r)}async errorFor(e){let t=await e.json().catch(()=>null);return new CC(t?.error?.message||`Persistence request failed with HTTP ${e.status}.`,{status:e.status,code:t?.error?.code})}async createPayload(e){let t=_C();return{...await this.request(this.url(),{method:`POST`,headers:{Authorization:`Bearer ${t}`,"Content-Type":`application/json`},body:JSON.stringify(e)}),writeCapability:t}}createAlias(e){return this.createPayload(wC(e))}createLockedAlias(e){if(e?.mode!==`locked`)throw Error(`Invalid locked diagram payload.`);return this.createPayload(e)}getAlias(e){if(!mC.test(e))throw Error(`Invalid diagram alias.`);return this.request(this.url(`/${e}`),{headers:{Accept:`application/json`}})}updatePayload(e,t,n,r){if(!mC.test(e))throw Error(`Invalid diagram alias.`);if(!Number.isSafeInteger(n)||n<1)throw Error(`Invalid alias revision.`);if(!hC.test(r))throw Error(`Invalid write capability.`);return this.request(this.url(`/${e}`),{method:`PUT`,headers:{Accept:`application/json`,Authorization:`Bearer ${r}`,"Content-Type":`application/json`,"If-Match":`"${n}"`},body:JSON.stringify(t)})}updateAlias(e,t,n,r){return this.updatePayload(e,wC(t),n,r)}updateLockedAlias(e,t,n,r){if(t?.mode!==`locked`)throw Error(`Invalid locked diagram payload.`);return this.updatePayload(e,t,n,r)}async uploadRender({aliasId:e,renderId:t,revision:n,writeCapability:r,format:i,mode:a,render:o}){if(!mC.test(e))throw Error(`Invalid diagram alias.`);if(!hC.test(t))throw Error(`Invalid render ID.`);if(!Number.isSafeInteger(n)||n<1)throw Error(`Invalid alias revision.`);if(!hC.test(r))throw Error(`Invalid write capability.`);if(i!==`svg`&&i!==`png`)throw Error(`Invalid render format.`);if(a!==`open`&&a!==`locked`)throw Error(`Invalid diagram mode.`);let s=a===`locked`?`application/json`:i===`svg`?`image/svg+xml`:`image/png`,c=a===`locked`?JSON.stringify(o):o,l=await this.fetch(this.url(`/${e}/renders/${i}`),{method:`PUT`,headers:{Authorization:`Bearer ${r}`,"Content-Type":s,"If-Match":`"${n}"`,"X-Render-Id":t},body:c});if(!l.ok)throw await this.errorFor(l)}async getEncryptedRender(e,t){if(!mC.test(e))throw Error(`Invalid diagram alias.`);if(t!==`svg`&&t!==`png`)throw Error(`Invalid render format.`);let n=this.url(`/${e}/renders/${t}`);n.searchParams.set(`encrypted`,`1`);let r=await this.fetch(n,{headers:{Accept:`application/json`}});if(!r.ok)throw await this.errorFor(r);return r.json()}},DC=6e5,OC=new TextEncoder,kC=new TextDecoder,AC=OC.encode(`diagram.zip:content:v1`),jC=OC.encode(`diagram.zip:metadata:v1`),MC=OC.encode(`diagram.zip:bundle-key:v1`);function NC(e){if(e!==`svg`&&e!==`png`)throw Error(`Invalid render format.`);return OC.encode(`diagram.zip:render:${e}:v1`)}function PC(e){let t=``,n=32768;for(let r=0;r<e.length;r+=n)t+=String.fromCharCode(...e.subarray(r,r+n));return btoa(t).replaceAll(`+`,`-`).replaceAll(`/`,`_`).replace(/=+$/,``)}function FC(e){if(typeof e!=`string`||!/^[A-Za-z0-9_-]+$/.test(e))throw Error(`Invalid encrypted diagram.`);let t=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=t+`=`.repeat((4-t.length%4)%4),r=atob(n),i=Uint8Array.from(r,e=>e.charCodeAt(0));if(PC(i)!==e)throw Error(`Invalid encrypted diagram.`);return i}function IC(e){return crypto.getRandomValues(new Uint8Array(e))}async function LC(e,t){if(!(e instanceof Uint8Array)||e.byteLength!==32)throw Error(`Invalid diagram key.`);return crypto.subtle.importKey(`raw`,e,{name:`AES-GCM`},!1,t)}async function RC(e,{iterations:t,salt:n}){if(typeof e!=`string`)throw Error(`Password is required.`);let r=await crypto.subtle.importKey(`raw`,OC.encode(e),`PBKDF2`,!1,[`deriveKey`]);return crypto.subtle.deriveKey({name:`PBKDF2`,hash:`SHA-256`,iterations:t,salt:n},r,{name:`AES-GCM`,length:256},!1,[`encrypt`,`decrypt`])}async function zC(e,t,n){let r=IC(12),i=await crypto.subtle.encrypt({name:`AES-GCM`,iv:r,additionalData:n,tagLength:128},t,e);return{v:1,alg:`A256GCM`,iv:PC(r),ciphertext:PC(new Uint8Array(i))}}async function BC(e,t,n){if(!e||e.v!==1||e.alg!==`A256GCM`)throw Error(`Invalid encrypted diagram.`);let r=FC(e.iv);if(r.byteLength!==12)throw Error(`Invalid encrypted diagram.`);let i=FC(e.ciphertext);if(i.byteLength<16)throw Error(`Invalid encrypted diagram.`);return new Uint8Array(await crypto.subtle.decrypt({name:`AES-GCM`,iv:r,additionalData:n,tagLength:128},t,i))}async function VC(e,t,n){return zC(OC.encode(JSON.stringify(e)),t,n)}async function HC(e,t,n){return JSON.parse(kC.decode(await BC(e,t,n)))}function UC(e,t){if(!e||typeof e!=`object`||!yx(e.type)||typeof e.source!=`string`||!e.options||typeof e.options!=`object`||Array.isArray(e.options))throw Error(`Invalid encrypted diagram.`);return{type:e.type,source:e.source,options:e.options,presentation:sC(e.presentation),meta:oC(t)}}async function WC(e,t){if(typeof t!=`string`||t.length<8)throw Error(`Password must be at least 8 characters.`);let n=IC(16),r={name:`PBKDF2`,hash:`SHA-256`,iterations:DC,salt:PC(n)};return{v:1,kdf:r,wrap:await zC(e,await RC(t,{iterations:r.iterations,salt:n}),MC)}}async function GC(e,t){if(!e||e.v!==1||e.kdf?.name!==`PBKDF2`||e.kdf.hash!==`SHA-256`||!Number.isInteger(e.kdf.iterations)||e.kdf.iterations<1e5||e.kdf.iterations>5e6)throw Error(`Invalid encrypted diagram.`);let n=FC(e.kdf.salt);if(n.byteLength!==16)throw Error(`Invalid encrypted diagram.`);let r=await RC(t,{iterations:e.kdf.iterations,salt:n}),i=await BC(e.wrap,r,MC);if(i.byteLength!==32)throw Error(`Invalid encrypted diagram.`);return i}async function KC(e,t,n){let r=UC({type:e.type,source:e.source,options:e.options??{},presentation:e.presentation},e.meta),i=await LC(t,[`encrypt`]);return{mode:`locked`,encryptedContent:await VC({type:r.type,source:r.source,options:r.options,presentation:r.presentation},i,AC),encryptedMetadata:await VC(r.meta,i,jC),keyEnvelope:n}}async function qC(e,t){let n=IC(32);return{bundleKey:n,payload:await KC(e,n,await WC(n,t))}}async function JC(e,t){if(!e||e.mode!==`locked`)throw Error(`Invalid encrypted diagram.`);try{let n=await GC(e.keyEnvelope,t),r=await LC(n,[`decrypt`]),[i,a]=await Promise.all([HC(e.encryptedContent,r,AC),HC(e.encryptedMetadata,r,jC)]);return{bundleKey:n,state:UC(i,a)}}catch{throw Error(`Wrong password or damaged diagram.`)}}async function YC(e,t,n){if(!(e instanceof Blob))throw Error(`Invalid rendered image.`);let r=await LC(t,[`encrypt`]);return zC(new Uint8Array(await e.arrayBuffer()),r,NC(n))}function XC(e,t,n){return Math.min(Math.max(e,t),n)}var ZC=class{constructor({stage:e,image:t,status:n,minimap:r,minimapImage:i,minimapViewport:a}){this.stage=e,this.image=t,this.status=n,this.minimap=r,this.minimapImage=i,this.minimapViewport=a,this.scale=1,this.x=0,this.y=0,this.objectUrl=null,this.abortController=null,this.requestNumber=0,this.pendingRender=null,this.renderLoop=null,this.latestRenderKey=null,this.latestSvgBlob=null,this.imageFallbackUrl=null,this.activeImageUrl=null,this.drag=null,this.image.addEventListener(`load`,()=>{this.image.src===this.activeImageUrl&&(this.imageFallbackUrl=null,this.setStatus(`Rendered`,`ready`),this.fit())}),this.image.addEventListener(`error`,()=>this.retryBlockedImage()),this.stage.addEventListener(`pointerdown`,e=>this.startPan(e)),this.stage.addEventListener(`pointermove`,e=>this.pan(e)),this.stage.addEventListener(`pointerup`,e=>this.endPan(e)),this.stage.addEventListener(`pointercancel`,e=>this.endPan(e)),this.stage.addEventListener(`wheel`,e=>this.wheelZoom(e),{passive:!1}),this.minimap.addEventListener(`pointerdown`,e=>this.moveFromMinimap(e)),new ResizeObserver(()=>this.updateTransform()).observe(this.stage)}render({type:e,source:t,options:n={},meta:r={},presentation:i={}}){if(!t.trim()){this.requestNumber++,this.pendingRender=null,this.abortController?.abort(),this.setStatus(`Write something to render.`,`idle`);return}let a=JSON.stringify({type:e,source:t,options:n,meta:r,presentation:i});return this.latestRenderKey===a&&this.latestSvgBlob?Promise.resolve():(this.pendingRender={type:e,source:t,options:n,meta:r,presentation:i,renderKey:a,requestNumber:++this.requestNumber},this.abortController?.abort(),this.renderLoop||=this.drainRenderQueue(),this.renderLoop)}async drainRenderQueue(){try{for(;this.pendingRender;){let e=this.pendingRender;this.pendingRender=null,await this.performRender(e)}}finally{this.renderLoop=null,this.pendingRender&&(this.renderLoop=this.drainRenderQueue())}}async performRender({type:e,source:t,options:n,meta:r,presentation:i,renderKey:a,requestNumber:o}){let s=new AbortController;this.abortController=s,this.setStatus(`Rendering…`,`loading`),this.stage.style.setProperty(`--render-background`,`var(--preview-bg)`);try{let c=await fetch(`/render/v1/svg`,{method:`POST`,headers:{Accept:`image/svg+xml`,"Content-Type":`application/json`},body:JSON.stringify({engine:e,source:t,format:`svg`,options:n,metadata:r,presentation:i}),signal:s.signal});if(o!==this.requestNumber)return;if(!c.ok)throw Error(await this.errorMessage(c));this.status.dataset.cache=c.headers.get(`X-Diagram-Cache`)?.toLowerCase()??`browser`;let{blob:l,background:u}=this.normalizedSvgBlob(await c.text());this.latestRenderKey=a,this.latestSvgBlob=l,this.imageFallbackUrl=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(await l.text())}`,this.stage.style.setProperty(`--render-background`,u);let d=URL.createObjectURL(l),f=this.objectUrl;this.objectUrl=d,this.activeImageUrl=d,this.image.src=d,this.minimapImage.src=d,this.image.hidden=!1,this.setStatus(`Loading image…`,`loading`),f&&URL.revokeObjectURL(f)}catch(e){if(e.name===`AbortError`)return;this.setStatus(e.message||`Could not render this diagram.`,`error`)}finally{this.abortController===s&&(this.abortController=null)}}retryBlockedImage(){if(this.image.src===this.activeImageUrl){if(this.imageFallbackUrl&&this.activeImageUrl.startsWith(`blob:`)){let e=this.imageFallbackUrl;this.imageFallbackUrl=null,this.objectUrl&&URL.revokeObjectURL(this.objectUrl),this.objectUrl=null,this.activeImageUrl=e,this.image.src=e,this.minimapImage.src=e;return}this.setStatus(`The rendered image was blocked or invalid.`,`error`)}}svgBlobFor(e){let{type:t,source:n,options:r={},meta:i={},presentation:a={}}=e,o=JSON.stringify({type:t,source:n,options:r,meta:i,presentation:a});return this.latestRenderKey===o?this.latestSvgBlob:null}async errorMessage(e){let t=await e.text();if(e.headers.get(`Content-Type`)?.includes(`application/json`))try{let e=JSON.parse(t);if(typeof e?.error?.message==`string`)return e.error.message}catch{}return new DOMParser().parseFromString(t,`text/html`).body.textContent.trim().replace(/\s+/g,` `)||`Render failed with HTTP ${e.status}.`}normalizedSvgBlob(e){let t=new DOMParser().parseFromString(e,`image/svg+xml`).documentElement,n=t.getAttribute(`viewBox`)?.trim().split(/[\s,]+/).map(Number);t.nodeName===`svg`&&n?.length===4&&n.every(Number.isFinite)&&(t.hasAttribute(`width`)||t.setAttribute(`width`,String(n[2])),t.hasAttribute(`height`)||t.setAttribute(`height`,String(n[3])),e=new XMLSerializer().serializeToString(t));let r=t.style.backgroundColor||t.style.background,i=r&&r!==`transparent`?r:`#ffffff`;return{blob:new Blob([e],{type:`image/svg+xml`}),background:i}}setStatus(e,t){this.status.textContent=e,this.status.dataset.state=t}zoom(e){if(this.image.hidden)return;let t=this.stage.getBoundingClientRect();this.zoomAt(e,t.width/2,t.height/2)}zoomAt(e,t,n){let r=XC(this.scale*e,.1,8),i=(t-this.x)/this.scale,a=(n-this.y)/this.scale;this.x=t-i*r,this.y=n-a*r,this.scale=r,this.updateTransform()}wheelZoom(e){if(this.image.hidden||e.target.closest(`.preview-toolbar, .minimap`))return;e.preventDefault();let t=this.stage.getBoundingClientRect(),n=e.deltaY*(e.deltaMode===WheelEvent.DOM_DELTA_LINE?16:e.deltaMode===WheelEvent.DOM_DELTA_PAGE?t.height:1);this.zoomAt(Math.exp(-XC(n,-240,240)*.002),e.clientX-t.left,e.clientY-t.top)}oneToOne(){if(this.image.hidden)return;let e=this.stage.getBoundingClientRect();this.scale=1,this.x=(e.width-this.image.naturalWidth)/2,this.y=(e.height-this.image.naturalHeight)/2,this.updateTransform()}fit(){if(!this.image.naturalWidth||!this.image.naturalHeight)return;let e=this.stage.getBoundingClientRect(),t=Math.min(64,e.width*.12);this.scale=XC(Math.min((e.width-t*2)/this.image.naturalWidth,(e.height-t*2)/this.image.naturalHeight),.1,2),this.x=(e.width-this.image.naturalWidth*this.scale)/2,this.y=(e.height-this.image.naturalHeight*this.scale)/2,this.updateTransform()}updateTransform(){this.image.style.transform=`translate(${this.x}px, ${this.y}px) scale(${this.scale})`,this.updateMinimap()}startPan(e){this.image.hidden||e.button!==0||e.target.closest(`.preview-toolbar, .minimap`)||(this.drag={pointerId:e.pointerId,x:e.clientX,y:e.clientY,originX:this.x,originY:this.y},this.stage.setPointerCapture(e.pointerId),this.stage.dataset.dragging=`true`)}pan(e){!this.drag||e.pointerId!==this.drag.pointerId||(this.x=this.drag.originX+e.clientX-this.drag.x,this.y=this.drag.originY+e.clientY-this.drag.y,this.updateTransform())}endPan(e){!this.drag||e.pointerId!==this.drag.pointerId||(this.drag=null,delete this.stage.dataset.dragging)}moveFromMinimap(e){if(!this.image.naturalWidth||!this.image.naturalHeight)return;let t=this.minimap.getBoundingClientRect(),n=Math.min(t.width/this.image.naturalWidth,t.height/this.image.naturalHeight),r=(t.width-this.image.naturalWidth*n)/2,i=(t.height-this.image.naturalHeight*n)/2,a=(e.clientX-t.left-r)/n,o=(e.clientY-t.top-i)/n,s=this.stage.getBoundingClientRect();this.x=s.width/2-a*this.scale,this.y=s.height/2-o*this.scale,this.updateTransform()}updateMinimap(){if(!this.image.naturalWidth||!this.image.naturalHeight)return;let e=this.stage.getBoundingClientRect(),t=this.image.naturalWidth*this.scale,n=this.image.naturalHeight*this.scale,r=this.x>=0&&this.y>=0&&this.x+t<=e.width&&this.y+n<=e.height;if(this.minimap.hidden=r,r)return;let i=this.minimap.getBoundingClientRect(),a=Math.min(i.width/this.image.naturalWidth,i.height/this.image.naturalHeight),o=(i.width-this.image.naturalWidth*a)/2,s=(i.height-this.image.naturalHeight*a)/2,c=XC(-this.x/this.scale,0,this.image.naturalWidth),l=XC(-this.y/this.scale,0,this.image.naturalHeight),u=XC((e.width-this.x)/this.scale,0,this.image.naturalWidth),d=XC((e.height-this.y)/this.scale,0,this.image.naturalHeight);this.minimapViewport.style.left=`${o+c*a}px`,this.minimapViewport.style.top=`${s+l*a}px`,this.minimapViewport.style.width=`${Math.max(4,(u-c)*a)}px`,this.minimapViewport.style.height=`${Math.max(4,(d-l)*a)}px`}},QC=`diagram.zip:draft:local:v2`,$C=`diagram.zip:draft:alias:v1:`,ew=`diagram.zip:write:v1:`,tw=new Oe,nw=new EC,rw,iw=!1,aw,ow=!1,sw=``,Q={aliasId:null,contentId:null,renderId:null,revision:null,mode:`open`,savedMode:null,writeCapability:null,savedState:null,savedSnapshot:null,bundleKey:null,keyEnvelope:null,encryptedContent:null,encryptedMetadata:null,keyEnvelopeDirty:!1,dirty:!0},cw=new Map;document.querySelector(`#app`).innerHTML=`16441644 <main class="app-shell">16451645 <header class="app-header">16461646 <a class="brand" href="/" aria-label="New diagram"><img class="brand-mark" src="/diagram.zip/icon.svg" alt=""><span>diagram.zip</span></a>@@ -1786,4 +1786,4 @@ Rel(bob, tandem, "Completes tasks")
17861786 </dialog>17871787 17881788 <div class="toast" id="toast" role="status" aria-live="polite"></div>1789-`;var cw=document.querySelector(`#diagram-type`),lw=document.querySelector(`#diagram-title`),uw=document.querySelector(`#diagram-description`),dw=document.querySelector(`#diagram-background`),fw=document.querySelector(`#diagram-padding`),pw=document.querySelector(`#diagram-frame`);for(let{id:e,label:t}of px)cw.add(new Option(t,e));var mw=await ww(),hw=oC(mw.meta),gw=sC(mw.presentation);cw.value=mw.type,lw.value=hw.title,uw.value=hw.description,dw.value=gw.background,fw.value=String(gw.padding),pw.checked=gw.frame,Qw(),iw=mw.type,sw.set(mw.type,mw);var _w=sd.define([{tag:q.keyword,color:`var(--syntax-keyword)`,fontWeight:`650`},{tag:q.string,color:`var(--syntax-string)`},{tag:[q.comment,q.lineComment,q.blockComment],color:`var(--syntax-comment)`,fontStyle:`italic`},{tag:[q.number,q.bool],color:`var(--syntax-number)`},{tag:[q.operator,q.punctuation],color:`var(--syntax-operator)`},{tag:[q.variableName,q.typeName],color:`var(--syntax-variable)`}]),vw=new H({state:k.create({doc:mw.source,extensions:[A_,ew.of(vx(mw.type)),dd(_w),H.theme({"&":{height:`100%`,backgroundColor:`transparent`},".cm-scroller":{fontFamily:`var(--font-mono)`,lineHeight:`1.65`},".cm-content":{padding:`28px 10px 80px`},".cm-gutters":{backgroundColor:`transparent`,border:`0`,paddingLeft:`12px`},".cm-activeLine, .cm-activeLineGutter":{backgroundColor:`var(--editor-active-line)`},".cm-cursor, .cm-dropCursor":{borderLeftColor:`var(--ink)`},"&.cm-focused":{outline:`none`}}),H.updateListener.of(e=>{e.docChanged&&!rw&&xw()})]}),parent:document.querySelector(`#editor`)}),yw=new XC({stage:document.querySelector(`#preview-stage`),image:document.querySelector(`#preview-image`),status:document.querySelector(`.render-status`),minimap:document.querySelector(`#minimap`),minimapImage:document.querySelector(`#minimap-image`),minimapViewport:document.querySelector(`#minimap-viewport`)});cw.addEventListener(`change`,()=>{sw.set(iw,bw());let e=cw.value;Tw(sw.get(e)??dC(e))}),document.querySelector(`#details`).addEventListener(`click`,()=>{document.querySelector(`#details-dialog`).showModal(),lw.focus()});for(let e of[lw,uw,dw,fw,pw])e.addEventListener(`input`,()=>{Qw(),clearTimeout(Cw.timeout),Cw.timeout=setTimeout(Cw,220)});document.querySelector(`#details-dialog`).addEventListener(`close`,Cw),document.querySelector(`#lock-diagram`).addEventListener(`click`,Pw),document.querySelector(`#change-password`).addEventListener(`click`,Fw),document.querySelector(`#unlock-diagram`).addEventListener(`click`,Iw),document.querySelector(`#save`).addEventListener(`click`,()=>kw()),document.querySelector(`#share`).addEventListener(`click`,Ew),document.querySelector(`#share-save`).addEventListener(`click`,async()=>{await kw(),Dw()}),document.querySelector(`.brand`).addEventListener(`click`,e=>{e.preventDefault(),confirm(`Start a new diagram?`)&&Zw()}),document.querySelectorAll(`[data-preview-action]`).forEach(e=>{e.addEventListener(`click`,()=>{let t=e.dataset.previewAction;t===`zoom-out`&&yw.zoom(.8),t===`zoom-in`&&yw.zoom(1.25),t===`fit`&&yw.fit(),t===`one-to-one`&&yw.oneToOne()})}),document.querySelectorAll(`.mobile-tabs button`).forEach(e=>{e.addEventListener(`click`,()=>{document.querySelector(`.workspace`).dataset.mobilePanel=e.dataset.panel,document.querySelectorAll(`.mobile-tabs button`).forEach(t=>{t.setAttribute(`aria-selected`,String(t===e))}),e.dataset.panel===`preview`&&yw.fit()})}),document.querySelectorAll(`[data-copy]`).forEach(e=>{e.addEventListener(`click`,async()=>{let t=document.querySelector(`#${e.dataset.copy}`);t.value&&(await $w(t.value),$(`Copied`))})}),window.addEventListener(`popstate`,()=>location.reload()),window.addEventListener(`keydown`,e=>{(e.metaKey||e.ctrlKey)&&e.key.toLowerCase()===`s`&&(e.preventDefault(),kw())});function bw(){let e=Math.min(256,Math.max(0,Number.parseInt(fw.value,10)||0));return{type:cw.value,source:vw.state.doc.toString(),options:{},meta:oC({title:lw.value,description:uw.value}),presentation:sC({background:dw.value,padding:e,frame:pw.checked})}}function xw(e=1200){clearTimeout(nw),nw=setTimeout(Sw,e)}function Sw(e=!0){let t=bw();Ww(t),Q.dirty=Q.keyEnvelopeDirty||Q.savedSnapshot!==Vw(t,Q.mode),Yw(),Xw(),e&&yw.render(t)}function Cw(){clearTimeout(Cw.timeout),Sw()}async function ww(){let e=_C(location.pathname);if(e){let t=xC(location.hash);t&&Kw(e,t),location.hash&&history.replaceState(null,``,`${location.pathname}${location.search}`);try{let n=await tw.getAlias(e),r=t??qw(e);if(n.mode===`locked`){let e=await Lw(n);return e?(Jw(n,r,e.state,e.bundleKey),e.state):(ow=`A password is required to open this diagram.`,dC(iC))}Jw(n,r,n.state);let i=Uw(`${QC}${e}`);return i?.revision===n.revision?i.state:n.state}catch(e){ow=e instanceof SC?e.message:`This saved diagram could not be loaded.`}}location.hash&&history.replaceState(null,``,`${location.pathname}${location.search}`);let t=Uw(ZC);return t?t.state:dC(iC)}function Tw(e,t=!0){if(!yx(e.type))throw Error(`Unsupported diagram type.`);let n=oC(e.meta),r=sC(e.presentation);rw=!0,iw=e.type,sw.set(e.type,{...e,meta:n,presentation:r}),cw.value=e.type,lw.value=n.title,uw.value=n.description,dw.value=r.background,fw.value=String(r.padding),pw.checked=r.frame,Qw(),vw.dispatch({changes:{from:0,to:vw.state.doc.length,insert:e.source},effects:ew.reconfigure(vx(e.type))}),rw=!1,t?Sw():yw.render(bw())}function Ew(){Sw(),Dw(),document.querySelector(`#share-dialog`).showModal()}function Dw(){let e=!!Q.aliasId,t=e&&Q.savedMode===`locked`,n=e?vC(location.origin,Q.aliasId):``,r=e&&Q.writeCapability?yC(location.origin,Q.aliasId,Q.writeCapability):``,i=t?null:Q.savedState,a=i?bC(location.origin,Q.aliasId,`svg`):``,o=document.querySelector(`#image-link`),s=document.querySelector(`#markdown-link`),c=i?.meta.title.trim().replace(/[\[\]\r\n]/g,` `)||`Diagram`;Ow(`viewer-link`,n,`Save to create a read link`),Ow(`editor-link`,r,e?`No write capability on this device`:`Save to create an edit link`),Ow(`image-link`,a,t?`Encrypted diagrams cannot be embedded`:e?`Unavailable for this diagram`:`Save to create an image link`),Ow(`markdown-link`,a?``:``,t?`Encrypted diagrams cannot be embedded`:e?`Unavailable for this diagram`:`Save to create Markdown`),document.querySelector(`#editor-link-note`).textContent=r?`Anyone with this link can update the diagram.`:e?`This device has read-only access. Save a copy to create a new edit link.`:`The edit link is a bearer credential and appears after saving.`,document.querySelector(`#image-link-note`).textContent=t?`This diagram is encrypted in your browser. Image embeds are disabled because they cannot ask for its password.`:``,o.dataset.state=t?`unavailable`:`ready`,s.dataset.state=t?`unavailable`:`ready`;let l=document.querySelector(`#share-status`),u=document.querySelector(`#share-save`);u.hidden=e&&!Q.dirty,e?Q.dirty&&Q.writeCapability?(l.textContent=`These links show the last saved version. Save your changes before sharing.`,u.textContent=`Save changes`):Q.dirty?(l.textContent=`You are editing a read-only diagram. Save a copy to share your changes.`,u.textContent=`Save as new`):l.textContent=t?Q.writeCapability?`The read link asks for the password. The edit link also grants permission to save changes.`:`The read link asks for the password. This device has read-only access.`:Q.writeCapability?`Copy a read link, or share the edit capability carefully.`:`This diagram is read-only on this device.`:(l.textContent=`Save this diagram to create stable share links.`,u.textContent=`Save diagram`)}function Ow(e,t,n){let r=document.querySelector(`#${e}`),i=document.querySelector(`[data-copy="${e}"]`);r.value=t,r.placeholder=t?``:n,i.disabled=!t}async function kw(){if(aw)return!1;Sw(!1);let e=bw();if(Q.aliasId&&!Q.dirty)return $(`Already saved`),!0;aw=!0,Yw();try{if(Q.aliasId&&Q.writeCapability)return Jw(Q.mode===`locked`?await tw.updateLockedAlias(Q.aliasId,await jw(e),Q.revision,Q.writeCapability):await tw.updateAlias(Q.aliasId,e,Q.revision,Q.writeCapability),Q.writeCapability,e,Q.mode===`locked`?Q.bundleKey:null),Gw(`${QC}${Q.aliasId}`),await Mw(e),$(`Saved`),!0;let t=!!Q.aliasId;return await Aw(e),$(t?`Saved as a new diagram`:`Saved`),!0}catch(t){return t instanceof SC&&t.status===412&&Q.aliasId?await zw(e):($(t instanceof Error?t.message:`Could not save this diagram.`),!1)}finally{aw=!1,Yw()}}async function Aw(e){let t=Q.aliasId,n=t?`${QC}${t}`:ZC,r=Q.bundleKey,i=Q.mode===`locked`?await tw.createLockedAlias(await jw(e)):await tw.createAlias(e);Jw(i,i.writeCapability,e,Q.mode===`locked`?r:null),Kw(i.aliasId,i.writeCapability),Gw(n),history.pushState(null,``,`/d/${i.aliasId}`),await Mw(e)}async function jw(e){if(!(Q.bundleKey instanceof Uint8Array)||!Q.keyEnvelope)throw Error(`This diagram is missing its browser encryption key.`);return Q.savedMode===`locked`&&Q.savedSnapshot===Vw(e,`locked`)&&Q.encryptedContent&&Q.encryptedMetadata?{mode:`locked`,encryptedContent:Q.encryptedContent,encryptedMetadata:Q.encryptedMetadata,keyEnvelope:Q.keyEnvelope}:GC(e,Q.bundleKey,Q.keyEnvelope)}async function Mw(e){if(!Q.aliasId||!Q.writeCapability)return;await yw.render(e);let t=yw.svgBlobFor(e);if(!t)return;let n=[[`svg`,t]];try{n.push([`png`,await Nw(t)])}catch{}let r=n.map(async([e,t])=>{let n=Q.mode===`locked`?await JC(t,Q.bundleKey,e):t;return tw.uploadRender({aliasId:Q.aliasId,renderId:Q.renderId,revision:Q.revision,writeCapability:Q.writeCapability,format:e,mode:Q.mode,render:n})});(await Promise.allSettled(r)).every(e=>e.status===`rejected`)&&yw.setStatus(`Saved; render cache unavailable`,`ready`)}async function Nw(e){let t=URL.createObjectURL(e);try{let e=new Image;e.src=t,await e.decode();let n=Math.min(1,4096/Math.max(e.naturalWidth,e.naturalHeight)),r=Math.max(1,Math.round(e.naturalWidth*n)),i=Math.max(1,Math.round(e.naturalHeight*n)),a=document.createElement(`canvas`);a.width=r,a.height=i;let o=a.getContext(`2d`);if(!o)throw Error(`Canvas is unavailable.`);o.drawImage(e,0,0,r,i);let s=await new Promise(e=>a.toBlob(e,`image/png`));if(!s)throw Error(`Could not create a PNG render.`);return s}finally{URL.revokeObjectURL(t)}}async function Pw(){let e=await Rw({title:`Lock diagram`,copy:`The source, metadata, and saved render will be encrypted in this browser. The password cannot be recovered.`,submitLabel:`Lock diagram`,confirm:!0});if(e!==null)try{let{bundleKey:t,payload:n}=await KC(bw(),e);Q.mode=`locked`,Q.bundleKey=t,Q.keyEnvelope=n.keyEnvelope,Q.encryptedContent=n.encryptedContent,Q.encryptedMetadata=n.encryptedMetadata,Q.keyEnvelopeDirty=!0,Gw(Q.aliasId?`${QC}${Q.aliasId}`:ZC),Sw(!1),$(`Locked in this browser — save to persist`)}catch(e){$(e instanceof Error?e.message:`Could not lock this diagram.`)}}async function Fw(){if(Q.mode!==`locked`||!(Q.bundleKey instanceof Uint8Array))return;let e=await Rw({title:`Change password`,copy:`Only the encrypted bundle key will be rewrapped. The diagram content does not need to be re-encrypted.`,submitLabel:`Change password`,confirm:!0});if(e!==null)try{Q.keyEnvelope=await UC(Q.bundleKey,e),Q.keyEnvelopeDirty=!0,Sw(!1),$(`Password changed — save to persist`)}catch(e){$(e instanceof Error?e.message:`Could not change the password.`)}}function Iw(){Q.mode===`locked`&&confirm(`Remove password protection? The next save will store the diagram in plaintext.`)&&(Q.mode=`open`,Q.keyEnvelopeDirty=!1,Sw(!1),$(`Password removed — save to persist`))}async function Lw(e){for(;;){let t=await Rw({title:`Unlock diagram`,copy:`Decryption happens only in this browser. The password is never sent to diagram.zip.`,submitLabel:`Unlock`,confirm:!1});if(t===null)return null;try{return await qC(e,t)}catch(e){$(e instanceof Error?e.message:`Could not unlock this diagram.`)}}}function Rw({title:e,copy:t,submitLabel:n,confirm:r}){let i=document.querySelector(`#password-dialog`),a=document.querySelector(`#password-form`),o=document.querySelector(`#diagram-password`),s=document.querySelector(`#diagram-password-confirm`),c=document.querySelector(`#password-confirm-field`),l=document.querySelector(`#password-error`);return document.querySelector(`#password-title`).textContent=e,document.querySelector(`#password-copy`).textContent=t,document.querySelector(`#password-submit`).textContent=n,c.hidden=!r,o.autocomplete=r?`new-password`:`current-password`,o.value=``,s.value=``,l.textContent=``,new Promise(e=>{let t=!1,n=n=>{t||(t=!0,a.removeEventListener(`submit`,c),document.querySelector(`#password-cancel`).removeEventListener(`click`,u),i.removeEventListener(`cancel`,u),i.removeEventListener(`close`,d),e(n))},c=e=>{e.preventDefault();let t=o.value;if(r&&t.length<8){l.textContent=`Use at least 8 characters.`,o.focus();return}if(r&&t!==s.value){l.textContent=`The passwords do not match.`,s.focus();return}i.close(),n(t)},u=e=>{e.preventDefault(),i.close(),n(null)},d=()=>n(null);a.addEventListener(`submit`,c),document.querySelector(`#password-cancel`).addEventListener(`click`,u),i.addEventListener(`cancel`,u),i.addEventListener(`close`,d),i.showModal(),o.focus()})}async function zw(e){let t=await Bw();if(t===`reload`){let e=Q.aliasId,t=Q.writeCapability;try{let n=await tw.getAlias(e),r=n.state,i=null;if(n.mode===`locked`){let e=await Lw(n);if(!e)return!1;r=e.state,i=e.bundleKey}return Jw(n,t,r,i),Gw(`${QC}${e}`),Tw(r,!1),Yw(),$(`Reloaded saved diagram`),!0}catch(e){return $(e instanceof Error?e.message:`Could not reload this diagram.`),!1}}return t===`copy`&&(await Aw(e),$(`Saved as a new diagram`),!0)}function Bw(){let e=document.querySelector(`#conflict-dialog`);return e.returnValue=``,e.showModal(),new Promise(t=>{e.addEventListener(`close`,()=>t(e.returnValue),{once:!0})})}function Vw(e,t=`open`){return JSON.stringify({mode:t,type:e.type,source:e.source,options:e.options??{},meta:oC(e.meta),presentation:sC(e.presentation)})}function Hw(e){if(!e||typeof e!=`object`||!yx(e.type)||typeof e.source!=`string`)return null;try{return{type:e.type,source:e.source,options:e.options??{},meta:oC(e.meta),presentation:sC(e.presentation)}}catch{return null}}function Uw(e){try{let t=JSON.parse(localStorage.getItem(e)),n=Hw(t?.state);return n?{state:n,revision:t.revision??null}:null}catch{return null}}function Ww(e){if(Q.mode===`locked`)return;let t=Q.aliasId?`${QC}${Q.aliasId}`:ZC;try{localStorage.setItem(t,JSON.stringify({state:e,revision:Q.revision}))}catch{}}function Gw(e){try{localStorage.removeItem(e)}catch{}}function Kw(e,t){try{localStorage.setItem(`${$C}${e}`,t)}catch{}}function qw(e){try{return xC(`#w=${localStorage.getItem(`${$C}${e}`)??``}`)}catch{return null}}function Jw(e,t,n,r=null){Q={aliasId:e.aliasId,contentId:e.contentId,renderId:e.renderId,revision:e.revision,mode:e.mode,savedMode:e.mode,writeCapability:t,savedState:n,savedSnapshot:Vw(n,e.mode),bundleKey:r,keyEnvelope:e.mode===`locked`?e.keyEnvelope:null,encryptedContent:e.mode===`locked`?e.encryptedContent:null,encryptedMetadata:e.mode===`locked`?e.encryptedMetadata:null,keyEnvelopeDirty:!1,dirty:!1},Xw(),Yw()}function Yw(){let e=document.querySelector(`#save`);if(aw){e.textContent=`Saving…`,e.disabled=!0;return}if(Q.aliasId&&!Q.dirty){e.textContent=`Saved`,e.disabled=!0;return}e.textContent=Q.aliasId&&!Q.writeCapability?`Save copy`:`Save`,e.disabled=!1}function Xw(){let e=Q.mode===`locked`;document.querySelector(`#privacy-label`).textContent=e?`Password locked`:`Open`,document.querySelector(`#privacy-description`).textContent=e?`Source, metadata, and saved renders are encrypted in your browser. Password-locked diagrams cannot be embedded.`:`Anyone with the read link can open and embed this diagram.`,document.querySelector(`#lock-diagram`).hidden=e,document.querySelector(`#change-password`).hidden=!e,document.querySelector(`#unlock-diagram`).hidden=!e}function Zw(){Q={aliasId:null,contentId:null,renderId:null,revision:null,mode:`open`,savedMode:null,writeCapability:null,savedState:null,savedSnapshot:null,bundleKey:null,keyEnvelope:null,encryptedContent:null,encryptedMetadata:null,keyEnvelopeDirty:!1,dirty:!0},sw.clear(),history.pushState(null,``,`/`),Tw(dC(iC))}function Qw(){let e=lw.value.trim(),t=uw.value.trim();document.title=e?`${e} — diagram.zip`:`diagram.zip`,document.querySelector(`meta[name="description"]`).content=t||`Create diagrams from text and share them as a link.`}async function $w(e){if(navigator.clipboard)return navigator.clipboard.writeText(e);let t=document.createElement(`textarea`);t.value=e,document.body.append(t),t.select(),document.execCommand(`copy`),t.remove()}function $(e){let t=document.querySelector(`#toast`);t.textContent=e,t.dataset.visible=`true`,clearTimeout($.timeout),$.timeout=setTimeout(()=>delete t.dataset.visible,1800)}Sw(),ow&&$(ow);\ No newline at end of file1789+`;var lw=document.querySelector(`#diagram-type`),uw=document.querySelector(`#diagram-title`),dw=document.querySelector(`#diagram-description`),fw=document.querySelector(`#diagram-background`),pw=document.querySelector(`#diagram-padding`),mw=document.querySelector(`#diagram-frame`);for(let{id:e,label:t}of px)lw.add(new Option(t,e));var hw=await Tw(),gw=oC(hw.meta),_w=sC(hw.presentation);lw.value=hw.type,uw.value=gw.title,dw.value=gw.description,fw.value=_w.background,pw.value=String(_w.padding),mw.checked=_w.frame,$w(),aw=hw.type,cw.set(hw.type,hw);var vw=sd.define([{tag:q.keyword,color:`var(--syntax-keyword)`,fontWeight:`650`},{tag:q.string,color:`var(--syntax-string)`},{tag:[q.comment,q.lineComment,q.blockComment],color:`var(--syntax-comment)`,fontStyle:`italic`},{tag:[q.number,q.bool],color:`var(--syntax-number)`},{tag:[q.operator,q.punctuation],color:`var(--syntax-operator)`},{tag:[q.variableName,q.typeName],color:`var(--syntax-variable)`}]),yw=new H({state:k.create({doc:hw.source,extensions:[A_,tw.of(vx(hw.type)),dd(vw),H.theme({"&":{height:`100%`,backgroundColor:`transparent`},".cm-scroller":{fontFamily:`var(--font-mono)`,lineHeight:`1.65`},".cm-content":{padding:`28px 10px 80px`},".cm-gutters":{backgroundColor:`transparent`,border:`0`,paddingLeft:`12px`},".cm-activeLine, .cm-activeLineGutter":{backgroundColor:`var(--editor-active-line)`},".cm-cursor, .cm-dropCursor":{borderLeftColor:`var(--ink)`},"&.cm-focused":{outline:`none`}}),H.updateListener.of(e=>{e.docChanged&&!iw&&Sw()})]}),parent:document.querySelector(`#editor`)}),bw=new ZC({stage:document.querySelector(`#preview-stage`),image:document.querySelector(`#preview-image`),status:document.querySelector(`.render-status`),minimap:document.querySelector(`#minimap`),minimapImage:document.querySelector(`#minimap-image`),minimapViewport:document.querySelector(`#minimap-viewport`)});lw.addEventListener(`change`,()=>{let e=lw.value;Ew(fC(cw,aw,e,xw(),dC))}),document.querySelector(`#details`).addEventListener(`click`,()=>{document.querySelector(`#details-dialog`).showModal(),uw.focus()});for(let e of[uw,dw,fw,pw,mw])e.addEventListener(`input`,()=>{$w(),clearTimeout(ww.timeout),ww.timeout=setTimeout(ww,220)});document.querySelector(`#details-dialog`).addEventListener(`close`,ww),document.querySelector(`#lock-diagram`).addEventListener(`click`,Fw),document.querySelector(`#change-password`).addEventListener(`click`,Iw),document.querySelector(`#unlock-diagram`).addEventListener(`click`,Lw),document.querySelector(`#save`).addEventListener(`click`,()=>Aw()),document.querySelector(`#share`).addEventListener(`click`,Dw),document.querySelector(`#share-save`).addEventListener(`click`,async()=>{await Aw(),Ow()}),document.querySelector(`.brand`).addEventListener(`click`,e=>{e.preventDefault(),confirm(`Start a new diagram?`)&&Qw()}),document.querySelectorAll(`[data-preview-action]`).forEach(e=>{e.addEventListener(`click`,()=>{let t=e.dataset.previewAction;t===`zoom-out`&&bw.zoom(.8),t===`zoom-in`&&bw.zoom(1.25),t===`fit`&&bw.fit(),t===`one-to-one`&&bw.oneToOne()})}),document.querySelectorAll(`.mobile-tabs button`).forEach(e=>{e.addEventListener(`click`,()=>{document.querySelector(`.workspace`).dataset.mobilePanel=e.dataset.panel,document.querySelectorAll(`.mobile-tabs button`).forEach(t=>{t.setAttribute(`aria-selected`,String(t===e))}),e.dataset.panel===`preview`&&bw.fit()})}),document.querySelectorAll(`[data-copy]`).forEach(e=>{e.addEventListener(`click`,async()=>{let t=document.querySelector(`#${e.dataset.copy}`);t.value&&(await eT(t.value),$(`Copied`))})}),window.addEventListener(`popstate`,()=>location.reload()),window.addEventListener(`keydown`,e=>{(e.metaKey||e.ctrlKey)&&e.key.toLowerCase()===`s`&&(e.preventDefault(),Aw())});function xw(){let e=Math.min(256,Math.max(0,Number.parseInt(pw.value,10)||0));return{type:lw.value,source:yw.state.doc.toString(),options:{},meta:oC({title:uw.value,description:dw.value}),presentation:sC({background:fw.value,padding:e,frame:mw.checked})}}function Sw(e=1200){clearTimeout(rw),rw=setTimeout(Cw,e)}function Cw(e=!0){let t=xw();Gw(t),Q.dirty=Q.keyEnvelopeDirty||Q.savedSnapshot!==Hw(t,Q.mode),Xw(),Zw(),e&&bw.render(t)}function ww(){clearTimeout(ww.timeout),Cw()}async function Tw(){let e=vC(location.pathname);if(e){let t=SC(location.hash);t&&qw(e,t),location.hash&&history.replaceState(null,``,`${location.pathname}${location.search}`);try{let n=await nw.getAlias(e),r=t??Jw(e);if(n.mode===`locked`){let e=await Rw(n);return e?(Yw(n,r,e.state,e.bundleKey),e.state):(sw=`A password is required to open this diagram.`,dC(iC))}Yw(n,r,n.state);let i=Ww(`${$C}${e}`);return i?.revision===n.revision?i.state:n.state}catch(e){sw=e instanceof CC?e.message:`This saved diagram could not be loaded.`}}location.hash&&history.replaceState(null,``,`${location.pathname}${location.search}`);let t=Ww(QC);return t?t.state:dC(iC)}function Ew(e,t=!0){if(!yx(e.type))throw Error(`Unsupported diagram type.`);let n=oC(e.meta),r=sC(e.presentation);iw=!0,aw=e.type,cw.set(e.type,{...e,meta:n,presentation:r}),lw.value=e.type,uw.value=n.title,dw.value=n.description,fw.value=r.background,pw.value=String(r.padding),mw.checked=r.frame,$w(),yw.dispatch({changes:{from:0,to:yw.state.doc.length,insert:e.source},effects:tw.reconfigure(vx(e.type))}),iw=!1,t?Cw():bw.render(xw())}function Dw(){Cw(),Ow(),document.querySelector(`#share-dialog`).showModal()}function Ow(){let e=!!Q.aliasId,t=e&&Q.savedMode===`locked`,n=e?yC(location.origin,Q.aliasId):``,r=e&&Q.writeCapability?bC(location.origin,Q.aliasId,Q.writeCapability):``,i=t?null:Q.savedState,a=i?xC(location.origin,Q.aliasId,`svg`):``,o=document.querySelector(`#image-link`),s=document.querySelector(`#markdown-link`),c=i?.meta.title.trim().replace(/[\[\]\r\n]/g,` `)||`Diagram`;kw(`viewer-link`,n,`Save to create a read link`),kw(`editor-link`,r,e?`No write capability on this device`:`Save to create an edit link`),kw(`image-link`,a,t?`Encrypted diagrams cannot be embedded`:e?`Unavailable for this diagram`:`Save to create an image link`),kw(`markdown-link`,a?``:``,t?`Encrypted diagrams cannot be embedded`:e?`Unavailable for this diagram`:`Save to create Markdown`),document.querySelector(`#editor-link-note`).textContent=r?`Anyone with this link can update the diagram.`:e?`This device has read-only access. Save a copy to create a new edit link.`:`The edit link is a bearer credential and appears after saving.`,document.querySelector(`#image-link-note`).textContent=t?`This diagram is encrypted in your browser. Image embeds are disabled because they cannot ask for its password.`:``,o.dataset.state=t?`unavailable`:`ready`,s.dataset.state=t?`unavailable`:`ready`;let l=document.querySelector(`#share-status`),u=document.querySelector(`#share-save`);u.hidden=e&&!Q.dirty,e?Q.dirty&&Q.writeCapability?(l.textContent=`These links show the last saved version. Save your changes before sharing.`,u.textContent=`Save changes`):Q.dirty?(l.textContent=`You are editing a read-only diagram. Save a copy to share your changes.`,u.textContent=`Save as new`):l.textContent=t?Q.writeCapability?`The read link asks for the password. The edit link also grants permission to save changes.`:`The read link asks for the password. This device has read-only access.`:Q.writeCapability?`Copy a read link, or share the edit capability carefully.`:`This diagram is read-only on this device.`:(l.textContent=`Save this diagram to create stable share links.`,u.textContent=`Save diagram`)}function kw(e,t,n){let r=document.querySelector(`#${e}`),i=document.querySelector(`[data-copy="${e}"]`);r.value=t,r.placeholder=t?``:n,i.disabled=!t}async function Aw(){if(ow)return!1;Cw(!1);let e=xw();if(Q.aliasId&&!Q.dirty)return $(`Already saved`),!0;ow=!0,Xw();try{if(Q.aliasId&&Q.writeCapability)return Yw(Q.mode===`locked`?await nw.updateLockedAlias(Q.aliasId,await Mw(e),Q.revision,Q.writeCapability):await nw.updateAlias(Q.aliasId,e,Q.revision,Q.writeCapability),Q.writeCapability,e,Q.mode===`locked`?Q.bundleKey:null),Kw(`${$C}${Q.aliasId}`),await Nw(e),$(`Saved`),!0;let t=!!Q.aliasId;return await jw(e),$(t?`Saved as a new diagram`:`Saved`),!0}catch(t){return t instanceof CC&&t.status===412&&Q.aliasId?await Bw(e):($(t instanceof Error?t.message:`Could not save this diagram.`),!1)}finally{ow=!1,Xw()}}async function jw(e){let t=Q.aliasId,n=t?`${$C}${t}`:QC,r=Q.bundleKey,i=Q.mode===`locked`?await nw.createLockedAlias(await Mw(e)):await nw.createAlias(e);Yw(i,i.writeCapability,e,Q.mode===`locked`?r:null),qw(i.aliasId,i.writeCapability),Kw(n),history.pushState(null,``,`/d/${i.aliasId}`),await Nw(e)}async function Mw(e){if(!(Q.bundleKey instanceof Uint8Array)||!Q.keyEnvelope)throw Error(`This diagram is missing its browser encryption key.`);return Q.savedMode===`locked`&&Q.savedSnapshot===Hw(e,`locked`)&&Q.encryptedContent&&Q.encryptedMetadata?{mode:`locked`,encryptedContent:Q.encryptedContent,encryptedMetadata:Q.encryptedMetadata,keyEnvelope:Q.keyEnvelope}:KC(e,Q.bundleKey,Q.keyEnvelope)}async function Nw(e){if(!Q.aliasId||!Q.writeCapability)return;await bw.render(e);let t=bw.svgBlobFor(e);if(!t)return;let n=[[`svg`,t]];try{n.push([`png`,await Pw(t)])}catch{}let r=n.map(async([e,t])=>{let n=Q.mode===`locked`?await YC(t,Q.bundleKey,e):t;return nw.uploadRender({aliasId:Q.aliasId,renderId:Q.renderId,revision:Q.revision,writeCapability:Q.writeCapability,format:e,mode:Q.mode,render:n})});(await Promise.allSettled(r)).every(e=>e.status===`rejected`)&&bw.setStatus(`Saved; render cache unavailable`,`ready`)}async function Pw(e){let t=URL.createObjectURL(e);try{let e=new Image;e.src=t,await e.decode();let n=Math.min(1,4096/Math.max(e.naturalWidth,e.naturalHeight)),r=Math.max(1,Math.round(e.naturalWidth*n)),i=Math.max(1,Math.round(e.naturalHeight*n)),a=document.createElement(`canvas`);a.width=r,a.height=i;let o=a.getContext(`2d`);if(!o)throw Error(`Canvas is unavailable.`);o.drawImage(e,0,0,r,i);let s=await new Promise(e=>a.toBlob(e,`image/png`));if(!s)throw Error(`Could not create a PNG render.`);return s}finally{URL.revokeObjectURL(t)}}async function Fw(){let e=await zw({title:`Lock diagram`,copy:`The source, metadata, and saved render will be encrypted in this browser. The password cannot be recovered.`,submitLabel:`Lock diagram`,confirm:!0});if(e!==null)try{let{bundleKey:t,payload:n}=await qC(xw(),e);Q.mode=`locked`,Q.bundleKey=t,Q.keyEnvelope=n.keyEnvelope,Q.encryptedContent=n.encryptedContent,Q.encryptedMetadata=n.encryptedMetadata,Q.keyEnvelopeDirty=!0,Kw(Q.aliasId?`${$C}${Q.aliasId}`:QC),Cw(!1),$(`Locked in this browser — save to persist`)}catch(e){$(e instanceof Error?e.message:`Could not lock this diagram.`)}}async function Iw(){if(Q.mode!==`locked`||!(Q.bundleKey instanceof Uint8Array))return;let e=await zw({title:`Change password`,copy:`Only the encrypted bundle key will be rewrapped. The diagram content does not need to be re-encrypted.`,submitLabel:`Change password`,confirm:!0});if(e!==null)try{Q.keyEnvelope=await WC(Q.bundleKey,e),Q.keyEnvelopeDirty=!0,Cw(!1),$(`Password changed — save to persist`)}catch(e){$(e instanceof Error?e.message:`Could not change the password.`)}}function Lw(){Q.mode===`locked`&&confirm(`Remove password protection? The next save will store the diagram in plaintext.`)&&(Q.mode=`open`,Q.keyEnvelopeDirty=!1,Cw(!1),$(`Password removed — save to persist`))}async function Rw(e){for(;;){let t=await zw({title:`Unlock diagram`,copy:`Decryption happens only in this browser. The password is never sent to diagram.zip.`,submitLabel:`Unlock`,confirm:!1});if(t===null)return null;try{return await JC(e,t)}catch(e){$(e instanceof Error?e.message:`Could not unlock this diagram.`)}}}function zw({title:e,copy:t,submitLabel:n,confirm:r}){let i=document.querySelector(`#password-dialog`),a=document.querySelector(`#password-form`),o=document.querySelector(`#diagram-password`),s=document.querySelector(`#diagram-password-confirm`),c=document.querySelector(`#password-confirm-field`),l=document.querySelector(`#password-error`);return document.querySelector(`#password-title`).textContent=e,document.querySelector(`#password-copy`).textContent=t,document.querySelector(`#password-submit`).textContent=n,c.hidden=!r,o.autocomplete=r?`new-password`:`current-password`,o.value=``,s.value=``,l.textContent=``,new Promise(e=>{let t=!1,n=n=>{t||(t=!0,a.removeEventListener(`submit`,c),document.querySelector(`#password-cancel`).removeEventListener(`click`,u),i.removeEventListener(`cancel`,u),i.removeEventListener(`close`,d),e(n))},c=e=>{e.preventDefault();let t=o.value;if(r&&t.length<8){l.textContent=`Use at least 8 characters.`,o.focus();return}if(r&&t!==s.value){l.textContent=`The passwords do not match.`,s.focus();return}i.close(),n(t)},u=e=>{e.preventDefault(),i.close(),n(null)},d=()=>n(null);a.addEventListener(`submit`,c),document.querySelector(`#password-cancel`).addEventListener(`click`,u),i.addEventListener(`cancel`,u),i.addEventListener(`close`,d),i.showModal(),o.focus()})}async function Bw(e){let t=await Vw();if(t===`reload`){let e=Q.aliasId,t=Q.writeCapability;try{let n=await nw.getAlias(e),r=n.state,i=null;if(n.mode===`locked`){let e=await Rw(n);if(!e)return!1;r=e.state,i=e.bundleKey}return Yw(n,t,r,i),Kw(`${$C}${e}`),Ew(r,!1),Xw(),$(`Reloaded saved diagram`),!0}catch(e){return $(e instanceof Error?e.message:`Could not reload this diagram.`),!1}}return t===`copy`&&(await jw(e),$(`Saved as a new diagram`),!0)}function Vw(){let e=document.querySelector(`#conflict-dialog`);return e.returnValue=``,e.showModal(),new Promise(t=>{e.addEventListener(`close`,()=>t(e.returnValue),{once:!0})})}function Hw(e,t=`open`){return JSON.stringify({mode:t,type:e.type,source:e.source,options:e.options??{},meta:oC(e.meta),presentation:sC(e.presentation)})}function Uw(e){if(!e||typeof e!=`object`||!yx(e.type)||typeof e.source!=`string`)return null;try{return{type:e.type,source:e.source,options:e.options??{},meta:oC(e.meta),presentation:sC(e.presentation)}}catch{return null}}function Ww(e){try{let t=JSON.parse(localStorage.getItem(e)),n=Uw(t?.state);return n?{state:n,revision:t.revision??null}:null}catch{return null}}function Gw(e){if(Q.mode===`locked`)return;let t=Q.aliasId?`${$C}${Q.aliasId}`:QC;try{localStorage.setItem(t,JSON.stringify({state:e,revision:Q.revision}))}catch{}}function Kw(e){try{localStorage.removeItem(e)}catch{}}function qw(e,t){try{localStorage.setItem(`${ew}${e}`,t)}catch{}}function Jw(e){try{return SC(`#w=${localStorage.getItem(`${ew}${e}`)??``}`)}catch{return null}}function Yw(e,t,n,r=null){Q={aliasId:e.aliasId,contentId:e.contentId,renderId:e.renderId,revision:e.revision,mode:e.mode,savedMode:e.mode,writeCapability:t,savedState:n,savedSnapshot:Hw(n,e.mode),bundleKey:r,keyEnvelope:e.mode===`locked`?e.keyEnvelope:null,encryptedContent:e.mode===`locked`?e.encryptedContent:null,encryptedMetadata:e.mode===`locked`?e.encryptedMetadata:null,keyEnvelopeDirty:!1,dirty:!1},Zw(),Xw()}function Xw(){let e=document.querySelector(`#save`);if(ow){e.textContent=`Saving…`,e.disabled=!0;return}if(Q.aliasId&&!Q.dirty){e.textContent=`Saved`,e.disabled=!0;return}e.textContent=Q.aliasId&&!Q.writeCapability?`Save copy`:`Save`,e.disabled=!1}function Zw(){let e=Q.mode===`locked`;document.querySelector(`#privacy-label`).textContent=e?`Password locked`:`Open`,document.querySelector(`#privacy-description`).textContent=e?`Source, metadata, and saved renders are encrypted in your browser. Password-locked diagrams cannot be embedded.`:`Anyone with the read link can open and embed this diagram.`,document.querySelector(`#lock-diagram`).hidden=e,document.querySelector(`#change-password`).hidden=!e,document.querySelector(`#unlock-diagram`).hidden=!e}function Qw(){Q={aliasId:null,contentId:null,renderId:null,revision:null,mode:`open`,savedMode:null,writeCapability:null,savedState:null,savedSnapshot:null,bundleKey:null,keyEnvelope:null,encryptedContent:null,encryptedMetadata:null,keyEnvelopeDirty:!1,dirty:!0},cw.clear(),history.pushState(null,``,`/`),Ew(dC(iC))}function $w(){let e=uw.value.trim(),t=dw.value.trim();document.title=e?`${e} — diagram.zip`:`diagram.zip`,document.querySelector(`meta[name="description"]`).content=t||`Create diagrams from text and share them as a link.`}async function eT(e){if(navigator.clipboard)return navigator.clipboard.writeText(e);let t=document.createElement(`textarea`);t.value=e,document.body.append(t),t.select(),document.execCommand(`copy`),t.remove()}function $(e){let t=document.querySelector(`#toast`);t.textContent=e,t.dataset.visible=`true`,clearTimeout($.timeout),$.timeout=setTimeout(()=>delete t.dataset.visible,1800)}Cw(),sw&&$(sw);\ No newline at end of file@@ -7,7 +7,7 @@
77 <meta name="description" content="Create diagrams from text and share them as a link.">88 <link rel="icon" href="/diagram.zip/icon.svg" type="image/svg+xml">99 <title>diagram.zip</title>10- <script type="module" crossorigin src="/diagram.zip/assets/index-BxheHZUk.js"></script>10+ <script type="module" crossorigin src="/diagram.zip/assets/index-DkSU_3-S.js"></script>1111 <link rel="stylesheet" crossorigin href="/diagram.zip/assets/index-B2XsKz37.css">1212 </head>1313 <body>