Document Model
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[]
}| Field | Description |
|---|---|
frontmatter | Structured data from the leading YAML frontmatter block |
meta | Data collected by plugins, such as a table of contents or summary |
nodes | Parsed Markdown and component content |
Frontmatter
Comark parses a leading YAML block into document.frontmatter:
---
title: Getting Started
draft: false
---
# Hellodocument.frontmatter
// { title: 'Getting Started', draft: false }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**.[
"p",
{},
"This is ",
["strong", {}, "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.
::[
"alert",
{ "type": "warning" },
["p", {}, "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 -->[null, {}, " 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" /><MarkdownDocument value={document} /><MarkdownDocument value={document} /><comark-markdown-document [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:
import { textContent } from 'comark/utils'
const heading = document.nodes[0]
console.log(textContent(heading)) // Hello worldVisit Nodes
visit() walks every node that matches a checker. A visitor can inspect, replace, or remove matching nodes:
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 MarkdownDocumentUse 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} />Installation
Install Comark and render your first Markdown with components in Vue, React, Svelte, Angular, or plain HTML in under 5 minutes.
Markdown
Comark supports all standard CommonMark and GitHub Flavored Markdown (GFM) features including headings, formatting, lists, tables, code blocks, and more.