MDX.org.ai
Structured Datamdxld

AST

Work with the MDXLD abstract syntax tree

AST Manipulation

mdxld provides utilities for working with the abstract syntax tree (AST) of MDX documents.

Converting to AST

import { parse, toAst } from 'mdxld'
 
const doc = parse(`---
title: Hello World
---
 
# Hello World
 
This is a **bold** statement.
 
- Item 1
- Item 2
`)
 
const ast = toAst(doc)

AST Structure

The AST follows the mdast (Markdown AST) specification:

interface MDXLDAst {
  type: 'root'
  children: MDXLDAstNode[]
}
 
interface MDXLDAstNode {
  type: string
  depth?: number       // For headings
  children?: MDXLDAstNode[]
  value?: string       // For text nodes
  url?: string         // For links and images
  alt?: string         // For images
  lang?: string        // For code blocks
  ordered?: boolean    // For lists
}

Node Types

Headings

{
  type: 'heading',
  depth: 1,  // 1-6
  children: [
    { type: 'text', value: 'Hello World' }
  ]
}

Paragraphs

{
  type: 'paragraph',
  children: [
    { type: 'text', value: 'This is ' },
    { type: 'strong', children: [{ type: 'text', value: 'bold' }] },
    { type: 'text', value: ' text.' }
  ]
}
{
  type: 'link',
  url: 'https://example.com',
  children: [
    { type: 'text', value: 'Click here' }
  ]
}

Code Blocks

{
  type: 'code',
  lang: 'typescript',
  value: 'const x = 1'
}

Lists

{
  type: 'list',
  ordered: false,
  children: [
    {
      type: 'listItem',
      children: [
        {
          type: 'paragraph',
          children: [{ type: 'text', value: 'Item 1' }]
        }
      ]
    }
  ]
}

Traversing the AST

Simple Traversal

function traverse(node, callback) {
  callback(node)
  if (node.children) {
    for (const child of node.children) {
      traverse(child, callback)
    }
  }
}
 
traverse(ast, (node) => {
  console.log(node.type)
})

Finding Nodes

function findAll(ast, type) {
  const results = []
 
  function search(node) {
    if (node.type === type) {
      results.push(node)
    }
    if (node.children) {
      for (const child of node.children) {
        search(child)
      }
    }
  }
 
  search(ast)
  return results
}
 
const headings = findAll(ast, 'heading')
const links = findAll(ast, 'link')

Extracting Text

function extractText(node) {
  if (node.value) return node.value
  if (!node.children) return ''
 
  return node.children
    .map(extractText)
    .join('')
}
 
// Get all text from a heading
const heading = ast.children.find(n => n.type === 'heading')
console.log(extractText(heading)) // 'Hello World'

Transforming the AST

Modify Nodes

function transformLinks(ast, baseUrl) {
  function transform(node) {
    if (node.type === 'link' && node.url?.startsWith('/')) {
      node.url = baseUrl + node.url
    }
    if (node.children) {
      node.children.forEach(transform)
    }
  }
 
  transform(ast)
  return ast
}
 
const transformed = transformLinks(ast, 'https://example.com')

Filter Nodes

function removeCodeBlocks(ast) {
  return {
    ...ast,
    children: ast.children
      .filter(node => node.type !== 'code')
      .map(node => {
        if (node.children) {
          return { ...node, children: removeCodeBlocks(node).children }
        }
        return node
      })
  }
}

Add Nodes

function addTableOfContents(ast) {
  const headings = findAll(ast, 'heading')
    .filter(h => h.depth === 2)
 
  const tocList = {
    type: 'list',
    ordered: false,
    children: headings.map(h => ({
      type: 'listItem',
      children: [{
        type: 'paragraph',
        children: [{
          type: 'link',
          url: `#${slugify(extractText(h))}`,
          children: [{ type: 'text', value: extractText(h) }]
        }]
      }]
    }))
  }
 
  return {
    ...ast,
    children: [tocList, ...ast.children]
  }
}

Converting Back to Document

import { fromAst } from 'mdxld/ast'
 
const modifiedAst = transformLinks(ast, 'https://example.com')
const doc = fromAst(modifiedAst)
 
console.log(doc.content) // Updated MDX content

Use Cases

Extract Table of Contents

function getTableOfContents(doc) {
  const ast = toAst(doc)
  const headings = findAll(ast, 'heading')
 
  return headings
    .filter(h => h.depth >= 2 && h.depth <= 3)
    .map(h => ({
      depth: h.depth,
      text: extractText(h),
      slug: slugify(extractText(h))
    }))
}

Count Words

function countWords(doc) {
  const ast = toAst(doc)
  const text = extractText(ast)
  return text.split(/\s+/).filter(Boolean).length
}
function getLinks(doc) {
  const ast = toAst(doc)
  const links = findAll(ast, 'link')
 
  return links.map(link => ({
    text: extractText(link),
    url: link.url
  }))
}