From c3b31033ed97446a13a6575927330dce6444f83c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 20:02:14 +0000 Subject: [PATCH] Report structures XMind sets on individual topics XMind lets any topic carry its own structure (a map whose main topics draw their children as org charts, as in the Career Directions file). The importer only reads the sheet's structure and dropped the others without saying so; they are now counted in the import report. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01G2FX9kDHnPYmocPxX3w7ov --- src/importreport.test.ts | 12 ++++++++++++ src/xmind.ts | 4 +++- src/xmindxml.ts | 5 +++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/importreport.test.ts b/src/importreport.test.ts index fe7cd21..58736b3 100644 --- a/src/importreport.test.ts +++ b/src/importreport.test.ts @@ -124,3 +124,15 @@ test("XMind 7 boundaries are read too", async () => { expect(roots[0].bounds).toEqual([roots[0].children[1].id, ...roots[0].children.map((k) => k.id)]); // "(0,)" runs to the last child expect(lost).toEqual(["boundary titles dropped", "boundaries over several topics drawn around each topic"]); }); + +test("a structure set on a single topic is reported, one equal to the sheet's is not", async () => { + const sheet = [{ id: "s", class: "sheet", title: "S", rootTopic: { id: "r", title: "root", structureClass: "org.xmind.ui.map.clockwise", children: { attached: [ + { id: "a", title: "A", structureClass: "org.xmind.ui.org-chart.down", children: { attached: [{ id: "a1", title: "A1", structureClass: "org.xmind.ui.org-chart.down" }] } }, + { id: "b", title: "B", structureClass: "org.xmind.ui.map.clockwise" }, + ] } } }]; + const { lost } = await importXmindReport(zip({ "content.json": JSON.stringify(sheet) })); + expect(lost).toEqual(["2 × structures set on individual topics dropped"]); + const xml = `r + ab`; + expect((await importXmindReport(zip({ "content.xml": xml }))).lost).toEqual(["structures set on individual topics dropped"]); +}); diff --git a/src/xmind.ts b/src/xmind.ts index 539fc95..fee2c93 100644 --- a/src/xmind.ts +++ b/src/xmind.ts @@ -192,7 +192,7 @@ const picturesOf = (files: Map, rep: Report): Pictures => (t }; /** Summaries and boundaries found while reading topics; both live on the root of a sheet. */ -type Acc = { sums: Summary[]; bounds: string[] }; +type Acc = { sums: Summary[]; bounds: string[]; rootClass?: string }; /** "(2,4)" -> [2, 4]: the first and last summarised child. */ const rangeOf = (r: string | undefined) => { @@ -201,6 +201,8 @@ const rangeOf = (r: string | undefined) => { }; const fromX = (t: XTopic, ids: Map, rep: Report, acc: Acc, pic: Pictures, isRoot = false): Node => { + if (isRoot) acc.rootClass = t.structureClass; + else if (t.structureClass && t.structureClass !== acc.rootClass) rep.add("structures set on individual topics dropped"); const children = (t.children?.attached ?? []).map((c) => fromX(c, ids, rep, acc, pic)); // Floating topics hang off the central topic; elsewhere XMind has none and neither do we. const floating = t.children?.detached ?? []; diff --git a/src/xmindxml.ts b/src/xmindxml.ts index 734e794..b876cc0 100644 --- a/src/xmindxml.ts +++ b/src/xmindxml.ts @@ -142,9 +142,10 @@ export function resolveTopicLinks(roots: Node[], ids: Map) { roots.forEach(fix); } -function topicFromXml(t: XNode, ids: Map, rep: Report, bounds: string[]): Node { +function topicFromXml(t: XNode, ids: Map, rep: Report, bounds: string[], rootClass?: string): Node { + if (rootClass !== undefined && t.attrs["structure-class"] && t.attrs["structure-class"] !== rootClass) rep.add("structures set on individual topics dropped"); const attached = kid(t, "children")?.children.find((c) => c.name === "topics" && (c.attrs.type ?? "attached") === "attached"); - const children = (attached?.children ?? []).filter((c) => c.name === "topic").map((c) => topicFromXml(c, ids, rep, bounds)); + const children = (attached?.children ?? []).filter((c) => c.name === "topic").map((c) => topicFromXml(c, ids, rep, bounds, rootClass ?? t.attrs["structure-class"] ?? "")); const subs = kid(t, "children")?.children.filter((c) => c.name === "topics") ?? []; rep.add("floating topics dropped", subs.filter((c) => c.attrs.type === "detached").reduce((n, c) => n + c.children.length, 0)); rep.add("summaries dropped", subs.filter((c) => c.attrs.type === "summary").reduce((n, c) => n + c.children.length, 0));