@holdenmatt/md-parser
v0.7.1
Published
Markdown parser primitives for frontmatter, body text, sections, and code blocks.
Readme
md-parser
Markdown parser primitives for frontmatter, body text, sections, code blocks, and links.
@holdenmatt/md-parser provides shared Markdown parser primitives for generic document structure. It parses untyped frontmatter, frontmatter-free body text, flat heading sections, code blocks, and ordinary inline links without assigning application meaning to the content.
YAML frontmatter is recognized only from an exact leading --- delimiter block. The Markdown body is preserved as source text and parsed separately so all AST and projection offsets remain body-relative.
Install
npm install @holdenmatt/md-parserUsage
import { parse, stringify } from "@holdenmatt/md-parser";
const document = parse(`---
title: Example
---
## Notes
Body text.
`);
document.raw; // original markdown
document.frontmatter; // Record<string, unknown>
document.body; // markdown without frontmatter
document.ast; // mdast Root parsed from body
document.sections[0]?.heading; // "Notes"
document.codeBlocks; // code blocks with body-relative source ranges
document.links; // ordinary inline links with body-relative source ranges
const markdown = stringify({
frontmatter: { title: "Example" },
body: "## Notes\n\nBody text.\n",
});If you already have a Markdown body, skip frontmatter parsing:
const bodyDocument = parse(markdownBody, { frontmatter: false });
bodyDocument.raw === markdownBody;
bodyDocument.body === markdownBody;
bodyDocument.frontmatter; // {}Node filesystem IO is available from the Node-only subpath:
import { parseFile } from "@holdenmatt/md-parser/node";
const document = await parseFile("notes.md");
const bodyDocument = await parseFile("body.md", { frontmatter: false });Generic mdast helpers are available from the browser-safe AST subpath:
import {
findNodes,
sourceRangeFromNode,
textFromNode,
visitNodes,
} from "@holdenmatt/md-parser/ast";
const codeBlocks = findNodes(document.ast, "code");
const firstRange = sourceRangeFromNode(codeBlocks[0]);API
parse(markdown, options?)
Parses a Markdown string and returns a MarkdownDocument.
Pass { frontmatter: false } to parse the input as body-only Markdown. In that mode, raw and body are the exact input, frontmatter is {}, and leading --- content is parsed as Markdown rather than YAML frontmatter.
parseFile(path)
Reads a UTF-8 Markdown file and returns a Promise<MarkdownDocument>. Import it from @holdenmatt/md-parser/node. It accepts the same parse options as parse.
stringify({ frontmatter, body })
Serializes frontmatter and body text back into Markdown. Empty or omitted frontmatter returns the body unchanged.
Frontmatter is emitted as YAML between exact --- delimiter lines without reformatting the Markdown body.
MarkdownDocument
parse returns one canonical document shape:
type MarkdownDocument = {
raw: string;
frontmatter: Record<string, unknown>;
body: string;
ast: Root;
sections: MarkdownSection[];
codeBlocks: MarkdownCodeBlock[];
links: MarkdownLink[];
};ast is the mdast Root parsed from body. Its positions and offsets are relative to MarkdownDocument.body, not the original raw Markdown with frontmatter. Treat the AST as immutable; ast, sections, codeBlocks, and links are one parse-time snapshot.
When frontmatter is false, body is the original input, so AST and projection offsets are relative to that unchanged input.
Code blocks expose a sourceRange when parser offsets are available:
type MarkdownCodeBlock = {
info: string;
language: string | undefined;
meta: string | undefined;
value: string;
sourceRange: MarkdownSourceRange | undefined;
};
type MarkdownSourceRange = {
start: number;
end: number;
};sourceRange offsets are relative to MarkdownDocument.body, not the original raw Markdown with frontmatter.
Links are a narrow structural projection of ordinary inline Markdown links:
type MarkdownLink = {
text: string;
destination: string;
sourceRange: MarkdownSourceRange | undefined;
};text is a plain-text label extracted from the link contents. destination is the URL reported by the Markdown parser. Images are excluded, and destinations are not resolved, decoded, validated, or interpreted.
AST helpers
The /ast subpath exports small mdast utilities:
visitNodes(root, visitor): deterministic depth-first traversal with(node, parent).findNodes(root, type): returns nodes of a requested mdast type with TypeScript narrowing.textFromNode(node): recursively collapses textual descendants into readable text.sourceRangeFromNode(node): returns{ start, end }from mdast offsets when available.
These helpers accept any mdast node or subtree, do not mutate nodes, and do not attach parent references.
Frontmatter is intentionally untyped. Application packages can refine it with their own schemas, or use @holdenmatt/md-schema for typed frontmatter parsing.
Parse and file-read failures throw MarkdownParseError. See SPEC.md for the structural parsing contract.
