mindmap-markmap-viewer
Turn Markdown outlines, notes, docs, or plans into an interactive mind map — a single self-contained HTML file that opens offline anywhere (markmap.js, white-on-dark, zoom/expand/export toolbar, search that keeps matches in context). Use whenever the user asks for a mind map, markmap, or concept map, wants to visualize or diagram the structure of a document or topic, summarize notes as a navigable tree, make an outline clickable or explorable, or embed such a map in Streamlit — even when they don't literally say 'mind map'.
The English summary is being prepared. Showing the original description.
See how to installFiles included (24)
- SKILL.md13.3 KB
- .claude-plugin/marketplace.json1017 B
- .claude-plugin/plugin.json558 B
- .gitattributes213 B
- .gitignore75 B
- assets/demo.gif258.0 KB
- assets/example.md514 B
- assets/one-line-demo.png326.0 KB
- assets/vendor/d3.min.js273.2 KB
- assets/vendor/markmap-lib.min.js332.2 KB
- assets/vendor/markmap-toolbar.min.css2.3 KB
- assets/vendor/markmap-toolbar.min.js6.4 KB
- assets/vendor/markmap-view.min.js24.9 KB
- assets/vendor/README.md1.7 KB
- docs/demo.md1010 B
- docs/index.html646.6 KB
- evals/evals.json1.9 KB
- evals/test_render_markmap.py11.3 KB
- LICENSE1.0 KB
- PRIVACY.md1.7 KB
- README.md5.1 KB
- references/internals.md8.4 KB
- references/lessons.md3.5 KB
- scripts/render_markmap.py23.8 KB
SKILL.md (original)
Review the instructions the agent will receive before you install.
Mindmap (markmap) Skill
Render hierarchical Markdown as an interactive SVG mind map with markmap.js, plus three custom layers: white-font CSS, expand-by-level control, and search that filters the tree.
source.md ──► filter / set expand level (Python) ──► build_html() ──► vendored markmap libs (local, offline) ──► SVG + toolbar
(outline) (manipulate the text) (HTML + CSS) (transform + render in the browser) (screen)
Helper functions live in scripts/render_markmap.py; a minimal source file is in assets/example.md; regression tests are in evals/. Deeper docs: references/internals.md (how the helpers work) and references/lessons.md (real-world lessons + the adversarial counter-review behind the current code).
When to use this skill
- The user asks for a mind map, markmap, or concept map of anything.
- Hierarchical content — an outline, notes, a plan, a document's structure, a taxonomy — should become navigable, scannable, or shareable.
- A map must be sent to someone or opened with no setup: the output is one self-contained
.htmlthat works offline. - A mind map should be embedded in a Streamlit app.
- An existing markmap renders blank, white-on-white, or truncated — this skill carries the known fixes (§2).
1. Source format
markmap derives hierarchy from headings (#) and nested list items — -, *, +, or numbered (1. / 1)) — indented by 2 spaces per level. (Tabs work too; the filter treats one tab as one level.) A YAML frontmatter block controls behavior:
---
markmap:
colorFreezeLevel: 2 # freeze color from level 2 down (branches keep the parent color)
initialExpandLevel: 2 # levels kept open; root is level 1, so 2 = root + branches (-1 = all)
maxWidth: 380 # max node width in px (forces wrapping)
---
# Root <- root (level 1)
## Branch A <- level 2
### Sub-branch A1 <- level 3
- Leaf <- level 4 (bullet under a level-3 heading)
- Detail <- level 5 (bullet indented +2 spaces)
Presets (apply_presets). You don't have to hand-write the markmap: block — apply_presets(src, color=None, max_width=380) fills sensible defaults without overriding anything you set: colorFreezeLevel: 2, maxWidth, and an initialExpandLevel sized by node count (expand-all for ≤30 nodes, else level 2). Pass color=["#7fd1ff", "#ffd479"] for a custom palette. Any key already in your frontmatter wins, so you can set one value and let presets fill the rest.
Content rule — term → parent / description → child. Put the label on the node and its explanation as a child, not on one line. Prefer:
- Term
- description of the term
over - Term — description. This keeps nodes short and the tree scannable.
Node text may contain <, >, and & freely (a < b, List<String>, even </div>). build_html HTML-escapes the source before embedding it and the browser decodes it back, so markmap sees exactly what you wrote. (Earlier versions broke on a literal <; that footgun is gone — don't pre-escape to < yourself or it shows up literally.)
2. Rendering (white font)
The renderer is: the vendored markmap <script>s (local files), an <svg class="markmap"> plus a hidden source <div> holding the Markdown, a small init script (transform → Markmap.create → toolbar), and the CSS. See build_html() / render_markmap() in scripts/render_markmap.py, with the rationale in references/internals.md.
Non-obvious points (these cost rework):
- Offline, single self-contained file by default.
build_htmlinlines the vendored markmap stack (d3 + markmap-view/-lib/-toolbar, pinned exact inassets/vendor/) directly into the HTML — no CDN, no network, and no sibling files — so the one.htmlopens offline anywhere you move or share it. (That single-file default is the fix for maps that failed to load their libs when opened alone.) Passinline=Falsefor a smaller HTML that instead references avendor/folder viavendor="vendor", which must then travel beside it. - LaTeX math is not rendered in the offline bundle. markmap's KaTeX support needs
window.katex, which isn't vendored, so$...$/$$...$$show as plain text. Everything else renders fully offline. - Navigation toolbar (bottom-right): zoom in/out, fit-to-window, expand-all, collapse-all, and download as SVG / PNG. Pass
toolbar=Falseto omit it. Expand/collapse set each node'sfoldthen re-render withsetData()and no argument — passing data re-derivesfoldfrominitialExpandLeveland would wipe the manual fold. Export snapshots the current fold state; the SVG inlines the white-font CSS + a dark backdrop so it stands alone, and PNG rasterizes at 2× (falling back to SVG if a browser refuses to rasterize the<foreignObject>labels — markmap draws node text as HTML-in-SVG, not<text>). - White font is invisible without a dark background. This is the #1 way the map "renders blank": the font is white, the surface is white, so nothing shows. A standalone
.htmlopens on the browser's white default, and a Streamlitcomponents.htmliframe is white by default too — neither inherits the host's dark theme. Sobuild_htmlpaints its own dark backdrop (background="#0e1117"by default). Only passbackground="transparent"when you know the host behind the iframe is already dark and you want a seamless blend. White font + dark background travel together — never set one without the other. - White font needs TWO selectors +
!important. markmap draws text as SVG<text>and sometimes as<foreignObject>(HTML inside SVG). Style both or half the labels stay dark:svg.markmap text { fill: #ffffff !important; } svg.markmap foreignObject, svg.markmap foreignObject * { color: #ffffff !important; } - Embed via an isolated iframe (
components.htmlin Streamlit, or a standalone.htmlfile). Host CSS won't leak in and the map's CSS won't leak out, so the<style>goes inline in the HTML string. - The source is HTML-escaped before embedding. The init script reads the Markdown from the hidden source div's
textContent, which the browser decodes — so escaping round-trips losslessly. Without it, a</div>orList<String>in the outline closes the div early and silently truncates the map.
3. Expand-by-level control
Don't rebuild the map — just rewrite initialExpandLevel in the frontmatter before rendering. Use set_expand_level(src, level) from the helper module:
level1/2/3 expands that many levels.level = -1expands everything (the "expand all" button).- Injection is scoped to the frontmatter and done in place: rewrite an existing
initialExpandLevel, else add one under themarkmap:key (block or inline form), else prepend a minimal frontmatter — but only when none exists. It never rewrites the word "initialExpandLevel" sitting in your body text, and never stacks a second---block on top of existing frontmatter (markmap reads only the first block, so stacking would silently drop your other settings).
4. Search that filters the tree
When there is a query, keep only nodes that match + their path to the root (ancestors) + their subtree (descendants), so a hit appears in context instead of floating alone. Algorithm in filter_markmap():
- Parse each line into a node. Heading level = number of
#. List-item level = (last heading level) + 1 + indentation, where indentation counts 2-space or tab units (expandtabs), and the marker may be-/*/+/numbered. - Build an explicit parent tree with a kind-aware stack — a heading is parented to the nearest shallower heading, never to a bullet, even when its
#-rank numerically exceeds a bullet's level. (Heading rank and bullet indentation share one number line, so comparing raw depths mis-nests anH4-after-a-bullet; the parent tree is what keeps ancestry correct.) - Mark each node match / no-match with an accent-insensitive comparison (
_norm). - Keep every match, every ancestor (walk parent pointers up), and every descendant (any node whose ancestor chain hits a match). Rebuild the Markdown and force
initialExpandLevel: -1.
_norm strips accents via Unicode NFD so "compliance", "COMPLIANCE", and accented variants all match. Zero matches yields a frontmatter-only (blank) map by design — callers branch on the returned count.
5. Minimal usage
The helpers live in this skill's scripts/ directory — and the working directory is
normally the user's project, not this folder, so a bare "scripts" on sys.path
won't resolve. Build paths from the skill's base directory (the path announced when
this skill loaded):
import sys
from pathlib import Path
SKILL_DIR = Path(r"<this skill's base directory>") # announced when the skill loaded
sys.path.insert(0, str(SKILL_DIR / "scripts"))
from render_markmap import write_mindmap, apply_presets, set_expand_level, filter_markmap
Single self-contained file (recommended) — writes the .md source of truth and a
fully inlined .html that opens offline anywhere, with no sibling vendor/:
src = Path("outline.md").read_text(encoding="utf-8") # the user's outline (sample: SKILL_DIR / "assets/example.md")
src = apply_presets(src) # fill default markmap options (no override)
# optional: src, n = filter_markmap(src, "branch b") # search + keep context
# optional: src = set_expand_level(src, -1) # expand all
write_mindmap(src, "out/mapa.html") # -> out/mapa.md + a self-contained out/mapa.html
# smaller HTML + a sibling vendor/ folder instead: write_mindmap(src, "out/mapa.html", inline=False)
Just the HTML string (e.g. to embed) — build_html(src) returns one self-contained
document with the libraries inlined:
from render_markmap import build_html
open("mindmap.html", "w", encoding="utf-8").write(build_html(src, height=850))
Inside Streamlit:
sys.path.insert(0, str(SKILL_DIR / "scripts")) # SKILL_DIR as in the setup above
from render_markmap import render_markmap, set_expand_level
render_markmap(set_expand_level(src, level), height=850)
6. Authoring guidelines (balanced, scannable maps)
The shape of a mind map carries meaning, so structure the content deliberately:
- 5–8 main branches off the root. Fewer feels thin; more is hard to scan at once — group related ideas under an intermediate node instead of widening the root.
- 3–6 children per branch, kept roughly even across branches. One giant branch beside several stubs reads as unfinished; rebalance or split it.
- Labels of 1–3 words. A node is a handle, not a sentence. Put the explanation on a child node (the term → parent / description → child rule from §1), never inline on the label.
- Balanced depth. Keep branches within ~1 level of each other; a single branch that plunges several levels deeper than its siblings unbalances the layout.
- Let
apply_presetsset the frontmatter (color, wrap width, expand level) so you don't hand-tune each map (§1).
Quality checklist (run before shipping a map)
- Root has 5–8 branches; none is a dumping ground.
- Every branch has 3–6 children, roughly balanced.
- Labels are 1–3 words; long text lives on child nodes, not labels.
- Depth is even across branches (no lone deep tunnel).
-
apply_presetsapplied, or the frontmatter set deliberately. - Opened the
.htmlwith the network off (and from a different folder) — it still renders (self-contained). - Searched a known term — it filters to that node + its context.
The 5 lessons this skill must carry
- White font travels with a dark background. White on a white/transparent surface renders blank — the most common failure.
build_htmlpaints its own dark backdrop by default; only go transparent over a host you know is dark. And style bothtextandforeignObject *with!important, or half the labels stay dark. build_htmlHTML-escapes the source, so</>/&in node text are safe and round-trip to markmap unchanged — don't pre-escape them yourself.- Expansion/search work by editing the Markdown text (regex/filter), not by calling the markmap API. Keep edits scoped to the frontmatter; never stack a second
---block. initialExpandLevel: -1= expand all.- Useful search = match + ancestors + descendants, compared without accents — and ancestry comes from an explicit parent tree, because heading rank and bullet indent share one number line.
Reviews
No reviews yet. Tell others what you thought.
Comments
No login needed. Share how it worked for you, ask a question, or note where it did not run.
No comments yet. Be the first to write one.