Repository: tobyqin/xmindparser
Branch: master
Commit: 86cb671c49b6
Files: 30
Total size: 87.2 KB
Directory structure:
gitextract_130vgu5j/
├── .github/
│ └── workflows/
│ └── test-publish.yml
├── .gitignore
├── CHANGELOG.md
├── DEVELOPER.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── README_CN.md
├── doc/
│ ├── example.json
│ ├── example.md
│ ├── example.xml
│ ├── example.yaml
│ ├── example_2026.json
│ ├── example_2026.yaml
│ ├── example_2026_with_id.json
│ ├── example_with_id.json
│ ├── example_zen.json
│ ├── example_zen.yaml
│ └── example_zen_with_id.json
├── setup.py
├── tests/
│ ├── __init__.py
│ ├── test_xmindparser.py
│ ├── xmind_2026.xmind
│ ├── xmind_2026.yaml
│ ├── xmind_pro.xmind
│ └── xmind_zen.xmind
└── xmindparser/
├── __init__.py
├── main.py
├── xreader.py
└── zenreader.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/test-publish.yml
================================================
name: Test and Publish
on:
push:
branches: [main, master]
tags:
- "v*"
pull_request:
branches: [main, master]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest dicttoxml pyyaml
- name: Run tests
run: |
pytest tests/ -v
build:
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Set version from tag
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_ENV
sed -i "s/version=\"[^\"]*\"/version=\"$VERSION\"/" setup.py
- name: Build package
run: |
python -m build
- name: Check package
run: |
twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload --skip-existing dist/*
build-sdist:
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build sdist
run: |
python -m build --sdist
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v1
with:
files: dist/*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.cache
out/
.idea/
.pytest_cache/
out/
.vscode/
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
uploads/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# sqlite
*.sqlite3
*.db3
.DS_Store
# Claude Code
CLAUDE.md
.claude/
================================================
FILE: CHANGELOG.md
================================================
## Change Log
1.2.2
- Add new config options: `showStructure` (default True) to include/exclude sheet structure info, and `showRelationship` (default False) to include relationship info for Xmind Zen/2026 files.
- Add support for parsing relationships in Xmind Zen/2026 files when `showRelationship` is enabled.
- Make structure inclusion conditional for Xmind Zen/2026 files based on `showStructure` config.
- Add comprehensive tests for the new config options.
- Update documentation in both English and Chinese READMEs.
1.2.1
- Fix config system to support dynamic logger reconfiguration.
- Add `apply_config()` function to apply logging configuration changes.
- Add comprehensive tests for all config options (hideEmptyValue, logFormat, logLevel, logName).
- Update documentation with proper config usage examples.
1.2.0
- Add YAML export support with `xmind_to_yaml()` function.
- Add support for Xmind 2026 file format.
- Update documentation with local Chinese README and developer guide.
- Rename Xmind legacy to Xmind 8, Xmind Zen to Xmind.
1.1.2
- Fix Chinese character encoding issue in JSON export by adding `ensure_ascii=False` to `json.dumps()`.
1.1.1
- Add Python 3.14 support.
1.1.0
- Add support for converting xmind to markdown format.
- New function `xmind_to_markdown()` to convert xmind file to markdown.
- Command line support: `xmindparser your.xmind -markdown`.
1.0.9
- Update Python version classifiers to support 3.9, 3.10, 3.11, 3.12, 3.13.
- Fix DeprecationWarning for element truth value testing in Python 3.13.
1.0.8
- Handle empty title name for xmind zen in some cases.
1.0.6
- Keep empty topic title as null but not "[Blank]"
1.0.5
- Support xmind zen file type.
1.0.4
- Support parse label feature.
1.0.2
- Rename config key names.
1.0.1
- Support parse xmind to xml file type.
1.0.0
- Support parse xmind to dict data type with Python.
- Support parse xmind to json file type.
================================================
FILE: DEVELOPER.md
================================================
# For Developers
## Publish New Version
This project uses GitHub Actions to automatically publish to PyPI when a new version tag is pushed.
1. Update version in [`setup.py`](setup.py:42) and [`CHANGELOG.md`](CHANGELOG.md)
2. Commit and push changes to GitHub
3. Create and push a new version tag:
```shell
git tag v1.1.0
git push origin v1.1.0
```
4. GitHub Actions will automatically:
- Run tests on Python 3.9-3.13
- Build and publish to PyPI
- Create GitHub Release with source distribution
**Note:** Requires `PYPI_API_TOKEN` secret to be configured in repository settings.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Toby Qin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
include LICENSE
include README.md
include CHANGELOG.md
include setup.py
================================================
FILE: README.md
================================================
# xmindparser
[](https://pypi.org/project/xmindparser/)
Parse xmind file to programmable data type (e.g. json, xml). Python 3.x required. Now it supports Xmind (including Xmind Zen and Xmind 2026) file type as well.
See also: [xmind2testlink](https://github.com/tobyqin/xmind2testlink) / [中文文档](README_CN.md)
## Installation
```shell
pip install xmindparser
```
## Usage - Command Line
```shell
cd /your/xmind/dir
xmindparser your.xmind -json
xmindparser your.xmind -xml
xmindparser your.xmind -yaml
xmindparser your.xmind -markdown
```
Note: Parse to xml/yaml file types require additional packages:
- xml: [dicttoxml](https://pypi.org/project/dicttoxml/)
- yaml: [pyyaml](https://pypi.org/project/pyyaml/)
## Usage - via Python
```python
from xmindparser import xmind_to_dict
d = xmind_to_dict('/path/to/your/xmind')
print(d)
```
See example output: [json](doc/example.json)
### Convert to Markdown
```python
from xmindparser import xmind_to_markdown
# Convert xmind to markdown file
output_file = xmind_to_markdown('/path/to/your/xmind')
print(f'Generated: {output_file}')
```
Or use the generic `xmind_to_file` function:
```python
from xmindparser import xmind_to_file
# Convert to markdown
output_file = xmind_to_file('/path/to/your/xmind', 'markdown')
print(f'Generated: {output_file}')
```
## Configuration
If you use `xmindparser` via Python, it provides a `config` object, check this example:
```python
import logging
from xmindparser import xmind_to_dict, config, apply_config
# Modify config settings
config['logName'] = 'your_log_name'
config['logLevel'] = logging.DEBUG
config['logFormat'] = '%(asctime)s %(levelname)-8s: %(message)s'
config['showTopicId'] = True # internal id will be included, default = False
config['hideEmptyValue'] = False # empty values will be hidden, default = True
config['showStructure'] = False # include structure info for sheets, default = True
config['showRelationship'] = True # include relationship info for Zen/2026 files, default = False
# Apply the config changes (required for logging settings to take effect)
apply_config()
d = xmind_to_dict('/path/to/your/xmind')
print(d)
```
**Note:** After modifying logging-related config options (`logName`, `logLevel`, `logFormat`), you must call `apply_config()` to apply the changes. The `showTopicId`, `hideEmptyValue`, `showStructure`, and `showRelationship` options take effect immediately without calling `apply_config()`.
## Limitations (for Xmind 8)
Please note, following xmind features will not be supported or partially supported.
- Will not parse Pro features, e.g. Task Info, Audio Note
- Will not parse floating topics.
- Will not parse linked topics.
- Will not parse summary info.
- Will not parse relationship info (except for Xmind Zen/2026 when `showRelationship` is enabled).
- Will not parse boundary info.
- Will not parse attachment object, only name it as `[Attachment] - name`
- Will not parse image object, only name it as `[Image]`
- Rich text format in notes will be parsed as plain text.
## Xmind (including Zen and 2026)
`xmindparser` will auto detect xmind file created by Xmind (including Zen/2026 version) or Xmind 8, you can pass in the file as usual.
```python
from xmindparser import xmind_to_dict
d = xmind_to_dict('/path/to/your/xmind_zen_file')
print(d)
```
Please note, there are a few differences between Xmind 8 and Xmind (Zen/2026).
- Comments feature removed, so I will not parse it in ZEN.
- Add feature - sticker, I parse it as `image` dict type.
- Add feature - callout, I parse it as `list` type. (not sure existed in legacy?)
Since Xmind (Zen/2026) uses json as the internal content file, you can read it by code like this:
```python
import json
def get_xmind_zen_json(file_path):
name = "content.json"
with ZipFile(file_path) as xmind:
if name in xmind.namelist():
content = xmind.open(name).read().decode('utf-8')
return json.loads(content)
raise AssertionError("Not a xmind zen file type!")
# xmindparser also provides a shortcut
from xmindparser import get_xmind_zen_builtin_json
content_json = get_xmind_zen_builtin_json(xmind_zen_file)
```
## Examples

- [Download xmind 8 example](tests/xmind_pro.xmind)
- [Download xmind (Zen/2026) example](tests/xmind_zen.xmind)
- [Download xmind 2026 example](tests/xmind_2026.xmind)
- Output: [json example](doc/example.json)
- Output: [xml example](doc/example.xml)
- Output: [yaml example](doc/example.yaml)
- Output: [markdown example](doc/example.md)
## License
MIT
================================================
FILE: README_CN.md
================================================
# xmindparser
[](https://pypi.org/project/xmindparser/)
将 xmind 文件转换为可编程的数据类型(如 json、xml)。需要 Python 3.x。支持 Xmind(包括 Xmind Zen 和 Xmind 2026)文件类型。
相关项目:[xmind2testlink](https://github.com/tobyqin/xmind2testlink)
English version: [README.md](README.md)
## 安装
```shell
pip install xmindparser
```
## 使用方法 - 命令行
```shell
cd /your/xmind/dir
xmindparser your.xmind -json
xmindparser your.xmind -xml
xmindparser your.xmind -yaml
xmindparser your.xmind -markdown
```
注意:解析为 xml/yaml 文件类型需要安装额外依赖:
- xml:[dicttoxml](https://pypi.org/project/dicttoxml/)
- yaml:[pyyaml](https://pypi.org/project/pyyaml/)
## 使用方法 - Python 代码
```python
from xmindparser import xmind_to_dict
d = xmind_to_dict('/path/to/your/xmind')
print(d)
```
查看示例输出:[json 示例](doc/example.json)
### 转换为 Markdown
```python
from xmindparser import xmind_to_markdown
# 将 xmind 转换为 markdown 文件
output_file = xmind_to_markdown('/path/to/your/xmind')
print(f'Generated: {output_file}')
```
或者使用通用的 `xmind_to_file` 函数:
```python
from xmindparser import xmind_to_file
# 转换为 markdown
output_file = xmind_to_file('/path/to/your/xmind', 'markdown')
print(f'Generated: {output_file}')
```
## 配置
如果您通过 Python 使用 `xmindparser`,它提供了一个 `config` 对象,请查看以下示例:
```python
import logging
from xmindparser import xmind_to_dict, config, apply_config
# 修改配置设置
config['logName'] = 'your_log_name'
config['logLevel'] = logging.DEBUG
config['logFormat'] = '%(asctime)s %(levelname)-8s: %(message)s'
config['showTopicId'] = True # 包含内部 id,默认为 False
config['hideEmptyValue'] = False # 隐藏空值,默认为 True
config['showStructure'] = False # 包含工作表的结构信息,默认为 True
config['showRelationship'] = True # 包含关系信息(针对 Zen/2026 文件),默认为 False
# 应用配置更改(日志相关设置需要调用此函数才能生效)
apply_config()
d = xmind_to_dict('/path/to/your/xmind')
print(d)
```
**注意:** 修改日志相关的配置选项(`logName`、`logLevel`、`logFormat`)后,必须调用 `apply_config()` 来应用更改。`showTopicId`、`hideEmptyValue`、`showStructure` 和 `showRelationship` 选项无需调用 `apply_config()` 即可立即生效。
## 限制(针对 Xmind 8)
请注意,以下 xmind 功能将不被支持或仅部分支持。
- 不会解析 Pro 功能,例如任务信息、音频笔记
- 不会解析浮动主题
- 不会解析链接主题
- 不会解析摘要信息
- 不会解析关系信息(除非针对 Xmind Zen/2026 启用 `showRelationship` 时)
- 不会解析边界信息
- 不会解析附件对象,仅将其命名为 `[Attachment] - name`
- 不会解析图片对象,仅将其命名为 `[Image]`
- 笔记中的富文本格式将解析为纯文本
## Xmind 支持(包括 Zen 和 2026)
`xmindparser` 会自动检测 Xmind(包括 Zen/2026 版本)或 Xmind 8 创建的 xmind 文件,您可以像平常一样传入文件。
```python
from xmindparser import xmind_to_dict
d = xmind_to_dict('/path/to/your/xmind_zen_file')
print(d)
```
请注意,Xmind 8 和 Xmind(Zen/2026)之间存在一些差异。
- 评论功能已移除,因此我不会在 ZEN 中解析它
- 新增功能 - 贴纸,我将其解析为 `image` 字典类型
- 新增功能 - 标注,我将其解析为 `list` 类型(不确定传统版中是否存在?)
由于 Xmind(Zen/2026)使用 json 作为内部内容文件,您可以通过以下代码读取:
```python
import json
def get_xmind_zen_json(file_path):
name = "content.json"
with ZipFile(file_path) as xmind:
if name in xmind.namelist():
content = xmind.open(name).read().decode('utf-8')
return json.loads(content)
raise AssertionError("Not a xmind zen file type!")
# xmindparser 也提供了快捷方式
from xmindparser import get_xmind_zen_builtin_json
content_json = get_xmind_zen_builtin_json(xmind_zen_file)
```
## 示例

- [下载 xmind 8 示例](tests/xmind_pro.xmind)
- [下载 xmind(Zen/2026)示例](tests/xmind_zen.xmind)
- [下载 xmind 2026 示例](tests/xmind_2026.xmind)
- 输出:[json 示例](doc/example.json)
- 输出:[xml 示例](doc/example.xml)
- 输出:[yaml 示例](doc/example.yaml)
- 输出:[markdown 示例](doc/example.md)
## 许可证
MIT
================================================
FILE: doc/example.json
================================================
[
{
"title": "Sheet 1",
"topic": {
"title": "test",
"makers": [
"star-orange"
],
"topics": [
{
"title": "a",
"link": "http://test.com",
"topics": [
{
"title": "e",
"topics": [
{
"title": "f",
"topics": [
{
"title": "g"
}
]
}
]
},
{
"title": "d",
"topics": [
{
"title": null
}
]
},
{
"title": "e",
"makers": [
"symbol-plus"
],
"topics": [
{
"title": null,
"note": "this is a note"
}
]
},
{
"title": null,
"topics": [
{
"title": "h",
"topics": [
{
"title": null,
"comment": [
{
"author": "toby.qin",
"content": "this a comments"
}
]
}
]
}
]
}
]
},
{
"title": "b",
"makers": [
"task-done",
"flag-red",
"people-red",
"arrow-up"
],
"topics": [
{
"title": "types",
"topics": [
{
"title": "comment",
"comment": [
{
"author": "toby.qin",
"content": "comment1"
},
{
"author": "toby.qin",
"content": "comment2"
},
{
"author": "toby.qin",
"content": "comment3\r\nnew line"
}
]
},
{
"title": "note",
"note": "1 line note",
"topics": [
{
"title": "note2",
"note": "line1\r\nline2"
},
{
"title": "note3",
"note": "note with style\r\nline with strike"
}
]
},
{
"title": "label",
"labels": [
"this is a label"
]
},
{
"title": "attachment",
"topics": [
{
"title": "[Attachment]test.txt"
}
]
},
{
"title": "pic",
"topics": [
{
"title": "[Image]"
}
]
},
{
"title": "links",
"topics": [
{
"title": "to url",
"link": "http://test.com"
},
{
"title": "to file",
"link": "file:////abc/ef"
},
{
"title": "to topic",
"link": "[To another xmind topic!]"
}
]
}
]
},
{
"title": "\u6d4b\u8bd5",
"topics": [
{
"title": "note",
"note": "\u6d4b\u8bd5\u4e2d\u6587"
},
{
"title": "comments",
"comment": [
{
"author": "toby.qin",
"content": "\u4e2d\u6587\u6d4b\u8bd5"
}
]
}
]
},
{
"title": ">{}[]*+-"
}
]
},
{
"title": "c",
"topics": [
{
"title": "a"
},
{
"title": "a"
},
{
"title": "a",
"link": "[To another xmind topic!]"
}
]
}
]
},
"structure": "org.xmind.ui.map.unbalanced"
},
{
"title": "Sheet 2",
"topic": {
"title": "test2",
"topics": [
{
"title": "a",
"topics": [
{
"title": "1"
},
{
"title": "2"
},
{
"title": "3"
}
]
},
{
"title": "b"
}
]
},
"structure": "org.xmind.ui.map.unbalanced"
}
]
================================================
FILE: doc/example.md
================================================
# Sheet 1
## test
### a
[Link](http://test.com)
#### e
##### f
###### g
#### d
#### e
> this is a note
##### h
**Comments:**
- **toby.qin**: this a comments
### b
#### types
##### comment
**Comments:**
- **toby.qin**: comment1
- **toby.qin**: comment2
- **toby.qin**: comment3
new line
##### note
> 1 line note
###### note2
> line1
> line2
###### note3
> note with style
> line with strike
##### label
**Labels:** this is a label
##### attachment
###### [Attachment]test.txt
##### pic
###### [Image]
##### links
###### to url
[Link](http://test.com)
###### to file
[Link](file:////abc/ef)
###### to topic
#### 测试
##### note
> 测试中文
##### comments
**Comments:**
- **toby.qin**: 中文测试
#### >{}[]*+-
### c
#### a
#### a
#### a
# Sheet 2
## test2
### a
#### 1
#### 2
#### 3
### b
================================================
FILE: doc/example.xml
================================================
-
org.xmind.ui.map.unbalanced
-
http://test.com
-
-
-
g
f
e
-
-
[Blank]
d
-
-
this is a note
[Blank]
e
- symbol-plus
-
-
-
-
this a comments
toby.qin
[Blank]
h
[Blank]
a
-
-
-
-
comment1
toby.qin
-
comment2
toby.qin
-
comment3
new line
toby.qin
comment
-
1 line note
-
line1
line2
note2
-
note with style
line with strike
note3
note
-
- this is a label
label
-
-
[Attachment]test.txt
attachment
-
-
[Image]
pic
-
-
http://test.com
to url
-
file:////abc/ef
to file
-
[To another xmind topic!]
to topic
links
types
-
-
测试中文
note
-
-
中文测试
toby.qin
comments
测试
-
</>{}[]*+-
b
- task-done
- flag-red
- people-red
- arrow-up
-
-
a
-
a
-
[To another xmind topic!]
a
c
test
- star-orange
Sheet 1
-
org.xmind.ui.map.unbalanced
-
-
1
-
2
-
3
a
-
b
test2
Sheet 2
================================================
FILE: doc/example.yaml
================================================
- structure: org.xmind.ui.map.unbalanced
title: Sheet 1
topic:
makers:
- star-orange
title: test
topics:
- link: http://test.com
title: a
topics:
- title: e
topics:
- title: f
topics:
- title: g
- title: d
topics:
- title: null
- makers:
- symbol-plus
title: e
topics:
- note: this is a note
title: null
- title: null
topics:
- title: h
topics:
- comment:
- author: toby.qin
content: this a comments
title: null
- makers:
- task-done
- flag-red
- people-red
- arrow-up
title: b
topics:
- title: types
topics:
- comment:
- author: toby.qin
content: comment1
- author: toby.qin
content: comment2
- author: toby.qin
content: "comment3\r\nnew line"
title: comment
- note: 1 line note
title: note
topics:
- note: "line1\r\nline2"
title: note2
- note: "note with style\r\nline with strike"
title: note3
- labels:
- this is a label
title: label
- title: attachment
topics:
- title: '[Attachment]test.txt'
- title: pic
topics:
- title: '[Image]'
- title: links
topics:
- link: http://test.com
title: to url
- link: file:////abc/ef
title: to file
- link: '[To another xmind topic!]'
title: to topic
- title: 测试
topics:
- note: 测试中文
title: note
- comment:
- author: toby.qin
content: 中文测试
title: comments
- title: >{}[]*+-
- title: c
topics:
- title: a
- title: a
- link: '[To another xmind topic!]'
title: a
- structure: org.xmind.ui.map.unbalanced
title: Sheet 2
topic:
title: test2
topics:
- title: a
topics:
- title: '1'
- title: '2'
- title: '3'
- title: b
================================================
FILE: doc/example_2026.json
================================================
[
{
"title": "test",
"topic": {
"title": "test",
"topics": [
{
"title": "a",
"topics": [
{
"title": "e",
"topics": [
{
"title": "f",
"topics": [
{
"title": "g"
}
]
}
]
},
{
"title": "d"
},
{
"title": "e",
"topics": [
{
"title": ""
}
]
},
{
"title": "",
"topics": [
{
"title": "h",
"topics": [
{
"title": ""
}
]
}
]
}
]
},
{
"title": "b",
"topics": [
{
"title": "types",
"topics": [
{
"title": "comment"
},
{
"title": "note",
"note": "hello",
"topics": [
{
"title": "note2",
"note": "hello"
},
{
"title": "note3",
"note": "hello"
}
]
},
{
"title": "label",
"labels": [
"this",
"that",
"then"
]
},
{
"title": "attachment",
"topics": [
{
"title": "test.txt"
}
]
},
{
"title": "marker",
"makers": [
"tag-orange",
"priority-2",
"task-oct",
"flag-orange",
"star-orange",
"people-orange",
"c_symbol_like"
]
},
{
"title": "pic",
"topics": [
{
"title": "0.jpeg",
"image": {
"src": "xap:resources/9afc60beae59d849e741ae11d19d638a7b89090ee6c7b772159db271142bcee6.jpeg"
}
}
]
},
{
"title": "sticker1",
"image": {
"src": "xap:resources/dd46d29fc64e6eb4e2aac3f78ab162b7177ae0608be431ebf261cad179b2fdad.svg"
}
},
{
"title": "sticker2",
"image": {
"src": "xap:resources/129b1b21c5946e3f1692f39dcd7d18e0241c66c0b92959cbdbf414a2d40defbd.svg"
}
},
{
"title": "link",
"topics": [
{
"title": "to url",
"link": "http://github.com"
},
{
"title": "to file"
},
{
"title": "to topic"
}
]
},
{
"title": "测试",
"topics": [
{
"title": " note"
},
{
"title": "comment"
}
]
},
{
"title": ">{}[]*+-"
}
]
}
]
},
{
"title": "c",
"topics": [
{
"title": "a"
},
{
"title": "a"
},
{
"title": "a"
}
]
}
]
},
"structure": "org.xmind.ui.map.clockwise"
}
]
================================================
FILE: doc/example_2026.yaml
================================================
- structure: org.xmind.ui.map.clockwise
title: test
topic:
title: test
topics:
- title: a
topics:
- title: e
topics:
- title: f
topics:
- title: g
- title: d
- title: e
topics:
- title: ''
- title: ''
topics:
- title: h
topics:
- title: ''
- title: b
topics:
- title: types
topics:
- title: comment
- note: hello
title: note
topics:
- note: hello
title: note2
- note: hello
title: note3
- labels:
- this
- that
- then
title: label
- title: attachment
topics:
- title: test.txt
- makers:
- tag-orange
- priority-2
- task-oct
- flag-orange
- star-orange
- people-orange
- c_symbol_like
title: marker
- title: pic
topics:
- image:
src: xap:resources/9afc60beae59d849e741ae11d19d638a7b89090ee6c7b772159db271142bcee6.jpeg
title: 0.jpeg
- image:
src: xap:resources/dd46d29fc64e6eb4e2aac3f78ab162b7177ae0608be431ebf261cad179b2fdad.svg
title: sticker1
- image:
src: xap:resources/129b1b21c5946e3f1692f39dcd7d18e0241c66c0b92959cbdbf414a2d40defbd.svg
title: sticker2
- title: link
topics:
- link: http://github.com
title: to url
- title: to file
- title: to topic
- title: 测试
topics:
- title: ' note'
- title: comment
- title: >{}[]*+-
- title: c
topics:
- title: a
- title: a
- title: a
================================================
FILE: doc/example_2026_with_id.json
================================================
[
{
"title": "test",
"topic": {
"title": "test",
"topics": [
{
"title": "a",
"topics": [
{
"title": "e",
"topics": [
{
"title": "f",
"topics": [
{
"title": "g",
"id": "d139621e-fcfd-47d9-8c88-baee2520c9d2"
}
],
"id": "50e70127-fd85-4f3d-8499-607cf40ba2f6"
}
],
"id": "8bd84d0e-2b5c-4909-99a8-c86602aecb97"
},
{
"title": "d",
"id": "ae1b8692-74a8-4b31-8a6d-53c1b9081b37"
},
{
"title": "e",
"topics": [
{
"title": "",
"id": "19689765-cc6f-4f2d-a8f3-29ff975f483e"
}
],
"id": "d48acb35-acbd-4355-84ef-3bb88cdd3df0"
},
{
"title": "",
"topics": [
{
"title": "h",
"topics": [
{
"title": "",
"id": "dbd07b55-4cdb-4bb9-88d7-18eec584de9e"
}
],
"id": "76b89635-c5b2-4050-a709-946eb722887b"
}
],
"id": "b5dadecb-9115-4e36-a440-f3adf707e445"
}
],
"id": "662b6d569ce8f4377df40a693f"
},
{
"title": "b",
"topics": [
{
"title": "types",
"topics": [
{
"title": "comment",
"id": "7e18864a-02cd-4cd0-a826-4ea28a095ac0"
},
{
"title": "note",
"note": "hello",
"topics": [
{
"title": "note2",
"note": "hello",
"id": "68429427-5328-470c-99f8-b851b5174495"
},
{
"title": "note3",
"note": "hello",
"id": "88c0585a-c331-4893-b1e6-489afae8a8d2"
}
],
"id": "f83cd16a-0b67-49a4-a050-4822325bd9b0"
},
{
"title": "label",
"labels": [
"this",
"that",
"then"
],
"id": "1cf6a76f-a16f-47eb-b104-ac145a825bbf"
},
{
"title": "attachment",
"topics": [
{
"title": "test.txt",
"id": "2b877785-e174-4bd8-b6f3-b386e8846658"
}
],
"id": "9e1820fa-c600-4db9-ab7e-5a53108c2399"
},
{
"title": "marker",
"makers": [
"tag-orange",
"priority-2",
"task-oct",
"flag-orange",
"star-orange",
"people-orange",
"c_symbol_like"
],
"id": "53e7347d-fcd1-4181-81d3-8fad62e9288c"
},
{
"title": "pic",
"topics": [
{
"title": "0.jpeg",
"image": {
"src": "xap:resources/9afc60beae59d849e741ae11d19d638a7b89090ee6c7b772159db271142bcee6.jpeg"
},
"id": "b0d151f3-52c2-4b62-8fed-6475fe0220db"
}
],
"id": "4977ba82-de19-47ab-9871-8fd95d6f1fca"
},
{
"title": "sticker1",
"image": {
"src": "xap:resources/dd46d29fc64e6eb4e2aac3f78ab162b7177ae0608be431ebf261cad179b2fdad.svg"
},
"id": "fca4f88c-f65f-428e-a0ad-d73758f97e5a"
},
{
"title": "sticker2",
"image": {
"src": "xap:resources/129b1b21c5946e3f1692f39dcd7d18e0241c66c0b92959cbdbf414a2d40defbd.svg"
},
"id": "8bc05dd2-9417-4087-9ccf-089bd64f81bb"
},
{
"title": "link",
"topics": [
{
"title": "to url",
"link": "http://github.com",
"id": "fe681a0b-665c-4da0-9e47-335b070591d7"
},
{
"title": "to file",
"id": "1b670faa-accb-4c7d-a6cb-5c9ac4af9834"
},
{
"title": "to topic",
"id": "e738e21f-ad7a-4e3f-bef8-7deccb6ce537"
}
],
"id": "68aea8cf-2f73-4727-9e01-0348ea1cda6e"
},
{
"title": "测试",
"topics": [
{
"title": " note",
"id": "97e8e40f-0231-4f19-90df-b84049175c56"
},
{
"title": "comment",
"id": "a3082b40-7cc8-4d0e-a6f5-22797e5706c9"
}
],
"id": "5ee53dbc-8e85-4aed-8ccd-e9759f761392"
},
{
"title": ">{}[]*+-",
"id": "02ff821b-6ff7-4ece-8a1f-e7f3f9cdf697"
}
],
"id": "ff301ff5-2823-474c-9c73-dce071016a0e"
}
],
"id": "86e396ce-f4b0-4b7f-9f6c-6af452212532"
},
{
"title": "c",
"topics": [
{
"title": "a",
"id": "84722f1f-8d30-48c2-b113-0f0f0a51a093"
},
{
"title": "a",
"id": "4178f058-7c9e-407e-b97f-1387aeaf3bd3"
},
{
"title": "a",
"id": "fa64feb7-d141-40ef-b905-ee8241d8cb9e"
}
],
"id": "b802e9b4-5823-4959-aaf2-277960434259"
}
],
"id": "d37854166e0a3adf36413a3c20"
},
"structure": "org.xmind.ui.map.clockwise",
"id": "4b48628d41abafc49e9119e2f4"
}
]
================================================
FILE: doc/example_with_id.json
================================================
[
{
"title": "Sheet 1",
"topic": {
"title": "test",
"makers": [
"star-orange"
],
"topics": [
{
"title": "a",
"link": "http://test.com",
"topics": [
{
"title": "e",
"topics": [
{
"title": "f",
"topics": [
{
"title": "g",
"id": "19u3o14ss5ib73ckoiuo4c614m"
}
],
"id": "015ofcedrsice6eak3gqk3lj7v"
}
],
"id": "04m9bbu1mrurjo5ssp5o7eharf"
},
{
"title": "d",
"topics": [
{
"title": null,
"id": "4hkgeo72lekalvffgj3n88cgk4"
}
],
"id": "219qo6qe1lmqjihk8st81o42oi"
},
{
"title": "e",
"makers": [
"symbol-plus"
],
"topics": [
{
"title": null,
"note": "this is a note",
"id": "26o5l2ejqp08l515h14tr0qetm"
}
],
"id": "5kmgtg6emhuk3da6p8gvl4ksnm"
},
{
"title": null,
"topics": [
{
"title": "h",
"topics": [
{
"title": null,
"comment": [
{
"author": "toby.qin",
"content": "this a comments",
"id": "1p36uh37vablt0qpcehk4668te"
}
],
"id": "1p36uh37vablt0qpcehk4668te"
}
],
"id": "5l94qndjrpqdu76q35amhe1oh9"
}
],
"id": "4a5aa28e149gnrlcl22f2d83t2"
}
],
"id": "2raf1ghptbsc7hvvb65kib62bp"
},
{
"title": "b",
"makers": [
"task-done",
"flag-red",
"people-red",
"arrow-up"
],
"topics": [
{
"title": "types",
"topics": [
{
"title": "comment",
"comment": [
{
"author": "toby.qin",
"content": "comment1",
"id": "0f7kql863ll2fdd66i91pn8h3p"
},
{
"author": "toby.qin",
"content": "comment2",
"id": "0f7kql863ll2fdd66i91pn8h3p"
},
{
"author": "toby.qin",
"content": "comment3\r\nnew line",
"id": "0f7kql863ll2fdd66i91pn8h3p"
}
],
"id": "0f7kql863ll2fdd66i91pn8h3p"
},
{
"title": "note",
"note": "1 line note",
"topics": [
{
"title": "note2",
"note": "line1\r\nline2",
"id": "09bb0va5jm47h4qa4jm2p1u2gk"
},
{
"title": "note3",
"note": "note with style\r\nline with strike",
"id": "0o9pe3lfq50ojsbhmsevc7laav"
}
],
"id": "041j66ar1uslmdh1ee2esiqjus"
},
{
"title": "label",
"labels": [
"this is a label"
],
"id": "01k5217iqhgi53qijfktbg5rep"
},
{
"title": "attachment",
"topics": [
{
"title": "[Attachment]test.txt",
"id": "1e37na1d9junmpol5n75mv240i"
}
],
"id": "4lnpmvpofafvqalvemhd45ufoo"
},
{
"title": "pic",
"topics": [
{
"title": "[Image]",
"id": "6pk67puq3vu9n7u8tta4rfgmae"
}
],
"id": "0j2vmutou9c0ophg25fgu2gt2f"
},
{
"title": "links",
"topics": [
{
"title": "to url",
"link": "http://test.com",
"id": "6n690hf3cot3crbj9e3kh0f60k"
},
{
"title": "to file",
"link": "file:////abc/ef",
"id": "56m7ffgbla4nmj6dp2pqqcj113"
},
{
"title": "to topic",
"link": "[To another xmind topic!]",
"id": "5roos0gmob6ndc1vpb5btka8ne"
}
],
"id": "29s8gjuk3cp0cmam9pqhtl2l5t"
}
],
"id": "7g9vfb0ehndv6no9qtng3pt8tt"
},
{
"title": "\u6d4b\u8bd5",
"topics": [
{
"title": "note",
"note": "\u6d4b\u8bd5\u4e2d\u6587",
"id": "107n29rkvobbaf0qk8ibgeifiq"
},
{
"title": "comments",
"comment": [
{
"author": "toby.qin",
"content": "\u4e2d\u6587\u6d4b\u8bd5",
"id": "3nt5tccmen04cjgfth7ehf8fqb"
}
],
"id": "3nt5tccmen04cjgfth7ehf8fqb"
}
],
"id": "67mpm88oa240d004l7acfr3m77"
},
{
"title": ">{}[]*+-",
"id": "3ib375f8hmg0abviusakg856sq"
}
],
"id": "0l01he3lh6r31omo23if7f486l"
},
{
"title": "c",
"topics": [
{
"title": "a",
"id": "2djipd1uflks01smve6nl2tb1n"
},
{
"title": "a",
"id": "4bjb1tgv0g2f3nfaeieshrdeas"
},
{
"title": "a",
"link": "[To another xmind topic!]",
"id": "2iblin5mer66nq9omm98htnmdm"
}
],
"id": "1jtiomk7kgh8n2lv99qgt7gi85"
}
],
"id": "4daq1j6fik7noda59olakoeu74"
},
"structure": "org.xmind.ui.map.unbalanced",
"id": "7c3c1opth8b21r6en8b8bj4aah"
},
{
"title": "Sheet 2",
"topic": {
"title": "test2",
"topics": [
{
"title": "a",
"topics": [
{
"title": "1",
"id": "2d9q5c8nsl0eh8ftroipptgan7"
},
{
"title": "2",
"id": "4kpslik9lmsb5qrhtj5n5kfprp"
},
{
"title": "3",
"id": "02ngl6ho0tv44u63hoinitotkj"
}
],
"id": "7hqpdbkseqls512fac08vkff7m"
},
{
"title": "b",
"id": "1bndoi9nc63r4g3f2ude6rjbg2"
}
],
"id": "4tmccl0gm2i50cploeik8jd9pc"
},
"structure": "org.xmind.ui.map.unbalanced",
"id": "0aqfgfibdj3enu5qmj45viis2q"
}
]
================================================
FILE: doc/example_zen.json
================================================
[
{
"title": "Map 1",
"topic": {
"title": "Central Topic",
"image": {
"src": "xap:resources/6cca606c3f2028e5ee7794459510ce1fd8855e5e8af44c5af91a212bc7f71462.svg",
"type": "image"
},
"topics": [
{
"title": "topic 1",
"makers": [
"priority-1"
],
"topics": [
{
"title": "note topic",
"note": "note info\nnew line of note"
},
{
"title": "label topic",
"labels": [
"great"
],
"topics": [
{
"title": "formatted note",
"note": "hello world"
}
]
},
{
"title": "link topic",
"link": "http://www.link.com"
},
{
"title": "callout topic",
"callout": [
"Callout"
]
},
{
"title": "makers topic",
"makers": [
"priority-2",
"smiley-smile",
"task-oct",
"flag-orange"
]
},
{
"title": "sticker topic",
"image": {
"src": "xap:resources/82d831f17e8c6cad32ff1791060d24488fab8c64186b07894e38b3a35013f736.svg",
"type": "image"
}
},
{
"title": "image topic",
"image": {
"src": "xap:resources/40af36d11c22d4747e35dd2d82cfa2c0e6ed91a333fa77b18c8d2e675e7b9fa0.png",
"type": "image"
}
},
{
"title": "attachment topic",
"topics": [
{
"title": "test.txt",
"link": "xap:resources/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.txt"
}
]
}
]
},
{
"title": "topic 2",
"makers": [
"priority-2"
],
"topics": [
{
"title": "123"
},
{
"title": "",
"topics": [
{
"title": "topic after empty"
}
]
},
{
"title": "\u4e2d\u6587"
},
{
"title": "<>{}*&^^&*^%@!",
"topics": [
{
"title": "topic with special chars"
}
]
},
{
"title": "formatted topic"
}
]
}
]
},
"structure": "org.xmind.ui.map.unbalanced"
},
{
"title": "Map 2",
"topic": {
"title": "another sheet",
"topics": [
{
"title": "Main Topic 1"
},
{
"title": "Main Topic 2"
}
]
},
"structure": "org.xmind.ui.map.unbalanced"
}
]
================================================
FILE: doc/example_zen.yaml
================================================
- structure: org.xmind.ui.map.unbalanced
title: Map 1
topic:
image:
src: xap:resources/6cca606c3f2028e5ee7794459510ce1fd8855e5e8af44c5af91a212bc7f71462.svg
type: image
title: Central Topic
topics:
- makers:
- priority-1
title: topic 1
topics:
- note: 'note info
new line of note'
title: note topic
- labels:
- great
title: label topic
topics:
- note: hello world
title: formatted note
- link: http://www.link.com
title: link topic
- callout:
- Callout
title: callout topic
- makers:
- priority-2
- smiley-smile
- task-oct
- flag-orange
title: makers topic
- image:
src: xap:resources/82d831f17e8c6cad32ff1791060d24488fab8c64186b07894e38b3a35013f736.svg
type: image
title: sticker topic
- image:
src: xap:resources/40af36d11c22d4747e35dd2d82cfa2c0e6ed91a333fa77b18c8d2e675e7b9fa0.png
type: image
title: image topic
- title: attachment topic
topics:
- link: xap:resources/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.txt
title: test.txt
- makers:
- priority-2
title: topic 2
topics:
- title: '123'
- title: ''
topics:
- title: topic after empty
- title: 中文
- title: <>{}*&^^&*^%@!
topics:
- title: topic with special chars
- title: formatted topic
- structure: org.xmind.ui.map.unbalanced
title: Map 2
topic:
title: another sheet
topics:
- title: Main Topic 1
- title: Main Topic 2
================================================
FILE: doc/example_zen_with_id.json
================================================
[
{
"title": "Map 1",
"topic": {
"title": "Central Topic",
"image": {
"src": "xap:resources/6cca606c3f2028e5ee7794459510ce1fd8855e5e8af44c5af91a212bc7f71462.svg",
"type": "image"
},
"topics": [
{
"title": "topic 1",
"makers": [
"priority-1"
],
"topics": [
{
"title": "note topic",
"note": "note info\nnew line of note",
"id": "62406618-32b1-486d-82b4-57f319fa633a"
},
{
"title": "label topic",
"labels": [
"great"
],
"topics": [
{
"title": "formatted note",
"note": "hello world",
"id": "f5a46c76-3dc0-48a4-b93f-59a8ecfc3340"
}
],
"id": "68405d18-e77d-4fb4-9db5-8bb003baa758"
},
{
"title": "link topic",
"link": "http://www.link.com",
"id": "b588cc21-4a33-4108-8a3d-4a066714581a"
},
{
"title": "callout topic",
"callout": [
"Callout"
],
"id": "1a14cbd3-9ead-4867-af1e-9781d2fe8927"
},
{
"title": "makers topic",
"makers": [
"priority-2",
"smiley-smile",
"task-oct",
"flag-orange"
],
"id": "44d2dea9-9d68-4237-b5c0-1d02632381d1"
},
{
"title": "sticker topic",
"image": {
"src": "xap:resources/82d831f17e8c6cad32ff1791060d24488fab8c64186b07894e38b3a35013f736.svg",
"type": "image"
},
"id": "6657af1e-4770-4ff4-9a09-66f546e4c720"
},
{
"title": "image topic",
"image": {
"src": "xap:resources/40af36d11c22d4747e35dd2d82cfa2c0e6ed91a333fa77b18c8d2e675e7b9fa0.png",
"type": "image"
},
"id": "a5ffd21b-2fda-4dd4-8a64-6490a9aaa46d"
},
{
"title": "attachment topic",
"topics": [
{
"title": "test.txt",
"link": "xap:resources/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.txt",
"id": "2ba4c9fa-5bb4-4715-985f-d8316e1c6b55"
}
],
"id": "2cac3535-1d74-44d9-9621-04708a134ac5"
}
],
"id": "b58888b5ceebbf0e68dada0656"
},
{
"title": "topic 2",
"makers": [
"priority-2"
],
"topics": [
{
"title": "123",
"id": "68bb7a52-1e41-4a29-b9b8-204b3d17ee5f"
},
{
"title": "",
"topics": [
{
"title": "topic after empty",
"id": "357e5088-f207-41c9-b920-01f4d955dcaa"
}
],
"id": "810acef9-36a1-4580-bec0-7cce95661838"
},
{
"title": "\u4e2d\u6587",
"id": "fae3efe0-dbcc-46c9-975c-d60ca8fc6455"
},
{
"title": "<>{}*&^^&*^%@!",
"topics": [
{
"title": "topic with special chars",
"id": "f8942451-183b-4dc6-9816-bc4b9baa7686"
}
],
"id": "6b9e69af-6e90-456b-b2d7-1fe564ec97ed"
},
{
"title": "formatted topic",
"id": "f0d31da3-59aa-4c68-b909-ab44a6dc76d7"
}
],
"id": "193b56735e689ae86a01d91513"
}
],
"id": "b9aa22deba98b3b20c7ac8aca2"
},
"structure": "org.xmind.ui.map.unbalanced",
"id": "1fb9723a328b06763d296b8fee"
},
{
"title": "Map 2",
"topic": {
"title": "another sheet",
"topics": [
{
"title": "Main Topic 1",
"id": "da88f9a5ad9a201fb6618a0d65"
},
{
"title": "Main Topic 2",
"id": "33284f5d0e89964530c77a7299"
}
],
"id": "495938ff229b6b4f6ef178ea3f"
},
"structure": "org.xmind.ui.map.unbalanced",
"id": "f3e8391738b81b4beac919ce7c"
}
]
================================================
FILE: setup.py
================================================
"""
Documentation
-------------
xmindparser is a tool to help you convert xmind file to programmable data type, e.g. json, xml.
Detail usage: https://github.com/tobyqin/xmindparser
"""
from codecs import open
from os import path
from setuptools import setup, find_packages
current_dir = path.abspath(path.dirname(__file__))
long_description = __doc__
with open(path.join(current_dir, "CHANGELOG.md"), encoding="utf-8") as f:
long_description += "\n" + f.read()
classifiers = ["License :: OSI Approved :: MIT License",
"Topic :: Software Development",
"Topic :: Utilities",
"Operating System :: Microsoft :: Windows",
"Operating System :: MacOS :: MacOS X"] + [
("Programming Language :: Python :: %s" % x) for x in "3.9 3.10 3.11 3.12 3.13 3.14".split()]
def command_line():
target = "xmindparser.main:main"
entry_points = []
entry_points.append("xmindparser=%s" % target)
return entry_points
def main():
setup(
name="xmindparser",
description="Convert xmind to programmable data types, support xmind pro and xmind zen file types.",
keywords="xmind parser converter json xml markdown",
long_description=long_description,
classifiers=classifiers,
version="1.2.2",
author="Toby Qin",
author_email="toby.qin@live.com",
url="https://github.com/tobyqin/xmindparser",
packages=find_packages(exclude=['tests', 'tests.*']),
package_data={},
install_requires=[],
entry_points={"console_scripts": command_line(), },
zip_safe=False
)
if __name__ == "__main__":
main()
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/test_xmindparser.py
================================================
from json import dumps, loads
from os import chdir
from os.path import join, dirname, exists
from pathlib import Path
import xmindparser
from xmindparser import *
from xmindparser import apply_config
chdir(dirname(dirname(__file__)))
xmind_pro_file = join(dirname(__file__), "xmind_pro.xmind")
xmind_zen_file = join(dirname(__file__), "xmind_zen.xmind")
xmind_2026_file = join(dirname(__file__), "xmind_2026.xmind")
expected_json_pro = join(dirname(dirname(__file__)), "doc/example.json")
expected_json_pro_with_id = join(dirname(dirname(__file__)), "doc/example_with_id.json")
expected_json_zen = join(dirname(dirname(__file__)), "doc/example_zen.json")
expected_json_zen_with_id = join(
dirname(dirname(__file__)), "doc/example_zen_with_id.json"
)
expected_json_2026 = join(dirname(dirname(__file__)), "doc/example_2026.json")
expected_json_2026_with_id = join(
dirname(dirname(__file__)), "doc/example_2026_with_id.json"
)
set_logger_level(logging.DEBUG)
# Get logger reference
logger = xmindparser.logger
def load_json(f):
return loads(Path(f).read_text())
def test_xmind_to_dict_debug_pro():
config["showTopicId"] = True
d = xmind_to_dict(xmind_pro_file)
logger.info(dumps(d))
assert load_json(expected_json_pro_with_id) == d
def test_xmind_to_dict_default_pro():
config["showTopicId"] = False
d = xmind_to_dict(xmind_pro_file)
logger.info(dumps(d))
assert load_json(expected_json_pro) == d
def test_xmind_to_dict_debug_zen():
config["showTopicId"] = True
d = xmind_to_dict(xmind_zen_file)
logger.info(dumps(d))
assert load_json(expected_json_zen_with_id) == d
def test_xmind_to_dict_default_zen():
config["showTopicId"] = False
d = xmind_to_dict(xmind_zen_file)
logger.info(dumps(d))
assert load_json(expected_json_zen) == d
def convert_to_file(method, xmind_file, out_file):
if exists(out_file):
os.remove(out_file)
method(xmind_file)
assert os.path.exists(out_file)
os.remove(out_file)
def test_xmind_to_json():
convert_to_file(xmind_to_json, xmind_pro_file, "tests/xmind_pro.json")
convert_to_file(xmind_to_json, xmind_zen_file, "tests/xmind_zen.json")
def test_xmind_to_xml():
convert_to_file(xmind_to_xml, xmind_pro_file, "tests/xmind_pro.xml")
convert_to_file(xmind_to_json, xmind_zen_file, "tests/xmind_zen.json")
def test_xmind_to_markdown():
convert_to_file(xmind_to_markdown, xmind_pro_file, "tests/xmind_pro.md")
convert_to_file(xmind_to_markdown, xmind_zen_file, "tests/xmind_zen.md")
def test_xmind_to_yaml():
try:
import yaml
except ImportError:
import pytest
pytest.skip("pyyaml not installed")
convert_to_file(xmind_to_yaml, xmind_pro_file, "tests/xmind_pro.yaml")
convert_to_file(xmind_to_yaml, xmind_zen_file, "tests/xmind_zen.yaml")
def test_xmind_to_yaml_content():
"""Test that YAML output matches the expected structure."""
try:
import yaml
except ImportError:
import pytest
pytest.skip("pyyaml not installed")
import io
# Test xmind pro
config["showTopicId"] = False
d = xmind_to_dict(xmind_pro_file)
yaml_str = yaml.dump(d, allow_unicode=True, default_flow_style=False)
yaml_data = yaml.safe_load(yaml_str)
assert yaml_data == d
# Test xmind zen
d = xmind_to_dict(xmind_zen_file)
yaml_str = yaml.dump(d, allow_unicode=True, default_flow_style=False)
yaml_data = yaml.safe_load(yaml_str)
assert yaml_data == d
# Test xmind 2026
d = xmind_to_dict(xmind_2026_file)
yaml_str = yaml.dump(d, allow_unicode=True, default_flow_style=False)
yaml_data = yaml.safe_load(yaml_str)
assert yaml_data == d
def test_read_builtin_xmind_zen():
out = get_xmind_zen_builtin_json(xmind_zen_file)
assert out
def test_xmind_to_dict_debug_2026():
config["showTopicId"] = True
d = xmind_to_dict(xmind_2026_file)
logger.info(dumps(d))
assert load_json(expected_json_2026_with_id) == d
def test_xmind_to_dict_default_2026():
config["showTopicId"] = False
d = xmind_to_dict(xmind_2026_file)
logger.info(dumps(d))
assert load_json(expected_json_2026) == d
def test_config_hide_empty_value():
"""Test hideEmptyValue config option."""
# Test with hideEmptyValue = True (default)
config["showTopicId"] = False
config["hideEmptyValue"] = True
d = xmind_to_dict(xmind_pro_file)
# Check that empty values are hidden
def check_no_empty_values(obj):
if isinstance(obj, dict):
for key, value in obj.items():
# title can be empty string
if key != "title":
assert value, f"Found empty value for key: {key}"
if isinstance(value, (dict, list)):
check_no_empty_values(value)
elif isinstance(obj, list):
for item in obj:
check_no_empty_values(item)
check_no_empty_values(d)
# Test with hideEmptyValue = False
config["hideEmptyValue"] = False
d2 = xmind_to_dict(xmind_pro_file)
# The result with hideEmptyValue=False should have more or equal keys
def count_keys(obj):
count = 0
if isinstance(obj, dict):
count += len(obj)
for value in obj.values():
count += count_keys(value)
elif isinstance(obj, list):
for item in obj:
count += count_keys(item)
return count
# With hideEmptyValue=False, we should have at least as many keys
assert count_keys(d2) >= count_keys(d)
# Reset to default
config["hideEmptyValue"] = True
def test_config_logging_format():
"""Test that logging config options work correctly."""
import io
# Create a string buffer to capture log output
log_capture = io.StringIO()
# Configure custom logging format
config["logFormat"] = "CUSTOM: %(levelname)s - %(message)s"
config["logLevel"] = logging.INFO
apply_config()
# The handler should now be writing to stdout, but we need to check the handler's stream
# Get the current logger and its handler
current_logger = xmindparser.logger
# Temporarily replace the handler's stream
original_stream = xmindparser._console_handler.stream
xmindparser._console_handler.stream = log_capture
try:
# Generate some log output
current_logger.info("Test message")
# Get the captured output
log_output = log_capture.getvalue()
# Verify custom format is applied
assert "CUSTOM:" in log_output, f"Custom format not found in: {log_output}"
assert "INFO" in log_output, f"Log level not found in: {log_output}"
assert "Test message" in log_output, f"Message not found in: {log_output}"
finally:
# Restore original stream
xmindparser._console_handler.stream = original_stream
# Reset to default config
config["logFormat"] = "%(asctime)s %(levelname)-8s: %(message)s"
config["logLevel"] = None
apply_config()
def test_config_logging_level():
"""Test that logLevel config option works correctly."""
import io
original_stream = xmindparser._console_handler.stream
try:
# Test with WARNING level (should not show INFO)
config["logLevel"] = logging.WARNING
apply_config()
log_capture = io.StringIO()
xmindparser._console_handler.stream = log_capture
xmindparser.logger.info("This should not appear")
xmindparser.logger.warning("This should appear")
log_output = log_capture.getvalue()
assert "This should not appear" not in log_output
assert "This should appear" in log_output
# Test with DEBUG level (should show everything)
config["logLevel"] = logging.DEBUG
apply_config()
log_capture = io.StringIO()
xmindparser._console_handler.stream = log_capture
xmindparser.logger.debug("Debug message")
xmindparser.logger.info("Info message")
log_output = log_capture.getvalue()
assert "Debug message" in log_output
assert "Info message" in log_output
finally:
xmindparser._console_handler.stream = original_stream
config["logLevel"] = None
apply_config()
def test_config_logging_name():
"""Test that logName config option works correctly."""
# Test with custom log name
config["logName"] = "custom_xmind_logger"
apply_config()
# Verify logger name is updated
current_logger = xmindparser.logger
assert current_logger.name == "custom_xmind_logger"
# Reset to default
config["logName"] = "xmindparser"
apply_config()
def test_config_show_structure():
"""Test showStructure config option."""
# Test with showStructure = True (default)
config["showStructure"] = True
d = xmind_to_dict(xmind_zen_file)
# Check that structure is included
for sheet in d:
assert "structure" in sheet
# Test with showStructure = False
config["showStructure"] = False
d2 = xmind_to_dict(xmind_zen_file)
# Check that structure is not included
for sheet in d2:
assert "structure" not in sheet
# Reset to default
config["showStructure"] = True
def test_config_show_relationship():
"""Test showRelationship config option."""
# Test with showRelationship = True
config["showRelationship"] = True
d = xmind_to_dict(xmind_zen_file)
# Check if relationships key exists (may be empty if no relationships)
for sheet in d:
assert "relationships" in sheet
# Test with showRelationship = False (default)
config["showRelationship"] = False
d2 = xmind_to_dict(xmind_zen_file)
# Check that relationships is not included
for sheet in d2:
assert "relationships" not in sheet
================================================
FILE: tests/xmind_2026.yaml
================================================
- structure: org.xmind.ui.map.clockwise
title: test
topic:
title: test
topics:
- title: a
topics:
- title: e
topics:
- title: f
topics:
- title: g
- title: d
- title: e
topics:
- title: ''
- title: ''
topics:
- title: h
topics:
- title: ''
- title: b
topics:
- title: types
topics:
- title: comment
- note: hello
title: note
topics:
- note: hello
title: note2
- note: hello
title: note3
- labels:
- this
- that
- then
title: label
- title: attachment
topics:
- title: test.txt
- makers:
- tag-orange
- priority-2
- task-oct
- flag-orange
- star-orange
- people-orange
- c_symbol_like
title: marker
- title: pic
topics:
- image:
src: xap:resources/9afc60beae59d849e741ae11d19d638a7b89090ee6c7b772159db271142bcee6.jpeg
title: 0.jpeg
- image:
src: xap:resources/dd46d29fc64e6eb4e2aac3f78ab162b7177ae0608be431ebf261cad179b2fdad.svg
title: sticker1
- image:
src: xap:resources/129b1b21c5946e3f1692f39dcd7d18e0241c66c0b92959cbdbf414a2d40defbd.svg
title: sticker2
- title: link
topics:
- link: http://github.com
title: to url
- title: to file
- title: to topic
- title: 测试
topics:
- title: ' note'
- title: comment
- title: >{}[]*+-
- title: c
topics:
- title: a
- title: a
- title: a
================================================
FILE: xmindparser/__init__.py
================================================
"""
Parse xmind to programmable data types.
"""
import json
import logging
import os
import sys
from zipfile import ZipFile
try:
import yaml
except ImportError:
yaml = None
config = {
"logName": __name__,
"logLevel": None,
"logFormat": "%(asctime)s %(levelname)-8s: %(message)s",
"showTopicId": False,
"hideEmptyValue": True,
"showRelationship": False,
"showStructure": True,
}
cache = {}
# Initialize logger with default config
logger = None
_console_handler = None
def _init_logger():
"""Initialize or reinitialize the logger based on current config."""
global logger, _console_handler
log_name = config["logName"] or __name__
log_level = config["logLevel"] or logging.WARNING
log_fmt = config["logFormat"] or "%(asctime)s %(levelname)-8s: %(message)s"
# Remove existing handler if present
if logger and _console_handler:
logger.removeHandler(_console_handler)
# Create or get logger
logger = logging.getLogger(log_name)
logger.setLevel(log_level)
# Create new console handler with current format
_console_handler = logging.StreamHandler(sys.stdout)
_console_handler.setFormatter(logging.Formatter(log_fmt))
logger.addHandler(_console_handler)
return logger
def set_logger_level(new_level):
"""Set logger level."""
if logger:
logger.setLevel(new_level)
def apply_config():
"""Apply current config settings, reinitializing logger if needed."""
_init_logger()
# Initialize logger on module import
_init_logger()
def is_xmind_zen(file_path):
"""Determine if this is a xmind zen file type."""
with ZipFile(file_path) as xmind:
return "content.json" in xmind.namelist()
def get_xmind_zen_builtin_json(file_path):
"""Read internal content.json from xmind zen file."""
name = "content.json"
with ZipFile(file_path) as xmind:
if name in xmind.namelist():
content = xmind.open(name).read().decode("utf-8")
return json.loads(content)
raise AssertionError("Not a xmind zen file type!")
def _get_out_file_name(xmind_file, suffix):
assert isinstance(xmind_file, str) and xmind_file.endswith(".xmind"), (
"Invalid xmind file!"
)
name = os.path.abspath(xmind_file[0:-5] + suffix)
return name
def xmind_to_dict(file_path):
"""Open and convert xmind to dict type."""
if is_xmind_zen(file_path):
from .zenreader import open_xmind, get_sheets, sheet_to_dict
else:
from .xreader import open_xmind, get_sheets, sheet_to_dict
open_xmind(file_path)
data = []
for s in get_sheets():
data.append(sheet_to_dict(s))
return data
def xmind_to_file(file_path, file_type):
if file_type == "json":
return xmind_to_json(file_path)
elif file_type == "xml":
return xmind_to_xml(file_path)
elif file_type == "markdown" or file_type == "md":
return xmind_to_markdown(file_path)
elif file_type == "yaml" or file_type == "yml":
return xmind_to_yaml(file_path)
else:
raise ValueError("Not supported file type: {}".format(file_type))
def xmind_to_json(file_path):
target = _get_out_file_name(file_path, "json")
with open(target, "w", encoding="utf8") as f:
f.write(json.dumps(xmind_to_dict(file_path), indent=2, ensure_ascii=False))
return target
def xmind_to_xml(file_path):
try:
from dicttoxml import dicttoxml
from xml.dom.minidom import parseString
target = _get_out_file_name(file_path, "xml")
xml = dicttoxml(xmind_to_dict(file_path), custom_root="root")
xml = parseString(xml.decode("utf8")).toprettyxml(encoding="utf8")
with open(target, "w", encoding="utf8") as f:
f.write(xml.decode("utf8"))
return target
except ImportError:
raise ImportError(
'Parse xmind to xml require "dicttoxml", try install via pip:\n'
+ "> pip install dicttoxml"
)
def xmind_to_markdown(file_path):
"""Convert xmind file to markdown format."""
data = xmind_to_dict(file_path)
target = _get_out_file_name(file_path, "md")
markdown_lines = []
for sheet in data:
markdown_lines.extend(_sheet_to_markdown(sheet))
markdown_lines.append("") # Add empty line between sheets
with open(target, "w", encoding="utf8") as f:
f.write("\n".join(markdown_lines))
return target
def xmind_to_yaml(file_path):
"""Convert xmind file to yaml format."""
if yaml is None:
raise ImportError(
'Parse xmind to yaml require "pyyaml", try install via pip:\n'
+ "> pip install pyyaml"
)
data = xmind_to_dict(file_path)
target = _get_out_file_name(file_path, "yaml")
with open(target, "w", encoding="utf8") as f:
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
return target
def _sheet_to_markdown(sheet):
"""Convert a sheet to markdown lines."""
lines = []
# Sheet title as h1
title = sheet.get("title", "Untitled")
lines.append("# {}".format(title))
lines.append("")
# Process root topic
topic = sheet.get("topic", {})
if topic:
lines.extend(_topic_to_markdown(topic, level=2))
return lines
def _topic_to_markdown(topic, level=2):
"""Convert a topic and its children to markdown lines."""
lines = []
# Topic title as heading
title = topic.get("title", "")
if title:
heading = "#" * level
lines.append("{} {}".format(heading, title))
# Add note if exists
note = topic.get("note", "")
if note:
lines.append("")
lines.append("> {}".format(note.replace("\n", "\n> ")))
# Add labels if exists
labels = topic.get("labels", [])
if labels:
lines.append("")
lines.append("**Labels:** {}".format(", ".join(labels)))
# Add link if exists
link = topic.get("link", "")
if link and not link.startswith("["):
lines.append("")
lines.append("[Link]({})".format(link))
# Add comments if exists
comments = topic.get("comment", [])
if comments:
lines.append("")
lines.append("**Comments:**")
for comment in comments:
author = comment.get("author", "Unknown")
content = comment.get("content", "")
lines.append("- **{}**: {}".format(author, content.replace("\n", " ")))
# Process children topics
children = topic.get("topics", [])
if children:
lines.append("")
for child in children:
lines.extend(_topic_to_markdown(child, level + 1))
return lines
================================================
FILE: xmindparser/main.py
================================================
"""
A tool to parse xmind file into programmable data types.
Check https://github.com/tobyqin/xmindparser to see supported types.
Usage:
xmindparser [path_to_xmind_file] -[type]
Example:
xmindparser C:\\tests\\my.xmind -json
xmindparser C:\\tests\\my.xmind -xml
xmindparser C:\\tests\\my.xmind -markdown
"""
import sys
from xmindparser import *
def main():
if len(sys.argv) == 3 and sys.argv[1].endswith('.xmind'):
xmind, out_types = sys.argv[1], sys.argv[2][1:]
out = xmind_to_file(xmind, out_types)
print('Generated: {}'.format(out))
else:
print(__doc__)
if __name__ == '__main__':
main()
================================================
FILE: xmindparser/xreader.py
================================================
import re
from xml.etree import ElementTree as ET
from xml.etree.ElementTree import Element
from zipfile import ZipFile
from . import cache, config, logger
content_xml = "content.xml"
comments_xml = "comments.xml"
def open_xmind(file_path):
"""open xmind as zip file and cache the content."""
cache.clear()
with ZipFile(file_path) as xmind:
for f in xmind.namelist():
for key in [content_xml, comments_xml]:
if f == key:
cache[key] = xmind.open(f).read().decode("utf-8")
def get_sheets():
"""get all sheet as generator and yield."""
tree = xmind_content_to_etree(cache[content_xml])
assert isinstance(tree, Element)
for sheet in tree.findall("sheet"):
yield sheet
def sheet_to_dict(sheet):
"""convert a sheet to dict type."""
topic = sheet.find("topic")
result = {
"title": title_of(sheet),
"topic": node_to_dict(topic),
"structure": get_sheet_structure(sheet),
}
if config["showTopicId"]:
result["id"] = sheet.attrib["id"]
if config["hideEmptyValue"]:
result = {k: v for k, v in result.items() if v}
return result
def get_sheet_structure(sheet):
root_topic = sheet.find("topic")
return root_topic.attrib.get("structure-class", None)
def node_to_dict(node):
"""parse Element to dict data type."""
child = children_topics_of(node)
d = {
"title": title_of(node),
"comment": comments_of(node),
"note": note_of(node),
"makers": maker_of(node),
"labels": labels_of(node),
"link": link_of(node),
}
if d["link"]:
if d["link"].startswith("xmind"):
d["link"] = "[To another xmind topic!]"
if d["link"].startswith("xap:attachments"):
del d["link"]
d["title"] = "[Attachment]{0}".format(d["title"])
if child:
d["topics"] = []
for c in child:
d["topics"].append(node_to_dict(c))
if config["showTopicId"]:
d["id"] = id_of(node)
if config["hideEmptyValue"]:
d = {k: v for k, v in d.items() if v or k == "title"}
return d
def xmind_content_to_etree(content):
# Remove the default namespace definition (xmlns="http://some/namespace")
xml_content = re.sub(r'\sxmlns="[^"]+"', "", content, count=1)
# Replace xml tag with namespace
xml_content = xml_content.replace("