From debb044bc88c029fc2e7c8a5e4a5cd313b3861b0 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 21 Oct 2026 23:36:42 +0200 Subject: [PATCH] fix(xmind): open maps saved with XMind 2020 and later XMind 2020 and later (XMind Zen) store the map in content.json and only write a map with a warning message to content.xml, which was all that got shown. Read content.json when the file has one, with the same titles, priority and progress markers, links and child topics as before. Fixes #164 Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- src/__tests__/plugins/xmind.spec.js | 34 ++++++++++++++++++++++++++++ src/plugins/xmind.js | 35 +++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/__tests__/plugins/xmind.spec.js b/src/__tests__/plugins/xmind.spec.js index 86ffb1e4..73aba72e 100644 --- a/src/__tests__/plugins/xmind.spec.js +++ b/src/__tests__/plugins/xmind.spec.js @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import JSZip from 'jszip' import { describe, expect, it } from 'vitest' import xmind from '../../plugins/xmind.js' @@ -105,9 +106,42 @@ describe('xmind plugin', () => { }) }) + describe('processJsonTopic', () => { + it('extracts title, markers, hyperlink and attached children', () => { + const obj = {} + xmind.processJsonTopic({ + title: 'Root', + href: 'https://example.com', + markers: [{ markerId: 'priority-3' }, { markerId: 'task-done' }, { markerId: 'unknown' }], + children: { + attached: [{ title: 'Child1' }, { title: 'Child2' }], + detached: [{ title: 'Floating' }], + }, + }, obj) + expect(obj.data).toEqual({ text: 'Root', hyperlink: 'https://example.com', priority: 3, progress: 9 }) + expect(obj.children.map((child) => child.data.text)).toEqual(['Child1', 'Child2']) + }) + + it('produces no children property for leaf topics', () => { + const obj = {} + xmind.processJsonTopic({ title: 'Leaf' }, obj) + expect(obj.children).toBeUndefined() + }) + }) + describe('readDocument', () => { it('rejects when content.xml is missing in the zip', async () => { await expect(xmind.readDocument('not a zip')).rejects.toBeDefined() }) + + it('reads content.json instead of the warning map in content.xml', async () => { + const zip = new JSZip() + zip.file('content.json', JSON.stringify([{ rootTopic: { title: 'Zen root' } }])) + zip.file('content.xml', 'Warning') + const data = await zip.generateAsync({ type: 'uint8array' }) + + const result = await xmind.readDocument(data) + expect(result.data.text).toBe('Zen root') + }) }) }) diff --git a/src/plugins/xmind.js b/src/plugins/xmind.js index 2910de8b..3e9ee1f2 100644 --- a/src/plugins/xmind.js +++ b/src/plugins/xmind.js @@ -89,15 +89,46 @@ export default { this.processTopic(topic, result) return result }, + processJsonTopic(topic, obj) { + obj.data = { + text: topic.title, + } + for (const marker of topic.markers || []) { + const type = this.markerMap[marker.markerId] + if (type) { + obj.data[type[0]] = type[1] + } + } + if (topic.href) { + obj.data.hyperlink = topic.href + } + const children = topic.children?.attached + if (children?.length) { + obj.children = children.map((child) => { + const childObj = {} + this.processJsonTopic(child, childObj) + return childObj + }) + } + }, + jsonToKm(text) { + const sheets = JSON.parse(text) + const result = {} + this.processJsonTopic(sheets[0].rootTopic, result) + return result + }, readDocument(file) { const self = this return new Promise(function(resolve, reject) { JSZip.loadAsync(file).then(function(zip) { - const contentFile = zip.file('content.xml') + // XMind 2020 and later keep the map in content.json and only + // write a warning map to content.xml + const jsonFile = zip.file('content.json') + const contentFile = jsonFile || zip.file('content.xml') if (contentFile !== null) { contentFile.async('text').then(function(text) { try { - const json = self.toKm(text) + const json = jsonFile ? self.jsonToKm(text) : self.toKm(text) resolve(json) } catch (e) { reject(e)