clone

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

fix(diagram.zip): preserve renderer labels and presentation

Dami committed 9366112
commit
9366112e06dec4579491c33f09ea1b85f356937d
parent
3bee574583cf31bc998399c3c22e2f3857677bc0
author
Dami <dami.etoile@gmail.com>
16 files changed, +261 -37
Mdiagramzip-render/README.md7+-
Mdiagramzip-render/package-lock.json4+-
Mdiagramzip-render/package.json2+-
Mdiagramzip-render/scripts/smoke.mjs34+++++++-
Mdiagramzip-render/src/catalog.ts2+
Mdiagramzip-render/src/index.ts2+-
Mdiagramzip-render/src/svg.ts138++++++++++++++++++++++++++----
Mdiagramzip-render/test/svg.test.ts53+++++++++++-
Mdiagramzip-render/worker-configuration.d.ts4+-
Mdiagramzip-render/wrangler.jsonc2+-
Mdiagramzip/src/main.js4+-
Adiagramzip/src/type-drafts.js4+
Adiagramzip/test/type-drafts.test.js34+++++++
Mdocs/refactor-engines.md2+
Rserver/src/main/resources/web/diagramzip/assets/index-BxheHZUk.js → server/src/main/resources/web/diagramzip/assets/index-DkSU_3-S.js4+-
Mserver/src/main/resources/web/diagramzip/index.html2+-
M diagramzip-render/README.md +4 -3
M diagramzip-render/package-lock.json +2 -2
M diagramzip-render/package.json +1 -1
M diagramzip-render/scripts/smoke.mjs +32 -2
@@ -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)) {
M diagramzip-render/src/catalog.ts +2 -0
M diagramzip-render/src/index.ts +1 -1
M diagramzip-render/src/svg.ts +119 -19
@@ -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 }
M diagramzip-render/test/svg.test.ts +52 -1
@@ -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 &lt;description&gt;</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 })
M diagramzip-render/worker-configuration.d.ts +2 -2
M diagramzip-render/wrangler.jsonc +1 -1
M diagramzip/src/main.js +2 -2
A diagramzip/src/type-drafts.js +4 -0
A diagramzip/test/type-drafts.test.js +34 -0
M docs/refactor-engines.md +2 -0
R server/src/main/resources/web/diagramzip/assets/index-BxheHZUk.js → server/src/main/resources/web/diagramzip/assets/index-DkSU_3-S.js +2 -2
@@ -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?`![${c}](${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?`![${c}](${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
M server/src/main/resources/web/diagramzip/index.html +1 -1