1import {2 EDITABLE_SVG_SCHEMA,3 attachEditableDocument,4 extractEditableDocument,5 materializePresentation,6 materializeSvg,7} from '../../../shared/svg/index.js'8import { isKnownDiagramType } from './diagram-types.js'9import { normalizeMetadata, normalizePresentation } from './state.js'1011const MAX_SOURCE_BYTES = 768 * 102412const MAX_METADATA_TITLE = 20013const MAX_METADATA_DESCRIPTION = 200014const encoder = new TextEncoder()1516export class EditableSvgError extends Error {17 constructor(code, message) {18 super(message)19 this.name = 'EditableSvgError'20 this.code = code21 }22}2324function fail(code, message) {25 throw new EditableSvgError(code, message)26}2728function record(value, label) {29 if (typeof value !== 'object' || value === null || Array.isArray(value)) fail('invalid_document', `${label} must be an object.`)30 return value31}3233function exactKeys(value, expected, label) {34 const actual = Object.keys(value).sort()35 const allowed = [...expected].sort()36 if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) {37 fail('invalid_document', `${label} has unsupported or missing fields.`)38 }39}4041function normalizedJson(value, label, depth = 0) {42 if (depth > 20) fail('invalid_document', `${label} is nested too deeply.`)43 if (value === null || typeof value === 'boolean' || typeof value === 'string') return value44 if (typeof value === 'number' && Number.isFinite(value)) return value45 if (Array.isArray(value)) return value.map(item => normalizedJson(item, label, depth + 1))46 const input = record(value, label)47 return Object.fromEntries(Object.keys(input).sort().map(key => [key, normalizedJson(input[key], label, depth + 1)]))48}4950export function normalizeEditableState(value) {51 const manifest = record(value, 'Editable SVG document')52 exactKeys(manifest, ['schema', 'diagram', 'metadata'], 'Editable SVG document')53 if (manifest.schema !== Number(EDITABLE_SVG_SCHEMA)) fail('unsupported_schema', 'Editable SVG uses an unsupported document schema.')5455 const diagram = record(manifest.diagram, 'diagram')56 exactKeys(diagram, ['type', 'source', 'options', 'presentation'], 'diagram')57 if (typeof diagram.type !== 'string' || !isKnownDiagramType(diagram.type)) fail('unsupported_type', 'Editable SVG uses an unsupported diagram type.')58 if (typeof diagram.source !== 'string') fail('invalid_document', 'diagram.source must be a string.')59 if (encoder.encode(diagram.source).byteLength > MAX_SOURCE_BYTES) fail('source_too_large', 'Editable SVG source is too large.')60 const options = normalizedJson(diagram.options, 'diagram.options')61 if (Array.isArray(options) || typeof options !== 'object' || options === null) fail('invalid_document', 'diagram.options must be an object.')6263 const metadataValue = record(manifest.metadata, 'metadata')64 exactKeys(metadataValue, ['title', 'description'], 'metadata')65 const meta = normalizeMetadata(metadataValue)66 if (meta.title.length > MAX_METADATA_TITLE || meta.description.length > MAX_METADATA_DESCRIPTION) {67 fail('invalid_document', 'Editable SVG metadata is too large.')68 }6970 const presentationValue = record(diagram.presentation, 'diagram.presentation')71 exactKeys(presentationValue, ['background', 'padding', 'frame', 'appearance'], 'diagram.presentation')72 let presentation73 try {74 presentation = normalizePresentation(presentationValue)75 } catch (error) {76 fail('invalid_document', error instanceof Error ? error.message : 'Editable SVG presentation is invalid.')77 }7879 return { type: diagram.type, source: diagram.source, options, meta, presentation }80}8182export function editableDocumentFor(state) {83 const normalized = normalizeEditableState({84 schema: Number(EDITABLE_SVG_SCHEMA),85 diagram: {86 type: state.type,87 source: state.source,88 options: state.options ?? {},89 presentation: normalizePresentation(state.presentation),90 },91 metadata: normalizeMetadata(state.meta),92 })93 return {94 schema: Number(EDITABLE_SVG_SCHEMA),95 diagram: {96 type: normalized.type,97 source: normalized.source,98 options: normalized.options,99 presentation: normalized.presentation,100 },101 metadata: normalized.meta,102 }103}104105export function exportEditableSvg(canonical, state) {106 const document = editableDocumentFor(state)107 const { appearance, background, padding, frame } = document.diagram.presentation108 const materialized = appearance === 'raw'109 ? materializePresentation(canonical, { background, padding, frame })110 : materializeSvg(canonical, appearance)111 return attachEditableDocument(materialized, document)112}113114export function importEditableSvg(source) {115 return normalizeEditableState(extractEditableDocument(source))116}