Full Code of tobyqin/xmindparser for AI

master 86cb671c49b6 cached
30 files
87.2 KB
24.0k tokens
67 symbols
1 requests
Download .txt
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

[![PyPI](https://img.shields.io/pypi/v/xmindparser.svg)](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

![Xmind Example](doc/xmind.png)

- [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

[![PyPI](https://img.shields.io/pypi/v/xmindparser.svg)](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 示例](doc/xmind.png)

- [下载 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
================================================
<?xml version="1.0" encoding="utf8"?>
<root>
	<item type="dict">
		<structure type="str">org.xmind.ui.map.unbalanced</structure>
		<topic type="dict">
			<topics type="list">
				<item type="dict">
					<link type="str">http://test.com</link>
					<topics type="list">
						<item type="dict">
							<topics type="list">
								<item type="dict">
									<topics type="list">
										<item type="dict">
											<title type="str">g</title>
										</item>
									</topics>
									<title type="str">f</title>
								</item>
							</topics>
							<title type="str">e</title>
						</item>
						<item type="dict">
							<topics type="list">
								<item type="dict">
									<title type="str">[Blank]</title>
								</item>
							</topics>
							<title type="str">d</title>
						</item>
						<item type="dict">
							<topics type="list">
								<item type="dict">
									<note type="str">this is a note</note>
									<title type="str">[Blank]</title>
								</item>
							</topics>
							<title type="str">e</title>
							<makers type="list">
								<item type="str">symbol-plus</item>
							</makers>
						</item>
						<item type="dict">
							<topics type="list">
								<item type="dict">
									<topics type="list">
										<item type="dict">
											<comment type="list">
												<item type="dict">
													<content type="str">this a comments</content>
													<author type="str">toby.qin</author>
												</item>
											</comment>
											<title type="str">[Blank]</title>
										</item>
									</topics>
									<title type="str">h</title>
								</item>
							</topics>
							<title type="str">[Blank]</title>
						</item>
					</topics>
					<title type="str">a</title>
				</item>
				<item type="dict">
					<topics type="list">
						<item type="dict">
							<topics type="list">
								<item type="dict">
									<comment type="list">
										<item type="dict">
											<content type="str">comment1</content>
											<author type="str">toby.qin</author>
										</item>
										<item type="dict">
											<content type="str">comment2</content>
											<author type="str">toby.qin</author>
										</item>
										<item type="dict">
											<content type="str">comment3
												new line
											</content>
											<author type="str">toby.qin</author>
										</item>
									</comment>
									<title type="str">comment</title>
								</item>
								<item type="dict">
									<note type="str">1 line note</note>
									<topics type="list">
										<item type="dict">
											<note type="str">line1
												line2
											</note>
											<title type="str">note2</title>
										</item>
										<item type="dict">
											<note type="str">note with style
												line with strike
											</note>
											<title type="str">note3</title>
										</item>
									</topics>
									<title type="str">note</title>
								</item>
								<item type="dict">
									<labels type="list">
										<item type="str">this is a label</item>
									</labels>
									<title type="str">label</title>
								</item>
								<item type="dict">
									<topics type="list">
										<item type="dict">
											<title type="str">[Attachment]test.txt</title>
										</item>
									</topics>
									<title type="str">attachment</title>
								</item>
								<item type="dict">
									<topics type="list">
										<item type="dict">
											<title type="str">[Image]</title>
										</item>
									</topics>
									<title type="str">pic</title>
								</item>
								<item type="dict">
									<topics type="list">
										<item type="dict">
											<link type="str">http://test.com</link>
											<title type="str">to url</title>
										</item>
										<item type="dict">
											<link type="str">file:////abc/ef</link>
											<title type="str">to file</title>
										</item>
										<item type="dict">
											<link type="str">[To another xmind topic!]</link>
											<title type="str">to topic</title>
										</item>
									</topics>
									<title type="str">links</title>
								</item>
							</topics>
							<title type="str">types</title>
						</item>
						<item type="dict">
							<topics type="list">
								<item type="dict">
									<note type="str">测试中文</note>
									<title type="str">note</title>
								</item>
								<item type="dict">
									<comment type="list">
										<item type="dict">
											<content type="str">中文测试</content>
											<author type="str">toby.qin</author>
										</item>
									</comment>
									<title type="str">comments</title>
								</item>
							</topics>
							<title type="str">测试</title>
						</item>
						<item type="dict">
							<title type="str">&lt;/&gt;{}[]*+-</title>
						</item>
					</topics>
					<title type="str">b</title>
					<makers type="list">
						<item type="str">task-done</item>
						<item type="str">flag-red</item>
						<item type="str">people-red</item>
						<item type="str">arrow-up</item>
					</makers>
				</item>
				<item type="dict">
					<topics type="list">
						<item type="dict">
							<title type="str">a</title>
						</item>
						<item type="dict">
							<title type="str">a</title>
						</item>
						<item type="dict">
							<link type="str">[To another xmind topic!]</link>
							<title type="str">a</title>
						</item>
					</topics>
					<title type="str">c</title>
				</item>
			</topics>
			<title type="str">test</title>
			<makers type="list">
				<item type="str">star-orange</item>
			</makers>
		</topic>
		<title type="str">Sheet 1</title>
	</item>
	<item type="dict">
		<structure type="str">org.xmind.ui.map.unbalanced</structure>
		<topic type="dict">
			<topics type="list">
				<item type="dict">
					<topics type="list">
						<item type="dict">
							<title type="str">1</title>
						</item>
						<item type="dict">
							<title type="str">2</title>
						</item>
						<item type="dict">
							<title type="str">3</title>
						</item>
					</topics>
					<title type="str">a</title>
				</item>
				<item type="dict">
					<title type="str">b</title>
				</item>
			</topics>
			<title type="str">test2</title>
		</topic>
		<title type="str">Sheet 2</title>
	</item>
</root>


================================================
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("<xhtml:img", "<img")

    # Replace link attrib with namespace
    xml_content = xml_content.replace("xlink:href", "href")
    return ET.fromstring(xml_content.encode("utf-8"))


def xmind_xml_to_etree(xml_path):
    with open(xml_path) as f:
        content = f.read()
        return xmind_content_to_etree(content)


def comments_of(node):
    if cache.get(comments_xml, None):
        node_id = node.attrib.get("id", None)

        if node_id:
            xml_root = xmind_content_to_etree(cache[comments_xml])
            result = []

            for c in xml_root.findall("comment"):
                if c.attrib["object-id"] == node_id:
                    i = {
                        "author": c.attrib["author"],
                        "content": c.find("content").text,
                    }

                    if config["showTopicId"]:
                        i["id"] = c.attrib["object-id"]

                    result.append(i)

            return result if result else None


def id_of(node):
    return node.attrib.get("id", None)


def image_of(node):
    img = node.find("img")

    if img is not None:
        return "[Image]"


def link_of(node):
    return node.attrib.get("href", None)


def title_of(node):
    if image_of(node):
        return image_of(node)

    title = node.find("title")

    if title is not None:
        return title.text


def labels_of(node):
    label_node = node.find("labels")

    if label_node is not None:
        labels = []
        for _ in label_node.findall("label"):
            labels.append(_.text)

        return labels if labels else None


def note_of(node):
    note_node = node.find("notes")

    if note_node is not None:
        note = note_node.find("plain").text
        return note.strip()


def debug_node(node, comments):
    s = ET.tostring(node)
    logger.debug("{}: {}".format(comments, s))
    return s


def maker_of(topic_node):
    maker_node = topic_node.find("marker-refs")
    if maker_node is not None:
        makers = []
        for maker in maker_node:
            makers.append(maker.attrib["marker-id"])

        return makers


def children_topics_of(topic_node):
    children = topic_node.find("children")

    if children is not None:
        return children.find('./topics[@type="attached"]')


================================================
FILE: xmindparser/zenreader.py
================================================
import json
from zipfile import ZipFile

from . import cache, config

content_json = "content.json"


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_json]:
                if f == key:
                    cache[key] = xmind.open(f).read().decode("utf-8")


def get_sheets():
    """get all sheet as generator and yield."""
    for sheet in json.loads(cache[content_json]):
        yield sheet


def sheet_to_dict(sheet):
    """convert a sheet to dict type."""
    topic = sheet["rootTopic"]
    result = {"title": sheet["title"], "topic": node_to_dict(topic)}

    if config["showTopicId"]:
        result["id"] = sheet["id"]

    if config["showStructure"]:
        result["structure"] = get_sheet_structure(sheet)

    if config["hideEmptyValue"]:
        result = {k: v for k, v in result.items() if v}

    if config["showRelationship"]:
        result["relationships"] = get_sheet_relationship(sheet)

    return result


def get_sheet_structure(sheet):
    root_topic = sheet["rootTopic"]
    return root_topic.get("structureClass", None)


def get_sheet_relationship(sheet):
    if sheet.get("relationships", None) is None:
        return []
    relationship = sheet["relationships"]
    for r in relationship:
        if r.get("controlPoints", None) is not None:
            del [r["controlPoints"]]
        if r.get("titleUnedited", None) is not None:
            del [r["titleUnedited"]]
        if not config["showTopicId"]:
            del [r["id"]]
    return relationship


def node_to_dict(node):
    """parse Element to dict data type."""
    child = children_topics_of(node)

    d = {
        "title": node.get("title", ""),
        "note": note_of(node),
        "makers": maker_of(node),
        "labels": labels_of(node),
        "link": link_of(node),
        "image": image_of(node),
        "callout": callout_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"] = node["id"]

    if config["hideEmptyValue"]:
        d = {k: v for k, v in d.items() if v or k == "title"}

    return d


def children_topics_of(topic_node):
    children = topic_node.get("children", None)

    if children:
        return children.get("attached", None)


def link_of(node):
    return node.get("href", None)


def image_of(node):
    return node.get("image", None)


def labels_of(node):
    return node.get("labels", None)


def note_of(node):
    note_node = node.get("notes", None)

    if note_node:
        note = note_node.get("plain", None)
        if note:
            return note.get("content", "").strip()


def maker_of(topic_node):
    maker_node = topic_node.get("markers", None)
    if maker_node is not None:
        makers = []
        for maker in maker_node:
            makers.append(maker.get("markerId", None))

        return makers


def callout_of(topic_node):
    callout = topic_node.get("children", None)
    if callout:
        callout = callout.get("callout", None)
        if callout:
            return [x["title"] for x in callout]
Download .txt
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
Download .txt
SYMBOL INDEX (67 symbols across 6 files)

FILE: setup.py
  function command_line (line 28) | def command_line():
  function main (line 35) | def main():

FILE: tests/test_xmindparser.py
  function load_json (line 30) | def load_json(f):
  function test_xmind_to_dict_debug_pro (line 34) | def test_xmind_to_dict_debug_pro():
  function test_xmind_to_dict_default_pro (line 41) | def test_xmind_to_dict_default_pro():
  function test_xmind_to_dict_debug_zen (line 48) | def test_xmind_to_dict_debug_zen():
  function test_xmind_to_dict_default_zen (line 55) | def test_xmind_to_dict_default_zen():
  function convert_to_file (line 62) | def convert_to_file(method, xmind_file, out_file):
  function test_xmind_to_json (line 71) | def test_xmind_to_json():
  function test_xmind_to_xml (line 76) | def test_xmind_to_xml():
  function test_xmind_to_markdown (line 81) | def test_xmind_to_markdown():
  function test_xmind_to_yaml (line 86) | def test_xmind_to_yaml():
  function test_xmind_to_yaml_content (line 98) | def test_xmind_to_yaml_content():
  function test_read_builtin_xmind_zen (line 129) | def test_read_builtin_xmind_zen():
  function test_xmind_to_dict_debug_2026 (line 134) | def test_xmind_to_dict_debug_2026():
  function test_xmind_to_dict_default_2026 (line 141) | def test_xmind_to_dict_default_2026():
  function test_config_hide_empty_value (line 148) | def test_config_hide_empty_value():
  function test_config_logging_format (line 193) | def test_config_logging_format():
  function test_config_logging_level (line 235) | def test_config_logging_level():
  function test_config_logging_name (line 276) | def test_config_logging_name():
  function test_config_show_structure (line 291) | def test_config_show_structure():
  function test_config_show_relationship (line 311) | def test_config_show_relationship():

FILE: xmindparser/__init__.py
  function _init_logger (line 33) | def _init_logger():
  function set_logger_level (line 57) | def set_logger_level(new_level):
  function apply_config (line 63) | def apply_config():
  function is_xmind_zen (line 72) | def is_xmind_zen(file_path):
  function get_xmind_zen_builtin_json (line 78) | def get_xmind_zen_builtin_json(file_path):
  function _get_out_file_name (line 89) | def _get_out_file_name(xmind_file, suffix):
  function xmind_to_dict (line 98) | def xmind_to_dict(file_path):
  function xmind_to_file (line 114) | def xmind_to_file(file_path, file_type):
  function xmind_to_json (line 131) | def xmind_to_json(file_path):
  function xmind_to_xml (line 140) | def xmind_to_xml(file_path):
  function xmind_to_markdown (line 160) | def xmind_to_markdown(file_path):
  function xmind_to_yaml (line 177) | def xmind_to_yaml(file_path):
  function _sheet_to_markdown (line 194) | def _sheet_to_markdown(sheet):
  function _topic_to_markdown (line 211) | def _topic_to_markdown(topic, level=2):

FILE: xmindparser/main.py
  function main (line 20) | def main():

FILE: xmindparser/xreader.py
  function open_xmind (line 12) | def open_xmind(file_path):
  function get_sheets (line 22) | def get_sheets():
  function sheet_to_dict (line 31) | def sheet_to_dict(sheet):
  function get_sheet_structure (line 49) | def get_sheet_structure(sheet):
  function node_to_dict (line 54) | def node_to_dict(node):
  function xmind_content_to_etree (line 89) | def xmind_content_to_etree(content):
  function xmind_xml_to_etree (line 101) | def xmind_xml_to_etree(xml_path):
  function comments_of (line 107) | def comments_of(node):
  function id_of (line 130) | def id_of(node):
  function image_of (line 134) | def image_of(node):
  function link_of (line 141) | def link_of(node):
  function title_of (line 145) | def title_of(node):
  function labels_of (line 155) | def labels_of(node):
  function note_of (line 166) | def note_of(node):
  function debug_node (line 174) | def debug_node(node, comments):
  function maker_of (line 180) | def maker_of(topic_node):
  function children_topics_of (line 190) | def children_topics_of(topic_node):

FILE: xmindparser/zenreader.py
  function open_xmind (line 9) | def open_xmind(file_path):
  function get_sheets (line 19) | def get_sheets():
  function sheet_to_dict (line 25) | def sheet_to_dict(sheet):
  function get_sheet_structure (line 45) | def get_sheet_structure(sheet):
  function get_sheet_relationship (line 50) | def get_sheet_relationship(sheet):
  function node_to_dict (line 64) | def node_to_dict(node):
  function children_topics_of (line 100) | def children_topics_of(topic_node):
  function link_of (line 107) | def link_of(node):
  function image_of (line 111) | def image_of(node):
  function labels_of (line 115) | def labels_of(node):
  function note_of (line 119) | def note_of(node):
  function maker_of (line 128) | def maker_of(topic_node):
  function callout_of (line 138) | def callout_of(topic_node):
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (100K chars).
[
  {
    "path": ".github/workflows/test-publish.yml",
    "chars": 2429,
    "preview": "name: Test and Publish\n\non:\n  push:\n    branches: [main, master]\n    tags:\n      - \"v*\"\n  pull_request:\n    branches: [m"
  },
  {
    "path": ".gitignore",
    "chars": 1173,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n.cache\nout/\n.idea/\n.pytest_cache/\nout/\n.vscode"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1966,
    "preview": "## Change Log\n\n1.2.2\n\n- Add new config options: `showStructure` (default True) to include/exclude sheet structure info, "
  },
  {
    "path": "DEVELOPER.md",
    "chars": 603,
    "preview": "# For Developers\n\n## Publish New Version\n\nThis project uses GitHub Actions to automatically publish to PyPI when a new v"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2018 Toby Qin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "MANIFEST.in",
    "chars": 71,
    "preview": "include LICENSE\ninclude README.md\ninclude CHANGELOG.md\ninclude setup.py"
  },
  {
    "path": "README.md",
    "chars": 4640,
    "preview": "# xmindparser\n\n[![PyPI](https://img.shields.io/pypi/v/xmindparser.svg)](https://pypi.org/project/xmindparser/)\n\nParse xm"
  },
  {
    "path": "README_CN.md",
    "chars": 3461,
    "preview": "# xmindparser\n\n[![PyPI](https://img.shields.io/pypi/v/xmindparser.svg)](https://pypi.org/project/xmindparser/)\n\n将 xmind "
  },
  {
    "path": "doc/example.json",
    "chars": 5315,
    "preview": "[\n  {\n    \"title\": \"Sheet 1\",\n    \"topic\": {\n      \"title\": \"test\",\n      \"makers\": [\n        \"star-orange\"\n      ],\n   "
  },
  {
    "path": "doc/example.md",
    "chars": 813,
    "preview": "# Sheet 1\n\n## test\n\n### a\n\n[Link](http://test.com)\n\n#### e\n\n##### f\n\n###### g\n#### d\n\n#### e\n\n\n> this is a note\n\n##### h"
  },
  {
    "path": "doc/example.xml",
    "chars": 6367,
    "preview": "<?xml version=\"1.0\" encoding=\"utf8\"?>\n<root>\n\t<item type=\"dict\">\n\t\t<structure type=\"str\">org.xmind.ui.map.unbalanced</st"
  },
  {
    "path": "doc/example.yaml",
    "chars": 2219,
    "preview": "- structure: org.xmind.ui.map.unbalanced\n  title: Sheet 1\n  topic:\n    makers:\n    - star-orange\n    title: test\n    top"
  },
  {
    "path": "doc/example_2026.json",
    "chars": 4376,
    "preview": "[\n  {\n    \"title\": \"test\",\n    \"topic\": {\n      \"title\": \"test\",\n      \"topics\": [\n        {\n          \"title\": \"a\",\n   "
  },
  {
    "path": "doc/example_2026.yaml",
    "chars": 1830,
    "preview": "- structure: org.xmind.ui.map.clockwise\n  title: test\n  topic:\n    title: test\n    topics:\n    - title: a\n      topics:\n"
  },
  {
    "path": "doc/example_2026_with_id.json",
    "chars": 6740,
    "preview": "[\n  {\n    \"title\": \"test\",\n    \"topic\": {\n      \"title\": \"test\",\n      \"topics\": [\n        {\n          \"title\": \"a\",\n   "
  },
  {
    "path": "doc/example_with_id.json",
    "chars": 7823,
    "preview": "[\n  {\n    \"title\": \"Sheet 1\",\n    \"topic\": {\n      \"title\": \"test\",\n      \"makers\": [\n        \"star-orange\"\n      ],\n   "
  },
  {
    "path": "doc/example_zen.json",
    "chars": 3136,
    "preview": "[\n  {\n    \"title\": \"Map 1\",\n    \"topic\": {\n      \"title\": \"Central Topic\",\n      \"image\": {\n        \"src\": \"xap:resource"
  },
  {
    "path": "doc/example_zen.yaml",
    "chars": 1709,
    "preview": "- structure: org.xmind.ui.map.unbalanced\n  title: Map 1\n  topic:\n    image:\n      src: xap:resources/6cca606c3f2028e5ee7"
  },
  {
    "path": "doc/example_zen_with_id.json",
    "chars": 4520,
    "preview": "[\n  {\n    \"title\": \"Map 1\",\n    \"topic\": {\n      \"title\": \"Central Topic\",\n      \"image\": {\n        \"src\": \"xap:resource"
  },
  {
    "path": "setup.py",
    "chars": 1691,
    "preview": "\"\"\"\nDocumentation\n-------------\nxmindparser is a tool to help you convert xmind file to programmable data type, e.g. jso"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_xmindparser.py",
    "chars": 9898,
    "preview": "from json import dumps, loads\nfrom os import chdir\nfrom os.path import join, dirname, exists\nfrom pathlib import Path\n\ni"
  },
  {
    "path": "tests/xmind_2026.yaml",
    "chars": 1830,
    "preview": "- structure: org.xmind.ui.map.clockwise\n  title: test\n  topic:\n    title: test\n    topics:\n    - title: a\n      topics:\n"
  },
  {
    "path": "xmindparser/__init__.py",
    "chars": 6710,
    "preview": "\"\"\"\nParse xmind to programmable data types.\n\"\"\"\n\nimport json\nimport logging\nimport os\nimport sys\nfrom zipfile import Zip"
  },
  {
    "path": "xmindparser/main.py",
    "chars": 648,
    "preview": "\"\"\"\nA tool to parse xmind file into programmable data types.\nCheck https://github.com/tobyqin/xmindparser to see support"
  },
  {
    "path": "xmindparser/xreader.py",
    "chars": 4729,
    "preview": "import re\nfrom xml.etree import ElementTree as ET\nfrom xml.etree.ElementTree import Element\nfrom zipfile import ZipFile\n"
  },
  {
    "path": "xmindparser/zenreader.py",
    "chars": 3515,
    "preview": "import json\nfrom zipfile import ZipFile\n\nfrom . import cache, config\n\ncontent_json = \"content.json\"\n\n\ndef open_xmind(fil"
  }
]

// ... and 3 more files (download for full content)

About this extraction

This page contains the full source code of the tobyqin/xmindparser GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (87.2 KB), approximately 24.0k tokens, and a symbol index with 67 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!