A CommonMark-compliant Markdown parser for Rust with an AST, extensions, and HTML and Markdown renderers. It is based on markdown-rs and adds Socketry's AST helpers and renderer APIs.
Socketry's documentation tooling needs to inspect, edit, and render Markdown sections as structured data. This fork builds on markdown-rs's CommonMark parser with AST editing helpers, reusable renderers, and optional syntax extensions for richer documentation.
The fork adds three parsing options to ParseOptions, all disabled by default so ordinary parsing retains CommonMark behavior:
inline_code_inforecognizes a language prefix before inline code, such asrust:`code`. It removes the prefix from the surrounding text, stores the language on the AST node, and emits alanguage-rustclass when rendering HTML.html_block_blank_lineslets ordinary HTML blocks continue across blank lines when subsequent content maintains consistent indentation. This keeps indented HTML content together instead of ending the block at the first blank line.html_tag_namespacesrecognizes namespace-prefixed HTML tags, such as<svg:circle />, in both block and inline HTML.
The existing Constructs::frontmatter option also supports language-agnostic frontmatter, following Markly and cmarkly. A closed, language-tagged backtick or tilde fence at the start of a document becomes frontmatter; format hints on --- or +++ do too. The AST preserves the full info string and raw body without parsing the named format, and HTML output omits it.
For tools that inspect code blocks and frontmatter, Node::code_fence() exposes the source opening fence's character, length, and indentation, following Markly's fence structure. This metadata remains available when nodes are cloned or detached, even after source positions are removed.
Beyond parsing, the fork adds AST traversal, text extraction, heading and section editing, and Fragment nodes for working with detached content. HtmlRenderer, MarkdownRenderer, and the custom Renderer interface render edited trees; Markdown serialization preserves language metadata and can unwrap soft line breaks. Optional CompileOptions::heading_ids generates unique heading anchors for in-page links and tables of contents.
The fork also includes fixes for stale MDX parser errors and parsing and serialization edge cases, including CRLF handling in inline code and math. See releases.md for changes and the API documentation for configuration details.
cargo add socketry-markdownThe library supports Rust 1.73 and later.
For direct conversion from Markdown source to HTML:
let html = socketry_markdown::to_html("## Hello, *world*!");
assert_eq!(html, "<h2>Hello, <em>world</em>!</h2>");Enable GFM constructs with Options::gfm():
use socketry_markdown::{to_html_with_options, Options};
let html = to_html_with_options("* [x] done", &Options::gfm()).unwrap();Parse to a syntax tree when you need to inspect or transform Markdown:
use socketry_markdown::{to_mdast, ParseOptions};
let document = to_mdast("# Introduction\n\nHello, *world*!", &ParseOptions::default()).unwrap();
assert_eq!(document.text_content(), "IntroductionHello, world!");Nodes provide traversal, text extraction, code-fence information, heading lookup, and child or section editing. extract_children() returns a Fragment that can be cloned, rendered, or serialized on its own.
Use HtmlRenderer or a custom Renderer to render an AST. Serialize nodes and fragments back to Markdown with Node::to_markdown():
let markdown = document.to_markdown();
assert!(markdown.starts_with("# Introduction"));MarkdownOptions::line_wrapping can preserve soft source line breaks or unwrap them into spaces when serializing Markdown.
Node::code_fence() exposes the opening marker, length, and indentation, following Markly's fence structure:
use socketry_markdown::{to_mdast, ParseOptions};
let document = to_mdast(" ~~~~rust\n let x = 1;\n ~~~~", &ParseOptions::default()).unwrap();
let fence = document.children().unwrap()[0].code_fence().unwrap();
assert_eq!(fence.character, '~');
assert_eq!(fence.length, 4);
assert_eq!(fence.indent, 2);Indentation is measured in columns relative to the containing block. Fence metadata survives cloning, detaching nodes, and removing source positions. The helper also supports frontmatter; inline and indented code return None. This describes the source opening fence, while ordinary code serialization uses MarkdownOptions to choose its output formatting.
The parser inherits support for CommonMark, GFM, MDX, frontmatter, and math constructs from markdown-rs. The fork-specific parsing options are described under Motivation. HTML output escapes raw HTML and omits MDX expressions by default. See the API documentation for parser options, renderer configuration, and AST types.
Enable the existing frontmatter construct to accept any format. The opening fence must be unindented on the first line of the document, outside list items and blockquotes, and have a matching closing fence:
use socketry_markdown::{mdast::Node, to_mdast, Constructs, ParseOptions};
let options = ParseOptions {
constructs: Constructs { frontmatter: true, ..Constructs::default() },
..ParseOptions::default()
};
let document = to_mdast("```json title=example\n{\"draft\": true}\n```\n\n# Hello", &options).unwrap();
if let Node::Frontmatter(frontmatter) = &document.children().unwrap()[0] {
assert_eq!(frontmatter.language(), Some("json"));
assert_eq!(frontmatter.info, "json title=example");
assert_eq!(frontmatter.value, "{\"draft\": true}\n");
}Tagged forms such as --- json and ---yaml use the same node. Untagged --- and +++ keep their existing Yaml and Toml nodes. Fences without a language, fences later in a document, and unclosed fences remain ordinary Markdown.
Markdown serialization retains the stored info string and body while normalizing spacing around the info string. It can lengthen fences or change their marker when edited content would close them prematurely, and adds a line ending before the closing fence when an edited body needs one. Round trips preserve content and metadata while allowing these formatting adjustments.
Prepare a release with cargo bake cargo:version:patch (or minor, major, or bump --version X.Y.Z), then run cargo bake cargo:release and open a pull request. After review and merge, GitHub Actions publishes the release when the configured crates-io environment approves it. Follow the shared Releasing skill for the standard process.
See releases.md for the full release history.
- Rename
HTMLRenderertoHtmlRendererto follow Rust acronym casing. Update imports and type references.
- Add
Node::code_fence()andmdast::CodeFence, exposing an opening fence's character, length, and indentation in columns, following Markly's fence structure. - Retain fence metadata on ordinary code blocks independently of source positions, and expose existing frontmatter fences through the same helper. Inline and indented code return
None. - Add
Code::fence: Option<CodeFence>; Rust struct literals must supply this field (useNonewhen no source metadata is available). Older serialized ASTs remain readable.
-
Add language-agnostic frontmatter through the existing
Constructs::frontmatteroption: closed, language-tagged backtick/tilde fences at document start and format hints on---/+++. -
Add
Node::Frontmatterwith a raw body, opaque info string, and opening/closing fence metadata; preserve legacy untagged YAML/TOML nodes. Exhaustive matches onNodemust handle the new variant. -
Preserve generic frontmatter during Markdown serialization, protect edited bodies with safe fences, and omit it from HTML.
-
Document the fork's motivation, optional parsing extensions, and AST and rendering additions.
markdown-rs— the upstream parser.- CommonMark — the Markdown specification.
Please open an issue or pull request on GitHub.
Run cargo bake agent:context:install to install shared context and skills. Read .agents/context/index.md to find relevant guides, follow agents.md if present, and apply skills under .agents/skills/. The installer preserves repository-owned agents.md; it does not create or regenerate that file.