Skip to content

Commit 8efcf99

Browse files
Merge pull request #407 from nextcloud/fix/freemind-import
fix(freemind): load FreeMind files again and import them more completely
2 parents 2df32ae + ca2a271 commit 8efcf99

3 files changed

Lines changed: 317 additions & 21 deletions

File tree

‎src/__tests__/plugins/freemind.spec.js‎

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,8 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import { beforeAll, describe, expect, it } from 'vitest'
6+
import { describe, expect, it } from 'vitest'
77
import freemind from '../../plugins/freemind.js'
8-
import util from '../../util.js'
9-
10-
// freemind.toKm references the legacy global FilesMindMap.Util
11-
beforeAll(() => {
12-
global.FilesMindMap = { Util: util }
13-
})
148

159
describe('freemind plugin', () => {
1610
it('has the correct name', () => {
@@ -67,6 +61,54 @@ describe('freemind plugin', () => {
6761
const obj = {}
6862
freemind.processTopic({ TEXT: 'Unknown', icon: { BUILTIN: 'unknown-marker' } }, obj)
6963
expect(obj.data.priority).toBeUndefined()
64+
expect(obj.data.text).toBe('Unknown')
65+
})
66+
67+
it('shows other icons as emoji before the text, or as the text of an icon-only node', () => {
68+
const withText = {}
69+
freemind.processTopic({ TEXT: 'Done', icon: [{ BUILTIN: 'button_ok' }, { BUILTIN: 'full-9' }, { BUILTIN: 'hourglass' }] }, withText)
70+
expect(withText.data.text).toBe('✅⌛ Done')
71+
expect(withText.data.priority).toBe(9)
72+
73+
const iconOnly = {}
74+
freemind.processTopic({ TEXT: '', icon: { BUILTIN: 'ksmiletris' } }, iconOnly)
75+
expect(iconOnly.data.text).toBe('😊')
76+
})
77+
78+
it('takes over the font and colours of the node, but not of its line', () => {
79+
const obj = {}
80+
freemind.processTopic({
81+
TEXT: 'RESULT',
82+
COLOR: '#ff0000',
83+
BACKGROUND_COLOR: '#99ff99',
84+
font: { BOLD: 'true', ITALIC: 'true', NAME: 'Monospaced', SIZE: '14' },
85+
edge: { COLOR: '#00ff00', STYLE: 'bezier' },
86+
}, obj)
87+
expect(obj.data).toEqual({
88+
text: 'RESULT',
89+
color: '#ff0000',
90+
background: '#99ff99',
91+
'font-weight': 'bold',
92+
'font-style': 'italic',
93+
'font-size': 14,
94+
'font-family': 'monospace',
95+
})
96+
})
97+
98+
it('leaves FreeMind\'s default font and unset styles to the editor', () => {
99+
const obj = {}
100+
freemind.processTopic({ TEXT: 'Plain', font: { NAME: 'SansSerif', SIZE: '12' } }, obj)
101+
expect(obj.data).toEqual({ text: 'Plain', 'font-size': 12 })
102+
103+
const named = {}
104+
freemind.processTopic({ TEXT: 'Named', font: { NAME: 'Arial', BOLD: 'false' } }, named)
105+
expect(named.data).toEqual({ text: 'Named', 'font-family': 'Arial' })
106+
})
107+
108+
it('uses FreeMind\'s black text on a background without a text colour', () => {
109+
const obj = {}
110+
freemind.processTopic({ TEXT: 'Root', BACKGROUND_COLOR: '#99ff99' }, obj)
111+
expect(obj.data).toMatchObject({ background: '#99ff99', color: '#000000' })
70112
})
71113

72114
it('processes a single child node', () => {
@@ -103,6 +145,70 @@ describe('freemind plugin', () => {
103145
expect(result.children[0].data.text).toBe('Child')
104146
})
105147

148+
it('takes the text of a node without TEXT from its rich content', async () => {
149+
const xml = '<map version="1.1.0"><node><richcontent TYPE="NODE"><html><body>'
150+
+ '<p>FreeMind</p><p> Template </p></body></html></richcontent>'
151+
+ '<richcontent TYPE="NOTE"><html><body><p>A note</p></body></html></richcontent>'
152+
+ '<node TEXT="Child"/></node></map>'
153+
const result = await freemind.decode(xml)
154+
expect(result.data.text).toBe('FreeMind\nTemplate')
155+
expect(result.children[0].data.text).toBe('Child')
156+
})
157+
158+
it('keeps line breaks in rich content but not the wrapping of its HTML', async () => {
159+
const xml = '<map version="1.0.1"><node><richcontent TYPE="NODE"><html><body>'
160+
+ '<p align="center">\n FreeMind<br/><small>- free mind\n mapping software -</small>\n</p>'
161+
+ '</body></html></richcontent></node></map>'
162+
const result = await freemind.decode(xml)
163+
expect(result.data.text).toBe('FreeMind\n- free mind mapping software -')
164+
})
165+
166+
it('takes the text of FreeMind 0.8 HTML in TEXT, with the chosen value of form fields', async () => {
167+
const xml = '<map version="0.8.0"><node TEXT="&lt;html&gt;&lt;img src=&quot;a.gif&quot;&gt;&lt;br&gt;Complete Man">'
168+
+ '<node TEXT="&lt;html&gt;Rating &lt;select&gt;&lt;option&gt;--&lt;/option&gt;&lt;option selected&gt;++&lt;/option&gt;&lt;/select&gt;"/>'
169+
+ '<node TEXT="&lt;html&gt;&lt;select&gt;&lt;option&gt;--&lt;/option&gt;&lt;/select&gt;"/>'
170+
+ '</node></map>'
171+
const result = await freemind.decode(xml)
172+
expect(result.data.text).toBe('Complete Man')
173+
expect(result.children[0].data.text).toBe('Rating ++')
174+
expect(result.children[1].data.text).toBe('')
175+
})
176+
177+
it('names an image-only node after the image and links web images', async () => {
178+
const html = (img) => '<richcontent TYPE="NODE"><html><body>' + img + '</body></html></richcontent>'
179+
const xml = '<map version="1.0.1"><node TEXT="Root">'
180+
+ '<node>' + html('<img src="http://example.com/images/menu/binah.gif"/>') + '</node>'
181+
+ '<node>' + html('<img src="https://example.com/a.png" alt="Tree of life"/>') + '</node>'
182+
+ '<node>' + html('<img src="file:/C:/Office/Bitmaps/ACBLENDS.GIF"/>') + '</node>'
183+
+ '<node LINK="https://nextcloud.com">' + html('<img src="http://example.com/x%20y.gif"/>') + '</node>'
184+
+ '<node TEXT="&lt;html&gt;&lt;img src=&quot;http://example.com/flag.gif&quot;&gt;&lt;br&gt;Spirit Form"/>'
185+
+ '</node></map>'
186+
const [web, alt, local, linked, withText] = (await freemind.decode(xml)).children.map((child) => child.data)
187+
expect(web).toMatchObject({ text: 'binah', hyperlink: 'http://example.com/images/menu/binah.gif' })
188+
expect(alt).toMatchObject({ text: 'Tree of life', hyperlink: 'https://example.com/a.png' })
189+
expect(local.text).toBe('ACBLENDS')
190+
expect(local.hyperlink).toBeUndefined()
191+
expect(linked).toMatchObject({ text: 'x y', hyperlink: 'https://nextcloud.com' })
192+
expect(withText).toMatchObject({ text: 'Spirit Form', hyperlink: 'http://example.com/flag.gif' })
193+
})
194+
195+
it('imports notes and folded branches', async () => {
196+
const xml = '<map version="1.0.1"><node TEXT="Root"><node TEXT="Folded" FOLDED="true">'
197+
+ '<richcontent TYPE="NOTE"><html><body><p>First</p><p>Second</p></body></html></richcontent>'
198+
+ '<node TEXT="Hidden"/></node></node></map>'
199+
const result = await freemind.decode(xml)
200+
expect(result.data.expandState).toBeUndefined()
201+
expect(result.children[0].data.expandState).toBe('collapse')
202+
expect(result.children[0].data.note).toBe('First\nSecond')
203+
expect(result.children[0].children[0].data.text).toBe('Hidden')
204+
})
205+
206+
it('rejects a file that is not a FreeMind map with a clear message', async () => {
207+
await expect(freemind.decode('<!DOCTYPE html><html><head><title>VIP.mm</title></head><body></body></html>'))
208+
.rejects.toThrow('This file is not a valid FreeMind mind map.')
209+
await expect(freemind.decode('<map version="1.0.1"></map>')).rejects.toThrow('not a valid FreeMind')
210+
})
211+
106212
it('rejects on malformed input', async () => {
107213
await expect(freemind.decode(null)).rejects.toBeDefined()
108214
})

‎src/mindmap.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ const FilesMindMap = {
143143

144144
success(data.filecontents)
145145
}, function(e) {
146-
failure(e)
146+
failure(e?.message ?? e)
147147
})
148148
}).catch(function(error) {
149149
failure(error.response?.data?.message || error.message)

0 commit comments

Comments
 (0)