feat: add deterministic Diagram IR v2 authoring

This commit is contained in:
2026-09-03 10:05:29 +00:00
parent 305006c8cb
commit 50a07b90a9
31 changed files with 2108 additions and 98 deletions
@@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { run as build } from "../build/action.js";
import { run as validate } from "./action.js";
test("validate accepts Draw.io generated with a standard XML declaration", () => {
const dir = mkdtempSync(join(tmpdir(), "drawio-validate-test-"));
const spec = join(dir, "spec.yaml");
const diagram = join(dir, "diagram.drawio");
try {
writeFileSync(spec, "version: 1\nnodes:\n - { id: node, label: Node }\n", "utf8");
build(spec, 0, diagram);
const result = validate(diagram);
assert.deepEqual(result.summary, { pages: 1, valid: true, invalidPages: 0 });
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
+2 -45
View File
@@ -26,30 +26,9 @@
* - Base64/deflate encoding errors in the <diagram> body
*/
import { readFileSync } from "node:fs";
import { JSDOM } from "jsdom";
import { inflateRaw } from "pako";
import { loadGraphStates } from "../../services/maxgraph-loader/graph-loader.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function getAttr(tagStr: string, attr: string): string {
const re = new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]*?)"|'([^']*?)')`, "i");
const m = re.exec(tagStr);
return m ? (m[1] ?? m[2] ?? "") : "";
}
function decodeDiagramContent(content: string): string {
try {
const decoded = Buffer.from(content.trim(), "base64");
const decompressed = inflateRaw(decoded);
return decodeURIComponent(new TextDecoder("utf-8").decode(decompressed));
} catch {
return content; // already plain XML
}
}
import { parseAllPages } from "../../services/drawio-parser/parser.js";
interface PageResult {
pageIndex: number;
@@ -82,29 +61,7 @@ function checkXmlWellFormed(xml: string): string | null {
// ---------------------------------------------------------------------------
export function run(filePath: string): Record<string, unknown> {
const raw = readFileSync(filePath, "utf-8").trim();
// Extract pages (support both mxfile wrapper and bare mxGraphModel)
const pages: Array<{ name: string; xml: string }> = [];
if (/^<mxfile/i.test(raw)) {
const re = /<diagram([^>]*)>([\s\S]*?)<\/diagram>/gi;
let match: RegExpExecArray | null;
let idx = 0;
while ((match = re.exec(raw)) !== null) {
const name = getAttr(match[1], "name") || `Page ${idx + 1}`;
const xml = decodeDiagramContent(match[2].trim());
pages.push({ name, xml });
idx++;
}
if (pages.length === 0) {
throw new Error("No <diagram> elements found in mxfile");
}
} else if (/^<mxGraphModel/i.test(raw)) {
pages.push({ name: "Page 1", xml: raw });
} else {
throw new Error(`Unrecognised draw.io file format in ${filePath}`);
}
const pages = parseAllPages(filePath).map((page) => ({ name: page.pageName, xml: page.graphModelXml }));
const results: PageResult[] = [];