Presentation¶
TL;DR¶
Presentation is not part of the language. A card is one source file; what a reader sees depends on which tool renders it, and no tool's behaviour is normative for the format.
Converting gives you a file. hmd render --to markdown inlines every embed
and turns every resolved name into an ordinary relative link; --to html gives a
self-contained page with callouts, mathematics, diagrams, and footnotes intact.
Both are erasure — the boundary between a card and the content it embedded is
gone from the output, and so is the provenance of every link. That is right for
something you are shipping and wrong for something you are still editing, so
conversion runs one way on purpose.
Presenting is what a viewer does, and a viewer can keep more than a file
carries. Three matter. Any plain-text surface — an editor, GitHub's file view,
less, an AI chat — shows a card as it is, wikilinks and all, and that is the
property the whole format exists to protect. The MkDocs plugin builds a tree
of cards into a published site, a hand-ordered book with a generated wiki inside
it, and erases the embed boundary the way conversion does. The VS Code
extension under construction keeps that boundary, showing embedded content as
visibly embedded with its source attached. Python stays canonical for semantics:
the specification and its conformance corpus are the contract between surfaces,
not a shared runtime.
Tools and targets¶
A card is one file, and one file goes many places. Point the converter at a card and it comes back as another single file — one in, one out, nothing bundled:
- Plain markdown — strip it down. Every embed is inlined where it was
written, every resolved name becomes an ordinary relative link, and what is
left is a
.mdfile that GitHub, Obsidian, or a plain renderer reads without knowing the format exists. - GFM — what GitHub renders when it shows a file in a repository, which is where a converted card is most likely to be read.
- HTML — the whole page, nothing lost. Callouts, mathematics, diagrams, footnotes, tables: everything the format can express is expressible here, in one self-contained file that needs no site around it.
hmd render doc/wiki/tokens.hmd --to markdown
hmd render doc/wiki/tokens.hmd --to html
Conversion runs one way on purpose. Flattening a card is erasure: the boundary between a card and the content it embedded is gone from the output, and so is the provenance of every link — whether it was found beside the card, named in an import, or swept up from the tree. That is exactly right for something you are shipping and exactly wrong for something you are still editing. Erasure is a shipping format, not an interchange format.
A viewer is not a target¶
Converting gives you a file. Presenting is what a viewer does with a card, and a viewer is free to show more than any file can carry. Three of them matter, and they differ in what they are willing to keep.
Anywhere, with nothing installed¶
The first viewer is the one you already have. An editor, GitHub's file view,
less, a terminal, an AI chat: a card is text, so all of them show it. The
wikilinks appear as [[bracketed text]] and nothing else is out of place.
This is not a fallback. It is the property the whole format is built to protect — the reason a card can be adopted one file at a time is that nobody has to install anything to read it.
MkDocs — the published book¶
The site you are reading. A tree of cards builds as a MkDocs site through a plugin, and the interesting case is not a wiki on its own — it is a book with a wiki inside it: hand-written chapters that explain a subject in order, plus a generated section of cross-linked cards the book hands you off to.
doc/
public/ the book — ordinary markdown, hand-ordered
wiki/ the namespace — .hmd cards, generated nav
proposals/ reference material, ordinary markdown
docs_dir covers all of doc/, while the [[…]] namespace is restricted
to doc/wiki. Cards resolve names against the wiki and nothing else; the rest
of the tree is reachable by ordinary relative links. That split is what lets one
build hold both kinds of writing without the resolver having an opinion about
the book.
docs_dir: doc
use_directory_urls: true # required
plugins:
- hyper-markdown:
root: doc/wiki # the namespace, restricted
nav:
- Home: index.md
- Introduction: public/introduction.md
- Wiki:
- Overview: wiki/README.md
- hmd://wiki # ← the generated section lands here
hmd://wiki is the whole integration. An authored nav wins everywhere except
where it asks for the wiki by name, so a book keeps its own order and still says
exactly where the generated cards belong. Omit the placeholder and the authored
nav is used verbatim; omit nav entirely and the whole nav is derived.
URLs. A card at wiki/a/b.hmd serves at /wiki/a/b/. A folder note at
wiki/a/b/index.hmd serves at that same URL — two names for one page, one URL,
which is why directory URLs are required rather than merely preferred. Cards
sort by path, or by a nav: integer in their frontmatter: keyed cards first,
ascending, the rest in path order, so adding nav: to one card does not
reshuffle its siblings.
What the build does. It registers every .hmd file, which MkDocs would
otherwise not see; expands embeds before Markdown runs, so the table of
contents and the footnotes see one finished document; rewrites each resolved
wikilink to a real relative link; and renders unresolved links as red links
rather than failing. Nothing is resolved twice — the build reuses the same
resolver hmd lint uses, so a link that lints clean and a link that renders
correctly are the same fact.
Two settings that are easy to miss. Without the first, MkDocs copies the raw
.hmd sources into the site beside the pages generated from them; without the
second, every ordinary link a card makes out to the repository is a fatal error
rather than a note.
exclude_docs: |
*.hmd
validation:
links:
not_found: info
Live editing. mkdocs serve works, and the plugin watches the namespace
root — which MkDocs would otherwise ignore, leaving a .hmd edit to trigger no
rebuild and the preview quietly stale.
This viewer erases the embed boundary, the same way conversion does. On a published page, content that came from another card is simply part of the page. For a site that is the correct answer.
The editor — live preview¶
The other viewer under construction is a VS Code extension, developed on the
feat/vsc-ext branch: a TypeScript document model (HMD-0020), the extension and
its preview surface (HMD-0021), and D2 rendering inside the webview's content
security policy (HMD-0022).
The load-bearing difference is the one MkDocs gives up. A preview keeps the embed boundary — it shows content that arrived from another card as visibly embedded, with its source attached, so you can see what you are editing and what you are merely including. That is the one thing a plain markdown previewer cannot do, and it is why the editor line rejected flat markdown as its transport even while erasure stays the right build output.
Python stays canonical for the semantics. A second parser that resolved a name differently from the command line would be worse than no second parser, so the specification and its conformance corpus are the contract between the two surfaces rather than a shared runtime.