---
title: "Parse API"
description: "Parse Markdown into a compact, serializable document with parseMarkdown() or createMarkdownParser(), on the server, in the browser, or from a stream."
canonical_url: "https://comark.dev/reference/parse"
---
# Parse API

> Parse Markdown into a compact, serializable document with parseMarkdown() or createMarkdownParser(), on the server, in the browser, or from a stream.

## `parseMarkdown(source, options?)`{lang="ts"}

Parses Markdown from a string and returns a complete `MarkdownDocument`. Default plugins add frontmatter, alerts, task lists, HTML, components, and attributes to the standard Markdown parser.

**Parameters:**

- `source` - The Markdown content as a string
- `options?` - Parser options including plugins

**Returns:** `MarkdownDocument` object containing:

- `nodes` - The parsed Markdown AST nodes
- `frontmatter` - Frontmatter data parsed from YAML
- `meta` - Additional metadata from plugins (for example, `toc`, `summary`)

**Example:**

<code-group>
~~~typescript [parse.ts]
import { parseMarkdown } from 'comark'

const content = `---
title: Hello World
---

This is a simple example
`

const result = await parseMarkdown(content)

console.log(result)
~~~


```json [Output]
{
  "nodes": [
    ["p", {}, "This is a simple example"]
  ],
  "frontmatter": {
    "title": "Hello World"
  },
  "meta": {}
}
```
</code-group>

