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)