Use Markdown as a Data Format
Keep your content human-editable, but make it machine-readable.
Use Markdown as a data format for your apps, APIs, and websites.
Parse text into typed, validated data using a robust AST.
import { datamark } from "datamark"const Blog = datamark({ schema, parse, stringify });const markdown = `# Hello WorldThis is a blog post with a [link](...).## A SectionMore content [here](...)...`let data = Blog.parse(markdown);// {// title: "Hello World",// snippet: "This is a blog post with a link.",// sections: [{ heading: "A Section", content: "More content here..." }]// links: [{ href: "...", text: "link" }, { href: "...", text: "here" }]// }data.title = "Hello from datamark!"const out = Blog.stringify(data);// → # Hello from datamark!\n\nThis is a blog post with...
Parse
datamark parses Markdown into a typed AST with sections, paragraphs, code blocks, lists, and tables. The result is a
Document you can traverse, query, and transform.import { parse } from "datamark"const doc = parse("# Hello");doc.root.children[0].heading.children// → [{ type: "text", value: "Hello" }]
Format
A format is a typed contract. Define a schema, a
parse function that walks the AST, and a stringify function for round-trip serialization. Add inline examples and your format becomes self-testing.import { datamark } from "datamark"import { heading, paragraph } from "datamark/stringify"import z from "zod"const Blog = datamark({schema: z.object({title: z.string(),body: z.string(),}),parse(doc) {return {title: doc.root.children[0].heading.children[0].value,body: doc.root.children[1].paragraph.children[0].value};},stringify(data) {return [heading(data.title),paragraph(data.body),].join("\\n\\n");}})
Extract
The AST SDK gives you primitives to query the tree:
findAll for filtering, textContent for extracting text, extractTodoItems for checkbox lists. Navigate sections by heading, split by depth, or flatten back to blocks.import { findAll, isCodeBlock, textContent } from "datamark"// Find all TypeScript code blocksconst blocks = findAll(doc.root,n => isCodeBlock(n, "typescript"),);// Extract all text from a sectionconst body = textContent(doc.root).trim();
Validate
Bring your own validator. datamark speaks Standard Schema v1, so Zod, Valibot, ArkType, and TypeBox all work out of the box. Schemas are validated automatically — bad data throws
ValidationError with a clear message, not an undefined panic.import v from "valibot"const Recipe = datamark({frontmatterSchema: v.object({prepTime: v.string(),servings: v.number(),}),schema: v.object({title: v.string(),ingredients: v.array(v.string()),}),parse(doc) {// frontmatter is typed — no `as any`const { prepTime, servings } = doc.frontmatter...},})
Why datamark?
Fully type-safe
Schemas drive the types. Your parse function receives typed frontmatter and returns a typed object. No manual type definitions, no as any escapes.
Self-documenting formats
A format is a schema + parse + stringify + examples. The inline examples are validated by .test() automatically — your documentation stays correct.
Round-trip fidelity
Parse Markdown into typed objects, then stringify them back. The builder primitives handle escaping and indentation so you never write brittle string templates again.