HyperMarkDown (.hmd) is ordinary GitHub-flavored markdown plus the part HTML
had on its first day: a link that means something, a page that can be made out
of other pages, a document that is part of a web rather than a file in a folder.
Every .md file is already valid .hmd, so a tree is adopted one rename at a
time.
This distribution is the canonical implementation of that format. It ships three things:
hmd, a command line tool — lint a tree of cards, render one, dump the link graph.- the library under it — scanner, resolver, embed expander, and renderer,
importable as
hypermarkdown. - a MkDocs plugin, installed with the
mkdocsextra, which builds a tree of cards into a website.
There is a second implementation in TypeScript for editors. Where the two disagree about a case the shared conformance corpus covers, this one defines the answer.
The format itself — every construct, the resolution algorithm, the rule IDs — is documented at hypermarkdown.org. This page is about the tool.
Install
pip install HyperMarkDown # or: uv pip install HyperMarkDown
pip install "HyperMarkDown[mkdocs]" # with the site builder
Python 3.11 through 3.14.
Sixty seconds
Point hmd at a tree of cards and lint it:
hmd lint --root path/to/your/wiki
glossary/index.hmd:11:3: warning[HMD001] [[idempotency]] does not resolve to a page
0 error(s), 1 warning(s)
Exit codes are pinned for CI: 0 clean, 1 diagnostics, 2 usage error. Add
--strict to fail on warnings, --format json for machine-readable output.
That output is
examples/small/,
a runnable wiki that exercises the spine walk, both import forms, use
inheritance, folder notes, and most of the syntax. It lints with zero errors and
exactly one deliberate warning — the red link above, showing what an unwritten
page looks like. The fixture travels in the repository rather than in the
distribution, so try it from a clone:
git clone https://github.com/ewiger/hypermarkdown
hmd lint --root hypermarkdown/examples/small
The card it is reading
---
tags: [area/auth, status/accepted]
use: [autodiscovery]
import:
- from /shared import tokens as shared-tokens
- from /glossary import *
---
# Login
- `[[tokens]]` → the sibling card, found on the spine
- `[[shared-tokens]]` → `shared/tokens.hmd`, a named import under an alias
- `[[token]]` → `glossary/token.hmd`, via the imported search path
See [[shared-tokens#Rotation|the rotation window]], and embed one block rather
than restating it:
![[token#^definition]]
Six constructs are what the format adds — wikilinks, aliased links, heading
links, block anchors, block references, and the three embed forms. Four
frontmatter keys mean something to this tool: tags, use, import, and nav.
Every other key is yours, and nothing will inspect it.
Commands
| Command | What it does |
|---|---|
hmd lint |
Parse, resolve, and report — HMD001–HMD017, text or JSON |
hmd render |
Expand embeds and rewrite links, to flat markdown or HTML |
hmd graph |
Dump the resolved link graph as JSON |
hmd info |
Show the resolved root and discovery policy |
hmd --version |
Print the installed version |
Each takes --root to override the namespace root, which otherwise comes from
the wiki setting in .hmd/config.toml (defaulting to doc/wiki). A .hmd/
directory doubles as the project root marker, so any subtree can be
self-contained.
hmd render --to markdown is one-way on purpose. Flattening a card erases
the embed boundary and the provenance of every link, which is right for
something you are shipping and wrong for something you are still editing.
The linter refuses to guess
A wiki that guesses is a wiki that quietly rots, because a link that silently changes meaning is indistinguishable from one that did not:
specs/auth/login.hmd:14:5: error[HMD002] [[tokens]] matches 2 pages; qualify it
(candidates: shared/tokens.hmd, specs/auth/tokens.hmd)
The distinction it draws is a compiler's. A link to a page you have not written yet is a warning — writing forward is how a wiki grows, so it renders as a red link instead of breaking the build. A malformed link or multiple autodiscovery matches is an error. Ordered wildcard imports instead use declaration precedence and report shadowing as HMD016.
Resolution runs in phases and stops at the first hit: explicit imports, then the
spine — this folder, then each folder above it, probed without recursion —
then imported search paths, then one sweep of the whole tree. A bare name
is a one-segment reference such as [[tokens]]; [[shared/tokens]] is an
unqualified path, not a bare name. The spine never searches sideways, but if
the earlier phases miss, autodiscovery may resolve a unique card elsewhere in
the namespace. Use [[/shared/tokens]] to make that dependency explicit.
Publish it: a book with a wiki in it
The mkdocs extra registers a plugin through the mkdocs.plugins entry point.
It registers .hmd files as pages, derives the nav from the namespace tree,
expands embeds, and rewrites every wikilink to a source-relative link that
MkDocs resolves and validates itself:
pip install "HyperMarkDown[mkdocs]"
mkdocs build --strict # or: mkdocs serve
The namespace does not have to be the whole site, and a wiki on its own is not
the interesting case — a book with a wiki inside it is. Point docs_dir at a
documentation tree and root at the part of it that is a namespace: the book is
hand-ordered markdown, the wiki is generated, and hmd://wiki says where the
generated section belongs in your nav.
# mkdocs.yml
docs_dir: doc
plugins:
- HyperMarkDown:
root: doc/wiki # only this subtree is a namespace
exclude_docs: |
*.hmd
nav:
- Home: index.md
- Introduction: public/introduction.md
- Wiki:
- Overview: wiki/README.md
- hmd://wiki # ← the derived section lands here
An authored nav wins everywhere except where it names the wiki. Omit the
placeholder and your nav is used verbatim; omit nav entirely and the whole
thing is derived from the namespace tree. A card at a/b.hmd serves at a/b/,
and so does a folder note at a/b/index.hmd — two names for one page, one URL.
Unresolved links render red rather than failing the build, so a wiki stays
publishable while it is still being written.
Publication is opt-in: nav: {visibility: public} is what puts a card on the
site, it inherits down a subtree from a folder note, and the default is private.
The failure mode is a missing page, never an unintended one.
hypermarkdown.org is that build, from this
repository's own doc/ tree.
Math, diagrams, callouts
TeX mathematics, D2 diagrams, callouts, tables, task lists, footnotes,
strikethrough. None of these are HyperMarkDown's own — they are the tier the
wider markdown world settled on, and both hmd render --to html and the site
build assume them present.
Diagrams go through the d2 binary, which is deliberately
not a Python dependency: without it a diagram degrades to its own labelled
source rather than failing the build.
The library
from pathlib import Path
from hypermarkdown import Workspace, config
from hypermarkdown.lint import check
workspace = Workspace(config.load(root_override=Path("doc/wiki")))
for diagnostic in check(workspace):
print(diagnostic.path, diagnostic.rule, diagnostic.message)
parse, resolve, embed, urls, and lint do not import MkDocs — the
plugin is one file, mkdocs_plugin.py, and swapping the renderer is that file
plus a mkdocs.yml rather than a re-specification. Every entry point that
ingests a document takes the document's text, not only a path, because the
language server this package will grow serves buffers that have never been
saved.
Versioning
Semantic, with the usual 0.x caveat made explicit: the format itself may
change between minor versions. Until 1.0, treat a minor bump as potentially
breaking for .hmd sources, not only for the Python API. What each release
changed, and what is deliberately not implemented, is in
CHANGELOG.md.
Development
This tool is tools/hmd/ in the
HyperMarkDown monorepo, whose root
is a uv workspace:
git clone https://github.com/ewiger/hypermarkdown
cd HyperMarkDown
uv sync --locked
uv run python -m pytest
Build the wheel with uv build --package HyperMarkDown — the --package flag
is load-bearing, since a bare uv build at a workspace root produces an empty
unknown-0.0.0 and exits zero.
- tools/hmd/DEVELOP.md — this tool's own guide: the test loop, the gates, dependency policy, packaging, and how a release is cut.
- DEVELOP.md — the repository's: the layout, how the documentation tree is organised, where progress is tracked, and how the site is published.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hypermarkdown-0.2.0.tar.gz.
File metadata
- Download URL: hypermarkdown-0.2.0.tar.gz
- Upload date:
- Size: 47.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
30d83dfb20e0d0217265ffc533d87231c09d6625616fb6d2e6bb213a0dcd8b64
|
|
| MD5 |
3dbff23b74009533b84fdc0c538b35ab
|
|
| BLAKE2b-256 |
7d00f32976849189acf59c38e6b62b0c6fae1ffa853a7a983f03f144db068640
|
Provenance
The following attestation bundles were made for hypermarkdown-0.2.0.tar.gz:
Publisher:
release.yml on ewiger/hypermarkdown
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hypermarkdown-0.2.0.tar.gz -
Subject digest:
30d83dfb20e0d0217265ffc533d87231c09d6625616fb6d2e6bb213a0dcd8b64 - Sigstore transparency entry: 2410231758
- Sigstore integration time:
-
Permalink:
ewiger/hypermarkdown@350096d6159cebd2eb86e4d10a3cc4714e94183a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/ewiger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@350096d6159cebd2eb86e4d10a3cc4714e94183a -
Trigger Event:
push
-
Statement type:
File details
Details for the file hypermarkdown-0.2.0-py3-none-any.whl.
File metadata
- Download URL: hypermarkdown-0.2.0-py3-none-any.whl
- Upload date:
- Size: 44.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
610ebd4001d039676010010f7b0b88cb54a778b17c6830b40a9c15fedcfab7be
|
|
| MD5 |
95ca62e5da20688b08bd32ce7dd91653
|
|
| BLAKE2b-256 |
f6277cc688b9377f4d32adb334322bb2cf88cf628428a1fdf883dbbf988dc59b
|
Provenance
The following attestation bundles were made for hypermarkdown-0.2.0-py3-none-any.whl:
Publisher:
release.yml on ewiger/hypermarkdown
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hypermarkdown-0.2.0-py3-none-any.whl -
Subject digest:
610ebd4001d039676010010f7b0b88cb54a778b17c6830b40a9c15fedcfab7be - Sigstore transparency entry: 2410231826
- Sigstore integration time:
-
Permalink:
ewiger/hypermarkdown@350096d6159cebd2eb86e4d10a3cc4714e94183a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/ewiger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@350096d6159cebd2eb86e4d10a3cc4714e94183a -
Trigger Event:
push
-
Statement type: