├── .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 | ### /Users/monster/.gitignore-boilerplates/Python.gitignore 2 | 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | __pycache__ 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | 41 | -------------------------------------------------------------------------------- /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 2 | 3 | **XMind SDK for python** to help Python developers to easily work with XMind files and build XMind extensions. 4 | 5 | ##Install XMind SDK for python 6 | 7 | Clone the repository to a local working directory 8 | 9 | git clone https://github.com/xmindltd/xmind-sdk-python.git 10 | 11 | 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**. 12 | 13 | python setup.py install 14 | 15 | *It is highly recommended to install __XMind SDK for python__ under an isolated python environment using [virtualenv](https://pypi.python.org/pypi/virtualenv)* 16 | 17 | ##Usage 18 | 19 | Open an existing XMind file or create a new XMind file and place it into a given path 20 | 21 | import xmind 22 | workbook = xmind.load(/path/to/file/) # Requires '.xmind' extension 23 | 24 | Save XMind file to a path. 25 | If the path is not given then the API will save to the path set in the workbook 26 | 27 | xmind.save(workbook) 28 | 29 | or: 30 | 31 | xmind.save(workbook, /save/file/to/path) 32 | 33 | ##LICENSE 34 | 35 | The MIT License (MIT) 36 | 37 | Copyright (c) 2013 XMind, Ltd 38 | 39 | Permission is hereby granted, free of charge, to any person obtaining a copy of 40 | this software and associated documentation files (the "Software"), to deal in 41 | the Software without restriction, including without limitation the rights to 42 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 43 | the Software, and to permit persons to whom the Software is furnished to do so, 44 | subject to the following conditions: 45 | 46 | The above copyright notice and this permission notice shall be included in all 47 | copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 50 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 51 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 52 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 53 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 54 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 55 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #-*- coding: utf-8 -*- 2 | import xmind 3 | from xmind.core import workbook,saver 4 | from xmind.core.topic import TopicElement 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 | 19 | t1=TopicElement() # create a new element 20 | t1.setTopicHyperlink(s1.getID()) # set a link from this topic to the first sheet given by s1.getID() 21 | t1.setTitle("redirection to the first sheet") # set its title 22 | 23 | t2=TopicElement() 24 | t2.setTitle("second node") 25 | t2.setURLHyperlink("https://xmind.net") # set an hyperlink 26 | 27 | t3=TopicElement() 28 | t3.setTitle("third node") 29 | t3.setPlainNotes("notes for this topic") # set notes (F4 in XMind) 30 | t3.setTitle("topic with \n notes") 31 | 32 | t4=TopicElement() 33 | t4.setFileHyperlink("logo.jpeg") # set a file hyperlink 34 | t4.setTitle("topic with a file") 35 | 36 | 37 | # then the topics must be added to the root element 38 | 39 | r2.addSubTopic(t1) 40 | r2.addSubTopic(t2) 41 | r2.addSubTopic(t3) 42 | r2.addSubTopic(t4) 43 | 44 | topics=r2.getSubTopics() # to loop on the subTopics 45 | for topic in topics: 46 | topic.addMarker("yes") 47 | 48 | w.addSheet(s2) # the second sheet is now added to the workbook 49 | rel=s2.createRelationship(t1.getID(),t2.getID(),"test") # create a relationship 50 | s2.addRelationship(rel) # and add to the sheet 51 | 52 | xmind.save(w,"test2.xmind") # and we save -------------------------------------------------------------------------------- /logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xmindltd/xmind-sdk-python/58b2c7f1971abd941cd0f28e88388ec93ed2c53d/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 | 39 | def main(): 40 | pass 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /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( 204 | self.TAG_NAME.decode("utf8")) 205 | 206 | def _elementConstructor(self, tag_name, 207 | namespaceURI=None, 208 | prefix=None, localName=None): 209 | element = DOM.Element(tag_name, namespaceURI, prefix, localName) 210 | 211 | prefix = self.getPrefix(tag_name) 212 | localName = self.getLocalName(tag_name) 213 | 214 | element.prefix = prefix 215 | element.localName = localName 216 | 217 | return element 218 | 219 | def getOwnerDocument(self): 220 | return self._node.ownerDocument 221 | 222 | def setOwnerDocument(self, doc_imple): 223 | self._node.ownerDocument = doc_imple 224 | 225 | def setAttributeNS(self, namespace, attr): 226 | """ 227 | Set attributes with namespace to DOM implementation. 228 | Please notice that namespace must be a namespace name and 229 | namespace value. Attr composed by namespceURI, localName and value. 230 | """ 231 | namespace_name, namespace_value = namespace 232 | if not self._node.hasAttribute(namespace_name): 233 | self._node.setAttribute(namespace_name, namespace_value) 234 | 235 | namespaceURI, localName, value = attr 236 | if not self._node.hasAttributeNS(namespaceURI, localName): 237 | qualifiedName = "%s:%s" % (namespace_name, localName) 238 | self._node.setAttributeNS(namespaceURI, qualifiedName, value) 239 | 240 | def getAttribute(self, attr_name): 241 | """ 242 | Get attribute with specified name. And allowed get attribute with 243 | specified name in ``prefix:localName`` format. 244 | """ 245 | if not self._node.hasAttribute(attr_name): 246 | localName = self.getLocalName(attr_name) 247 | if localName != attr_name: 248 | return self.getAttribute(localName) 249 | return 250 | 251 | return self._node.getAttribute(attr_name) 252 | 253 | def setAttribute(self, attr_name, attr_value=None): 254 | """ 255 | Set attribute to element. Please notice that if ``attr_value`` is 256 | None and attribute with specified ``attr_name`` is exist, 257 | attribute will be removed. 258 | """ 259 | if attr_value is not None: 260 | self._node.setAttribute(attr_name, 261 | str(attr_value).decode("utf8")) 262 | elif self._node.hasAttribute(attr_name): 263 | self._node.removeAttribute(attr_name) 264 | 265 | def createElement(self, tag_name): 266 | """ 267 | Create new element. But created element doesn't add to the child 268 | node list of this element, invoke :func: ``self.appendChild`` or :func: 269 | ``self.insertBefore`` to add created element to the child node list of 270 | this element. 271 | """ 272 | pass 273 | 274 | def addIdAttribute(self, attr_name): 275 | if not self._node.hasAttribute(attr_name): 276 | id = utils.generate_id() 277 | self._node.setAttribute(attr_name, id) 278 | 279 | if self.getOwnerDocument(): 280 | self._node.setIdAttribute(attr_name) 281 | 282 | def getIndex(self): 283 | parent = self.getParentNode() 284 | if parent: 285 | index = 0 286 | for node in parent.childNodes: 287 | if self._node is node: 288 | return index 289 | index += 1 290 | 291 | return -1 292 | 293 | def getTextContent(self): 294 | text = [] 295 | for node in self._node.childNodes: 296 | if node.nodeType == DOM.Node.TEXT_NODE: 297 | text.append(node.data) 298 | 299 | if not len(text) > 0: 300 | return 301 | 302 | text = "\n".join(text) 303 | return text 304 | 305 | def setTextContent(self, data): 306 | for node in self._node.childNodes: 307 | if node.nodeType == DOM.Node.TEXT_NODE: 308 | self._node.removeChild(node) 309 | 310 | text = DOM.Text() 311 | text.data = data.decode("utf8") 312 | 313 | self._node.appendChild(text) 314 | 315 | 316 | def main(): 317 | pass 318 | 319 | if __name__ == '__main__': 320 | main() 321 | -------------------------------------------------------------------------------- /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 | 71 | FILE_PROTOCOL = "file://" 72 | TOPIC_PROTOCOL = "xmind:#" 73 | HTTP_PROTOCOL = "http://" 74 | HTTPS_PROTOCOL = "https://" 75 | 76 | HTML_FORMAT_NOTE = "html" 77 | PLAIN_FORMAT_NOTE = "plain" 78 | -------------------------------------------------------------------------------- /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 | 59 | def main(): 60 | pass 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /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 getFamilly(self): 30 | return self.name.split('-')[0] 31 | 32 | 33 | MarkerId.starRed = 'star-red' 34 | MarkerId.starOrange = 'star-orange' 35 | MarkerId.starYellow = 'star-yellow' 36 | MarkerId.starBlue = 'star-blue' 37 | MarkerId.starGreen = 'star-green' 38 | MarkerId.starPurple = 'star-purple' 39 | 40 | MarkerId.priority1 = 'priority-1' 41 | MarkerId.priority2 = 'priority-2' 42 | MarkerId.priority3 = 'priority-3' 43 | MarkerId.priority4 = 'priority-4' 44 | MarkerId.priority5 = 'priority-5' 45 | MarkerId.priority6 = 'priority-6' 46 | MarkerId.priority7 = 'priority-7' 47 | MarkerId.priority8 = 'priority-8' 48 | MarkerId.priority9 = 'priority-9' 49 | 50 | MarkerId.smileySmile = 'smiley-smile' 51 | MarkerId.smileyLaugh = 'smiley-laugh' 52 | MarkerId.smileyAngry = 'smiley-angry' 53 | MarkerId.smileyCry = 'smiley-cry' 54 | MarkerId.smileySurprise = 'smiley-surprise' 55 | MarkerId.smileyBoring = 'smiley-boring' 56 | 57 | MarkerId.task0_8 = 'task-start' 58 | MarkerId.task1_8 = 'task-oct' 59 | MarkerId.task2_8 = 'task-quarter' 60 | MarkerId.task3_8 = 'task-3oct' 61 | MarkerId.task4_8 = 'task-half' 62 | MarkerId.task5_8 = 'task-5oct' 63 | MarkerId.task6_8 = 'task-3quar' 64 | MarkerId.task7_8 = 'task-7oct' 65 | MarkerId.task8_8 = 'task-done' 66 | 67 | MarkerId.flagRed = 'flag-red' 68 | MarkerId.flagOrange = 'flag-orange' 69 | MarkerId.flagYellow = 'flag-yellow' 70 | MarkerId.flagBlue = 'flag-blue' 71 | MarkerId.flagGreen = 'flag-green' 72 | MarkerId.flagPurple = 'flag-purple' 73 | 74 | MarkerId.peopleRed = 'people-red' 75 | MarkerId.peopleOrange = 'people-orange' 76 | MarkerId.peopleYellow = 'people-yellow' 77 | MarkerId.peopleBlue = 'people-blue' 78 | MarkerId.peopleGreen = 'people-green' 79 | MarkerId.peoplePurple = 'people-purple' 80 | 81 | MarkerId.arrowUp = 'arrow-up' 82 | MarkerId.arrowUpRight = 'arrow-up-right' 83 | MarkerId.arrowRight = 'arrow-right' 84 | MarkerId.arrowDownRight = 'arrow-down-right' 85 | MarkerId.arrowDown = 'arrow-down' 86 | MarkerId.arrowDownLeft = 'arrow-down-left' 87 | MarkerId.arrowLeft = 'arrow-left' 88 | MarkerId.arrowUpLeft = 'arrow-up-left' 89 | MarkerId.arrowRefresh = 'arrow-refresh' 90 | 91 | MarkerId.symbolPlus = 'symbol-plus' 92 | MarkerId.symbolMinus = 'symbol-minus' 93 | MarkerId.symbolQuestion = 'symbol-question' 94 | MarkerId.symbolExclam = 'symbol-exclam' 95 | MarkerId.symbolInfo = 'symbol-info' 96 | MarkerId.symbolWrong = 'symbol-wrong' 97 | MarkerId.symbolRight = 'symbol-right' 98 | 99 | MarkerId.monthJan = 'month-jan' 100 | MarkerId.monthFeb = 'month-feb' 101 | MarkerId.monthMar = 'month-mar' 102 | MarkerId.monthApr = 'month-apr' 103 | MarkerId.monthMay = 'month-may' 104 | MarkerId.monthJun = 'month-jun' 105 | MarkerId.monthJul = 'month-jul' 106 | MarkerId.monthAug = 'month-aug' 107 | MarkerId.monthSep = 'month-sep' 108 | MarkerId.monthOct = 'month-oct' 109 | MarkerId.monthNov = 'month-nov' 110 | MarkerId.monthDec = 'month-dec' 111 | 112 | MarkerId.weekSun = 'week-sun' 113 | MarkerId.weekMon = 'week-mon' 114 | MarkerId.weekTue = 'week-tue' 115 | MarkerId.weekWed = 'week-wed' 116 | MarkerId.weekThu = 'week-thu' 117 | MarkerId.weekFri = 'week-fri' 118 | MarkerId.weekSat = 'week-sat' 119 | 120 | 121 | class MarkerRefsElement(WorkbookMixinElement): 122 | TAG_NAME = const.TAG_MARKERREFS 123 | 124 | def __init__(self, node=None, ownerWorkbook=None): 125 | super(MarkerRefsElement, self).__init__(node, ownerWorkbook) 126 | 127 | class MarkerRefElement(WorkbookMixinElement): 128 | TAG_NAME = const.TAG_MARKERREF 129 | 130 | def __init__(self, node=None, ownerWorkbook=None): 131 | super(MarkerRefElement, self).__init__(node, ownerWorkbook) 132 | 133 | def getMarkerId(self): 134 | return MarkerId(self.getAttribute(const.ATTR_MARKERID)) 135 | 136 | def setMarkerId(self, val): 137 | self.setAttribute(const.ATTR_MARKERID, str(val)) 138 | 139 | def main(): 140 | pass 141 | 142 | if __name__ == '__main__': 143 | main() 144 | -------------------------------------------------------------------------------- /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=None, ownerWorkbook=None): 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, long(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 | 75 | 76 | def main(): 77 | pass 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /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 | 74 | def main(): 75 | pass 76 | 77 | if __name__ == '__main__': 78 | main() 79 | -------------------------------------------------------------------------------- /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=None, ownerWorkbook=None): 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 | 41 | def main(): 42 | pass 43 | 44 | if __name__ == '__main__': 45 | main() 46 | -------------------------------------------------------------------------------- /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=None, ownerWorkbook=None): 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=None, ownerWorkbook=None): 88 | super(RelationshipsElement, self).__init__(node, ownerWorkbook) 89 | 90 | 91 | def main(): 92 | pass 93 | 94 | if __name__ == "__main__": 95 | main() 96 | -------------------------------------------------------------------------------- /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 | 61 | def main(): 62 | pass 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /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=None, ownerWorkbook=None): 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(ownerWorkbook=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 not be added to 51 | sheet. Call `addRelationship()` to add rel to sheet. 52 | 53 | :param end1: topic ID 54 | :param end2: topic ID 55 | :param title: relationship title, default by None 56 | 57 | """ 58 | rel = RelationshipElement(ownerWorkbook=self.getOwnerWorkbook()) 59 | rel.setEnd1ID(end1) 60 | rel.setEnd2ID(end2) 61 | 62 | if title is not None: 63 | rel.setTitle(title) 64 | 65 | return rel 66 | 67 | def _getRelationships(self): 68 | return self.getFirstChildNodeByTagName(const.TAG_RELATIONSHIPS) 69 | 70 | def addRelationship(self, rel): 71 | """ 72 | Add relationship to sheet 73 | """ 74 | _rels = self._getRelationships() 75 | owner_workbook = self.getOwnerWorkbook() 76 | 77 | rels = RelationshipsElement(_rels, owner_workbook) 78 | 79 | if not _rels: 80 | self.appendChild(rels) 81 | 82 | rels.appendChild(rel) 83 | 84 | def removeRelationship(self, rel): 85 | """ 86 | Remove a relationship between two different topics 87 | """ 88 | rels = self._getRelationships() 89 | 90 | if not rels: 91 | return 92 | 93 | rel = rel.getImplementation() 94 | rels.removeChild(rel) 95 | if not rels.hasChildNodes(): 96 | self.getImplementation().removeChild(rels) 97 | 98 | self.updateModifiedTime() 99 | 100 | def getRootTopic(self): 101 | return self._root_topic 102 | 103 | def _get_title(self): 104 | return self.getFirstChildNodeByTagName(const.TAG_TITLE) 105 | 106 | # FIXME: convert to getter/setter 107 | def getTitle(self): 108 | title = self._get_title() 109 | if title: 110 | title = TitleElement(title, self.getOwnerWorkbook()) 111 | return title.getTextContent() 112 | 113 | def setTitle(self, text): 114 | _title = self._get_title() 115 | title = TitleElement(_title, self.getOwnerWorkbook()) 116 | title.setTextContent(text) 117 | 118 | if _title is None: 119 | self.appendChild(title) 120 | 121 | self.updateModifiedTime() 122 | 123 | def getParent(self): 124 | workbook = self.getOwnerWorkbook() 125 | if workbook: 126 | parent = self.getParentNode() 127 | 128 | if (parent == workbook.getWorkbookElement().getImplementation()): 129 | return workbook 130 | 131 | def updateModifiedTime(self): 132 | super(SheetElement, self).updateModifiedTime() 133 | 134 | workbook = self.getParent() 135 | if workbook: 136 | workbook.updateModifiedTime() 137 | 138 | 139 | def main(): 140 | pass 141 | 142 | 143 | if __name__ == '__main__': 144 | main() 145 | -------------------------------------------------------------------------------- /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=None, ownerWorkbook=None): 24 | super(TitleElement, self).__init__(node, ownerWorkbook) 25 | 26 | 27 | def main(): 28 | pass 29 | 30 | if __name__ == '__main__': 31 | main() 32 | -------------------------------------------------------------------------------- /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=None, ownerWorkbook=None): 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): 112 | 113 | if not markerId: 114 | return None 115 | if type(markerId) == str: 116 | markerId = MarkerId(markerId) 117 | 118 | refs = self._get_markerrefs() 119 | if not refs: 120 | tmp = MarkerRefsElement(None, self.getOwnerWorkbook()) 121 | self.appendChild(tmp) 122 | else: 123 | tmp = MarkerRefsElement(refs, self.getOwnerWorkbook()) 124 | markers = tmp.getChildNodesByTagName(const.TAG_MARKERREF) 125 | if markers: 126 | for m in markers: 127 | mre = MarkerRefElement(m, self.getOwnerWorkbook()) 128 | # look for a marker of same familly 129 | if mre.getMarkerId().getFamilly() == markerId.getFamilly(): 130 | mre.setMarkerId(markerId) 131 | return mre 132 | # not found so let's append it 133 | mre = MarkerRefElement(None, self.getOwnerWorkbook()) 134 | mre.setMarkerId(markerId) 135 | tmp.appendChild(mre) 136 | return mre 137 | 138 | def setFolded(self): 139 | self.setAttribute(const.ATTR_BRANCH, const.VAL_FOLDED) 140 | 141 | # self.updateModifiedTime() 142 | 143 | def getPosition(self): 144 | """ Get a pair of integer located topic position. 145 | 146 | return (x, y) indicate x and y 147 | """ 148 | position = self._get_position() 149 | if position is None: 150 | return 151 | 152 | position = PositionElement(position, self.getOwnerWorkbook()) 153 | 154 | x = position.getX() 155 | y = position.getY() 156 | 157 | if x is None and y is None: 158 | return 159 | 160 | x = x or 0 161 | y = y or 0 162 | 163 | return (int(x), int(y)) 164 | 165 | def setPosition(self, x, y): 166 | ownerWorkbook = self.getOwnerWorkbook() 167 | position = self._get_position() 168 | 169 | if not position: 170 | position = PositionElement(ownerWorkbook=ownerWorkbook) 171 | self.appendChild(position) 172 | else: 173 | position = PositionElement(position, ownerWorkbook) 174 | 175 | position.setX(x) 176 | position.setY(y) 177 | 178 | # self.updateModifiedTime() 179 | 180 | def removePosition(self): 181 | position = self._get_position() 182 | if position is not None: 183 | self.getImplementation().removeChild(position) 184 | 185 | # self.updateModifiedTime() 186 | 187 | def getType(self): 188 | parent = self.getParentNode() 189 | if not parent: 190 | return 191 | 192 | if parent.tagName == const.TAG_SHEET: 193 | return const.TOPIC_ROOT 194 | 195 | if parent.tagName == const.TAG_TOPICS: 196 | topics = TopicsElement(parent, self.getOwnerWorkbook()) 197 | return topics.getType() 198 | 199 | def getTopics(self, topics_type=const.TOPIC_ATTACHED): 200 | topic_children = self._get_children() 201 | 202 | if topic_children: 203 | topic_children = ChildrenElement( 204 | topic_children, 205 | self.getOwnerWorkbook()) 206 | 207 | return topic_children.getTopics(topics_type) 208 | 209 | def getSubTopics(self, topics_type=const.TOPIC_ATTACHED): 210 | """ List all sub topics under current topic, If not sub topics, 211 | return None. 212 | """ 213 | topics = self.getTopics(topics_type) 214 | if not topics: 215 | return 216 | 217 | return topics.getSubTopics() 218 | 219 | def getSubTopicByIndex(self, index, topics_type=const.TOPIC_ATTACHED): 220 | """ Get sub topic by speicifeid index 221 | """ 222 | sub_topics = self.getSubTopics(topics_type) 223 | if sub_topics is None: 224 | return 225 | 226 | if index < 0 or index >= len(sub_topics): 227 | return sub_topics 228 | 229 | return sub_topics[index] 230 | 231 | def addSubTopic(self, topic=None, index=-1, 232 | topics_type=const.TOPIC_ATTACHED): 233 | """ 234 | Add a sub topic to the current topic and return added sub topic 235 | 236 | :param topic: `TopicElement` object. If not `TopicElement` object 237 | passed then created new one automatically. 238 | :param index: if index not given then passed topic will append to 239 | sub topics list. Otherwise, index must be less than 240 | length of sub topics list and insert passed topic 241 | before given index. 242 | """ 243 | ownerWorkbook = self.getOwnerWorkbook() 244 | topic = topic or self.__class__(None, ownerWorkbook) 245 | 246 | topic_children = self._get_children() 247 | if not topic_children: 248 | topic_children = ChildrenElement(ownerWorkbook=ownerWorkbook) 249 | self.appendChild(topic_children) 250 | else: 251 | topic_children = ChildrenElement(topic_children, ownerWorkbook) 252 | 253 | topics = topic_children.getTopics(topics_type) 254 | if not topics: 255 | topics = TopicsElement(ownerWorkbook=ownerWorkbook) 256 | topics.setAttribute(const.ATTR_TYPE, topics_type) 257 | topic_children.appendChild(topics) 258 | 259 | topic_list = [] 260 | for i in topics.getChildNodesByTagName(const.TAG_TOPIC): 261 | topic_list.append(TopicElement(i, ownerWorkbook)) 262 | 263 | if index < 0 or len(topic_list) >= index: 264 | topics.appendChild(topic) 265 | else: 266 | topics.insertBefore(topic, topic_list[index]) 267 | 268 | return topic 269 | 270 | def getIndex(self): 271 | parent = self.getParentNode() 272 | if parent and parent.tagName == const.TAG_TOPICS: 273 | index = 0 274 | for child in parent.childNodes: 275 | if self.getImplementation() == child: 276 | return index 277 | index += 1 278 | return -1 279 | 280 | def getHyperlink(self): 281 | return self.getAttribute(const.ATTR_HREF) 282 | 283 | def setFileHyperlink(self, path): 284 | """ 285 | Set file as topic hyperlink 286 | 287 | :param path: path of specified file 288 | 289 | """ 290 | protocol, content = split_hyperlink(path) 291 | if not protocol: 292 | path = const.FILE_PROTOCOL + utils.get_abs_path(path) 293 | 294 | self._set_hyperlink(path) 295 | 296 | def setTopicHyperlink(self, tid): 297 | """ 298 | Set topic as topic hyperlink 299 | 300 | :param id: given topic's id 301 | 302 | """ 303 | protocol, content = split_hyperlink(tid) 304 | if not protocol: 305 | if tid.startswith("#"): 306 | tid = tid[1:] 307 | 308 | tid = const.TOPIC_PROTOCOL + tid 309 | self._set_hyperlink(tid) 310 | 311 | def setURLHyperlink(self, url): 312 | """ Set URL as topic hyperlink 313 | 314 | :param url: HTTP URL to specified website 315 | 316 | """ 317 | protocol, content = split_hyperlink(url) 318 | if not protocol: 319 | url = const.HTTP_PROTOCOL + content 320 | 321 | self._set_hyperlink(url) 322 | 323 | def getNotes(self): 324 | """ 325 | Return `NotesElement` object` and invoke 326 | `NotesElement.getContent()` to get notes content. 327 | """ 328 | 329 | notes = self.getFirstChildNodeByTagName(const.TAG_NOTES) 330 | 331 | if notes is not None: 332 | return NotesElement(notes, self) 333 | 334 | def _set_notes(self): 335 | notes = self.getNotes() 336 | 337 | if notes is None: 338 | notes = NotesElement(ownerTopic=self) 339 | self.appendChild(notes) 340 | 341 | return notes 342 | 343 | def setPlainNotes(self, content): 344 | """ Set plain text notes to topic 345 | 346 | :param content: utf8 plain text 347 | 348 | """ 349 | notes = self._set_notes() 350 | new = PlainNotes(content, None, self) 351 | 352 | old = notes.getFirstChildNodeByTagName(new.getFormat()) 353 | if old is not None: 354 | notes.getImplementation().removeChild(old) 355 | 356 | notes.appendChild(new) 357 | 358 | 359 | class ChildrenElement(WorkbookMixinElement): 360 | TAG_NAME = const.TAG_CHILDREN 361 | 362 | def __init__(self, node=None, ownerWorkbook=None): 363 | super(ChildrenElement, self).__init__(node, ownerWorkbook) 364 | 365 | def getTopics(self, topics_type): 366 | topics = self.iterChildNodesByTagName(const.TAG_TOPICS) 367 | for i in topics: 368 | t = TopicsElement(i, self.getOwnerWorkbook()) 369 | if topics_type == t.getType(): 370 | return t 371 | 372 | 373 | class TopicsElement(WorkbookMixinElement): 374 | TAG_NAME = const.TAG_TOPICS 375 | 376 | def __init__(self, node=None, ownerWorkbook=None): 377 | super(TopicsElement, self).__init__(node, ownerWorkbook) 378 | 379 | def getType(self): 380 | return self.getAttribute(const.ATTR_TYPE) 381 | 382 | def getSubTopics(self): 383 | """ 384 | List all sub topics on the current topic 385 | """ 386 | topics = [] 387 | ownerWorkbook = self.getOwnerWorkbook() 388 | for t in self.getChildNodesByTagName(const.TAG_TOPIC): 389 | topics.append(TopicElement(t, ownerWorkbook)) 390 | 391 | return topics 392 | 393 | def getSubTopicByIndex(self, index): 394 | """ 395 | Get specified sub topic by index 396 | """ 397 | sub_topics = self.getSubTopics() 398 | if index < 0 or index >= len(sub_topics): 399 | return sub_topics 400 | 401 | return sub_topics[index] 402 | 403 | 404 | def main(): 405 | pass 406 | 407 | if __name__ == '__main__': 408 | main() 409 | -------------------------------------------------------------------------------- /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=None, ownerWorkbook=None): 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=None): 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 _create_relationship(self): 151 | return RelationshipElement(None, self) 152 | 153 | def createRelationship(self, end1, end2): 154 | """ Create relationship with two topics. Pass two 155 | `TopicElement` object and retuen `RelationshipElement` object 156 | """ 157 | sheet1 = end1.getOwnerSheet() 158 | sheet2 = end2.getOwnerSheet() 159 | 160 | if sheet1 and sheet2: 161 | if sheet1.getImplementation() == sheet2.getImplementation(): 162 | 163 | rel = self._create_relationship() 164 | rel.setEnd1ID(end1.getID()) 165 | rel.setEnd2ID(end2.getID()) 166 | 167 | sheet1.addRelationships(rel) 168 | 169 | return rel 170 | 171 | raise Exception("Topics not on the same sheet!") 172 | 173 | def createTopic(self): 174 | """ 175 | Create new `TopicElement` object and return. Please notice that 176 | this topic will not be added to the workbook. 177 | """ 178 | return TopicElement(None, self) 179 | 180 | def getSheets(self): 181 | """ 182 | List all sheets under workbook, if not sheets then return 183 | empty list 184 | """ 185 | return self._workbook_element.getSheets() 186 | 187 | def getPrimarySheet(self): 188 | """ 189 | Get the first sheet under workbook. 190 | """ 191 | return self._workbook_element.getSheetByIndex(0) 192 | 193 | def createSheet(self): 194 | """ 195 | Create new sheet. But please notice the new created sheet 196 | hasn't been added to the workbook. Invoke :method addSheet: to do that. 197 | """ 198 | return self._workbook_element.createSheet() 199 | 200 | def addSheet(self, sheet, index=None): 201 | """ 202 | Add a sheet to the workbook. 203 | 204 | :param sheet: add passed `SheetElement` object to workbook. 205 | 206 | :param index: insert sheet before another sheet that given by 207 | index. If index not given, append sheet to the 208 | sheets list. 209 | """ 210 | self._workbook_element.addSheet(sheet, index) 211 | 212 | def removeSheet(self, sheet): 213 | """ 214 | Remove a sheet from the workbook 215 | 216 | :param sheet: remove passed `SheetElement` object 217 | """ 218 | self._workbook_element.removeSheet(sheet) 219 | 220 | def moveSheet(self, original_index, target_index): 221 | """ 222 | Move a sheet from the original index to the target index 223 | 224 | :param original_index: index of the sheet will be moved. 225 | `original_index` must be positive integer and 226 | less than `target_index`. 227 | :param target_index: index that sheet want to move to. 228 | `target_index` must be positive integer and 229 | less than the length of sheets list. 230 | """ 231 | self._workbook_element.moveSheet(original_index, target_index) 232 | 233 | def getVersion(self): 234 | return self._workbook_element.getVersion() 235 | 236 | def getModifiedTime(self): 237 | return self._workbook_element.getModifiedTime() 238 | 239 | def updateModifiedTime(self): 240 | return self._workbook_element.updateModifiedTime() 241 | 242 | def setModifiedTime(self): 243 | return self._workbook_element.setModifiedTime() 244 | 245 | def get_path(self): 246 | if self._path: 247 | return utils.get_abs_path(self._path) 248 | 249 | def set_path(self, path): 250 | self._path = utils.get_abs_path(path) 251 | 252 | 253 | def main(): 254 | pass 255 | 256 | if __name__ == '__main__': 257 | main() 258 | -------------------------------------------------------------------------------- /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 long(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())).hexdigest() 116 | lotter = md5(str(random.random())).hexdigest() # :) 117 | 118 | id = timestamp[19:] + lotter[:13] 119 | 120 | return id.decode("utf8") 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 | 156 | def main(): 157 | pass 158 | 159 | if __name__ == '__main__': 160 | main() 161 | --------------------------------------------------------------------------------