Stringify & Markdown Primitives
Serialize typed data back to Markdown with builder functions from datamark/stringify.
stringify receives your typed data and returns a Markdown string. A good stringify function is the inverse of your parse function — round-tripping through parse(stringify(data)) should give you back equivalent data.
Import path
Markdown builder functions live under datamark/stringify, separate from the Parse SDK:
import {
heading, paragraph, codeBlock, list, frontmatter
} from "datamark/stringify";This keeps your imports explicit: datamark for parsing and AST utilities, datamark/stringify for serialization.
import {
frontmatter,
heading,
paragraph,
codeBlock,
list,
blockquote,
horizontalRule,
strong,
em,
codeSpan,
link,
image,
strikethrough,
} from "datamark/stringify";
const fm = frontmatter({ title: "Hello", tags: ["a", "b"] });
const h1 = heading("Title", 1);
const h2 = heading("Subtitle", 2);
const p = paragraph("A paragraph of text.");
const cb = codeBlock("const x = 1;", "typescript");
const bulletList = list(["First", "Second"]);
const orderedList = list(["A", "B"], true);
const quote = blockquote("A wise quote.");
const hr = horizontalRule();
const bold = strong("bold");
const italic = em("italic");
const code = codeSpan("code");
const a = link("text", "https://example.com");
const img = image("alt", "img.png");
const del = strikethrough("deleted");The stringify contract
Provide stringify in your FormatConfig:
import { datamark } from "datamark";
import { heading, paragraph } from "datamark/stringify";
import * as z from "zod";
const MyFormat = datamark({
schema: z.object({ title: z.string(), body: z.string() }),
parse(doc) {
const title = doc.root.children.find((n: any) => n.type === "heading")?.children?.[0]?.value ?? "";
const body = doc.root.children.find((n: any) => n.type === "paragraph")?.children?.[0]?.value ?? "";
return { title, body };
},
stringify(data) {
return heading(data.title) + "\n\n" + paragraph(data.body) + "\n";
},
});stringify() throws at runtime if your format was created without a stringify function.
Markdown primitives
Manual string interpolation is fragile. datamark provides typed builder functions that handle escaping, indentation, and formatting correctly.
Frontmatter
import { frontmatter } from "datamark/stringify";
frontmatter({ title: "Hello", tags: ["a", "b"] });
// "---\ntitle: Hello\ntags:\n - a\n - b\n---\n"Uses stringifyYaml internally, so complex nested objects work too.
Headings
import { heading } from "datamark/stringify";
heading("Title", 1) // "# Title"
heading("Subtitle", 2) // "## Subtitle"Depth is clamped to 1–6 automatically.
Code blocks
import { codeBlock } from "datamark/stringify";
codeBlock("const x = 1;", "typescript")
// "```typescript\nconst x = 1;\n```"Lists
import { list } from "datamark/stringify";
list(["First", "Second"]) // "- First\n- Second"
list(["A", "B"], true) // "1. A\n2. B"
list(["A", "B"], true, 5) // "5. A\n6. B"Multi-line items are indented correctly.
Blockquotes
import { blockquote } from "datamark/stringify";
blockquote("line1\nline2")
// "> line1\n> line2"Inline formatting
import {
strong,
em,
codeSpan,
link,
image,
strikethrough,
} from "datamark/stringify";
strong("bold") // "**bold**"
em("italic") // "*italic*"
codeSpan("code") // "`code`"
link("text", "https://...") // "[text](https://...)"
image("alt", "img.png") // ""
strikethrough("deleted") // "~~deleted~~"Building a full document
Combine primitives to build a complete document:
import { datamark } from "datamark";
import { inlineText } from "datamark/parse";
import { heading, paragraph } from "datamark/stringify";
import * as z from "zod";
const NoteSchema = z.object({ title: z.string(), body: z.string() });
const NoteFormat = datamark({
schema: NoteSchema,
parse(doc) {
const h1 = doc.root.children.find((n) => n.type === "section") as any;
const title = h1 ? inlineText(h1.heading.children) : "";
const firstPara = h1?.children?.find((n: any) => n.type === "paragraph");
const body = firstPara ? inlineText(firstPara.children) : "";
return { title, body };
},
stringify(data) {
return heading(data.title) + "\n\n" + paragraph(data.body) + "\n";
},
});const data = NoteFormat.parse("# Hello\n\nWorld");
const md = NoteFormat.stringify(data);
// "# Hello\n\nWorld\n"Round-trip fidelity
For perfect round-tripping, your parse and stringify should be inverses:
const data = NoteFormat.parse("# Hello\n\nWorld");
const roundTrip = NoteFormat.parse(NoteFormat.stringify(data));
// data and roundTrip should be deep-equalMarkdown is lossy by design — parse normalizes whitespace and stringify may emit slightly different formatting. If exact round-tripping is required, store the original Markdown alongside the parsed data.