Getting Started

Document Model

Understand the serializable Markdown document returned by Comark, including its nodes, frontmatter, metadata and rendering workflow.

parseMarkdown() returns a MarkdownDocument: a plain, serializable object containing parsed nodes, frontmatter, and plugin metadata.

import { parseMarkdown } from 'comark'

const document = await parseMarkdown(`---
title: Hello Comark
---

# Getting **started**`)

// {
//   frontmatter: {
//     title: 'Hello Comark'
//   },
//   meta: {},
//   nodes: [
//     ['h1', { id: 'getting-started' }, 'Getting ', ['strong', {}, 'started']]
//   ]
// }

The document is the shared boundary between parsing and rendering. You can parse once, serialize or cache the result, inspect or transform its nodes, and render it with HTML, ANSI, Vue, React, Svelte, or Angular.

Document Structure

The root object has three fields:

interface MarkdownDocument {
  frontmatter: Record<string, any>
  meta: Record<string, any>
  nodes: Node[]
}
FieldDescription
frontmatterStructured data from the leading YAML frontmatter block
metaData collected by plugins, such as a table of contents or summary
nodesParsed Markdown and component content

Frontmatter

Comark parses a leading YAML block into document.frontmatter:

---
title: Getting Started
draft: false
---

# Hello

Plugin Metadata

Plugins store derived document data in document.meta. For example, the table-of-contents plugin adds meta.toc:

import { parseMarkdown } from 'comark'
import toc from 'comark/plugins/toc'

const document = await parseMarkdown('# Guide\n\n## Install', {
  plugins: [toc()],
})

console.log(document.meta.toc)

Plugin types flow into MarkdownDocument, so metadata and frontmatter can stay strongly typed throughout your application.

Node Model

Comark uses compact tuples instead of an object for every syntax node. A Node is a text string, an element tuple, or a comment tuple:

type Node = TextNode | ElementNode | CommentNode

type TextNode = string

type ElementNode = [string, ElementNodeAttributes, ...Node[]]

type CommentNode = [null, ElementNodeAttributes, string]

This format follows the shape of rendered markup: the tag is at index 0, attributes are at index 1, and child nodes start at index 2.

Text Nodes

Text is stored directly as a string:

"Hello, world!"

Element Nodes

Elements use [tag, attributes, ...children]:

This is **important**.

Markdown elements use familiar HTML tag names such as h1, p, strong, a, ul, and li. Comark components use their component name as the tag:

::alert{type="warning"}
Check your configuration.
::

Attributes

Element attributes are stored in a key-value object. The optional $ field contains parser metadata used by renderers and tooling:

interface ElementNodeAttributes {
  [key: string]: unknown
  $?: {
    line?: number
    html?: 0 | 1
    block?: 0 | 1
  }
}

Treat $ as structural metadata. Component props and standard HTML attributes live alongside it.

Comment Nodes

Comments use null as their tag:

<!-- Keep this note -->

Parse Once, Render Many

Every renderer accepts the same MarkdownDocument. Parsing once is useful for server rendering, build pipelines, content APIs, caches, and multiple output targets.

import { parseMarkdown } from 'comark'
import { renderHtmlFromDocument } from '@comark/html'
import { renderAnsiFromDocument } from '@comark/ansi'

const document = await parseMarkdown(source)

const html = await renderHtmlFromDocument(document)
const ansi = await renderAnsiFromDocument(document)

Framework renderers expose a MarkdownDocument component for the same workflow:

<MarkdownDocument :value="document" />

These components render a parsed document without importing or running the parser in the client bundle.

Inspect Documents

Use the utilities from comark/utils instead of writing traversal logic for common operations.

Extract Text

textContent() returns the plain text inside a node:

text-content.ts
import { textContent } from 'comark/utils'

const heading = document.nodes[0]
console.log(textContent(heading)) // Hello world

Visit Nodes

visit() walks every node that matches a checker. A visitor can inspect, replace, or remove matching nodes:

external-links.ts
import type { ElementNode, Node } from 'comark'
import { visit } from 'comark/utils'

const isExternalLink = (node: Node): node is ElementNode => {
  return Array.isArray(node) && node[0] === 'a' && String(node[1].href).startsWith('https://')
}

visit(document, isExternalLink, (node) => {
  return [node[0], { ...node[1], target: '_blank', rel: 'noopener' }, ...node.slice(2)]
})

The visitor mutates document.nodes when a node is replaced or removed. Create a copy first when your application requires immutable updates.

Serialize and Cache Documents

A MarkdownDocument contains only plain arrays, objects, strings, and values contributed by plugins. You can send it over an API boundary or persist it as JSON:

import type { MarkdownDocument } from 'comark'

const document = await parseMarkdown(source)

await cache.set(key, JSON.stringify(document))

const cachedDocument = JSON.parse(await cache.get(key)) as MarkdownDocument

Use the same Comark and plugin versions when reading persisted documents. Rebuild cached documents after an upgrade that changes node output or plugin metadata.

Type Documents

Pass metadata and frontmatter types to MarkdownDocument when documents cross application boundaries:

interface ArticleMeta {
  readingTime: number
}

interface ArticleFrontmatter {
  title: string
  description?: string
}

type ArticleDocument = MarkdownDocument<ArticleMeta, ArticleFrontmatter>

Parser plugins can contribute these types automatically when you parse content directly.

Use “AST” when discussing the node representation itself. Use “document” for the root object returned by the parser and accepted by renderers.

const document = await parseMarkdown(markdown)
return <MarkdownDocument value={document} />