<tip>
For the complete parsed document and node types, see the [Document Model](https://comark.dev/getting-started/document-model#node-model).
</tip>

### Frontmatter

The parse function automatically extracts and parses YAML frontmatter:

<code-group>
~~~typescript [parse.ts]
const content = `---
title: My Document
tags:
  - javascript
  - markdown
author:
  name: John Doe
  email: john@example.com
---

# Content here
`

const result = await parseMarkdown(content)
console.log(result.frontmatter)
~~~


```json [Output]
{
  "title": "My Document",
  "tags": ["javascript", "markdown"],
  "author": { "name": "John Doe", "email": "john@example.com" }
}
```
</code-group>

### Table of contents

Register the [toc plugin](https://comark.dev/plugins/built-in/toc) to generate a table of contents based on headings:

<code-group>
~~~typescript [parse.ts]
import toc from 'comark/plugins/toc'

const content = `# Main Title

## Section 1

Some content here.

### Subsection 1.1

More content.

## Section 2

Final content.
`

const result = await parseMarkdown(content, { plugins: [toc()] })
console.log(result.meta.toc)
~~~


```json [Output]
{
  "title": "Main Title",
  "depth": 2,
  "searchDepth": 2,
  "links": [
    { "id": "section-1", "text": "Section 1", "depth": 2, "children": [
      { "id": "subsection-11", "text": "Subsection 1.1", "depth": 3 }
    ]},
    { "id": "section-2", "text": "Section 2", "depth": 2 }
  ]
}
```
</code-group>

### HTML parsing

HTML tags embedded in Comark content are parsed into AST nodes by default and can be mixed freely with Comark components and markdown syntax.

<code-group>
~~~typescript [parse.ts]
const content = `
<div class="note">
  ::alert{type="info"}
  Hello <strong class="text-red-500">world</strong>
  ::
</div>
`

const result = await parseMarkdown(content)
console.log(result.nodes)
~~~


```json [Output]
[
  ["div", { "class": "note" },
    ["alert", { "type": "info" },
      "Hello ",
      ["strong", { "class": "text-red-500" }, "world"]
    ]
  ]
]
```
</code-group>

HTML parsing is provided by the built-in [`html` plugin](https://comark.dev/plugins/defaults/html).

### Summary

<tip>
Summary extraction requires the [summary plugin](https://comark.dev/plugins/built-in/summary).
</tip>

Content before the `<!-- more -->` comment is extracted as a summary when using the summary plugin:

~~~typescript [parse.ts]
import { parseMarkdown } from 'comark'
import summary from 'comark/plugins/summary'

const content = `# Article Title

This is the introduction paragraph that will be used as a summary.

<!-- more -->

This is the full article content that won't appear in the summary.
`

const result = await parseMarkdown(content, {
  plugins: [summary()]
})

console.log(result.meta.summary)
// Node[] with only the content before <!-- more -->
~~~

---

## `createMarkdownParser(options?)`{lang="ts"}

Creates a reusable parser function with pre-configured options. Unlike `parseMarkdown()` which creates a new parser instance on each call, `createMarkdownParser()` returns a parser function that can be called multiple times with the same configuration.

**Parameters:**

- `options?` - Parser options (same as `parseMarkdown()`)

**Returns:** An async parser function `(source: string) => Promise<MarkdownDocument>`

**Example:**

```typescript [parse.ts]
import { createMarkdownParser } from 'comark'
import shiki from 'comark/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import emoji from 'comark/plugins/emoji'
import toc from 'comark/plugins/toc'

// Create a parser with specific configuration
const parse = createMarkdownParser({
  autoUnwrap: true,
  autoClose: true,
  plugins: [
    shiki({
      themes: { light: githubLight, dark: githubDark }
    }),
    emoji(),
    toc()
  ]
})

// Reuse the parser for multiple documents
const doc1 = await parse('# Document 1\n\nContent...')
const doc2 = await parse('# Document 2\n\nMore content...')
const doc3 = await parse('# Document 3\n\nEven more...')
```

### Use cases

Here are some use cases for `createMarkdownParser()`:

#### Static site generator

Use `Promise.all` to parse all files in parallel. Since `createMarkdownParser()` initializes the parser once, the returned function is safe to call concurrently.

```typescript [build.ts]
import { createMarkdownParser } from 'comark'
import { readdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { renderHtmlFromDocument } from '@comark/html'
import shiki from 'comark/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import toc from 'comark/plugins/toc'
import emoji from 'comark/plugins/emoji'

async function buildSite(contentDir: string, outDir: string) {
  // Create parser once with all desired plugins
  const parse = createMarkdownParser({
    plugins: [
      shiki({
        themes: { light: githubLight, dark: githubDark }
      }),
      toc({ depth: 3 }),
      emoji()
    ]
  })

  const files = await readdir(contentDir)
  const mdFiles = files.filter(f => f.endsWith('.md'))

  // Parse all files in parallel with the same parser instance
  await Promise.all(
    mdFiles.map(async (file) => {
      const content = await readFile(join(contentDir, file), 'utf-8')
      const doc = await parse(content)
      const html = await renderHtmlFromDocument(doc)
      await writeFile(join(outDir, file.replace('.md', '.html')), html)
    })
  )

  console.log(`Built ${mdFiles.length} pages`)
}

await buildSite('./content', './dist')
```

#### API server

```typescript [server.ts]
import { createMarkdownParser } from 'comark'
import security from 'comark/plugins/security'

// Create parser once when server starts
const parse = createMarkdownParser({
  plugins: [
    security() // Sanitize user-generated content
  ]
})

// Reuse parser for every request
app.post('/api/markdown', async (req, res) => {
  try {
    const tree = await parse(req.body.content)
    res.json({ success: true, tree })
  } catch (error) {
    res.status(400).json({ error: 'Invalid markdown' })
  }
})
```

### Benchmark

Using `createMarkdownParser()` has several benefits over calling `parseMarkdown()` multiple times:

- **Performance**: Parser and plugins are initialized once, not on every parse
- **Consistency**: All documents parsed with the same configuration
- **Memory efficiency**: Single parser instance handles multiple documents
- **Ideal for batch processing**: Perfect when parsing many files

```typescript [benchmark.ts]
import { parseMarkdown, createMarkdownParser } from 'comark'
import shiki from 'comark/plugins/shiki'

const content = '```js\nconsole.log("hello")\n```'

// ❌ Slow: Creates new parser + highlighter for each parse
console.time('parse x1000')
for (let i = 0; i < 1000; i++) {
  await parseMarkdown(content, {
    plugins: [shiki()]
  })
}
console.timeEnd('parse x1000')
// → ~8000ms (parser + highlighter recreated 1000 times)

// ✅ Fast: Reuses same parser + highlighter instance
console.time('createMarkdownParser x1000')
const parse = createMarkdownParser({
  plugins: [shiki()]
})
for (let i = 0; i < 1000; i++) {
  await parse(content)
}
console.timeEnd('createMarkdownParser x1000')
// → ~800ms (10x faster! parser + highlighter created once)
```

---

## Options

Both `parseMarkdown()` and `createMarkdownParser()` accept the same `ParserOptions`:

| Option                   | Type                                      | Default     | Description                                                                                                                                                                                                                                                                                                |
| ------------------------ | ----------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autoUnwrap`             | `boolean`                                 | `true`      | Remove unnecessary `<p>` wrappers from single-element containers                                                                                                                                                                                                                                           |
| `autoClose`              | `boolean \| (markdown: string) => string` | `true`      | Auto-close incomplete markdown syntax, or use a custom completion function                                                                                                                                                                                                                                 |
| `unwrap`                 | `boolean \| string \| string[]`           | `false`     | Remove wrapper tags from the tree, hoisting their children (MDC `unwrap` behaviour). `true` unwraps `p`; a comma/whitespace-separated string or array unwraps the listed tags; `'*'` matches any tag. Tags apply sequentially (each descends one level), and adjacent text is merged into a single string. |
| `html`                   | `boolean`                                 | `true`      | **Deprecated** (warns). Prefer `registerDefaultPlugins: false` and register `html()` explicitly. `html: false` still skips the default html plugin.                                                                                                                                                        |
| `linkify`                | `boolean`                                 | `true`      | Auto-convert URL-like text into links. Set `false` to disable                                                                                                                                                                                                                                              |
| `headingIds`             | `boolean`                                 | `true`      | Auto-generate `id` attributes for `h1`–`h6` headings. Set `false` to disable                                                                                                                                                                                                                               |
| `registerDefaultPlugins` | `boolean`                                 | `true`      | Register the built-in default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`). Set `false` to disable them.                                                                                                                                                              |
| `plugins`                | `ComarkPlugin[]`                          | `[]`        | Ordered plugins to run after the defaults. A same-name plugin replaces its default; duplicate explicit names keep the first instance. See [Default plugins](https://comark.dev/plugins#default-plugins).                                                                                                   |
| `tracer`                 | `ComarkTracer`                            | `undefined` | Timing recorder for the parse pipeline — see [Timing the parse](#timing-the-parse)                                                                                                                                                                                                                         |

### Timing the parse

Pass a `tracer` to time each phase of the pipeline and every plugin hook, so you can see where parse time goes (for example, a slow `post` shiki hook). The contract is a structural subset of [OpenTelemetry](https://opentelemetry.io/docs/languages/js/instrumentation/#creating-spans) `Tracer` — `startSpan` and `startActiveSpan` — so a real OTel tracer works as-is:

```ts
import { trace } from '@opentelemetry/api'
import shiki from 'comark/plugins/shiki'

const parse = createMarkdownParser({
  // Uses the OpenTelemetry provider registered by your app or hosting platform.
  tracer: trace.getTracer('comark'),
  plugins: [shiki()],
})
```

<note>
`@opentelemetry/api` defines the tracing API but does not export spans by itself. `trace.getTracer()` uses the globally registered OpenTelemetry provider; without one, it returns a no-op tracer. Hosting platforms often register and configure that provider for you. Otherwise, initialize an OpenTelemetry SDK and exporter before creating the parser.
</note>

See the [complete Node.js example](https://github.com/comarkdown/comark/blob/main/examples/3.cli/perf-trace/otel.ts), including an OTLP/HTTP exporter and commands for viewing traces locally with `otel-front`.

Or a minimal recorder:

```ts
const spans: { name: string, duration: number }[] = []
const tracer = {
  startSpan(name) {
    const start = performance.now()
    return { end: () => spans.push({ name, duration: performance.now() - start }) }
  },
  startActiveSpan(name, optionsOrFn, maybeFn) {
    const fn = typeof optionsOrFn === 'function' ? optionsOrFn : maybeFn
    const span = tracer.startSpan(name)
    // Nested startActiveSpan/startSpan calls become children via your context/stack.
    return fn(span) // caller (comark) ends the span
  },
}

const parse = createMarkdownParser({ tracer, plugins: [shiki()] })
await parse(markdown)
// spans → comark:parse
//           ├─ comark:autoclose
//           ├─ comark:pre:frontmatter
//           ├─ comark:tokenize
//           ├─ comark:nodes
//           ├─ comark:post:alert
//           └─ comark:post:shiki
//           …
```

Recorded spans: a root `comark:parse` active span enclosing `comark:autoclose`, `comark:tokenize` (markdown parsing), `comark:nodes` (token → AST conversion and unwrapping), and `comark:pre:<name>` / `comark:post:<name>` for each plugin hook. Nested `startActiveSpan` calls form the parent → child hierarchy (OTel active context, or a stack in a simple recorder). There is no timing overhead when `tracer` is omitted, and no Node-specific API is used — it works in the browser too.

### Inline rendering

Use `unwrap: 'p'` (or `unwrap: true`) to render markdown without the
wrapping `<p>`, which is handy for buttons, badges, and other inline hosts.
Adjacent text is merged into a single string, matching MDC's `unwrap`:

```ts
await parseMarkdown('Hello **world**', { unwrap: 'p' })
// nodes: ['Hello ', ['strong', {}, 'world']]

await parseMarkdown('a\n\nb', { unwrap: true })
// nodes: ['ab']   (paragraphs merged, no separator)
```

Tags are applied **sequentially**, each descending one level into the result of
the previous one — so a space-separated (or comma-separated) list peels nested
wrappers. `'*'` matches any tag:

```ts
// Unwrap the <ul>, then the <li> inside it
await parseMarkdown('- Buy milk', { unwrap: 'ul li' })
// nodes: ['Buy milk']
```

On the framework components this is exposed as an `unwrap` shorthand prop:

```vue
<UButton><Markdown :value="text" unwrap /></UButton>
<UButton><Markdown :value="text" unwrap="ul li" /></UButton>
<UButton><Markdown :value="text" :options="{ unwrap: 'p' }" /></UButton>
```

---

---

- [Document Model](https://comark.dev/getting-started/document-model)
- [Render API](https://comark.dev/reference/render)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
