import type { Interface, Widget, SignalRef, PlotLayout, StateVar, LogicGraph } from './types'; /** Escape a string for use as an XML attribute value. */ function xmlEsc(s: string): string { return s .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } /** Serialize a panel-logic flow graph to indented XML lines. */ function serializeLogic(graph: LogicGraph, lines: string[]): void { lines.push(` `); for (const node of graph.nodes) { lines.push( ` ` ); for (const [key, value] of Object.entries(node.params)) { lines.push(` `); } lines.push(` `); } for (const wire of graph.wires) { const portAttr = wire.fromPort && wire.fromPort !== 'out' ? ` port="${xmlEsc(wire.fromPort)}"` : ''; lines.push(` `); } lines.push(` `); } /** Serialize a PlotLayout subtree to indented XML lines. */ function serializeLayout(node: PlotLayout, indent: string, lines: string[]): void { if (node.type === 'leaf') { lines.push(`${indent}`); return; } lines.push(`${indent}`); serializeLayout(node.a, indent + ' ', lines); serializeLayout(node.b, indent + ' ', lines); lines.push(`${indent}`); } /** Serialize an Interface object to an XML string. */ export function serializeInterface(iface: Interface): string { const lines: string[] = []; lines.push(``); const kindAttr = iface.kind === 'plot' ? ` kind="plot"` : ''; lines.push( `` ); if (iface.kind === 'plot' && iface.layout) { lines.push(` `); serializeLayout(iface.layout, ' ', lines); lines.push(` `); } for (const sv of iface.statevars ?? []) { const attrs = [ `name="${xmlEsc(sv.name)}"`, `type="${xmlEsc(sv.type ?? 'number')}"`, `initial="${xmlEsc(sv.initial ?? '')}"`, ]; if (sv.unit) attrs.push(`unit="${xmlEsc(sv.unit)}"`); if (sv.low !== undefined) attrs.push(`low="${sv.low}"`); if (sv.high !== undefined) attrs.push(`high="${sv.high}"`); lines.push(` `); } if (iface.logic && (iface.logic.nodes.length > 0 || iface.logic.wires.length > 0)) { serializeLogic(iface.logic, lines); } for (const w of iface.widgets) { lines.push( ` ` ); for (const sig of w.signals) { const colorAttr = sig.color ? ` color="${xmlEsc(sig.color)}"` : ''; lines.push(` `); } for (const [key, value] of Object.entries(w.options)) { lines.push(` `); } lines.push(``); return lines.join('\n'); } /** * Parse an interface XML string into an Interface object. * * Expected XML format: * * * * * */ /** Parse a / element into a PlotLayout node. */ function parseLayout(el: Element): PlotLayout { if (el.tagName === 'leaf') { return { type: 'leaf', widget: el.getAttribute('widget') ?? '' }; } // split const dir = el.getAttribute('dir') === 'v' ? 'v' : 'h'; const ratio = parseFloat(el.getAttribute('ratio') ?? '0.5'); const children = Array.from(el.children).filter( c => c.tagName === 'split' || c.tagName === 'leaf' ); const a = children[0] ? parseLayout(children[0]) : { type: 'leaf', widget: '' } as PlotLayout; const b = children[1] ? parseLayout(children[1]) : { type: 'leaf', widget: '' } as PlotLayout; return { type: 'split', dir, ratio: isNaN(ratio) ? 0.5 : ratio, a, b }; } /** Parse a element into a LogicGraph (nodes + wires). */ function parseLogic(logicEl: Element): LogicGraph { const nodes: LogicGraph['nodes'] = []; const wires: LogicGraph['wires'] = []; for (const el of Array.from(logicEl.children)) { if (el.tagName === 'node') { const id = el.getAttribute('id') ?? ''; if (!id) continue; const params: Record = {}; for (const child of Array.from(el.children)) { if (child.tagName !== 'param') continue; const key = child.getAttribute('key'); const value = child.getAttribute('value'); if (key !== null && value !== null) params[key] = value; } nodes.push({ id, kind: (el.getAttribute('kind') ?? 'action.write') as LogicGraph['nodes'][number]['kind'], x: parseFloat(el.getAttribute('x') ?? '0'), y: parseFloat(el.getAttribute('y') ?? '0'), params, }); } else if (el.tagName === 'wire') { const from = el.getAttribute('from'); const to = el.getAttribute('to'); const port = el.getAttribute('port'); if (from && to) wires.push({ from, to, ...(port ? { fromPort: port } : {}) }); } } return { nodes, wires }; } export function parseInterface(xml: string): Interface { const parser = new DOMParser(); const doc = parser.parseFromString(xml, 'application/xml'); // Check for parse errors const parseError = doc.querySelector('parsererror'); if (parseError) { throw new Error(`XML parse error: ${parseError.textContent}`); } const root = doc.documentElement; if (root.tagName !== 'interface') { throw new Error(`Expected root element , got <${root.tagName}>`); } const ifaceId = root.getAttribute('id') ?? ''; const name = root.getAttribute('name') ?? 'Untitled'; const version = parseInt(root.getAttribute('version') ?? '1', 10); const w = parseInt(root.getAttribute('width') ?? '1200', 10); const h = parseInt(root.getAttribute('height') ?? '800', 10); const kind = root.getAttribute('kind') === 'plot' ? 'plot' : undefined; let layout: PlotLayout | undefined; if (kind === 'plot') { const layoutEl = Array.from(root.children).find(el => el.tagName === 'layout'); const rootNode = layoutEl ? Array.from(layoutEl.children).find(el => el.tagName === 'split' || el.tagName === 'leaf') : undefined; if (rootNode) layout = parseLayout(rootNode); } const statevars: StateVar[] = []; for (const el of Array.from(root.children)) { if (el.tagName !== 'statevar') continue; const name = el.getAttribute('name') ?? ''; if (!name) continue; const type = el.getAttribute('type'); const low = el.getAttribute('low'); const high = el.getAttribute('high'); const unit = el.getAttribute('unit'); statevars.push({ name, type: type === 'bool' || type === 'string' ? type : 'number', initial: el.getAttribute('initial') ?? '', ...(unit ? { unit } : {}), ...(low !== null ? { low: parseFloat(low) } : {}), ...(high !== null ? { high: parseFloat(high) } : {}), }); } const logicEl = Array.from(root.children).find(el => el.tagName === 'logic'); const logic = logicEl ? parseLogic(logicEl) : null; const widgets: Widget[] = []; for (const el of Array.from(root.children)) { if (el.tagName !== 'widget') continue; const id = el.getAttribute('id') ?? ''; const type = el.getAttribute('type') ?? 'unknown'; const x = parseFloat(el.getAttribute('x') ?? '0'); const y = parseFloat(el.getAttribute('y') ?? '0'); const w = parseFloat(el.getAttribute('w') ?? '100'); const h = parseFloat(el.getAttribute('h') ?? '60'); const signals: Array = []; const options: Record = {}; for (const child of Array.from(el.children)) { if (child.tagName === 'signal') { const ds = child.getAttribute('ds') ?? ''; const sigName = child.getAttribute('name') ?? ''; const color = child.getAttribute('color') ?? undefined; signals.push({ ds, name: sigName, ...(color ? { color } : {}) }); } else if (child.tagName === 'option') { const key = child.getAttribute('key'); const value = child.getAttribute('value'); if (key !== null && value !== null) { options[key] = value; } } } widgets.push({ id, type, x, y, w, h, signals, options }); } return { id: ifaceId, name, version, w, h, widgets, ...(kind ? { kind } : {}), ...(layout ? { layout } : {}), ...(statevars.length > 0 ? { statevars } : {}), ...(logic && (logic.nodes.length > 0 || logic.wires.length > 0) ? { logic } : {}), }; }