import xmlFormatter from 'xml-formatter'; import { Mindmap } from '../..'; import INodeModel, { TopicShape } from '../model/INodeModel'; import RelationshipModel from '../model/RelationshipModel'; import IconModel from '../model/IconModel'; import FeatureModel from '../model/FeatureModel'; import LinkModel from '../model/LinkModel'; import NoteModel from '../model/NoteModel'; import Exporter from './Exporter'; import FreemindConstant from './freemind/FreemindConstant'; import VersionNumber from './freemind/importer/VersionNumber'; import ObjectFactory from './freemind/ObjectFactory'; import FreemindMap from './freemind/Map'; import FreeminNode from './freemind/Node'; import Arrowlink from './freemind/Arrowlink'; import Richcontent from './freemind/Richcontent'; import Icon from './freemind/Icon'; import Edge from './freemind/Edge'; import Font from './freemind/Font'; type PositionNodeType = {x: number, y: number} class FreemindExporter extends Exporter { private mindmap: Mindmap; private nodeMap: Map = null; private version: VersionNumber = FreemindConstant.SUPPORTED_FREEMIND_VERSION; private objectFactory: ObjectFactory; private static wisweToFreeFontSize: Map = new Map(); constructor(mindmap: Mindmap) { super(FreemindConstant.SUPPORTED_FREEMIND_VERSION.getVersion(), 'application/xml'); this.mindmap = mindmap; } exportAndEncode(): Promise { throw new Error('Method not implemented.'); } getContentType(): string { throw new Error('Method not implemented.'); } static { this.wisweToFreeFontSize.set(6, 10); this.wisweToFreeFontSize.set(8, 12); this.wisweToFreeFontSize.set(10, 18); this.wisweToFreeFontSize.set(15, 24); } private static parserXMLString(xmlStr: string, mimeType: DOMParserSupportedType): Document { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlStr, mimeType); // FIXME: Fix error "unclosed tag: p" when exporting bug2 and enc // Is there any parsing error ?. /* if (xmlDoc.getElementsByTagName('parsererror').length > 0) { const xmmStr = new XMLSerializer().serializeToString(xmlDoc); console.log(xmmStr); throw new Error(`Unexpected error parsing: ${xmlStr}. Error: ${xmmStr}`); } */ return xmlDoc; } extension(): string { return 'mm'; } async export(): Promise { this.objectFactory = new ObjectFactory(); this.nodeMap = new Map(); const freemainMap: FreemindMap = this.objectFactory.createMap(); freemainMap.setVesion(this.getVersionNumber()); const main: FreeminNode = this.objectFactory.createNode(); freemainMap.setNode(main); const centralTopic: INodeModel = this.mindmap.getCentralTopic(); if (centralTopic) { this.nodeMap.set(centralTopic.getId(), main); this.setTopicPropertiesToNode({ freemindNode: main, mindmapTopic: centralTopic, isRoot: true }); this.addNodeFromTopic(centralTopic, main); } const relationships: Array = this.mindmap.getRelationships(); relationships.forEach((relationship: RelationshipModel) => { const srcNode: FreeminNode = this.nodeMap.get(relationship.getFromNode()); const destNode: FreeminNode = this.nodeMap.get(relationship.getToNode()); if (srcNode && destNode) { const arrowlink: Arrowlink = this.objectFactory.crateArrowlink(); arrowlink.setDestination(destNode.getId()); if (relationship.getEndArrow() && relationship.getEndArrow()) arrowlink.setEndarrow('Default'); if (relationship.getStartArrow() && relationship.getStartArrow()) arrowlink.setStartarrow('Default'); srcNode.setArrowlinkOrCloudOrEdge(arrowlink); } }); const freeToXml = freemainMap.toXml(); const xmlToString = new XMLSerializer().serializeToString(freeToXml); const formatXml = xmlFormatter(xmlToString, { indentation: ' ', collapseContent: true, lineSeparator: '\n', }); return Promise.resolve(formatXml); } private setTopicPropertiesToNode({ freemindNode, mindmapTopic, isRoot }: { freemindNode: FreeminNode; mindmapTopic: INodeModel; isRoot: boolean; }): void { freemindNode.setId(`ID_${mindmapTopic.getId()}`); const text = mindmapTopic.getText(); if (text) { if (!text.includes('\n')) { freemindNode.setText(text); } else { const richcontent: Richcontent = this.buildRichcontent(text, 'NODE'); freemindNode.setArrowlinkOrCloudOrEdge(richcontent); } } const wiseShape: string = mindmapTopic.getShapeType(); if (wiseShape && TopicShape.LINE !== wiseShape) { freemindNode.setBackgorundColor(this.rgbToHex(mindmapTopic.getBackgroundColor())); } if (wiseShape) { const isRootRoundedRectangle = isRoot && TopicShape.ROUNDED_RECT !== wiseShape; const notIsRootLine = !isRoot && TopicShape.LINE !== wiseShape; if (isRootRoundedRectangle || notIsRootLine) { let style: string = wiseShape; if (TopicShape.ROUNDED_RECT === style || TopicShape.ELLIPSE === style) { style = 'bubble'; } freemindNode.setStyle(style); } } else if (!isRoot) freemindNode.setStyle('fork'); this.addFeautreNode(freemindNode, mindmapTopic); this.addFontNode(freemindNode, mindmapTopic); this.addEdgeNode(freemindNode, mindmapTopic); } private addNodeFromTopic(mainTopic: INodeModel, destNode: FreeminNode): void { const curretnTopics: Array = mainTopic.getChildren(); curretnTopics.forEach((currentTopic: INodeModel) => { const newNode: FreeminNode = this.objectFactory.createNode(); this.nodeMap.set(currentTopic.getId(), newNode); this.setTopicPropertiesToNode({ freemindNode: newNode, mindmapTopic: currentTopic, isRoot: false }); destNode.setArrowlinkOrCloudOrEdge(newNode); this.addNodeFromTopic(currentTopic, newNode); const position: PositionNodeType = currentTopic.getPosition(); if (position) { const xPos: number = position.x; newNode.setPosition((xPos < 0 ? 'left' : 'right')); } else newNode.setPosition('left'); }); } private buildRichcontent(text: string, type: string): Richcontent { const richconent: Richcontent = this.objectFactory.createRichcontent(); richconent.setType(type); const textSplit = text.split('\n'); let html = ''; textSplit.forEach((line: string) => { html += `

${line.trim()}

`; }); html += ''; const richconentDocument: Document = FreemindExporter.parserXMLString(html, 'application/xml'); const xmlResult = new XMLSerializer().serializeToString(richconentDocument); richconent.setHtml(xmlResult); return richconent; } private addFeautreNode(freemindNode: FreeminNode, mindmapTopic: INodeModel): void { const branches: Array = mindmapTopic.getFeatures(); branches .forEach((feature: FeatureModel) => { const type = feature.getType(); if (type === 'link') { const link = feature as LinkModel; freemindNode.setLink(link.getUrl()); } if (type === 'note') { const note = feature as NoteModel; const richcontent: Richcontent = this.buildRichcontent(note.getText(), 'NOTE'); freemindNode.setArrowlinkOrCloudOrEdge(richcontent); } if (type === 'icon') { const icon = feature as IconModel; const freemindIcon: Icon = new Icon(); freemindIcon.setBuiltin(icon.getIconType()); freemindNode.setArrowlinkOrCloudOrEdge(freemindIcon); } }); } private addEdgeNode(freemainMap: FreeminNode, mindmapTopic: INodeModel): void { if (mindmapTopic.getBorderColor()) { const edgeNode: Edge = this.objectFactory.createEdge(); edgeNode.setColor(this.rgbToHex(mindmapTopic.getBorderColor())); freemainMap.setArrowlinkOrCloudOrEdge(edgeNode); } } private addFontNode(freemindNode: FreeminNode, mindmapTopic: INodeModel): void { const fontFamily: string = mindmapTopic.getFontFamily(); const fontSize: number = mindmapTopic.getFontSize(); const fontColor: string = mindmapTopic.getFontColor(); const fontWeigth: string | number | boolean = mindmapTopic.getFontWeight(); const fontStyle: string = mindmapTopic.getFontStyle(); if (fontFamily || fontSize || fontColor || fontWeigth || fontStyle) { const font: Font = this.objectFactory.createFont(); let fontNodeNeeded = false; if (fontFamily) { font.setName(fontFamily); } if (fontSize) { const freeSize = FreemindExporter.wisweToFreeFontSize.get(fontSize); if (freeSize) { font.setSize(freeSize.toString()); fontNodeNeeded = true; } } if (fontColor) { freemindNode.setColor(fontColor); } if (fontWeigth) { if (typeof fontWeigth === 'boolean') { font.setBold(String(fontWeigth)); } else { font.setBold(String(true)); } fontNodeNeeded = true; } if (fontStyle === 'italic') { font.setItalic(String(true)); } if (fontNodeNeeded) { if (!font.getSize()) { font.setSize(FreemindExporter.wisweToFreeFontSize.get(8).toString()); } freemindNode.setArrowlinkOrCloudOrEdge(font); } } } private rgbToHex(color: string): string { let result: string = color; if (result) { const isRgb = new RegExp('^rgb\\([0-9]{1,3}, [0-9]{1,3}, [0-9]{1,3}\\)$'); if (isRgb.test(result)) { const rgb: string[] = color.substring(4, color.length - 1).split(','); const r: string = rgb[0].trim(); const g: string = rgb[1].trim(); const b: string = rgb[2].trim(); result = `#${r.length === 1 ? `0${r}` : r}${g.length === 1 ? `0${g}` : g}${b.length === 1 ? `0${b}` : b}`; } } return result; } private getVersion(): VersionNumber { return this.version; } private getVersionNumber(): string { return this.getVersion().getVersion(); } } export default FreemindExporter;