Markdown, plus one idea: you can embed opaque resources that declare their type and encoding, and those resources can nest inside each other.
This repository contains the Markdown2 specification and its reference implementation:
- The specification:
idea.md, Working Draft 0.2. - The reference parser and handler runtime: framework-free TypeScript packages that run in Node, Deno, Bun, workers and browsers.
- The reader and editor: an installable, offline-capable PWA.
- The conformance suite: language-neutral fixtures that any implementation can run.
The reference implementation is a realisation of the spec, not the spec. If the code and idea.md disagree, idea.md wins, and the disagreement is a bug. Ambiguities in the spec are logged in spec-issues.md rather than resolved silently in code.
A resource starts with <<<, a terminator token you choose, and some attributes. It ends at the first line that holds just that token:
<<<NOTE type="text/plain" title="A note"
Anything at all stays opaque here.
# This is not a heading, and ``` is not a fence.
NOTE
Every resource answers three questions:
| Attribute | Question | Example |
|---|---|---|
type |
What are these bytes? | image/svg+xml |
encoding |
How do I obtain them? | base64 gzip |
rel |
What is this to its parent document? | attachment |
Attachments, embedded images, compressed assets, diagrams, remote resources, frontmatter, nested documents and encrypted sections all follow from this one primitive. None of them is new syntax:
<<<VAULT type="text/markdown" encoding="base64 age" title="Q3 notes"
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCBk…
VAULT
A few rules hold everywhere:
- Opacity. Nothing inside a resource is interpreted until its terminator. Nothing needs escaping.
- No sniffing. The declared type is authoritative.
image/jpgis notimage/jpeg. - Fail closed. A malformed, unterminated or undecodable resource becomes a
failednode. Its bytes never leak into the prose. - No implicit authority. Opening a document never grants network, filesystem or key access. Encrypted and
uriresources staylockeduntil the user acts. - Graceful degradation. Older Markdown readers see legible text. Unsupported resources are shown, never dropped.
Read idea.md for the full specification, or open the PWA for a live tour.
markdown2/
├─ packages/
│ ├─ core/ types, limits, static state precedence, tree serialiser
│ ├─ micromark-extension/ resource construct, commitment, frontmatter, wrapper
│ ├─ mdast/ from/to mdast, node identity
│ ├─ parser/ public parse() API
│ ├─ decoders/ utf-8, base64, gzip, uri, age; pipeline; quotas
│ ├─ runtime/ handler registry, grants, recursive render, fallback
│ ├─ handlers/ images, SVG, Markdown, JSON, YAML, math, Mermaid, …
│ ├─ codemirror/ editor language support
│ ├─ conformance/ fixtures, JSON Schema, runner
│ └─ cli/ the m2 command
├─ apps/
│ └─ pwa/ Vite + Preact reader and editor
├─ docs/ architecture, ADRs, API reference
├─ idea.md the specification, Draft 0.2
├─ plan.md the implementation plan
└─ spec-issues.md ambiguities found during implementation
Dependencies point one way only, from core up to the app and CLI. The core, parser and decoders packages have no DOM dependency.
Requirements: Node 22 LTS and pnpm 9.
pnpm install
pnpm build # build every package and the app
pnpm test # unit tests and the conformance suiteTo work on the PWA:
pnpm --filter pwa devEnd-to-end tests run in Chromium, Firefox and WebKit through Playwright:
pnpm exec playwright install
pnpm --filter pwa test:e2eimport { parse } from '@markdown2/parser';
const doc = parse(source);
for (const node of doc.resources) {
console.log(node.id, node.resource.type, node.structuralError ?? 'ok');
}Parsing never runs a decoder. Decode state is computed separately, in two steps:
staticState()from@markdown2/coreapplies steps 1–3 of the spec's state precedence (structural error, unknown decoder, unmet capability) without touching the payload.@markdown2/decodersruns the streaming pipeline for runnable resources, with byte quotas enforced while each stage produces output.
Handler selection is an exact lookup on the declared type, keyed by capability × media type.
The m2 CLI parses, validates and dumps trees:
m2 parse notes.md # print the parse tree
m2 validate notes.md # report structural errors and diagnostics
m2 tree notes.md # print the normalised conformance tree as JSON
m2 render notes.md # render to HTMLDiagnostics name the line, the error code and the resource:
notes.md:1: missing-type [0]: Resource OOPS: missing-type
m2 render is also the reference for server-side uri resolution. It refuses loopback, link-local and private addresses, and re-checks after every redirect.
The PWA is a reader and editor for Markdown2 documents. It works offline, installs as an app and can open .md files from the operating system.
- Layouts: Write (Ctrl+1), Split (Ctrl+2) with live preview, and Read (Ctrl+3).
- Inspector (Ctrl+.): diagnostics with jump-to-source, every resource with its true state, and style choices. It sits outside the page, so no document can fake or hide it.
- Locks: encrypted and remote resources show a sealed card until you choose Unlock…. Keys live in the tab's memory only. Forget keys clears them all.
- Remote policy:
uriresources are locked for every scheme by default. Only you can add sites to the allow list. - Safe saving: resources save as
.binby default. An active extension such as.htmlneeds your confirmation. - Opening by URL:
/?url=/docs/styles.mdopens a document. Otherhttps:origins ask first and send no cookies. Documents opened this way are never autosaved over your draft.
Handlers that run third-party code, such as Mermaid, the code viewer, charts and embedded pages, run in sandboxed frames with an opaque origin. SVG is drawn only through <img>, and maths is rendered as an inert image.
The built-in help is itself a set of Markdown2 documents, in apps/pwa/public/docs/. Start with index.md.
Some features of the PWA go beyond the spec and are deliberately not part of the format:
- Document styles. Resources with
type="text/css"andrel="stylesheet", grouped withmd2-group,md2-defaultandmd2-class. The spec defines nostylesheetrelationship (§10), so these are vendor conventions. Styles are scoped to their own document, cannot fetch, and can be switched off with Ctrl+Shift+U. - Fence aliases. Fenced
mermaid,mathand language-named code blocks select handlers as compatibility aliases (§7.2). They never change the parse tree. - Extra handlers.
text/uri-listembeds andapplication/vnd.google.charts+jsoncharts. Both wait for permission before reaching the network.
A document that relies on them still reads correctly in any other Markdown2 reader.
The conformance suite in packages/conformance covers categories A–H of §12 of the spec. Each case is:
- a source document,
*.md - an expected normalised tree,
*.tree.json, valid against the published JSON Schema
The fixtures are language-neutral. Implementations in other languages can run them without any code from this repository. A conforming parser must pass every case in categories A–E, G and H. Category F is informative.
Every MUST and SHOULD in the spec has a requirement ID, such as R-4.1-commit. docs/traceability.md maps each ID to its tests, and CI fails if a MUST has none.
The security model follows §11 of the spec. In particular:
- No rendering path leads from a failed, locked or unsupported payload to prose HTML. Property-based tests enforce this.
- Encryption decoders never run without a user action. Grants are scoped to a node and its payload digest, and never propagate to children.
- Byte quotas are enforced while bytes are produced, so a gzip bomb is aborted mid-stream.
- YAML is loaded with a safe schema and never instantiates tags.
- The PWA ships with a strict CSP, Trusted Types and COOP
same-origin.
Browsers cannot see IP addresses during fetch, so the SSRF rules apply to the CLI and server-side renderers. The PWA relies on locked-by-default resources, user policy, CORS and CSP connect-src.
Please report vulnerabilities privately to the maintainers rather than in a public issue.
This is an implementation of Working Draft 0.2. The format may still change. See plan.md for milestones and scope.
Out of scope for 0.2:
- the Viewer API companion specification (a pragmatic subset is implemented internally)
- decoder profiles beyond
age, such asgpg,zstdandbr - same-document references, deferred by the spec
- collaborative editing and sync
- Behaviour is defined by the spec. A change in behaviour starts with a reference to the section it implements.
- If the spec is ambiguous, add an entry to
spec-issues.md, choose the most fail-closed reading, mark the code with the issue ID and add a fixture. - Every change needs passing tests on Node and all three browser engines, an updated traceability table where relevant, and a changeset.
- No new
any, and no ESLint or type errors.