---
title: "Plugin API"
description: "Type-safe API for creating Comark plugins with pre/post lifecycle hooks."
canonical_url: "https://comark.dev/plugins/custom/plugin-api"
---
# Plugin API

> Type-safe API for creating Comark plugins with pre/post lifecycle hooks.

The `comark/parse` module exports `defineComarkPlugin`, a typed factory wrapper for building plugins that extend the parser.

## `defineComarkPlugin(factory)`{lang="ts"}

Wraps a plugin factory function to provide type safety for both options and the returned plugin.

**Parameters:**

- `factory` - A function `(options?: O) => ComarkPlugin` where `O` is an optional options type. When the plugin is instantiated, `options` receives the values passed by the caller. See [`ComarkPlugin`](#comarkplugin) for the full shape of the returned object.

**Type parameters:**

| Parameter      | Default   | Description                                                                                                      |
| -------------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
| `Options`      | `unknown` | Shape of the options the factory accepts.                                                                        |
| `TMeta`        | `{}`      | Keys this plugin contributes to `tree.meta`. Surfaces on the `parse` result and constrains writes inside `post`. |
| `TFrontmatter` | `{}`      | Keys this plugin contributes to `tree.frontmatter`. This is experimental and may change in future versions.      |

**Returns:** A typed plugin factory `(options?: O) => ComarkPlugin<TMeta, TFrontmatter>`

**Example:**

```typescript
import { defineComarkPlugin } from 'comark/parse'

export default defineComarkPlugin(() => ({
  name: 'my-plugin',
  pre(state) {},
  post(state) {},
}))
```

With typed options:

```typescript
import { defineComarkPlugin } from 'comark/parse'

interface MyOptions {
  prefix?: string
}

export default defineComarkPlugin((options: MyOptions = {}) => ({
  name: 'my-plugin',
  post(state) {
    // options.prefix is typed
  },
}))
```

---

## Typed contributions

Plugins that write to `tree.meta` can declare what they contribute via the second and third type parameters of `defineComarkPlugin`. Two things follow from that declaration:

1. **Inside `post(state)`**, `state.tree.meta` and `state.tree.frontmatter` are typed as the declared shape, so writes are checked against it.
2. **At the `parse` callsite**, the contribution is intersected into the resulting `tree.meta` / `tree.frontmatter`: `result.meta.toc` reads back as the declared type instead of `any`.

### Declaring a contribution

```typescript
import { defineComarkPlugin } from 'comark/parse'

interface TocTree {
  title: string
  depth: number
  links: { id: string; text: string }[]
}

export default defineComarkPlugin<{}, { toc: TocTree }>(() => ({
  name: 'toc',
  post(state) {
    state.tree.meta.toc = { title: 'Contents', depth: 2, links: [] }

    // @ts-expect-error wrong value type for the declared key
    state.tree.meta.toc = 123

    // @ts-expect-error `summary` was not declared in the contribution
    state.tree.meta.summary = []
  },
}))
```

A plugin that contributes more than one key lists them all in the same object literal:

```typescript
defineComarkPlugin<HeadingsOptions, { title?: string; description?: string }>(() => ({
  name: 'headings',
  post(state) {
    state.tree.meta.title = 'Hello'
    state.tree.meta.description = undefined
  },
}))
```

### Inference at the `parse` callsite

`parse` and `createMarkdownParser` infer the plugins tuple and intersect each plugin's contribution into the result type. No explicit generic argument is needed:

```typescript
import { parseMarkdown } from 'comark'
import toc from 'comark/plugins/toc'
import summary from 'comark/plugins/summary'

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

tree.meta.toc       // Toc
tree.meta.summary   // Node[]
tree.meta.something // unknown: undeclared keys are typed as unknown
```

### Back-compat for unannotated plugins

Plugins that omit the meta/frontmatter type parameters keep the pre-typed free-form behavior: `state.tree.meta` is `Record<string, any>` and any key can be written. The result type on the `parse` callsite also stays `Record<string, any>` when no plugin declared a contribution (or when `plugins` is passed as a widened `ComarkPlugin[]` variable rather than an inline array).

```typescript
// Old style: still works, no narrowing
defineComarkPlugin(() => ({
  name: 'wordcount',
  post(state) {
    state.tree.meta.wordCount = 42 // ok, no declared contribution
  },
}))
```

---

## Lifecycle

Plugins hook into two phases of the parsing pipeline:

```
Markdown string
    │
    ▼
┌──────────┐
│   pre()  │  ← Modify raw markdown before parsing
└──────────┘
    │
    ▼
  Parse & Build MarkdownDocument
    │
    ▼
┌──────────┐
│  post()  │  ← Transform the AST after parsing
└──────────┘
    │
    ▼
  MarkdownDocument
```

### `pre(state)`{lang="ts"}

Called before markdown is tokenized. Use it to transform the raw markdown string.

```typescript
import { defineComarkPlugin } from 'comark/parse'

export default defineComarkPlugin(() => ({
  name: 'strip-comments',
  pre(state) {
    state.markdown = state.markdown.replace(/<!--[\s\S]*?-->/g, '')
  },
}))
```

**`ComarkParsePreState`:**

| Field      | Type                                                          | Description                                     |
| ---------- | ------------------------------------------------------------- | ----------------------------------------------- |
| `markdown` | `string`                                                      | The raw markdown; modify to change parser input |
| `options`  | [`ParserOptions`](https://comark.dev/reference/parse#options) | The parser configuration                        |

### `post(state)`{lang="ts"}

Called after the AST is built. Use it to traverse nodes or populate `meta` with extracted data.

```typescript
import { defineComarkPlugin } from 'comark/parse'
import { visit } from 'comark/utils'

export default defineComarkPlugin(() => ({
  name: 'word-count',
  post(state) {
    let count = 0
    visit(state.tree,
      (node) => typeof node === 'string',
      (node) => { count += (node as string).split(/\s+/).filter(Boolean).length }
    )
    state.tree.meta.wordCount = count
  },
}))
```

**`ComarkParsePostState<TMeta, TFrontmatter>`:**

| Field      | Type                                                                                                            | Description                                                                                                                                |
| ---------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `markdown` | `string`                                                                                                        | The original markdown input                                                                                                                |
| `tree`     | [`MarkdownDocument<TMeta, TFrontmatter>`](https://comark.dev/getting-started/document-model#document-structure) | The parsed document; modify it to transform output. `tree.meta` / `tree.frontmatter` are typed against the plugin's declared contribution. |
| `options`  | [`ParserOptions`](https://comark.dev/reference/parse#options)                                                   | The parser configuration                                                                                                                   |
| `tokens`   | `unknown[]`                                                                                                     | The raw markdown-it tokens                                                                                                                 |

<tip to="https://comark.dev/plugins/custom/ast-api">
To traverse and transform `tree` nodes, see the AST API page for the `visit()` utility.
</tip>

---

## `ComarkPlugin<TMeta, TFrontmatter>`

| Property            | Type                                                                          | Description                                                                                                                                                               |
| ------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`              | `string`                                                                      | A unique identifier for the plugin                                                                                                                                        |
| `markdownItPlugins` | [`MarkdownItPlugin[]`](https://markdown-it.github.io/markdown-it/#MarkdownIt) | markdown-it plugins to register on the parser, see [Markdown-it Plugins](https://comark.dev/plugins/custom/markdown-it)                                                   |
| `pre`               | `(state: ComarkParsePreState) => Promise<void> \| void`                       | Hook called before parsing                                                                                                                                                |
| `post`              | `(state: ComarkParsePostState<TMeta, TFrontmatter>) => Promise<void> \| void` | Hook called after the AST is built. `state.tree.meta` / `state.tree.frontmatter` are typed as the declared contribution, see [Typed contributions](#typed-contributions). |

---

## Usage

Pass plugins to `parseMarkdown()` or the `<Markdown>` component:

<code-group>
```typescript [Parse API]
import { parseMarkdown } from 'comark'
import myPlugin from './my-plugin'

const tree = await parseMarkdown(content, {
  plugins: [myPlugin()]
})
```


```vue [Vue]
<script setup lang="ts">
import { Markdown } from '@comark/vue'
import myPlugin from './my-plugin'
</script>

<template>
  <Markdown :plugins="[myPlugin()]">{{ content }}</Markdown>
</template>
```


```tsx [React]
import { Markdown } from '@comark/react'
import myPlugin from './my-plugin'

<Markdown plugins={[myPlugin()]}>{content}</Markdown>
```


```svelte [Svelte]
<script>
  import { Markdown } from '@comark/svelte'
  import myPlugin from './my-plugin'
</script>

<Markdown value={content} plugins={[myPlugin()]} />
```
</code-group>

---

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


## Sitemap

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