Skip to main content

HyperMarkDown

The Python implementation, and the hmd command

PyPI Python versions CI Documentation License: MIT

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 mkdocs extra, 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 — HMD001HMD017, 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

hypermarkdown-0.2.0.tar.gz (47.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hypermarkdown-0.2.0-py3-none-any.whl (44.5 kB view details)

Uploaded Python 3

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

Hashes for hypermarkdown-0.2.0.tar.gz
Algorithm Hash digest
SHA256 30d83dfb20e0d0217265ffc533d87231c09d6625616fb6d2e6bb213a0dcd8b64
MD5 3dbff23b74009533b84fdc0c538b35ab
BLAKE2b-256 7d00f32976849189acf59c38e6b62b0c6fae1ffa853a7a983f03f144db068640

See more details on using hashes here.

Provenance

The following attestation bundles were made for hypermarkdown-0.2.0.tar.gz:

Publisher: release.yml on ewiger/hypermarkdown

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for hypermarkdown-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 610ebd4001d039676010010f7b0b88cb54a778b17c6830b40a9c15fedcfab7be
MD5 95ca62e5da20688b08bd32ce7dd91653
BLAKE2b-256 f6277cc688b9377f4d32adb334322bb2cf88cf628428a1fdf883dbbf988dc59b

See more details on using hashes here.

Provenance

The following attestation bundles were made for hypermarkdown-0.2.0-py3-none-any.whl:

Publisher: release.yml on ewiger/hypermarkdown

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page