Format SDK

Parsing Patterns

Navigate the AST, extract data, and map Markdown to typed objects.

Your parse function receives a Document and returns typed data. This page covers the common patterns for walking the tree and extracting structured content.

The document structure

import { parse } from "datamark";

const doc = parse(markdown);
// doc.frontmatter — parsed YAML or null
// doc.root — SectionNode containing the full tree
import { parse, datamark } from "datamark";
import {
  find,
  findAll,
  isHeading,
  isCodeBlock,
  sectionsAtDepth,
  sectionsByHeading,
  splitBy,
  between,
  after,
  before,
  inlineText,
  textContent,
  extractTodoItems,
} from "datamark/parse";
import * as z from "zod";

// Sample document for pattern demonstrations
const doc = parse(`---
title: API Reference
---

# API Reference

Introduction paragraph.

## Authentication

Use Bearer tokens.

\`\`\`json
{ "token": "abc123" }
\`\`\`

## Endpoints

### GET /users

Returns a list of users.

### POST /users

Creates a user.

## Parameters

| Name | Type |
|------|------|
| id   | number |

## Action Items

- [ ] Write tests
- [x] Update docs

Final paragraph.`);

// Finding sections
const rootSection = doc.root.children.find(
  (n) => n.type === "section"
) as any;
const topSections = rootSection?.children?.filter(
  (n: any) => n.type === "section"
) ?? [];

const firstH1 = find(doc.root, (n) => isHeading(n, 1));
const h2Sections = sectionsAtDepth(doc.root, 2);
const paramSections = sectionsByHeading(doc.root, "Parameters");

// Walking subsections
const subSections = rootSection?.children?.filter(
  (n: any) => n.type === "section"
) ?? [];

// Extracting structured data
const jsonBlocks = findAll(doc.root, (n) => isCodeBlock(n, "json"));
const allCode = findAll(doc.root, (n) => isCodeBlock(n));

// Lists
const lists = findAll(doc.root, (n) => n.type === "list") as any[];

// Tables
const tables = findAll(doc.root, (n) => n.type === "table") as any[];
const tableHeaders =
  tables[0]?.children[0]?.children.map((c: any) =>
    inlineText(c.children)
  ) ?? [];

// Inline text
const introPara = rootSection?.children?.find(
  (n: any) => n.type === "paragraph"
) as any;
const introText = introPara ? inlineText(introPara.children) : "";

// Recursive text
const allText = textContent(doc.root).trim();

// Todo items
const todos = extractTodoItems(doc.root);

// Positions
const titleHeading = find(doc.root, (n) => isHeading(n, 1));
const titleLine = titleHeading?.position?.start.line;

// Splitting — flatten first since splitBy operates on flat BlockNode[]
const flatBlocks = [...doc.root.children];
const groups = splitBy(flatBlocks, (n) => isHeading(n, 2));
const afterAuth = after(flatBlocks, (n) => isHeading(n, 2));
const beforeParams = before(flatBlocks, (n) => isHeading(n, 2));

// Format using patterns
const ApiFormat = datamark({
  schema: z.object({
    title: z.string(),
    endpoints: z.array(z.string()),
    todos: z.array(z.object({ text: z.string(), completed: z.boolean() })),
  }),
  parse(parsedDoc) {
    const title = inlineText(
      (parsedDoc.root.children.find((n) => n.type === "section") as any)
        ?.heading?.children ?? []
    );
    const endpointSection = sectionsByHeading(parsedDoc.root, "Endpoints")[0];
    const endpoints = endpointSection
      ? (findAll(endpointSection, (n) => n.type === "heading") as any[]).map(
          (h) => inlineText(h.children)
        )
      : [];
    return { title, endpoints, todos: extractTodoItems(parsedDoc.root) };
  },
});

doc.root is a SectionNode that contains the entire document. Its children contains both content nodes (paragraphs, code blocks, lists) and nested SectionNodes.

Finding sections

The root contains top-level sections as SectionNodes in its children:

import { parse, inlineText } from "datamark/parse";

const doc = parse(markdown);
const rootSection = doc.root.children.find(n => n.type === "section") as SectionNode;
const topSections = rootSection.children.filter(n => n.type === "section") as SectionNode[];

const firstHeading = topSections[0]?.heading;
const title = firstHeading ? inlineText(firstHeading.children) : "";

Helper functions

import { sectionsAtDepth, sectionsByHeading } from "datamark/parse";

// All h2 sections anywhere in the document
const h2s = sectionsAtDepth(doc.root, 2);

// All sections whose heading text is exactly "Parameters"
const paramSections = sectionsByHeading(doc.root, "Parameters");

Walking subsections

Each section can contain more sections. Walk them recursively or split by heading:

import { parse, inlineText } from "datamark/parse";

const doc = parse(markdown);
const h1 = doc.root.children.find(n => n.type === "section") as SectionNode;
const subSections = h1.children.filter(n => n.type === "section") as SectionNode[];

for (const section of subSections) {
  const heading = inlineText(section.heading!.children).toLowerCase();
  if (heading.includes("description")) {
    // extract description
  }
  if (heading.includes("parameters")) {
    // extract parameters
  }
}

Extracting structured data

Code blocks as data

Code blocks are the standard way to embed structured data in Markdown. Extract them by language and parse their contents:

import { findAll, isCodeBlock } from "datamark/parse";

// All JSON code blocks in a section
const jsonBlocks = findAll(section, n => isCodeBlock(n, "json"));
for (const block of jsonBlocks) {
  const data = JSON.parse(block.value);
}

// All code blocks regardless of language
const allCode = findAll(section, n => isCodeBlock(n));

Lists

Extract list items and their text:

import { findAll, textContent } from "datamark/parse";

const lists = findAll(doc.root, n => n.type === "list") as ListNode[];
for (const list of lists) {
  for (const item of list.children) {
    const text = textContent(item).trim();
    console.log(text);
  }
}

Tables

import { findAll, inlineText } from "datamark/parse";

const tables = findAll(doc.root, n => n.type === "table") as TableNode[];
for (const table of tables) {
  const rows = table.children;
  const headers = rows[0].children.map(c => inlineText(c.children));
  const bodyRows = rows.slice(1).map(row =>
    row.children.map(c => inlineText(c.children))
  );
}

Inline text

inlineText() extracts plain text from inline nodes (paragraphs, headings). It handles bold, italic, links, and images:

import { parse, inlineText } from "datamark/parse";

const doc = parse(markdown);
const paragraph = doc.root.children.find(n => n.type === "paragraph") as ParagraphNode;
const text = inlineText(paragraph.children);
// "Visit the docs at example.com" — link text is preserved, href is not

textContent() is the recursive version. It extracts all text from any node:

import { textContent } from "datamark/parse";

const section = doc.root.children.find(n => n.type === "section") as SectionNode;
const allText = textContent(section).trim();

Todo items

Use extractTodoItems() to find all checkbox items:

import { extractTodoItems } from "datamark/parse";

const todos = extractTodoItems(doc.root);
for (const todo of todos) {
  console.log(todo.text, todo.completed);
}

Positions and source mapping

Every node carries an optional position with start/end line/column. This is useful for pointing to where an error occurred in the source file:

import { find, isHeading } from "datamark/parse";

const heading = find(doc.root, n => isHeading(n, 1));
if (heading?.position) {
  console.log(`Title is on line ${heading.position.start.line}`);
}

Positions are computed relative to the original input string, even when frontmatter is stripped. Error messages point to the right place in your source file.

On this page