Creating Plugins
Comark plugins let you extend the parser with new syntax and transform the resulting AST. Every plugin is a simple object that conforms to the ComarkPlugin interface.
Plugin Interface
Use the defineComarkPlugin helper to create plugins with full type safety and editor autocomplete:
import { defineComarkPlugin } from 'comark/parse'export default defineComarkPlugin(() => ({ name: 'my-plugin', markdownItPlugins: [], // Optional: extend the markdown-it parser pre(state) {}, // Optional: run before parsing post(state) {}, // Optional: run after parsing}))| Property | Description |
|---|---|
name | A unique identifier for the plugin |
markdownItPlugins | Array of markdown-it plugins to register on the parser |
pre | Hook called before markdown is tokenized. Receives ComarkParsePreState |
post | Hook called after the AST is built. Receives ComarkParsePostState |
defineComarkPlugin wraps the entire factory function, providing type safety for both the options and the returned plugin:
import { defineComarkPlugin } from 'comark/parse'export default defineComarkPlugin((options: MyOptions = {}) => ({ name: 'my-plugin', // ...}))Plugin Lifecycle
Plugins hook into two phases of the parsing pipeline:
Markdown string │ ▼┌──────────┐│ pre() │ ← Modify raw markdown before parsing└──────────┘ │ ▼ Parse & Build ComarkTree AST │ ▼┌──────────┐│ post() │ ← Transform the AST after parsing└──────────┘ │ ▼ ComarkTreePre Hook
The pre hook runs before tokenization. Use it to transform the raw markdown string.
import { defineComarkPlugin } from 'comark/parse'export default defineComarkPlugin(() => ({ name: 'remove-comments', pre(state) { // state.markdown — the raw markdown input state.markdown = state.markdown.replace(/<!--[\s\S]*?-->/g, '') },}))ComarkParsePreState:
| Field | Type | Description |
|---|---|---|
markdown | string | The raw markdown — modify it to change parser input |
options | ParseOptions | The parser configuration |
Post Hook
The post hook runs after the AST is built. Use it to traverse and transform nodes, or to 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:
| Field | Type | Description |
|---|---|---|
markdown | string | The original markdown input |
tree | ComarkTree | The parsed AST — modify it to transform output |
options | ParseOptions | The parser configuration |
tokens | unknown[] | The raw markdown-it tokens |
Markdown-it Compatibility
Comark's default parser is built on top of markdown-it (via markdown-exit, a maintained fork). This means most markdown-it plugins work with Comark with minimal effort.
When you provide markdownItPlugins in a Comark plugin, they are registered directly on the underlying markdown-it instance. This gives them full access to the markdown-it API, including:
- Inline rules —
md.inline.ruler - Block rules —
md.block.ruler - Core rules —
md.core.ruler - Renderer rules —
md.renderer.rules(limited use — Comark uses its own rendering pipeline)
Using Existing markdown-it Plugins
To use an existing markdown-it plugin, wrap it with defineComarkPlugin:
import { defineComarkPlugin } from 'comark/parse'import markdownItSub from 'markdown-it-sub'import markdownItSup from 'markdown-it-sup'const subscript = defineComarkPlugin(() => ({ name: 'subscript', markdownItPlugins: [markdownItSub],}))const superscript = defineComarkPlugin(() => ({ name: 'superscript', markdownItPlugins: [markdownItSup],}))Then pass them to parse() or the <Comark> component:
import { parse } from 'comark'const tree = await parse('H~2~O is water, 2^10^ is 1024', { plugins: [subscript(), superscript()]})<script setup lang="ts">import { Comark } from '@comark/vue'const plugins = [subscript(), superscript()]</script><template> <Comark :plugins="plugins">H~2~O is water</Comark></template>What Works
Most markdown-it plugins that add parsing rules (inline, block, or core) work out of the box:
- Plugins that add new inline syntax (e.g., subscript, superscript, mark, insert)
- Plugins that add new block syntax (e.g., containers, definition lists, footnotes)
- Plugins that transform tokens via core rules (e.g., abbreviations, replacements)
What Doesn't Work
Plugins that rely on markdown-it's renderer will not work as expected, because Comark uses its own rendering pipeline instead of md.renderer:
- Plugins that only modify
md.renderer.rulesto change HTML output — their rendering logic will be ignored - Plugins that depend on renderer state or environment variables set during rendering
If a plugin only customizes rendering, you can handle the rendering side in Comark using custom components:
<script setup lang="ts">import { Comark } from '@comark/vue'// Register a custom component for the element the plugin producesconst components = { mark: (props, { slots }) => h('mark', { class: 'highlight' }, slots.default?.())}</script><template> <Comark :components="components">==highlighted text==</Comark></template>import { Comark } from '@comark/react'const components = { mark: ({ children, ...props }) => <mark className="highlight" {...props}>{children}</mark>}<Comark components={components}>{content}</Comark>markdown-exit vs markdown-it
Comark uses markdown-exit (a fork of markdown-it) as its base parser. The fork maintains API compatibility with markdown-it, so plugins written for markdown-it work without modification. The type MarkdownItPlugin accepted by Comark is:
type MarkdownItPlugin = (md: MarkdownIt) => voidThis is the standard markdown-it plugin signature — a function that receives the markdown-it instance and modifies it.
Creating a markdown-it Parser Plugin
To add new syntax, write a markdown-it plugin and include it in markdownItPlugins. Here's a complete example that adds ==highlighted text== syntax:
import type { MarkdownItPlugin } from 'comark'import { defineComarkPlugin } from 'comark/parse'const highlightRule = (state: any, silent: boolean) => { const start = state.pos const max = state.posMax // Must start with '==' if (start + 1 >= max) return false if (state.src.charCodeAt(start) !== 0x3D /* = */) return false if (state.src.charCodeAt(start + 1) !== 0x3D /* = */) return false // Find closing '==' let pos = start + 2 while (pos + 1 < max) { if (state.src.charCodeAt(pos) === 0x3D && state.src.charCodeAt(pos + 1) === 0x3D) { if (!silent) { const token = state.push('mark_open', 'mark', 1) token.markup = '==' state.pos = start + 2 state.posMax = pos state.md.inline.tokenize(state) state.push('mark_close', 'mark', -1) state.posMax = max } state.pos = pos + 2 return true } pos++ } return false}const markdownItHighlight: MarkdownItPlugin = (md) => { md.inline.ruler.before('emphasis', 'mark', highlightRule)}export default defineComarkPlugin(() => ({ name: 'highlight', markdownItPlugins: [markdownItHighlight],}))The markdown-it plugin adds a parsing rule, and Comark automatically converts the resulting tokens into AST nodes. In this case, ==hello== becomes:
["mark", {}, "hello"]Creating an AST Transform Plugin
AST transform plugins use the post hook and the visit() utility to traverse and modify nodes.
The visit() Utility
import { visit } from 'comark/utils'visit(tree, checker, visitor)| Parameter | Type | Description |
|---|---|---|
checker | (node: ComarkNode) => boolean | Return true for nodes to process |
visitor | (node: ComarkNode) => ComarkNode | false | void | Transform the node |
The visitor can:
- Return nothing — leave the node unchanged
- Return a new node — replace the node
- Return
false— remove the node from the tree
Example: Auto-linking URLs
import type { ComarkNode } from 'comark'import { defineComarkPlugin } from 'comark/parse'import { visit } from 'comark/utils'export default defineComarkPlugin(() => ({ name: 'auto-link', post(state) { const urlPattern = /https?:\/\/[^\s]+/g visit(state.tree, (node) => typeof node === 'string', (node) => { const text = node as string if (!urlPattern.test(text)) return // Replace text node with a link element return ['a', { href: text }, text] as ComarkNode } ) },}))Example: Adding Classes to Elements
import { defineComarkPlugin } from 'comark/parse'import { visit } from 'comark/utils'export default defineComarkPlugin(() => ({ name: 'styled-tables', post(state) { visit(state.tree, (node) => Array.isArray(node) && node[0] === 'table', (node) => { const el = node as [string, Record<string, any>, ...any[]] el[1].class = [el[1].class, 'styled-table'].filter(Boolean).join(' ') } ) },}))The ComarkTree Structure
Understanding the AST is key to writing transform plugins.
interface ComarkTree { nodes: ComarkNode[] // The AST nodes frontmatter: Record<string, any> // Parsed YAML frontmatter meta: Record<string, any> // Plugin-contributed metadata}Nodes come in three forms:
// Text node — a plain string"Hello world"// Element node — [tag, attributes, ...children]["strong", {}, "bold text"]// Comment node — [null, {}, content][null, {}, "this is a comment"]Elements can be nested:
// <p>Hello <strong>world</strong></p>["p", {}, "Hello ", ["strong", {}, "world"]]Using Your Plugin
import { parse } from 'comark'import myPlugin from './my-plugin'const tree = await parse(content, { plugins: [myPlugin()]})Or with framework components:
<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()]} />