compmd
The CompMD processor, in Rust: it parses, validates and expands documents written in CompMD (Component Markdown), as the specification describes (version 1, draft).
Status: in development. The processor implements stages 1 to 4b of the spec (§11) and its CEL profile (§8.2), does no I/O of its own, and passes every case of the conformance set that needs no host language: it is a "compmd/1 processor with remote data and origins" (§13.1). Running host-language components is a renderer's job; the processor expands their invocations to nodes for it.
The stages are the states of a compmd::Processor: each stage takes the processor and gives it back in its next state, so they can only run in order, and each state gives what its stage made and the diagnostics so far (§10.5). What the host provides comes through the small traits of compmd::host, each stage taking only the ones it uses: Files (the document's root folder), Packs, Fetch (remote data), Data (host: data) and Time (time zones). The processor owns what it is given, so a host can keep it as long as it likes; a host that needs its files or data too passes an Rc and keeps a clone of it.
- Stage 1,
parse: the document tree (§12). - Stage 2,
resolve:- Loads a document's imports from the host's
FilesandPacks, with@compmd/corebuilt in and the host's ambient packs (compmd::Ambient). - Binds every component name.
- Loads a document's imports from the host's
- Stage 3,
validate:- Checks invocations, slots, interpolation paths, the types of expressions, bound inputs and data sources.
- Records what it refused, which the later stages leave out.
- Stage 4a,
load:- Gives the inputs their values for a render context.
- Loads every data import, typed by its schema, from the root folder's files, packs,
https:URLs (Fetch) andhost:sources (Data). - Before it, the validated processor's
sourcesgives the URLs and host sources a render needs (host::Needed, each once), so the host can fetch them first. The expanded processor'ssourcesgives them for the context it keeps, after an edit to the frontmatter.
- Stage 4b,
expand: makes the expanded tree (§12.3), with every node's origin when asked, and checks its layout. reload, from the loaded or expanded state: loads (and expands) again for another render context or newly fetched data, without running stages 1 to 3 again.edit, from the expanded state: the processor after acompmd::TextEditof its text (a range in code points and its replacement), with everything it was given before. Every stage only redoes the top-level blocks the edit touches and moves the rest (incremental processing); an edit to the frontmatter or to a component definition redoes the stages after parsing on the whole document.frontmatter, from the expanded state: what the document's frontmatter gives the render (frontmatter::Frontmatter): its own fields (md.doc), its settings, each data import with what was loaded, and its inputs, for a tool that shows them.components, from the expanded state: the components the document can name (components::Components), grouped by the file that defines them, each with its kind, its signature, the range of every use and, for its own, of its definition; and the names it uses that stand for none, for a tool that lists them.hint_at, from the expanded state: what is at a place in the text, and what it takes (hints::PositionalHint): a component's name, a param's key or value, a param list, a slot, a step of an interpolation's or an expression's path, a function, a frontmatter key or a type name; with what may be written there and the diagnostics there. It is a lookup, so an editor asks on every move of the caret, and it reads the caret's line again, so half-typed text has a hint too (its design).splice, from the expanded state: what the last edit changed in the expanded tree (processor::Splice), for a host that keeps its own copy of the tree.host_components, from the expanded state: each host-language component the expanded tree can name, with its language, runtime and code (processor::HostComponent), for a renderer that runs them: the tree names a component but doesn't hold its code. A name two documents give different components is the processed document's, or else the first loaded.
use ;
let processor = new
.parse
.resolve
.validate
.load
.expand;
let tree = processor.expanded; // and processor.diagnostics()
let processor = processor.reload; // another input value, the same document
let processor = processor.edit; // the text changed
let splice = processor.splice; // nodes `from..to` became `splice.nodes`
- A host that holds everything in memory can use maps:
HashMap<String, String>isFiles,HashMap<String, Vec<host::LoadedRelease>>isPacks,HashMap<String, host::Fetched>isFetchandHashMap<String, serde_json::Value>isData.compmd::context::parse_timegives a render time from RFC 3339 text. The WebAssembly build,@compmd/processor, runs the stages this way on a JSON request. - What a param takes is
compmd::values(its design): oneKindper kind of value (its tokens, forms and examples), which checks a value, says in an F009 what the param takes, and suggests a misspelt token.values::vocabulary()is the fixed vocabulary every document can write, the common params, the attributes andbox's params, each with its kind;conformance/params.jsonis its JSON, for the renderer and editors. A diagnostic whose message suggests a name or a token carries the edit as itsfix. compmd::plain::htmlgives what a plain CommonMark and GFM renderer shows of a document (spec §14): markdown-rs with CompMD's syntax off.- Expressions follow the CEL profile with the
md.functions: each is type-checked against the variables' declared types before it runs, and runs within a work budget. Formatting uses ICU4X with all its compiled locale data, and time zone offsets come from the host'sTime. - Parsing alone is the first two calls:
use ;
let parsed = new
.parse;
let tree = to_value?; // the JSON of §12.4
It is published on crates.io as compmd, from version 0.1.0; 0.1.1 is the same, released with the npm packages of that version. Its parser, the fork of markdown-rs in markdown-rs/, is published first, as compmd-markdown. The library keeps the name markdown, so here it is a path dependency with that version, and the published crate depends on the version.
Commands
The parser is a fork of markdown-rs in the submodule markdown-rs/; fetch it once after cloning. cargo make ci (with cargo-make) runs every check: formatting, clippy with warnings as errors, the tests with the conformance set, cargo-deny, line coverage of at least 99% with cargo-llvm-cov, and the TypeScript packages built and tested (which needs pnpm), the renderer's tests checking it against params.json.
License
MIT. Copyright © 2026 Hakan Serce.