1import assert from 'node:assert/strict'2import { readFile } from 'node:fs/promises'3import test from 'node:test'4import { fileURLToPath } from 'node:url'5import { attachEditableDocument, canonicalizeSvg, materializeSvg } from '../../../shared/svg/index.js'6import {7 EditableSvgError,8 editableDocumentFor,9 exportEditableSvg,10 importEditableSvg,11} from '../src/editable-svg.js'1213const fixture = name => readFile(fileURLToPath(new URL(`./fixtures/editable-svg/${name}.svg`, import.meta.url)), 'utf8')1415const cases = [16 {17 name: 'graphviz',18 version: 'graphviz@15.1.1',19 state: {20 type: 'graphviz',21 source: 'digraph G { A -> B [label="café ☕"] }',22 options: { engine: 'dot' },23 meta: { title: 'Café flow', description: 'Unicode survives: こんにちは' },24 presentation: { appearance: 'raw', background: '#ffffff', padding: 8, frame: true },25 },26 },27 {28 name: 'mermaid',29 version: 'mermaid@11.17.0',30 state: {31 type: 'mermaid',32 source: 'flowchart LR\n idea[Idea] --> shipped[Shipped]',33 options: {},34 meta: { title: 'Idea flow', description: 'A client-rendered diagram.' },35 presentation: { appearance: 'dark-transparent', background: '', padding: 0, frame: false },36 },37 },38 {39 name: 'diagramsnet',40 version: 'diagrams.net@29.6.1',41 state: {42 type: 'diagramsnet',43 source: '<mxGraphModel><root/></mxGraphModel>',44 options: {},45 meta: { title: 'Authored drawing', description: '' },46 presentation: { appearance: 'auto-framed', background: '', padding: 0, frame: false },47 },48 },49 {50 name: 'safe-raw',51 engine: 'pikchr',52 version: 'pikchr@future',53 state: {54 type: 'pikchr',55 source: 'box "raw fallback"',56 options: { scale: 1 },57 meta: { title: '', description: 'Unknown renderer builds remain round-trippable as raw SVG.' },58 presentation: { appearance: 'raw', background: '', padding: 0, frame: false },59 },60 },61]6263test('uses the embedded presentation as the packed SVG palette selector', async () => {64 const state = cases[1].state65 const rendered = await fixture('mermaid')66 const canonical = canonicalizeSvg(rendered, state.meta, state.type, cases[1].version)67 const light = materializeSvg(canonical, 'light-transparent')68 const packed = attachEditableDocument(light, editableDocumentFor(state))6970 assert.match(packed, /data-dz-appearance=\"dark-transparent\"/)71 assert.ok(packed.includes('data-dz-appearance^=\"light-\"'))72 assert.ok(packed.includes('data-dz-appearance^=\"dark-\"'))73})7475test('exports and reimports a deterministic golden set of renderer SVGs', async () => {76 for (const entry of cases) {77 const state = entry.state78 const rendered = await fixture(entry.name)79 const canonical = canonicalizeSvg(rendered, state.meta, entry.engine ?? state.type, entry.version)80 const first = exportEditableSvg(canonical, state)81 const repeated = exportEditableSvg(canonical, state)82 const imported = importEditableSvg(first)83 const reexported = exportEditableSvg(canonical, imported)8485 assert.equal(first, repeated, `${entry.name} export must be deterministic`)86 assert.equal(first, reexported, `${entry.name} import/export must be byte-stable`)87 assert.deepEqual(imported, state, `${entry.name} state must round trip exactly`)88 assert.ok(first.includes("data-dz-appearance=\"" + state.presentation.appearance + "\""))89 assert.match(first, /data-dz-document="1"/)90 assert.equal((first.match(/<metadata data-dz-kind="document"/g) ?? []).length, 1)91 }92})9394test('canonicalizes option keys before writing the editable document', () => {95 const state = {96 ...cases[0].state,97 options: { z: 1, a: { second: true, first: false } },98 }99 assert.deepEqual(editableDocumentFor(state).diagram.options, {100 a: { first: false, second: true },101 z: 1,102 })103})104105test('rejects ordinary, ambiguous, unsupported, and lossy SVG documents', async () => {106 const plain = await fixture('safe-raw')107 assert.throws(() => importEditableSvg(plain), error => error.code === 'not_editable')108109 const state = cases[3].state110 const canonical = canonicalizeSvg(plain, state.meta, state.type, cases[3].version)111 const editable = exportEditableSvg(canonical, state)112 assert.throws(113 () => importEditableSvg(editable.replace('data-dz-document="1"', 'data-dz-document="2"')),114 error => error.code === 'not_editable',115 )116 assert.throws(117 () => importEditableSvg(editable.replace('</svg>', '<metadata data-dz-kind="document" data-dz-schema="1">{}</metadata></svg>')),118 error => error.code === 'invalid_editable_document',119 )120 assert.throws(121 () => importEditableSvg(editable.replace('"type":"pikchr"', '"type":"unknown"')),122 error => error instanceof EditableSvgError && error.code === 'unsupported_type',123 )124 assert.throws(125 () => importEditableSvg(editable.replace('"appearance":"raw"', '"appearance":"invented"')),126 error => error instanceof EditableSvgError && error.code === 'invalid_document',127 )128})