Plugin API
The comark/parse module exports defineComarkPlugin — a typed factory wrapper for building plugins that extend the parser.
defineComarkPlugin(factory)
Wraps a plugin factory function to provide type safety for both options and the returned plugin.
Parameters:
factory- A function(options?: O) => ComarkPluginwhereOis an optional options type. When the plugin is instantiated,optionsreceives the values passed by the caller. SeeComarkPluginfor 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:
import { defineComarkPlugin } from 'comark/parse'
export default defineComarkPlugin(() => ({
name: 'my-plugin',
pre(state) {},
post(state) {},
}))With typed options:
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:
- Inside
post(state),state.tree.metaandstate.tree.frontmatterare typed as the declared shape, so writes are checked against it. - At the
parsecallsite, the contribution is intersected into the resultingtree.meta/tree.frontmatter—result.meta.tocreads back as the declared type instead ofany.
Declaring a contribution
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:
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 createParse infer the plugins tuple and intersect each plugin's contribution into the result type. No explicit generic argument is needed:
import { parse } from 'comark'
import toc from 'comark/plugins/toc'
import summary from 'comark/plugins/summary'
const tree = await parse(content, { plugins: [toc(), summary()] })
tree.meta.toc // Toc
tree.meta.summary // ComarkNode[]
tree.meta.something // unknown — undeclared keys are typed as unknownBack-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).
// 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 ComarkTree
│
▼
┌──────────┐
│ post() │ ← Transform the AST after parsing
└──────────┘
│
▼
ComarkTreepre(state)
Called before markdown is tokenized. Use it to transform the raw markdown string.
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 | ParseOptions | The parser configuration |
post(state)
Called after the AST is built. Use it to traverse nodes or populate meta with extracted data.
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 | ComarkTree<TMeta, TFrontmatter> | The parsed AST — modify to transform output. tree.meta / tree.frontmatter are typed against the plugin's declared contribution. |
options | ParseOptions | The parser configuration |
tokens | unknown[] | The raw markdown-it tokens |
ComarkPlugin<TMeta, TFrontmatter>
| Property | Type | Description |
|---|---|---|
name | string | A unique identifier for the plugin |
markdownItPlugins | MarkdownItPlugin[] | markdown-it plugins to register on the parser — see Markdown-it Plugins |
pre | (state: ComarkParsePreState) => void | Hook called before parsing |
post | (state: ComarkParsePostState<TMeta, TFrontmatter>) => void | Hook called after the AST is built. state.tree.meta / state.tree.frontmatter are typed as the declared contribution — see Typed contributions. |
Usage
Pass plugins to parse() or the <Comark> component:
import { parse } from 'comark'
import myPlugin from './my-plugin'
const tree = await parse(content, {
plugins: [myPlugin()]
})<script setup lang="ts">
import { Comark } from '@comark/vue'
import myPlugin from './my-plugin'
</script>
<template>
<Comark :plugins="[myPlugin()]">{{ content }}</Comark>
</template>import { Comark } from '@comark/react'
import myPlugin from './my-plugin'
<Comark plugins={[myPlugin()]}>{content}</Comark><script>
import { Comark } from '@comark/svelte'
import myPlugin from './my-plugin'
</script>
<Comark markdown={content} plugins={[myPlugin()]} />