Younian385 downloadsEditable mind maps backed by plain Markdown lists — priority, progress and flag marks, drag to reorganize.
English · 简体中文
An Obsidian plugin that turns an indented Markdown list into an editable mind map, with priority, progress and flag marks, and per-node notes. The source of truth stays a plain .md file — the same structured Markdown you and any AI read and write.

The example file open in mindmap view.
Mindmap plugins in the Obsidian community mostly fall into two groups: read-only previews built on markmap (they render a note as a mind map but you cannot edit on the map), and plugins built on Obsidian Canvas, whose data is .canvas JSON.
This plugin uses the same hierarchy model as markmap — headings plus nested lists — so a note you have been previewing with one of those plugins opens here and is editable on the map. It deliberately does not implement markmap's own dialect: the markmap: frontmatter options and the <!-- markmap: fold --> magic comments. Collapse state here lives in the mindmap-collapsed frontmatter key, and having two sources for it would force write-back to arbitrate between them — which conflicts with the promise that write-back does only the five normalizations listed below.
This one aims for:
Editing is not unique to this plugin — several others manage it too. The mark system is, at the time of writing, uncommon in this category.
Copy examples/conference-talk.md into your vault and open it in mindmap view. It covers: ## / ### sections forming the outer hierarchy, all seven priorities, all seven flag colours, progress across every stage, nested lists, wiki links, inline formatting, a parenthesized group that is not a mark, a collapsed heading recorded in mindmap-collapsed, plus frontmatter keys, and a prose paragraph that the plugin carries along without showing. (Notes came later; neither the example file nor the screenshot above uses one yet.)
---
mindmap: true
tags:
- talk
- 2026
speaker: me
mindmap-collapsed:
- Logistics
---
# Conference Talk: Taming Legacy Code
## Outline
- (p1 100%) Opening story — the 3 a.m. pager incident
- (p1 67%) Core argument
- (p2) Legacy code is simply code without tests
- (p2 50%) A safety net comes before any refactor
- Seams and sprout methods
- Characterization tests
- (p3 33%) The rare case where a rewrite actually wins
- (p2 17%) Live demo
- (flag:red) Rehearse fully offline — venue wifi is never reliable
- (flag:orange) Record a fallback video just in case
- (p4 0%) Closing and call to action
## Slides
### Deck
- (p2 60%) Draft the deck in [[Talk Slides]]
- (p5) Before/after call graphs — the **one diagram** people remember
### Polish
- (p6) Accessibility pass: contrast, alt text, `font-size >= 24pt`
- (p7 0%) Speaker notes
## Rehearsal
Run through it three times; the third one is the one that counts.
- (p1 83%) Timing — 25 min talk, 5 min Q&A
- (p3 50%) Dry run with a colleague
- (flag:yellow) Record it and watch it back — *unpleasant but effective*
- (flag:green) Cut the ~~long tangent about monorepos~~
## Logistics
- (flag:blue) Flights booked
- (flag:purple) Hotel confirmation filed under [[Travel 2026]]
- (flag:gray) Expense report — after the trip
- (draft) this line opens with parentheses but is not a mark
## Follow-up
1. Ordered lists are nodes too, numbering preserved
2. Nested ones work the same way
1. Including their own numbering
- (p4 0%) Publish the written version
- (p5 0%) Share slides and the demo repository
## Notes
Headings and nested lists together form the hierarchy, so every `##` above is a
node on the map. What is *not* on the map: this paragraph and the `tags` and
`speaker` keys in the frontmatter — both carried along invisibly. Open this
file in mindmap view, switch back to source,
and it should come back byte for byte identical.
From the community plugin browser (recommended)
Settings → Community plugins → Browse, search for Rich Mindmap, install and enable it. The listing is at community.obsidian.md/plugins/rich-mindmap.
Requires Obsidian 1.8.7 or newer (the interface-language setting reads Obsidian's own language via an API added in that version).
From a release
Download main.js, manifest.json and styles.css from the latest release, put all three into <your vault>/.obsidian/plugins/rich-mindmap/, then enable Rich Mindmap under Settings → Community plugins.
From source
git clone https://github.com/YounianC/obsidian-rich-mindmap.git
cd obsidian-rich-mindmap
npm install
npm run build # produces main.js
Then copy (or symlink) main.js, manifest.json and styles.css into <your vault>/.obsidian/plugins/rich-mindmap/.
main.jsis a build artifact and is not committed — build it yourself or take it from a release.
Top-of-line ATX headings (# through ######) and nested lists (unordered -/*/+ and ordered 1./1)) together form the hierarchy — the same hierarchy model markmap uses, so notes written for a markmap-based preview plugin open here directly.
# T followed by ### C puts C under T (a level jump is fine), and a second # X in the same file attaches to the root rather than nesting.On a heading node, Tab adds a list item child, and Enter adds a sibling heading reusing the same # prefix. What a heading node cannot do:
## A under ## B would be written as ## B followed by ## A, which re-reads as A being B's sibling — the move would be silently undone. The format cannot express "an H2 nested inside an H2" without rewriting the # count, and rewriting it is not allowed.Prose paragraphs, tables, code blocks and images under a heading are not shown on the map. They are carried along invisibly and replayed byte-for-byte on write-back — which is exactly why deleting a heading that carries them is refused.
A parenthesized group at the very start of a node's text is parsed as marks only if every space-separated token in it is a known mark:
| Syntax | Meaning |
|---|---|
p1 … p7 |
Priority (p1 red, p2 orange, p3 yellow, p4+ grey) |
0% … 100% |
Progress (mapped to a 7-stage pie; the value you wrote is preserved verbatim) |
flag:red flag:orange flag:yellow flag:green flag:blue flag:purple flag:gray |
Flag |
Combine them freely: - (p1 60% flag:blue) node text. Order does not matter when reading.
If the group contains anything unrecognised, the whole group is treated as ordinary text — - (draft) some note is not a mark.
Marks work on heading nodes too, written after the #: ## (p2) Slides. Be aware that this leaks outside the plugin in a way marks on list items do not — heading text is addressable in Obsidian, so (p2) will show up in the Outline panel, in search results and in the graph, and adding a mark to a heading breaks any existing [[note#heading]] reference to it. The root node's H1 is the one exception: marks are refused there, because a file without an H1 has nowhere to write them.
A run of blockquote lines directly under a node line is that node's note:
- Q3 goals
> Waiting on A to confirm the definition.
> See last quarter's retention breakdown.
- Acquisition
The map shows a badge; hovering it pops the note up. Select the node and use the toolbar's "Note" button to edit, Cmd/Ctrl + Enter to save.
It is an ordinary Markdown blockquote — it renders in reading view, it is searchable, and it stays readable and editable without this plugin. A blockquote separated from the node line by other content is not a note. The root node does not support notes, for the same reason marks are refused there: a file without an H1 has nowhere to write them.
As long as you leave a note alone, its lines are preserved byte for byte — >note with no space, > note with two, a Tab indent, all survive untouched. Only two actions rewrite them: editing that note on the map, or dragging a node that carries one to a different depth (the indent has to follow the new level). A node with a note can still be deleted and dragged — a note is visible on the map, so it does not count as hidden carried content.
After the marks group is stripped, the rest of a node's text is rendered as inline Markdown on the canvas:
| Syntax | Renders as |
|---|---|
**bold** |
bold |
*italic* |
italic (only * — _ is left as a literal underscore, so font_size doesn't turn into emphasis) |
~~strikethrough~~ |
strikethrough |
`code` |
inline code |
[[page]] / `[[page |
alias]]` |
[text](url) |
a clickable external link, opened in a new tab |
Unmatched or malformed markers (an unclosed **, a stray *) fall back to plain text — they are never dropped or turned into an error. #tags and raw HTML are not rendered; a node's text is preserved as-is (that's how #tags keep working elsewhere in your vault). There is no image support:  is not recognised as an image syntax, so it degrades to a literal ! followed by a plain clickable link to the image file — the same treatment any other [text](url) gets — not an embedded picture. This rendering is what the toolbar's bold/italic/strikethrough/link buttons write.
The plugin embeds no AI. Point any AI at the .md file and let it edit directly — adding a node is one ordinary list item, with no ids, coordinates or JSON involved. When the file changes on disk the canvas re-parses and refreshes, keeping your viewport and selection where they were. If you had a local edit that had not yet been written to disk, the external content wins and a notice tells you so.
Content outside the list block — other frontmatter keys, preamble, code blocks, trailing paragraphs — is never rewritten.
Opening a file in mindmap view and switching away can cause Obsidian to rewrite it even if you changed nothing. That rewrite may produce these five byte-level changes and only these five:
1. to 1)) also starts a new one, so a blank line is only compacted when both sides belong to the same list.CRLF (\r\n) becomes LF (\n).- is two columns wide and 1. is three, so mixing ordered and unordered in one block hits the first case more often.(flag:blue 60% p3) becomes (p3 60% flag:blue)).Everything else is byte-for-byte stable — including * and + list markers (never converted to -), ordered item numbers and delimiters (never renumbered or pulled back to start at 1), the # count and surrounding whitespace of every heading line, trailing whitespace after the frontmatter fence, and every line that is not a node line.
## with no space after it is a valid empty ATX heading in CommonMark, but is not recognised here. That space requirement is precisely why a line starting with #tag is not mistaken for a heading. ## x inside a list item's continuation from being mistaken for a heading.A followed by ===) are not recognised.## A ## yields the node text A ##. The bytes round-trip, but the text on the map carries the trailing ##.- a / blank / prose / - b, the - b is treated as a child of a, whereas CommonMark would have ended the list and read it as a code block. Same root cause as the pre-existing 4-space limitation.If a file cannot be parsed as a mind map, the canvas shows an error card with a "switch to source mode" button. In that state the plugin does not write to the file at all.
| Key | Action |
|---|---|
Tab |
Add child node |
Enter |
Add sibling node |
F2 / double-click |
Edit text |
Delete / Backspace |
Delete node and its subtree |
| Arrow keys | Move selection through the tree |
Space |
Collapse / expand |
Esc |
Cancel editing / clear selection |
Cmd/Ctrl + wheel |
Zoom (see Zoom below) |
| Drag empty space | Pan |
Dragging a node changes its parent and its position among siblings.
The controls in the top-right corner of the canvas are Fit to window, Zoom out, the current zoom level, and Zoom in. The percentage in the middle is itself a button: click it to reset to 100% (actual size). It keeps the centre of the viewport fixed, so whatever you were reading grows in place instead of being pulled back to the root node (to go back to the root, hit Fit to window first, then the percentage). The range is 20%–300%.
Fitting a large mindmap to the window makes it tiny — a few hundred nodes typically land around 30%, which is barely readable. If you would rather open at actual size, set Settings → Rich Mindmap → Zoom when opening a mindmap to 100% (actual size): the viewport starts at the root node on the left, at 100%. The default is still Fit to window. The setting only affects views opened from that point on, not ones already open.
Selecting a node brings up a floating toolbar next to it: bold / italic / strikethrough, the marks panel (priority, progress, flag — clicking an active item clears it), insert a [[link]], edit the note (a multi-line box; Cmd/Ctrl + Enter saves, Esc cancels), and button equivalents for add child, add sibling, delete and collapse. The note button is disabled on the root node.
未命名思维导图.md in that folder (auto-numbered 未命名思维导图 1.md, 2, … if taken), containing a mindmap: true frontmatter block plus an H1 matching the file name, then opens it in mindmap view in the current pane. The H1 is there so the root node can be renamed directly in the map; renaming the file afterwards does not update the root text.mindmap: true so the file opens in mindmap view from then on (can be turned off in settings). On a file with no frontmatter it creates one. This command also normalizes the whole file's line endings to LF (normalization 3 above); visible content is unaffected.With auto-open enabled, once a file is already showing in mindmap view in some pane, other panes showing the same file are no longer flipped automatically — this is the rule that keeps the source split alive. The cost: opening the same file as a mindmap in a second pane leaves that pane in source mode; use Toggle mindmap / source view to switch it by hand.
The interface is available in English and Simplified Chinese. Settings → Rich Mindmap → Interface language offers three choices: Follow Obsidian (the default — it shows the language code it detected in parentheses), 简体中文, and English. Switching takes effect immediately, including the names in the command palette.
The default name of a file created by New mindmap follows the language too (Untitled Mindmap.md / 未命名思维导图.md). Existing files are never renamed.
Two limits worth knowing: zh-TW and zh-HK fall back to Simplified Chinese, and the plugin description in manifest.json is English only — Obsidian does not support localized manifests.
npm install
npm run dev # esbuild watch
npm test # vitest
npm run typecheck
npm run check:purity
npm run check:i18n
src/model/ (collapse-state.ts, marks.ts, parser.ts, serializer.ts, tree-ops.ts, types.ts) plus src/view/layout.ts and src/view/camera.ts form a pure-function layer with no dependency on the Obsidian API or the DOM. npm run check:purity enforces that boundary; all of it is unit tested.
Pushing a version tag builds and publishes a release automatically:
# 1. Bump version in manifest.json and add the matching entry to versions.json
# 2. Tag it — the name must equal manifest.version exactly, with no v prefix
git tag -a 0.2.0 -m "..."
git push origin 0.2.0
GitHub Actions runs the type check, unit tests, purity check and build, and only then creates the release with main.js, manifest.json and styles.css attached.
Because the plugin rewrites your notes, verify write-back behaviour in a test vault or a git-tracked directory first, and only point it at notes you care about once you are satisfied.