├── .gitignore ├── LICENSE.md ├── README.md ├── example.py ├── logo.jpeg ├── setup.py └── xmind ├── __init__.py ├── core ├── __init__.py ├── const.py ├── loader.py ├── markerref.py ├── mixin.py ├── notes.py ├── position.py ├── relationship.py ├── saver.py ├── sheet.py ├── title.py ├── topic.py └── workbook.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Backup files 2 | *~ 3 | \#*\# 4 | 5 | # Generated sample 6 | test2.xmind 7 | 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Packages 14 | *.egg 15 | *.egg-info 16 | dist 17 | build 18 | eggs 19 | parts 20 | bin 21 | var 22 | sdist 23 | develop-eggs 24 | .installed.cfg 25 | lib 26 | lib64 27 | __pycache__ 28 | 29 | # Installer logs 30 | pip-log.txt 31 | 32 | # Unit test / coverage reports 33 | .coverage 34 | .tox 35 | nosetests.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | #MIT License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2013 XMind, Ltd 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #XMind SDK for python 3.x. This fork does not work with python 2.x 2 | 3 | **XMind SDK for python 3** to help Python developers to easily work with XMind files and build XMind extensions. 4 | (This fork is a port from python2 to python3 of https://github.com/xmindltd/xmind-sdk-python.git). 5 | 6 | ##Install XMind SDK for python 3 7 | 8 | Clone the repository to a local working directory 9 | 10 | git clone https://github.com/jmoraleda/xmind-sdk-python3.git 11 | 12 | Now there will be a directory named `xmind-sdk-python` under the current directory. Change to the directory `xmind-sdk-python` and install **XMind SDK for python**. 13 | 14 | python3 setup.py install 15 | 16 | *It is highly recommended to install __XMind SDK for python__ under an isolated python environment using [virtualenv](https://pypi.python.org/pypi/virtualenv)* 17 | 18 | ##Usage 19 | 20 | Open an existing XMind file or create a new XMind file and place it into a given path 21 | 22 | import xmind 23 | workbook = xmind.load(/path/to/file/) # Requires '.xmind' extension 24 | 25 | Save XMind file to a path. 26 | If the path is not given then the API will save to the path set in the workbook 27 | 28 | xmind.save(workbook) 29 | 30 | or: 31 | 32 | xmind.save(workbook, /save/file/to/path) 33 | 34 | ##LICENSE 35 | 36 | The MIT License (MIT) 37 | 38 | Copyright (c) 2013 XMind, Ltd 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining a copy of 41 | this software and associated documentation files (the "Software"), to deal in 42 | the Software without restriction, including without limitation the rights to 43 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 44 | the Software, and to permit persons to whom the Software is furnished to do so, 45 | subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all 48 | copies or substantial portions of the Software. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 51 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 52 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 53 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 54 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 55 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 56 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | import xmind 3 | from xmind.core.const import TOPIC_DETACHED 4 | from xmind.core.markerref import MarkerId 5 | 6 | w = xmind.load("test.xmind") # load an existing file or create a new workbook if nothing is found 7 | 8 | s1=w.getPrimarySheet() # get the first sheet 9 | s1.setTitle("first sheet") # set its title 10 | r1=s1.getRootTopic() # get the root topic of this sheet 11 | r1.setTitle("we don't care of this sheet") # set its title 12 | 13 | s2=w.createSheet() # create a new sheet 14 | s2.setTitle("second sheet") 15 | r2=s2.getRootTopic() 16 | r2.setTitle("root node") 17 | 18 | # Empty topics are created from the root element and then filled. 19 | # Examples: 20 | 21 | # Create a topic with a link to the first sheet given by s1.getID() 22 | t1 = r2.addSubTopic() 23 | t1.setTopicHyperlink(s1.getID()) 24 | t1.setTitle("redirection to the first sheet") # set its title 25 | 26 | # Create a topic with a hyperlink 27 | t2 = r2.addSubTopic() 28 | t2.setTitle("second node") 29 | t2.setURLHyperlink("https://xmind.net") 30 | 31 | # Create a topic with notes 32 | t3 = r2.addSubTopic() 33 | t3.setTitle("third node") 34 | t3.setPlainNotes("notes for this topic") 35 | t3.setTitle("topic with \n notes") 36 | 37 | # Create a topic with a file hyperlink 38 | t4 = r2.addSubTopic() 39 | t4.setFileHyperlink("logo.jpeg") 40 | t4.setTitle("topic with a file") 41 | 42 | # Create topic that is a subtopic of another topic 43 | t41 = t4.addSubTopic() 44 | t41.setTitle("a subtopic") 45 | 46 | # create a detached topic whose (invisible) parent is the root 47 | d1 = r2.addSubTopic(topics_type = TOPIC_DETACHED) 48 | d1.setTitle("detached topic") 49 | d1.setPosition(0,20) 50 | 51 | # loop on the (attached) subTopics 52 | topics=r2.getSubTopics() 53 | # Demonstrate creating a marker 54 | for topic in topics: 55 | topic.addMarker(MarkerId.starBlue) 56 | 57 | # create a relationship 58 | rel=s2.createRelationship(t1.getID(),t2.getID(),"test") 59 | 60 | # and we save 61 | xmind.save(w,"test2.xmind") 62 | -------------------------------------------------------------------------------- /logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmoraleda/xmind-sdk-python3/46f61fc16efb3cc43aab2321b73a982d9adf2b8b/logo.jpeg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/env/bin python 2 | #-*- coding: utf-8 -*- 3 | 4 | from setuptools import setup, find_packages 5 | 6 | setup( 7 | name="xmind", 8 | version="0.1a.0", 9 | packages=find_packages(), 10 | 11 | install_requires=["distribute"], 12 | 13 | author="Woody Ai", 14 | author_email="aiqi@xmind.net", 15 | description="The offical XMind python SDK", 16 | license="MIT", 17 | keywords="XMind, SDK, mind mapping", 18 | url="https://github.com/xmindltd/xmind-sdk-python" 19 | ) 20 | -------------------------------------------------------------------------------- /xmind/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind 6 | ~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | 14 | __version__ = "0.1a.0" 15 | __author__ = "aiqi@xmind.net " 16 | 17 | from xmind.core.loader import WorkbookLoader 18 | from xmind.core.saver import WorkbookSaver 19 | 20 | 21 | def load(path): 22 | """ Load XMind workbook from given path. If file no 23 | exist on given path then created new one. 24 | 25 | """ 26 | loader = WorkbookLoader(path) 27 | return loader.get_workbook() 28 | 29 | 30 | def save(workbook, path=None): 31 | """ Save workbook to given path. If path not given, then 32 | will save to path that set to workbook. 33 | 34 | """ 35 | saver = WorkbookSaver(workbook) 36 | saver.save(path) 37 | 38 | -------------------------------------------------------------------------------- /xmind/core/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core 6 | ~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | 16 | from xml.dom import minidom as DOM 17 | 18 | from .. import utils 19 | 20 | 21 | def create_document(): 22 | """:cls: ``xml.dom.Document`` object constructor 23 | """ 24 | return DOM.Document() 25 | 26 | 27 | def create_element(tag_name, namespaceURI=None, prefix=None, localName=None): 28 | """:cls: ``xml.dom.Element`` object constructor 29 | """ 30 | element = DOM.Element(tag_name, namespaceURI, prefix, localName) 31 | 32 | # if ":" in tag_name: 33 | # prefix, local_name = tag_name.split(":") 34 | # else: 35 | # local_name = tag_name.split(":") 36 | # element.prefix = prefix 37 | # element.localName = local_name 38 | 39 | return element 40 | 41 | 42 | class Node(object): 43 | """ 44 | All of components of XMind workbook subclass Node 45 | """ 46 | def __init__(self, node): 47 | self._node = node 48 | 49 | def _equals(self, obj=None): 50 | """ 51 | Compare the passed object with the current instance 52 | """ 53 | if obj is None or not isinstance(obj, self.__class__): 54 | return False 55 | if obj == self: 56 | return True 57 | return self.getImplementation() == obj.getImplementation() 58 | 59 | def getImplementation(self): 60 | """ 61 | Get DOM implementation of passed node. Provides an interface to 62 | manipulate the DOM directly 63 | """ 64 | return self._node 65 | 66 | def getOwnerDocument(self): 67 | raise NotImplementedError("This method requires an implementation!") 68 | 69 | def setOwnerDocument(self, doc): 70 | raise NotImplementedError("This method requires an implementation!") 71 | 72 | def getLocalName(self, qualifiedName): 73 | index = qualifiedName.find(":") 74 | if index >= 0: 75 | return qualifiedName[index + 1:] 76 | else: 77 | return qualifiedName 78 | 79 | def getPrefix(self, qualifiedName): 80 | index = qualifiedName.find(":") 81 | if index >= 0: 82 | return qualifiedName[:index + 1] 83 | 84 | def appendChild(self, node): 85 | """ 86 | Append passed node to the end of child node list of this node 87 | """ 88 | node.setOwnerDocument(self.getOwnerDocument()) 89 | 90 | node_impel = node.getImplementation() 91 | 92 | return self._node.appendChild(node_impel) 93 | 94 | def insertBefore(self, new_node, ref_node): 95 | """ 96 | Insert new node before ref_node. Please notice that ref_node 97 | must be a child of this node. 98 | """ 99 | new_node.setOwnerDocument(self.getOwnerDocument()) 100 | 101 | new_node_imple = new_node.getImplementation() 102 | ref_node_imple = ref_node.getImplementation() 103 | 104 | return self._node.insertBefore(new_node_imple, ref_node_imple) 105 | 106 | def getChildNodesByTagName(self, tag_name): 107 | """ 108 | Search for all children with specified tag name under passed DOM 109 | implementation, instead of all descendants 110 | """ 111 | child_nodes = [] 112 | for node in self._node.childNodes: 113 | if node.nodeType == node.TEXT_NODE: 114 | continue 115 | 116 | if node.tagName == tag_name: 117 | child_nodes.append(node) 118 | 119 | return child_nodes 120 | 121 | def getFirstChildNodeByTagName(self, tag_name): 122 | child_nodes = self.getChildNodesByTagName(tag_name) 123 | 124 | if len(child_nodes) >= 1: 125 | return child_nodes[0] 126 | 127 | def getParentNode(self): 128 | return self._node.parentNode 129 | 130 | def _isOrphanNode(self, node): 131 | if node is None: 132 | return True 133 | if node.nodeType == node.DOCUMENT_NODE: 134 | return False 135 | 136 | return self._isOrphanNode(node.parentNode) 137 | 138 | def isOrphanNode(self): 139 | return self._isOrphanNode(self._node) 140 | 141 | def iterChildNodesByTagName(self, tag_name): 142 | for node in self._node.childNodes: 143 | if node.nodeType == node.TEXT_NODE: 144 | continue 145 | 146 | if node.tagName == tag_name: 147 | yield node 148 | 149 | def removeChild(self, child_node): 150 | child_node = child_node.getImplementation() 151 | self._node.removeChild(child_node) 152 | 153 | def output(self, output_stream): 154 | return self._node.writexml(output_stream, 155 | addindent="", 156 | newl="", 157 | encoding="utf-8") 158 | 159 | 160 | class Document(Node): 161 | def __init__(self, node=None): 162 | # FIXME: Should really call the base class 163 | #super(Document, self).__init__() 164 | self._node = node or self._documentConstructor() 165 | # self.arg = arg 166 | 167 | def _documentConstructor(self): 168 | return DOM.Document() 169 | 170 | @property 171 | def documentElement(self): 172 | """ 173 | Get root element of passed DOM implementation for manipulate 174 | """ 175 | return self._node.documentElement 176 | 177 | def getOwnerDocument(self): 178 | return self._node 179 | 180 | def createElement(self, tag_name): 181 | return self._node.createElement(tag_name) 182 | 183 | def setVersion(self, version): 184 | element = self.documentElement 185 | if element and not element.hasAttribute("version"): 186 | element.setAttribute("version", version) 187 | 188 | def replaceVersion(self, version): 189 | element = self.documentElement 190 | if element: 191 | element.setAttribute("version", version) 192 | 193 | def getElementById(self, id): 194 | return self._node.getElementById(id) 195 | 196 | 197 | class Element(Node): 198 | TAG_NAME = "" 199 | 200 | def __init__(self, node=None): 201 | # FIXME: Should really call the base class 202 | #super(Element, self).__init__() 203 | self._node = node or self._elementConstructor(self.TAG_NAME) 204 | 205 | def _elementConstructor(self, tag_name, 206 | namespaceURI=None, 207 | prefix=None, localName=None): 208 | return DOM.Element(tag_name, namespaceURI, 209 | self.getPrefix(tag_name), 210 | self.getLocalName(tag_name)) 211 | 212 | def getOwnerDocument(self): 213 | return self._node.ownerDocument 214 | 215 | def setOwnerDocument(self, doc_imple): 216 | self._node.ownerDocument = doc_imple 217 | 218 | def setAttributeNS(self, namespace, attr): 219 | """ 220 | Set attributes with namespace to DOM implementation. 221 | Please notice that namespace must be a namespace name and 222 | namespace value. Attr composed by namespceURI, localName and value. 223 | """ 224 | namespace_name, namespace_value = namespace 225 | if not self._node.hasAttribute(namespace_name): 226 | self._node.setAttribute(namespace_name, namespace_value) 227 | 228 | namespaceURI, localName, value = attr 229 | if not self._node.hasAttributeNS(namespaceURI, localName): 230 | qualifiedName = "%s:%s" % (namespace_name, localName) 231 | self._node.setAttributeNS(namespaceURI, qualifiedName, value) 232 | 233 | def getAttribute(self, attr_name): 234 | """ 235 | Get attribute with specified name. And allowed get attribute with 236 | specified name in ``prefix:localName`` format. 237 | """ 238 | if not self._node.hasAttribute(attr_name): 239 | localName = self.getLocalName(attr_name) 240 | if localName != attr_name: 241 | return self.getAttribute(localName) 242 | return 243 | 244 | return self._node.getAttribute(attr_name) 245 | 246 | def setAttribute(self, attr_name, attr_value=None): 247 | """ 248 | Set attribute to element. Please notice that if ``attr_value`` is 249 | None and attribute with specified ``attr_name`` is exist, 250 | attribute will be removed. 251 | """ 252 | if attr_value is not None: 253 | self._node.setAttribute(attr_name,str(attr_value)) 254 | elif self._node.hasAttribute(attr_name): 255 | self._node.removeAttribute(attr_name) 256 | 257 | def createElement(self, tag_name): 258 | """ 259 | Create new element. But created element doesn't add to the child 260 | node list of this element, invoke :func: ``self.appendChild`` or :func: 261 | ``self.insertBefore`` to add created element to the child node list of 262 | this element. 263 | """ 264 | pass 265 | 266 | def addIdAttribute(self, attr_name): 267 | if not self._node.hasAttribute(attr_name): 268 | id = utils.generate_id() 269 | self._node.setAttribute(attr_name, id) 270 | 271 | if self.getOwnerDocument(): 272 | self._node.setIdAttribute(attr_name) 273 | 274 | def getIndex(self): 275 | parent = self.getParentNode() 276 | if parent: 277 | index = 0 278 | for node in parent.childNodes: 279 | if self._node is node: 280 | return index 281 | index += 1 282 | 283 | return -1 284 | 285 | def getTextContent(self): 286 | text = [] 287 | for node in self._node.childNodes: 288 | if node.nodeType == DOM.Node.TEXT_NODE: 289 | text.append(node.data) 290 | 291 | if not len(text) > 0: 292 | return 293 | 294 | text = "\n".join(text) 295 | return text 296 | 297 | def setTextContent(self, data): 298 | for node in self._node.childNodes: 299 | if node.nodeType == DOM.Node.TEXT_NODE: 300 | self._node.removeChild(node) 301 | 302 | text = DOM.Text() 303 | text.data = data 304 | 305 | self._node.appendChild(text) 306 | -------------------------------------------------------------------------------- /xmind/core/const.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.const 6 | ~~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | """ 11 | 12 | __author__ = "aiqi@xmind.net " 13 | 14 | XMIND_EXT = ".xmind" 15 | 16 | VERSION = "2.0" 17 | NAMESPACE = "xmlns" 18 | XMAP = "urn:xmind:xmap:xmlns:content:2.0" 19 | 20 | ATTACHMENTS_DIR = "attachments/" 21 | MARKERS_DIR = "markers/" 22 | META_INF_DIR = "META-INF/" 23 | REVISIONS_DIR = "Revisions/" 24 | 25 | CONTENT_XML = "content.xml" 26 | STYLES_XML = "styles.xml" 27 | META_XML = "meta.xml" 28 | MANIFEST_XML = "META-INF/manifest.xml" 29 | MARKER_SHEET_XML = "markerSheet.xml" 30 | MARKER_SHEET = MARKERS_DIR + MARKER_SHEET_XML 31 | REVISIONS_XML = "revisions.xml" 32 | 33 | TAG_WORKBOOK = "xmap-content" 34 | TAG_TOPIC = "topic" 35 | TAG_TOPICS = "topics" 36 | TAG_SHEET = "sheet" 37 | TAG_TITLE = "title" 38 | TAG_POSITION = "position" 39 | TAG_CHILDREN = "children" 40 | TAG_NOTES = "notes" 41 | TAG_RELATIONSHIP = "relationship" 42 | TAG_RELATIONSHIPS = "relationships" 43 | TAG_MARKERREFS = "marker-refs" 44 | TAG_MARKERREF = "marker-ref" 45 | ATTR_VERSION = "version" 46 | ATTR_ID = "id" 47 | ATTR_STYLE_ID = "style-id" 48 | ATTR_TIMESTAMP = "timestamp" 49 | ATTR_THEME = "theme" 50 | ATTR_X = "svg:x" 51 | ATTR_Y = "svg:y" 52 | ATTR_HREF = "xlink:href" 53 | ATTR_BRANCH = "branch" 54 | ATTR_TYPE = "type" 55 | ATTR_END1 = "end1" 56 | ATTR_END2 = "end2" 57 | ATTR_MARKERID = "marker-id" 58 | 59 | NS_URI = "http://www.w3.org/1999/xhtml" 60 | 61 | NS_XHTML = (NS_URI, "xhtml", "http://www.w3.org/1999/xhtml") 62 | NS_XLINK = (NS_URI, "xlink", "http://www.w3.org/1999/xlink") 63 | NS_SVG = (NS_URI, "svg", "http://www.w3.org/2000/svg") 64 | NS_FO = (NS_URI, "fo", "http://www.w3.org/1999/XSL/Format") 65 | 66 | VAL_FOLDED = "folded" 67 | 68 | TOPIC_ROOT = "root" 69 | TOPIC_ATTACHED = "attached" 70 | TOPIC_DETACHED = "detached" 71 | 72 | FILE_PROTOCOL = "file://" 73 | TOPIC_PROTOCOL = "xmind:#" 74 | HTTP_PROTOCOL = "http://" 75 | HTTPS_PROTOCOL = "https://" 76 | 77 | HTML_FORMAT_NOTE = "html" 78 | PLAIN_FORMAT_NOTE = "plain" 79 | -------------------------------------------------------------------------------- /xmind/core/loader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.loader 6 | ~~~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | from . import const 16 | from .workbook import WorkbookDocument 17 | 18 | from .. import utils 19 | 20 | 21 | class WorkbookLoader(object): 22 | def __init__(self, path): 23 | """ Load XMind workbook from given path 24 | 25 | :param path: path to XMind file. If not an existing file, 26 | will not raise an exception. 27 | 28 | """ 29 | super(WorkbookLoader, self).__init__() 30 | self._input_source = utils.get_abs_path(path) 31 | 32 | file_name, ext = utils.split_ext(self._input_source) 33 | 34 | if ext != const.XMIND_EXT: 35 | raise Exception("The XMind filename is missing the '%s' extension!" % const.XMIND_EXT) 36 | 37 | # Input Stream 38 | self._content_stream = None 39 | 40 | try: 41 | with utils.extract(self._input_source) as input_stream: 42 | for stream in input_stream.namelist(): 43 | if stream == const.CONTENT_XML: 44 | self._content_stream = utils.parse_dom_string( 45 | input_stream.read(stream)) 46 | except: 47 | pass 48 | 49 | def get_workbook(self): 50 | """ Parse XMind file to `WorkbookDocument` object and return 51 | """ 52 | content = self._content_stream 53 | path = self._input_source 54 | 55 | workbook = WorkbookDocument(content, path) 56 | return workbook 57 | 58 | -------------------------------------------------------------------------------- /xmind/core/markerref.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.markerref 6 | ~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "stanypub@gmail.com " 14 | 15 | from . import const 16 | from .mixin import WorkbookMixinElement 17 | 18 | 19 | class MarkerId: 20 | def __init__(self, name): 21 | self.name = name 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | def __repr__(self): 27 | return "" % self 28 | 29 | def __eq__(self, other): 30 | """Override the default Equals behavior""" 31 | if isinstance(other, self.__class__): 32 | return self.name == other.name 33 | return False 34 | 35 | def getFamily(self): 36 | return self.name.split('-')[0] 37 | 38 | 39 | MarkerId.starRed = 'star-red' 40 | MarkerId.starOrange = 'star-orange' 41 | MarkerId.starYellow = 'star-yellow' 42 | MarkerId.starBlue = 'star-blue' 43 | MarkerId.starGreen = 'star-green' 44 | MarkerId.starPurple = 'star-purple' 45 | 46 | MarkerId.priority1 = 'priority-1' 47 | MarkerId.priority2 = 'priority-2' 48 | MarkerId.priority3 = 'priority-3' 49 | MarkerId.priority4 = 'priority-4' 50 | MarkerId.priority5 = 'priority-5' 51 | MarkerId.priority6 = 'priority-6' 52 | MarkerId.priority7 = 'priority-7' 53 | MarkerId.priority8 = 'priority-8' 54 | MarkerId.priority9 = 'priority-9' 55 | 56 | MarkerId.smileySmile = 'smiley-smile' 57 | MarkerId.smileyLaugh = 'smiley-laugh' 58 | MarkerId.smileyAngry = 'smiley-angry' 59 | MarkerId.smileyCry = 'smiley-cry' 60 | MarkerId.smileySurprise = 'smiley-surprise' 61 | MarkerId.smileyBoring = 'smiley-boring' 62 | 63 | MarkerId.task0_8 = 'task-start' 64 | MarkerId.task1_8 = 'task-oct' 65 | MarkerId.task2_8 = 'task-quarter' 66 | MarkerId.task3_8 = 'task-3oct' 67 | MarkerId.task4_8 = 'task-half' 68 | MarkerId.task5_8 = 'task-5oct' 69 | MarkerId.task6_8 = 'task-3quar' 70 | MarkerId.task7_8 = 'task-7oct' 71 | MarkerId.task8_8 = 'task-done' 72 | 73 | MarkerId.flagRed = 'flag-red' 74 | MarkerId.flagOrange = 'flag-orange' 75 | MarkerId.flagYellow = 'flag-yellow' 76 | MarkerId.flagBlue = 'flag-blue' 77 | MarkerId.flagGreen = 'flag-green' 78 | MarkerId.flagPurple = 'flag-purple' 79 | 80 | MarkerId.peopleRed = 'people-red' 81 | MarkerId.peopleOrange = 'people-orange' 82 | MarkerId.peopleYellow = 'people-yellow' 83 | MarkerId.peopleBlue = 'people-blue' 84 | MarkerId.peopleGreen = 'people-green' 85 | MarkerId.peoplePurple = 'people-purple' 86 | 87 | MarkerId.arrowUp = 'arrow-up' 88 | MarkerId.arrowUpRight = 'arrow-up-right' 89 | MarkerId.arrowRight = 'arrow-right' 90 | MarkerId.arrowDownRight = 'arrow-down-right' 91 | MarkerId.arrowDown = 'arrow-down' 92 | MarkerId.arrowDownLeft = 'arrow-down-left' 93 | MarkerId.arrowLeft = 'arrow-left' 94 | MarkerId.arrowUpLeft = 'arrow-up-left' 95 | MarkerId.arrowRefresh = 'arrow-refresh' 96 | 97 | MarkerId.symbolPlus = 'symbol-plus' 98 | MarkerId.symbolMinus = 'symbol-minus' 99 | MarkerId.symbolQuestion = 'symbol-question' 100 | MarkerId.symbolExclam = 'symbol-exclam' 101 | MarkerId.symbolInfo = 'symbol-info' 102 | MarkerId.symbolWrong = 'symbol-wrong' 103 | MarkerId.symbolRight = 'symbol-right' 104 | 105 | MarkerId.monthJan = 'month-jan' 106 | MarkerId.monthFeb = 'month-feb' 107 | MarkerId.monthMar = 'month-mar' 108 | MarkerId.monthApr = 'month-apr' 109 | MarkerId.monthMay = 'month-may' 110 | MarkerId.monthJun = 'month-jun' 111 | MarkerId.monthJul = 'month-jul' 112 | MarkerId.monthAug = 'month-aug' 113 | MarkerId.monthSep = 'month-sep' 114 | MarkerId.monthOct = 'month-oct' 115 | MarkerId.monthNov = 'month-nov' 116 | MarkerId.monthDec = 'month-dec' 117 | 118 | MarkerId.weekSun = 'week-sun' 119 | MarkerId.weekMon = 'week-mon' 120 | MarkerId.weekTue = 'week-tue' 121 | MarkerId.weekWed = 'week-wed' 122 | MarkerId.weekThu = 'week-thu' 123 | MarkerId.weekFri = 'week-fri' 124 | MarkerId.weekSat = 'week-sat' 125 | 126 | 127 | class MarkerRefsElement(WorkbookMixinElement): 128 | TAG_NAME = const.TAG_MARKERREFS 129 | 130 | def __init__(self, node, ownerWorkbook): 131 | super(MarkerRefsElement, self).__init__(node, ownerWorkbook) 132 | 133 | class MarkerRefElement(WorkbookMixinElement): 134 | TAG_NAME = const.TAG_MARKERREF 135 | 136 | def __init__(self, node, ownerWorkbook): 137 | super(MarkerRefElement, self).__init__(node, ownerWorkbook) 138 | 139 | def getMarkerId(self): 140 | return MarkerId(self.getAttribute(const.ATTR_MARKERID)) 141 | 142 | def setMarkerId(self, val): 143 | self.setAttribute(const.ATTR_MARKERID, str(val)) 144 | 145 | -------------------------------------------------------------------------------- /xmind/core/mixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.mixin 6 | ~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | from . import const 16 | from . import Element 17 | 18 | from .. import utils 19 | 20 | 21 | class WorkbookMixinElement(Element): 22 | """ 23 | """ 24 | def __init__(self, node, ownerWorkbook): 25 | super(WorkbookMixinElement, self).__init__(node) 26 | self._owner_workbook = ownerWorkbook 27 | self.registOwnerWorkbook() 28 | 29 | def registOwnerWorkbook(self): 30 | if self._owner_workbook: 31 | self.setOwnerDocument(self._owner_workbook.getOwnerDocument()) 32 | 33 | def getOwnerWorkbook(self): 34 | return self._owner_workbook 35 | 36 | def setOwnerWorkbook(self, workbook): 37 | if not self._owner_workbook: 38 | self._owner_workbook = workbook 39 | 40 | def getModifiedTime(self): 41 | timestamp = self.getAttribute(const.ATTR_TIMESTAMP) 42 | if timestamp: 43 | return utils.readable_time(timestamp) 44 | 45 | def setModifiedTime(self, time): 46 | self.setAttribute(const.ATTR_TIMESTAMP, int(time)) 47 | 48 | def updateModifiedTime(self): 49 | self.setModifiedTime(utils.get_current_time()) 50 | 51 | def getID(self): 52 | return self.getAttribute(const.ATTR_ID) 53 | 54 | 55 | class TopicMixinElement(Element): 56 | def __init__(self, node, ownerTopic): 57 | super(TopicMixinElement, self).__init__(node) 58 | self._owner_topic = ownerTopic 59 | 60 | def getOwnerTopic(self): 61 | return self._owner_topic 62 | 63 | def getOwnerSheet(self): 64 | if not self._owner_topic: 65 | return 66 | 67 | return self._owner_topic.getOwnerSheet() 68 | 69 | def getOwnerWorkbook(self): 70 | if not self._owner_topic: 71 | return 72 | 73 | return self._owner_topic.getOwnerWorkbook() 74 | -------------------------------------------------------------------------------- /xmind/core/notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.notes 6 | ~~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | from . import const 16 | 17 | from .mixin import TopicMixinElement 18 | 19 | 20 | class NotesElement(TopicMixinElement): 21 | TAG_NAME = const.TAG_NOTES 22 | 23 | def __init__(self, node=None, ownerTopic=None): 24 | super(NotesElement, self).__init__(node, ownerTopic) 25 | 26 | def getContent(self, format=const.PLAIN_FORMAT_NOTE): 27 | """ Get notes content 28 | 29 | :parma format: specified returned content format, plain text 30 | by default. 31 | """ 32 | 33 | content = self.getFirstChildNodeByTagName(format) 34 | 35 | if not content: 36 | return 37 | 38 | if format is const.PLAIN_FORMAT_NOTE: 39 | content = PlainNotes(node=content, ownerTopic=self.getOwnerTopic()) 40 | else: 41 | raise Exception("Only support plain text notes right now") 42 | 43 | return content.getTextContent() 44 | 45 | 46 | class _NoteContentElement(TopicMixinElement): 47 | def __init__(self, node=None, ownerTopic=None): 48 | super(_NoteContentElement, self).__init__(node, ownerTopic) 49 | 50 | def getFormat(self): 51 | return self.getImplementation().tagName 52 | 53 | 54 | class PlainNotes(_NoteContentElement): 55 | """ Plain text notes 56 | 57 | :param content: utf8 plain text. 58 | :param node: `xml.dom.Element` object` 59 | :param ownerTopic: `xmind.core.topic.TopicElement` object 60 | 61 | """ 62 | 63 | TAG_NAME = const.PLAIN_FORMAT_NOTE 64 | 65 | def __init__(self, content=None, node=None, ownerTopic=None): 66 | super(PlainNotes, self).__init__(node, ownerTopic) 67 | if content is not None: 68 | self.setTextContent(content) 69 | 70 | def setContent(self, content): 71 | self.setTextContent(content) 72 | 73 | -------------------------------------------------------------------------------- /xmind/core/position.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.position 6 | ~~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | from . import const 16 | 17 | from .mixin import WorkbookMixinElement 18 | 19 | 20 | class PositionElement(WorkbookMixinElement): 21 | TAG_NAME = const.TAG_POSITION 22 | 23 | def __init__(self, node, ownerWorkbook): 24 | super(PositionElement, self).__init__(node, ownerWorkbook) 25 | 26 | # FIXME: These should be converted to getter/setters 27 | 28 | def getX(self): 29 | return self.getAttribute(const.ATTR_X) 30 | 31 | def getY(self): 32 | return self.getAttribute(const.ATTR_Y) 33 | 34 | def setX(self, x): 35 | self.setAttribute(const.ATTR_X, int(x)) 36 | 37 | def setY(self, y): 38 | self.setAttribute(const.ATTR_Y, int(y)) 39 | 40 | -------------------------------------------------------------------------------- /xmind/core/relationship.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.relationship 6 | ~~~~~~~~~~~~~~~~~~~ 7 | 8 | 9 | :copyright: 10 | :license: 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | from . import const 16 | 17 | from .mixin import WorkbookMixinElement 18 | from .topic import TopicElement 19 | from .title import TitleElement 20 | 21 | 22 | class RelationshipElement(WorkbookMixinElement): 23 | TAG_NAME = const.TAG_RELATIONSHIP 24 | 25 | def __init__(self, node, ownerWorkbook): 26 | super(RelationshipElement, self).__init__(node, ownerWorkbook) 27 | 28 | self.addIdAttribute(const.ATTR_ID) 29 | 30 | def _get_title(self): 31 | return self.getFirstChildNodeByTagName(const.TAG_TITLE) 32 | 33 | def _find_end_point(self, id): 34 | owner_workbook = self.getOwnerWorkbook() 35 | if owner_workbook is None: 36 | return 37 | 38 | end_point = owner_workbook.getElementById(id) 39 | if end_point is None: 40 | return 41 | 42 | if end_point.tagName == const.TAG_TOPIC: 43 | return TopicElement(end_point, owner_workbook) 44 | 45 | # FIXME: Convert the following to getter/setter 46 | 47 | def getEnd1ID(self): 48 | return self.getAttribute(const.ATTR_END1) 49 | 50 | def setEnd1ID(self, id): 51 | self.setAttribute(const.ATTR_END1, id) 52 | self.updateModifiedTime() 53 | 54 | def getEnd2ID(self): 55 | return self.getAttribute(const.ATTR_END2) 56 | 57 | def setEnd2ID(self, id): 58 | self.setAttribute(const.ATTR_END2, id) 59 | self.updateModifiedTime() 60 | 61 | def getEnd1(self, end1_id): 62 | return self._find_end_point(end1_id) 63 | 64 | def getEnd2(self, end2_id): 65 | return self._find_end_point(end2_id) 66 | 67 | def getTitle(self): 68 | title = self._get_title() 69 | if title: 70 | title = TitleElement(title, self.getOwnerWorkbook()) 71 | return title.getTextContent() 72 | 73 | def setTitle(self, text): 74 | _title = self._get_title() 75 | title = TitleElement(_title, self.getOwnerWorkbook()) 76 | title.setTextContent(text) 77 | 78 | if _title is None: 79 | self.appendChild(title) 80 | 81 | self.updateModifiedTime() 82 | 83 | 84 | class RelationshipsElement(WorkbookMixinElement): 85 | TAG_NAME = const.TAG_RELATIONSHIPS 86 | 87 | def __init__(self, node, ownerWorkbook): 88 | super(RelationshipsElement, self).__init__(node, ownerWorkbook) 89 | 90 | 91 | def getRelationships(self): 92 | """ 93 | List all relationships 94 | """ 95 | relationships = [] 96 | ownerWorkbook = self.getOwnerWorkbook() 97 | for t in self.getChildNodesByTagName(const.TAG_RELATIONSHIP): 98 | relationships.append(TopicElement(t, ownerWorkbook)) 99 | 100 | return relationships 101 | 102 | 103 | -------------------------------------------------------------------------------- /xmind/core/saver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.saver 6 | ~~~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | import codecs 16 | 17 | from . import const 18 | from .. import utils 19 | 20 | 21 | class WorkbookSaver(object): 22 | def __init__(self, workbook): 23 | """ Save `WorkbookDocument` as XMind file. 24 | 25 | :param workbook: `WorkbookDocument` object 26 | 27 | """ 28 | self._workbook = workbook 29 | 30 | def _get_content(self): 31 | content_path = utils.join_path(utils.temp_dir(), const.CONTENT_XML) 32 | 33 | with codecs.open(content_path, "w", encoding="utf-8") as f: 34 | self._workbook.output(f) 35 | 36 | return content_path 37 | 38 | def save(self, path=None): 39 | """ 40 | Save the workbook to the given path. If the path is not given, then 41 | will save to the path set in workbook. 42 | """ 43 | path = path or self._workbook.get_path() 44 | 45 | if not path: 46 | raise Exception("Please specify a filename for the XMind file") 47 | 48 | path = utils.get_abs_path(path) 49 | 50 | file_name, ext = utils.split_ext(path) 51 | 52 | if ext != const.XMIND_EXT: 53 | raise Exception("XMind filenames require a '%s' extension" % const.XMIND_EXT) 54 | 55 | content = self._get_content() 56 | 57 | f=utils.compress(path) 58 | f.write(content, const.CONTENT_XML) 59 | 60 | -------------------------------------------------------------------------------- /xmind/core/sheet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.sheet 6 | ~~~~~~~~~~~~~~~~ 7 | 8 | :mod:``xmind.core.sheet` command XMind sheets manipulation 9 | 10 | :copytright: 11 | :license: 12 | """ 13 | 14 | __author__ = "aiqi@xmind.net " 15 | 16 | from . import const 17 | 18 | from .mixin import WorkbookMixinElement 19 | from .topic import TopicElement 20 | from .title import TitleElement 21 | from .relationship import RelationshipElement, RelationshipsElement 22 | 23 | 24 | class SheetElement(WorkbookMixinElement): 25 | TAG_NAME = const.TAG_SHEET 26 | 27 | def __init__(self, node, ownerWorkbook): 28 | super(SheetElement, self).__init__(node, ownerWorkbook) 29 | 30 | self.addIdAttribute(const.ATTR_ID) 31 | self._root_topic = self._get_root_topic() 32 | 33 | def _get_root_topic(self): 34 | # This method initialize root topic, if not root topic 35 | # DOM implementation, then create one 36 | topics = self.getChildNodesByTagName(const.TAG_TOPIC) 37 | owner_workbook = self.getOwnerWorkbook() 38 | if len(topics) >= 1: 39 | root_topic = topics[0] 40 | root_topic = TopicElement(root_topic, owner_workbook) 41 | else: 42 | root_topic = TopicElement(None, owner_workbook) 43 | self.appendChild(root_topic) 44 | 45 | return root_topic 46 | 47 | def createRelationship(self, end1, end2, title=None): 48 | """ 49 | Create a relationship between two different topics and return the 50 | created rel. Please notice that the created rel will be added to 51 | sheet. 52 | 53 | :param end1: topic or topic ID 54 | :param end2: topic or topic ID 55 | :param title: relationship title, default by None 56 | 57 | """ 58 | rel = RelationshipElement(None, self.getOwnerWorkbook()) 59 | 60 | rel.setEnd1ID(end1 if isinstance(end1, str) else end1.getID()) 61 | rel.setEnd2ID(end2 if isinstance(end2, str) else end2.getID()) 62 | 63 | 64 | if title is not None: 65 | rel.setTitle(title) 66 | 67 | self._addRelationship(rel) 68 | 69 | return rel 70 | 71 | def _getRelationships(self): 72 | return self.getFirstChildNodeByTagName(const.TAG_RELATIONSHIPS) 73 | 74 | def getRelationships(self): 75 | """ 76 | Get list of relationships in this sheet 77 | """ 78 | _rels = self._getRelationships() 79 | if not _rels: 80 | return [] 81 | owner_workbook = self.getOwnerWorkbook() 82 | return RelationshipsElement(_rels, owner_workbook).getRelationships() 83 | 84 | def _addRelationship(self, rel): 85 | """ 86 | Add relationship to sheet 87 | """ 88 | _rels = self._getRelationships() 89 | owner_workbook = self.getOwnerWorkbook() 90 | 91 | rels = RelationshipsElement(_rels, owner_workbook) 92 | 93 | if not _rels: 94 | self.appendChild(rels) 95 | 96 | rels.appendChild(rel) 97 | 98 | def removeRelationship(self, rel): 99 | """ 100 | Remove a relationship between two different topics 101 | """ 102 | rels = self._getRelationships() 103 | 104 | if not rels: 105 | return 106 | 107 | rel = rel.getImplementation() 108 | rels.removeChild(rel) 109 | if not rels.hasChildNodes(): 110 | self.getImplementation().removeChild(rels) 111 | 112 | self.updateModifiedTime() 113 | 114 | def getRootTopic(self): 115 | return self._root_topic 116 | 117 | def _get_title(self): 118 | return self.getFirstChildNodeByTagName(const.TAG_TITLE) 119 | 120 | # FIXME: convert to getter/setter 121 | def getTitle(self): 122 | title = self._get_title() 123 | if title: 124 | title = TitleElement(title, self.getOwnerWorkbook()) 125 | return title.getTextContent() 126 | 127 | def setTitle(self, text): 128 | _title = self._get_title() 129 | title = TitleElement(_title, self.getOwnerWorkbook()) 130 | title.setTextContent(text) 131 | 132 | if _title is None: 133 | self.appendChild(title) 134 | 135 | self.updateModifiedTime() 136 | 137 | def getParent(self): 138 | workbook = self.getOwnerWorkbook() 139 | if workbook: 140 | parent = self.getParentNode() 141 | 142 | if (parent == workbook.getWorkbookElement().getImplementation()): 143 | return workbook 144 | 145 | def updateModifiedTime(self): 146 | super(SheetElement, self).updateModifiedTime() 147 | 148 | workbook = self.getParent() 149 | if workbook: 150 | workbook.updateModifiedTime() 151 | -------------------------------------------------------------------------------- /xmind/core/title.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.title 6 | ~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | from . import const 16 | 17 | from .mixin import WorkbookMixinElement 18 | 19 | 20 | class TitleElement(WorkbookMixinElement): 21 | TAG_NAME = const.TAG_TITLE 22 | 23 | def __init__(self, node, ownerWorkbook): 24 | super(TitleElement, self).__init__(node, ownerWorkbook) 25 | 26 | -------------------------------------------------------------------------------- /xmind/core/topic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.topic 6 | ~~~~~~~~~~~~~~~~ 7 | 8 | :copyright: 9 | :license: 10 | 11 | """ 12 | 13 | __author__ = "aiqi@xmind.net " 14 | 15 | from . import const 16 | 17 | from .mixin import WorkbookMixinElement 18 | from .title import TitleElement 19 | from .position import PositionElement 20 | from .notes import NotesElement, PlainNotes 21 | from .markerref import MarkerRefElement 22 | from .markerref import MarkerRefsElement 23 | from .markerref import MarkerId 24 | 25 | from .. import utils 26 | 27 | 28 | def split_hyperlink(hyperlink): 29 | colon = hyperlink.find(":") 30 | if colon < 0: 31 | protocol = None 32 | else: 33 | protocol = hyperlink[:colon] 34 | 35 | hyperlink = hyperlink[colon + 1:] 36 | while hyperlink.startswith("/"): 37 | hyperlink = hyperlink[1:] 38 | 39 | return (protocol, hyperlink) 40 | 41 | 42 | class TopicElement(WorkbookMixinElement): 43 | TAG_NAME = const.TAG_TOPIC 44 | 45 | def __init__(self, node, ownerWorkbook): 46 | super(TopicElement, self).__init__(node, ownerWorkbook) 47 | 48 | self.addIdAttribute(const.ATTR_ID) 49 | 50 | def _get_title(self): 51 | return self.getFirstChildNodeByTagName(const.TAG_TITLE) 52 | 53 | def _get_markerrefs(self): 54 | return self.getFirstChildNodeByTagName(const.TAG_MARKERREFS) 55 | 56 | def _get_position(self): 57 | return self.getFirstChildNodeByTagName(const.TAG_POSITION) 58 | 59 | def _get_children(self): 60 | return self.getFirstChildNodeByTagName(const.TAG_CHILDREN) 61 | 62 | def _set_hyperlink(self, hyperlink): 63 | self.setAttribute(const.ATTR_HREF, hyperlink) 64 | #self.updateModifiedTime() 65 | 66 | def getOwnerSheet(self): 67 | parent = self.getParentNode() 68 | 69 | while parent and parent.tagName != const.TAG_SHEET: 70 | parent = parent.parentNode 71 | 72 | if not parent: 73 | return 74 | 75 | owner_workbook = self.getOwnerWorkbook() 76 | if not owner_workbook: 77 | return 78 | 79 | for sheet in owner_workbook.getSheets(): 80 | if parent is sheet.getImplementation(): 81 | return sheet 82 | 83 | def getTitle(self): 84 | title = self._get_title() 85 | if title: 86 | title = TitleElement(title, self.getOwnerWorkbook()) 87 | return title.getTextContent() 88 | 89 | def setTitle(self, text): 90 | _title = self._get_title() 91 | title = TitleElement(_title, self.getOwnerWorkbook()) 92 | title.setTextContent(text) 93 | 94 | if _title is None: 95 | self.appendChild(title) 96 | 97 | # self.updateModifiedTime() 98 | 99 | def getMarkers(self): 100 | refs = self._get_markerrefs() 101 | if not refs: 102 | return None 103 | tmp = MarkerRefsElement(refs, self.getOwnerWorkbook()) 104 | markers = tmp.getChildNodesByTagName(const.TAG_MARKERREF) 105 | marker_list = [] 106 | if markers: 107 | for i in markers: 108 | marker_list.append(MarkerRefElement(i, self.getOwnerWorkbook())) 109 | return marker_list 110 | 111 | def addMarker(self, markerId, replaceSameFamily = True): 112 | ''' 113 | Adds a marker to this topic. 114 | @param markerId a MarkerID object indicating the marker to add 115 | @param replaceSameFamily. Whether an existing marker of the same 116 | family should be replaced or added (this would allow more 117 | than one marker of the same family). 118 | ''' 119 | refs = self._get_markerrefs() 120 | if not refs: 121 | tmp = MarkerRefsElement(None, self.getOwnerWorkbook()) 122 | self.appendChild(tmp) 123 | else: 124 | tmp = MarkerRefsElement(refs, self.getOwnerWorkbook()) 125 | markers = tmp.getChildNodesByTagName(const.TAG_MARKERREF) 126 | if markers and replaceSameFamily: 127 | for m in markers: 128 | mre = MarkerRefElement(m, self.getOwnerWorkbook()) 129 | # look for a marker of same familly 130 | if mre.getMarkerId().getFamily() == markerId.getFamily(): 131 | mre.setMarkerId(markerId) 132 | return mre 133 | # not found so let's append it 134 | mre = MarkerRefElement(None, self.getOwnerWorkbook()) 135 | mre.setMarkerId(markerId) 136 | tmp.appendChild(mre) 137 | return mre 138 | 139 | def setFolded(self): 140 | self.setAttribute(const.ATTR_BRANCH, const.VAL_FOLDED) 141 | 142 | # self.updateModifiedTime() 143 | 144 | def getPosition(self): 145 | """ Get a pair of integer located topic position. 146 | 147 | return (x, y) indicate x and y 148 | """ 149 | position = self._get_position() 150 | if position is None: 151 | return 152 | 153 | position = PositionElement(position, self.getOwnerWorkbook()) 154 | 155 | x = position.getX() 156 | y = position.getY() 157 | 158 | if x is None and y is None: 159 | return 160 | 161 | x = x or 0 162 | y = y or 0 163 | 164 | return (int(x), int(y)) 165 | 166 | def setPosition(self, x, y): 167 | ownerWorkbook = self.getOwnerWorkbook() 168 | position = self._get_position() 169 | 170 | if not position: 171 | position = PositionElement(None, ownerWorkbook) 172 | self.appendChild(position) 173 | else: 174 | position = PositionElement(position, ownerWorkbook) 175 | 176 | position.setX(x) 177 | position.setY(y) 178 | 179 | # self.updateModifiedTime() 180 | 181 | def removePosition(self): 182 | position = self._get_position() 183 | if position is not None: 184 | self.getImplementation().removeChild(position) 185 | 186 | # self.updateModifiedTime() 187 | 188 | def getType(self): 189 | parent = self.getParentNode() 190 | if not parent: 191 | return 192 | 193 | if parent.tagName == const.TAG_SHEET: 194 | return const.TOPIC_ROOT 195 | 196 | if parent.tagName == const.TAG_TOPICS: 197 | topics = TopicsElement(parent, self.getOwnerWorkbook()) 198 | return topics.getType() 199 | 200 | def getTopics(self, topics_type=const.TOPIC_ATTACHED): 201 | topic_children = self._get_children() 202 | 203 | if topic_children: 204 | topic_children = ChildrenElement( 205 | topic_children, 206 | self.getOwnerWorkbook()) 207 | 208 | return topic_children.getTopics(topics_type) 209 | 210 | def getSubTopics(self, topics_type=const.TOPIC_ATTACHED): 211 | """ List all sub topics under current topic, If not sub topics, 212 | return empty list. 213 | """ 214 | topics = self.getTopics(topics_type) 215 | if not topics: 216 | return [] 217 | 218 | return topics.getSubTopics() 219 | 220 | def getSubTopicByIndex(self, index, topics_type=const.TOPIC_ATTACHED): 221 | """ Get sub topic by speicifeid index 222 | """ 223 | sub_topics = self.getSubTopics(topics_type) 224 | if sub_topics is None: 225 | return 226 | 227 | if index < 0 or index >= len(sub_topics): 228 | return sub_topics 229 | 230 | return sub_topics[index] 231 | 232 | def addSubTopic(self, index=-1, 233 | topics_type=const.TOPIC_ATTACHED): 234 | """ 235 | Create empty sub topic to the current topic and return added sub topic 236 | @param index: if index not given then passed topic will append to 237 | sub topics list. Otherwise, index must be less than 238 | length of sub topics list and insert passed topic 239 | before given index. 240 | @param topics_tipe TOPIC_ATTACHED or TOPIC_DETACHED 241 | """ 242 | ownerWorkbook = self.getOwnerWorkbook() 243 | topic = self.__class__(None, ownerWorkbook) 244 | 245 | topic_children = self._get_children() 246 | if not topic_children: 247 | topic_children = ChildrenElement(None, ownerWorkbook) 248 | self.appendChild(topic_children) 249 | else: 250 | topic_children = ChildrenElement(topic_children, ownerWorkbook) 251 | 252 | topics = topic_children.getTopics(topics_type) 253 | if not topics: 254 | topics = TopicsElement(None, ownerWorkbook) 255 | topics.setAttribute(const.ATTR_TYPE, topics_type) 256 | topic_children.appendChild(topics) 257 | 258 | topic_list = [] 259 | for i in topics.getChildNodesByTagName(const.TAG_TOPIC): 260 | topic_list.append(TopicElement(i, ownerWorkbook)) 261 | 262 | if index < 0 or len(topic_list) >= index: 263 | topics.appendChild(topic) 264 | else: 265 | topics.insertBefore(topic, topic_list[index]) 266 | 267 | return topic 268 | 269 | def getIndex(self): 270 | parent = self.getParentNode() 271 | if parent and parent.tagName == const.TAG_TOPICS: 272 | index = 0 273 | for child in parent.childNodes: 274 | if self.getImplementation() == child: 275 | return index 276 | index += 1 277 | return -1 278 | 279 | def getHyperlink(self): 280 | return self.getAttribute(const.ATTR_HREF) 281 | 282 | def setFileHyperlink(self, path): 283 | """ 284 | Set file as topic hyperlink 285 | 286 | :param path: path of specified file 287 | 288 | """ 289 | protocol, content = split_hyperlink(path) 290 | if not protocol: 291 | path = const.FILE_PROTOCOL + utils.get_abs_path(path) 292 | 293 | self._set_hyperlink(path) 294 | 295 | def setTopicHyperlink(self, tid): 296 | """ 297 | Set topic as topic hyperlink 298 | 299 | :param id: given topic's id 300 | 301 | """ 302 | protocol, content = split_hyperlink(tid) 303 | if not protocol: 304 | if tid.startswith("#"): 305 | tid = tid[1:] 306 | 307 | tid = const.TOPIC_PROTOCOL + tid 308 | self._set_hyperlink(tid) 309 | 310 | def setURLHyperlink(self, url): 311 | """ Set URL as topic hyperlink 312 | 313 | :param url: HTTP URL to specified website 314 | 315 | """ 316 | protocol, content = split_hyperlink(url) 317 | if not protocol: 318 | url = const.HTTP_PROTOCOL + content 319 | 320 | self._set_hyperlink(url) 321 | 322 | def getNotes(self): 323 | """ 324 | Return `NotesElement` object` and invoke 325 | `NotesElement.getContent()` to get notes content. 326 | """ 327 | 328 | notes = self.getFirstChildNodeByTagName(const.TAG_NOTES) 329 | 330 | if notes is not None: 331 | return NotesElement(notes, self) 332 | 333 | def _set_notes(self): 334 | notes = self.getNotes() 335 | 336 | if notes is None: 337 | notes = NotesElement(ownerTopic=self) 338 | self.appendChild(notes) 339 | 340 | return notes 341 | 342 | def setPlainNotes(self, content): 343 | """ Set plain text notes to topic 344 | 345 | :param content: utf8 plain text 346 | 347 | """ 348 | notes = self._set_notes() 349 | new = PlainNotes(content, None, self) 350 | 351 | old = notes.getFirstChildNodeByTagName(new.getFormat()) 352 | if old is not None: 353 | notes.getImplementation().removeChild(old) 354 | 355 | notes.appendChild(new) 356 | 357 | 358 | class ChildrenElement(WorkbookMixinElement): 359 | TAG_NAME = const.TAG_CHILDREN 360 | 361 | def __init__(self, node, ownerWorkbook): 362 | super(ChildrenElement, self).__init__(node, ownerWorkbook) 363 | 364 | def getTopics(self, topics_type): 365 | topics = self.iterChildNodesByTagName(const.TAG_TOPICS) 366 | for i in topics: 367 | t = TopicsElement(i, self.getOwnerWorkbook()) 368 | if topics_type == t.getType(): 369 | return t 370 | 371 | 372 | class TopicsElement(WorkbookMixinElement): 373 | TAG_NAME = const.TAG_TOPICS 374 | 375 | def __init__(self, node, ownerWorkbook): 376 | super(TopicsElement, self).__init__(node, ownerWorkbook) 377 | 378 | def getType(self): 379 | return self.getAttribute(const.ATTR_TYPE) 380 | 381 | def getSubTopics(self): 382 | """ 383 | List all sub topics on the current topic 384 | """ 385 | topics = [] 386 | ownerWorkbook = self.getOwnerWorkbook() 387 | for t in self.getChildNodesByTagName(const.TAG_TOPIC): 388 | topics.append(TopicElement(t, ownerWorkbook)) 389 | 390 | return topics 391 | 392 | def getSubTopicByIndex(self, index): 393 | """ 394 | Get specified sub topic by index 395 | """ 396 | sub_topics = self.getSubTopics() 397 | if index < 0 or index >= len(sub_topics): 398 | return sub_topics 399 | 400 | return sub_topics[index] 401 | -------------------------------------------------------------------------------- /xmind/core/workbook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.core.workbook 6 | ~~~~~~~~~~~~~~~~~~~ 7 | 8 | :mod:``xmind.core.workbook`` implements the command XMind 9 | manipulations. 10 | 11 | :copyright: 12 | :license: 13 | """ 14 | 15 | __author__ = "aiqi@xmind.net " 16 | 17 | 18 | from . import Document 19 | from . import const 20 | 21 | from .mixin import WorkbookMixinElement 22 | from .sheet import SheetElement 23 | from .topic import TopicElement 24 | from .relationship import RelationshipElement 25 | 26 | from .. import utils 27 | 28 | 29 | class WorkbookElement(WorkbookMixinElement): 30 | TAG_NAME = const.TAG_WORKBOOK 31 | 32 | def __init__(self, node, ownerWorkbook): 33 | super(WorkbookElement, self).__init__(node, ownerWorkbook) 34 | 35 | # Initialize WorkbookElement with default attribute 36 | namespace = (const.NAMESPACE, const.XMAP) 37 | attrs = [const.NS_FO, const.NS_XHTML, const.NS_XLINK, const.NS_SVG] 38 | 39 | for attr in attrs: 40 | self.setAttributeNS(namespace, attr) 41 | 42 | # Initialize WorkbookElement need contains at least one SheetElement 43 | if not self.getSheets(): 44 | sheet = self.createSheet() 45 | self.addSheet(sheet) 46 | 47 | def setOwnerWorkbook(self, workbook): 48 | raise Exception( 49 | """WorkbookDocument allowed only contains one WorkbookElement 50 | """) 51 | 52 | def getSheets(self): 53 | sheets = self.getChildNodesByTagName(const.TAG_SHEET) 54 | owner_workbook = self.getOwnerWorkbook() 55 | sheets = [SheetElement(sheet, owner_workbook) for sheet in sheets] 56 | 57 | return sheets 58 | 59 | def getSheetByIndex(self, index): 60 | sheets = self.getSheets() 61 | 62 | if index < 0 or index >= len(sheets): 63 | return 64 | 65 | return sheets[index] 66 | 67 | def createSheet(self): 68 | sheet = SheetElement(None, self.getOwnerWorkbook()) 69 | return sheet 70 | 71 | def addSheet(self, sheet, index=-1): 72 | sheets = self.getSheets() 73 | if index < 0 or index >= len(sheets): 74 | self.appendChild(sheet) 75 | else: 76 | self.insertBefore(sheet, sheets[index]) 77 | 78 | self.updateModifiedTime() 79 | 80 | def removeSheet(self, sheet): 81 | sheets = self.getSheets() 82 | if len(sheets) <= 1: 83 | return 84 | 85 | if sheet.getParentNode() == self.getImplementation(): 86 | self.removeChild(sheet) 87 | self.updateModifiedTime() 88 | 89 | def moveSheet(self, original_index, target_index): 90 | if original_index < 0 or original_index == target_index: 91 | return 92 | 93 | sheets = self.getSheets() 94 | if original_index >= len(sheets): 95 | return 96 | 97 | sheet = sheets[original_index] 98 | if not target_index < 0 and target_index < len(sheets) - 1: 99 | if original_index < target_index: 100 | target_index += 1 101 | else: 102 | target_index = target_index 103 | 104 | target = sheets[target_index] 105 | if target != sheet: 106 | self.removeChild(sheet) 107 | self.insertBefore(sheet, target) 108 | else: 109 | self.removeChild(sheet) 110 | self.appendChild(sheet) 111 | 112 | self.updateModifiedTime() 113 | 114 | def getVersion(self): 115 | return self.getAttribution(const.ATTR_VERSION) 116 | 117 | 118 | class WorkbookDocument(Document): 119 | """ `WorkbookDocument` as central object correspond XMind workbook. 120 | """ 121 | def __init__(self, node=None, path=None): 122 | """ 123 | Construct new `WorkbookDocument` object 124 | 125 | :param node: pass DOM node object and parse as `WorkbookDocument` 126 | object. if node not given then created new one. 127 | 128 | :param path: set workbook will to be placed. 129 | 130 | """ 131 | super(WorkbookDocument, self).__init__(node) 132 | self._path = path 133 | # Initialize WorkbookDocument to make sure that contains 134 | # WorkbookElement as root. 135 | _workbook_element = self.getFirstChildNodeByTagName( 136 | const.TAG_WORKBOOK) 137 | 138 | self._workbook_element = WorkbookElement( 139 | _workbook_element, 140 | self) 141 | 142 | if not _workbook_element: 143 | self.appendChild(self._workbook_element) 144 | 145 | self.setVersion(const.VERSION) 146 | 147 | def getWorkbookElement(self): 148 | return self._workbook_element 149 | 150 | def createRelationship(self, end1, end2, title=None): 151 | """ Create relationship with two topics. Convenience method 152 | to access the sheet method of the same name 153 | """ 154 | 155 | sheet1 = end1.getOwnerSheet() 156 | sheet2 = end2.getOwnerSheet() 157 | 158 | if sheet1.getImplementation() == sheet2.getImplementation(): 159 | rel = sheet1.create_relationship(end1.getID(),end2.getID(),title) 160 | return rel 161 | else: 162 | raise Exception("Topics not on the same sheet!") 163 | 164 | def createTopic(self): 165 | """ 166 | Create new `TopicElement` object and return. Please notice that 167 | this topic will not be added to the workbook. 168 | """ 169 | return TopicElement(None, self) 170 | 171 | def getSheets(self): 172 | """ 173 | List all sheets under workbook, if not sheets then return 174 | empty list 175 | """ 176 | return self._workbook_element.getSheets() 177 | 178 | def getPrimarySheet(self): 179 | """ 180 | Get the first sheet under workbook. 181 | """ 182 | return self._workbook_element.getSheetByIndex(0) 183 | 184 | def createSheet(self, index=-1): 185 | """ 186 | Create new sheet. Please notice the new created sheet 187 | has been added to the workbook. 188 | :param index: insert sheet before another sheet that given by 189 | index. If index not given, append sheet to the 190 | sheets list. 191 | """ 192 | sheet = self._workbook_element.createSheet() 193 | self._workbook_element.addSheet(sheet, index) 194 | return sheet; 195 | 196 | def removeSheet(self, sheet): 197 | """ 198 | Remove a sheet from the workbook 199 | 200 | :param sheet: remove passed `SheetElement` object 201 | """ 202 | self._workbook_element.removeSheet(sheet) 203 | 204 | def moveSheet(self, original_index, target_index): 205 | """ 206 | Move a sheet from the original index to the target index 207 | 208 | :param original_index: index of the sheet will be moved. 209 | `original_index` must be positive integer and 210 | less than `target_index`. 211 | :param target_index: index that sheet want to move to. 212 | `target_index` must be positive integer and 213 | less than the length of sheets list. 214 | """ 215 | self._workbook_element.moveSheet(original_index, target_index) 216 | 217 | def getVersion(self): 218 | return self._workbook_element.getVersion() 219 | 220 | def getModifiedTime(self): 221 | return self._workbook_element.getModifiedTime() 222 | 223 | def updateModifiedTime(self): 224 | return self._workbook_element.updateModifiedTime() 225 | 226 | def setModifiedTime(self): 227 | return self._workbook_element.setModifiedTime() 228 | 229 | def get_path(self): 230 | if self._path: 231 | return utils.get_abs_path(self._path) 232 | 233 | def set_path(self, path): 234 | self._path = utils.get_abs_path(path) 235 | 236 | -------------------------------------------------------------------------------- /xmind/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #-*- coding: utf-8 -*- 3 | 4 | """ 5 | xmind.utils 6 | ~~~~~~~~~~~ 7 | 8 | :mod:``xmind.utils`` provide a handy way for internal used by 9 | xmind, and excepted that function defined here will be useful to 10 | others. 11 | 12 | :copyright: 13 | :license: 14 | """ 15 | 16 | __author__ = "aiqi@xmind.net " 17 | 18 | import os 19 | import time 20 | import random 21 | import tempfile 22 | import zipfile 23 | 24 | from hashlib import md5 25 | from functools import wraps 26 | from xml.dom.minidom import parse, parseString 27 | 28 | 29 | temp_dir = tempfile.mkdtemp 30 | 31 | 32 | def extract(path): 33 | return zipfile.ZipFile(path, "r") 34 | 35 | 36 | def compress(path): 37 | return zipfile.ZipFile(path, "w") 38 | 39 | ######################## Path ######################### 40 | 41 | join_path = os.path.join 42 | split_ext = os.path.splitext 43 | 44 | 45 | def get_abs_path(path): 46 | """ 47 | Return the absolute path of a file 48 | 49 | If path contains a start point (eg Unix '/') then use the specified start point 50 | instead of the current working directory. The starting point of the file 51 | path is allowed to begin with a tilde "~", which will be replaced with the user's 52 | home directory. 53 | """ 54 | 55 | fp, fn = os.path.split(path) 56 | if not fp: 57 | fp = os.getcwd() 58 | 59 | fp = os.path.abspath(os.path.expanduser(fp)) 60 | 61 | return join_path(fp, fn) 62 | 63 | 64 | ######################### Time ############################### 65 | 66 | def get_current_time(): 67 | """ 68 | Get the current time in milliseconds 69 | """ 70 | return int(round(time.time() * 1000)) 71 | 72 | 73 | def readable_time(timestamp): 74 | """ 75 | Convert timestamp to human-readable time format 76 | """ 77 | # Timestamp in milliseconds, convert to seconds 78 | # Cause Python handle time in seconds 79 | timestampe_in_seconds = float(timestamp) / 1000 80 | return time.strftime( 81 | "%m/%d/%Y %H:%M:%S", 82 | time.gmtime(timestampe_in_seconds)) 83 | 84 | 85 | ########################## DOM ########################### 86 | 87 | parse_dom = parse 88 | parse_dom_string = parseString 89 | # def create_document(): 90 | # return dom.Document() 91 | # 92 | # 93 | # def create_element(tagName, namespaceURI=None, prefix=None, localName=None): 94 | # return dom.Element(tagName, namespaceURI, prefix, localName) 95 | # 96 | # 97 | # def load_XML(stream): 98 | # """ 99 | # Create new Document while occure load XML error 100 | # """ 101 | # try: 102 | # return dom.parse(stream) 103 | # except: 104 | # return create_document() 105 | 106 | 107 | ########################## Misc ######################### 108 | 109 | def generate_id(): 110 | """ 111 | Generate unique 26-digit random string 112 | """ 113 | # FIXME: Why not use something like the builtin uuid.uuid1() method? 114 | # md5 current time get 32-digit random string 115 | timestamp = md5(str(get_current_time()).encode('utf-8')).hexdigest() 116 | lotter = md5(str(random.random()).encode('utf-8')).hexdigest() # :) 117 | 118 | id = timestamp[19:] + lotter[:13] 119 | 120 | return id 121 | 122 | 123 | ############################ Decorator ########################### 124 | 125 | def prevent(func): 126 | """ 127 | Decorate func with this to prevent raising an Exception when 128 | an error is encountered 129 | """ 130 | @wraps(func) 131 | def wrapper(*args, **kwargs): 132 | try: 133 | return func(*args, **kwargs) 134 | except: 135 | return 136 | 137 | return wrapper 138 | 139 | 140 | def check(attr): 141 | def decorator(method): 142 | """ 143 | Decorate method with this to check whether the object 144 | has an attribute with the given name. 145 | """ 146 | @wraps(method) 147 | def wrapper(self, *args, **kwargs): 148 | if hasattr(self, attr): 149 | return method(self, *args, **kwargs) 150 | 151 | return None 152 | return wrapper 153 | return decorator 154 | 155 | --------------------------------------------------------------------------------