Comark vs remark and unified
TL;DR: remark is the Markdown part of unified. It parses Markdown into mdast, transforms it with plugins, and converts it to HTML through hast. It's the foundation of MDX and react-markdown. Comark parses into a compact tuple document with component syntax, completes incomplete Markdown for streaming, and renders natively in Vue, React, Svelte, and Angular. Comark doesn't run remark or rehype plugins.
At a glance
| Comark | remark and unified | |
|---|---|---|
| Syntax tree | MarkdownDocument with tuple nodes | mdast for Markdown, hast for HTML |
| Spec compliance | CommonMark and GFM with documented differences | Strict CommonMark, GFM with remark-gfm |
| Plugins | Comark plugins and markdown-it plugins | remark and rehype plugins |
| Tree transforms | visit() from comark/utils in a post hook | unist-util-visit and other unist utilities |
| Custom components in content | Yes, ::alert{type="info"} | Through extensions such as MDX or directives |
| Incomplete Markdown | autoClose completes the syntax | No handling in the core |
| Renderers | Vue, React, Svelte, Angular, Nuxt, HTML, ANSI | HTML with rehype-stringify, React with react-markdown |
| Raw HTML by default | Kept as written | Dropped unless you set allowDangerousHtml |
| Back to Markdown | renderMarkdown() | remark-stringify |
Two syntax trees or one document
unified works in stages. remark-parse creates mdast, remark plugins transform it, and remark-rehype converts it to hast. Then rehype plugins run, and rehype-stringify outputs HTML:
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm'
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
const file = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeStringify)
.process('# Hello **World**')Comark has one document. GFM tables, strikethrough, task lists, frontmatter, components, and raw HTML are parsed by default. Every renderer reads the same nodes:
import { parseMarkdown } from 'comark'
const document = await parseMarkdown('# Hello **World**')
// [['h1', { id: 'hello-world' }, 'Hello ', ['strong', {}, 'World']]]mdast nodes are objects with type, children, and fields such as depth. Comark nodes are arrays: a tag, an attributes object, then children. Text nodes are plain strings. The document is JSON-safe, so you can cache it or send it to a client. See the document model.
Transform the tree
Both projects let you change the tree after parsing. In remark, a plugin returns a transformer that visits mdast nodes. In Comark, a plugin uses visit() in a post hook.
import { visit } from 'unist-util-visit'
export default function remarkWordCount() {
return (tree, file) => {
let count = 0
visit(tree, 'text', (node) => {
count += node.value.split(/\s+/).filter(Boolean).length
})
file.data.wordCount = count
}
}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
},
}))remark plugins don't run in Comark, because the trees have different shapes. To port one, rewrite its transform with visit(), or look for a markdown-it plugin that adds the same syntax. See the AST API and the plugin API.
Components and streaming
In the unified ecosystem, components in Markdown usually mean MDX, which compiles JSX and JavaScript expressions. Comark components are data: ::alert{type="info"} becomes ['alert', { type: 'info' }, ...]. Your app maps the tag to a component, and nothing in the Markdown executes code. See Comark vs MDX.
remark expects complete input. For LLM output, Comark closes unterminated bold, links, tables, and components on every parse, and renders an open code fence as a growing code block. See AI chat streaming.
Security defaults
remark is cautious by default. remark-rehype drops raw HTML unless you set allowDangerousHtml and add rehype-raw. The unified docs recommend rehype-sanitize for untrusted input.
Comark keeps raw HTML, attributes, and component props as written. Markdown link syntax with a javascript:, vbscript:, or data:text/html URL doesn't become a link. For untrusted input, add the security plugin with a blockedTags list.
What remark does well
- Spec compliance: remark follows CommonMark strictly. Comark changes some output on purpose. See Differences from CommonMark and GFM.
- Plugin ecosystem: lint rules, tables of contents, math, MDX, and many rehype transforms. Comark has a smaller set of built-in plugins.
- Tooling beyond rendering: remark is also used to lint and format Markdown, not only to convert it to HTML.
- Safe defaults: raw HTML is dropped unless you allow it.
If your pipeline depends on remark or rehype plugins, or you need strict spec output, stay with unified.
FAQ
visit() for tree transforms.parseMarkdown() instead.Comark vs markdown-it
Comark is built on markdown-exit, a TypeScript rewrite of markdown-it. Compare their output, plugin compatibility, defaults, streaming, and framework rendering.
Comark vs markdown-to-jsx
markdown-to-jsx renders JSX tags in Markdown with zero dependencies and many output targets. Comark adds a component syntax with YAML props and named slots, streaming auto-close, and Svelte, Angular, and terminal renderers.