v0.11.0 - MIT - forbid(unsafe_code)
Markdoc, in Rust.
Parse, validate, transform, render and format the Markdoc language. A library with no I/O, no configuration and no HTML policy of its own -- and the same engine, compiled to WebAssembly, running in the playground on this site.
$
cargo add accent-proust
use accent_proust::{builtins, parse, render, transform};
let document = parse::parse("# Title\n\nSome *text*.\n");
let tree = transform::transform(&document, &builtins::config());
assert_eq!(
render::render_all(&tree.into_vec()),
"<article><h1>Title</h1><p>Some <em>text</em>.</p></article>",
);
// Same five stages, same output, in a browser:
// import { renderHtml } from "accent-proust";
The pipeline
Five stages, and you can stop at any of them
Markdoc is CommonMark plus a tag syntax, which turns a document into structured content instead of pre-rendered HTML. Each stage below is a separate entry point: a linter stops after validate, an editor stops after transform, and a static site runs the lot.
-
parse
Source text to an AST. Block tags, inline spans and annotations, with a byte range on every node.
-
validate
The AST against a schema. Returns a list of errors, never a failure, so an editor shows all of them at once.
-
transform
A validated AST to a renderable tree. Variables and functions resolve here; tags become nodes a host can map to components.
-
render
The tree to HTML. Escaping and element policy live behind a trait, so a host that wants different markup supplies it.
-
format
A tree back to canonical Markdoc source. Idempotent, and round-trips without losing anything.
Conformance
The conformance corpus is the test suite
Upstream's own 105 cases are vendored into the repository and run on every commit, against a baseline file that fails the build if the numbers move in either direction. Nothing fails; the ten cases that do not match upstream exactly are counted apart, because giving something up should stay visible.
What you get
What the library actually promises
A short list, because each item on it is a thing the code is arranged around rather than a thing it happens to do.
Upstream's error ids, unchanged
attribute-missing-required, tag-undefined and the rest are reproduced exactly. External tooling binds to those codes, so they are the field this crate is least free to invent.
Diagnostics are data
Validation returns a Vec, not a Result. A document with mistakes still transforms and still renders, because a preview pane that blanks on the first error is worse than one that shows the error.
Bring your own CommonMark parser
The bundled tokenizer sits behind a default feature. Turn it off, implement Tokenizer, and nothing above it changes. A CI job builds and tests the crate in exactly that shape.
Three seams, all yours
Tokenizer segments Markdown, SchemaSource answers where a schema comes from, and TagRenderer owns escaping and HTML policy. The crate does no I/O and reads no configuration.
The same engine in a browser, and in a shell
A WebAssembly binding publishes validate, renderHtml, transform and format to npm, with diagnostic positions in UTF-16 code units. The accent-proust command formats, validates and renders from the shell, reads the same schema file, and exits 1 when a document is wrong: a documentation repository's CI gate.
Panic-freedom as a lint, not a habit
unsafe_code is forbidden and panic/unwrap/expect/indexing_slicing are denied in CI. This is an open parser fed arbitrary text: its attack surface is part of its API.
Why a port, and why this one
Markdoc's reference implementation is TypeScript. A Rust program that wants the language has three options: shell out to Node, reimplement the parts it needs, or use a port. The first two are how content pipelines acquire a Node dependency and a subtly different dialect.
This is the third. Upstream's TypeScript is vendored into the repository unmodified, so a change that ports a behaviour shows its source in the diff and the yearly refresh is a diff rather than an archaeology exercise. Where the two cannot agree -- upstream builds on markdown-it and this crate on pulldown-cmark, and those disagree about CommonMark edge cases -- the difference is written down in Divergences and never emulated silently.
The tag language and the error ids are the contract. CommonMark edge behaviour is not, and pretending otherwise would be the more expensive lie.
Type into it and watch all five stages run
The playground is this engine compiled to WebAssembly. Edit the source on the left and the HTML, the diagnostics and the renderable tree update on the right -- locally, with nothing sent anywhere.