import xmlFormatter from 'xml-formatter'; import { Mindmap } from '../..'; import INodeModel, { TopicShapeType } from '../model/INodeModel'; import RelationshipModel from '../model/RelationshipModel'; import SvgIconModel from '../model/SvgIconModel'; import FeatureModel from '../model/FeatureModel'; import LinkModel from '../model/LinkModel'; import NoteModel from '../model/NoteModel'; import PositionNodeType from '../PositionType'; 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'; class FreemindExporter extends Exporter { private mindmap: Mindmap; private nodeMap: Map; 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; } 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 bug3 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'; } 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 | undefined = this.nodeMap.get(relationship.getFromNode()); const destNode: FreeminNode | undefined = this.nodeMap.get(relationship.getToNode()); if (srcNode && destNode) { const arrowlink: Arrowlink = this.objectFactory.crateArrowlink(); const idRel = destNode.getId(); if (idRel) { arrowlink.setDestination(idRel); } 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: TopicShapeType = mindmapTopic.getShapeType(); if (wiseShape && wiseShape !== 'line') { const color = mindmapTopic.getBackgroundColor(); if (color) { freemindNode.setBackgorundColor(this.rgbToHex(color)); } } if (wiseShape) { const isRootRoundedRectangle = isRoot && wiseShape !== 'rounded rectangle'; const notIsRootLine = !isRoot && wiseShape !== 'line'; if (isRootRoundedRectangle || notIsRootLine) { let style: string = wiseShape; if (style === 'rounded rectangle' || style === 'elipse') { 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 SvgIconModel; 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(); const color = mindmapTopic.getBorderColor(); if (color) { edgeNode.setColor(this.rgbToHex(color)); } freemainMap.setArrowlinkOrCloudOrEdge(edgeNode); } } private addFontNode(freemindNode: FreeminNode, mindmapTopic: INodeModel): void { const fontFamily: string | undefined = mindmapTopic.getFontFamily(); const fontSize: number | undefined = mindmapTopic.getFontSize(); const fontColor: string | undefined = mindmapTopic.getFontColor(); const fontWeigth: string | number | boolean | undefined = mindmapTopic.getFontWeight(); const fontStyle: string | undefined = 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()) { const size = FreemindExporter.wisweToFreeFontSize.get(8); if (size) { font.setSize(size.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;