# Comark: Markdown Parser and Renderer for Any Framework > Open-source Markdown parser and renderer with components, attributes and plugins. Streams AI output to React, Vue, Svelte, Angular, HTML and the terminal. ::u-page-hero{orientation="horizontal"} :::landing-hero-demo{playground="/play" source="## Hello World A JavaScript library to **parse and render Markdown** anywhere. ```ts import { parseMarkdown } from 'comark' const doc = await parseMarkdown('# Hello **World**') ``` ## Features - CommonMark and GFM support - HTML, ANSI, and framework renderers - Streaming, components, and plugins ::callout{icon=\"i-lucide-info\"} Built on markdown-exit, a TypeScript rewrite of markdown-it. ::"} ::: #title The Markdown parser and renderer for everything. #description Open-source Markdown parser and renderer with components, attributes and plugins. Streams AI output to React, Vue, Svelte, Angular, HTML and the terminal. #links :::u-button{to="https://comark.dev/getting-started/introduction" size="lg" trailing-icon="i-lucide-arrow-right"} Get started ::: :::u-button ```yaml [props] icon: i-simple-icons-github color: neutral variant: ghost size: lg to: https://github.com/comarkdown/comark target: _blank ``` View on GitHub ::: :: ::landing-features #headline Renderers #title Parse once, render anywhere #description Comark parses Markdown into a compact [`MarkdownDocument`](https://comark.dev/getting-started/document-model). Parse on the server, during a build, or as content streams in, then render it natively in your framework. Your content outlasts your stack. #default :::landing-feature-card{icon="i-vscode-icons-file-type-html" to="https://comark.dev/rendering/html" color="#e34f26"} #title HTML #description A plain string renderer for static site generators, RSS feeds or emails. No framework required. ::: :::landing-feature-card{icon="i-lucide-terminal" to="https://comark.dev/rendering/ansi" color="var(--ui-text-highlighted)"} #title ANSI #description Render Markdown as styled terminal output using ANSI escape codes, perfect for CLIs, scripts, and developer tooling. ::: :::landing-feature-card{icon="i-logos-angular-icon" to="https://comark.dev/rendering/angular" color="#dd0031"} #title Angular #description Standalone components for Angular 17+, with the same props and streaming support. ::: :::landing-feature-card{icon="i-logos-react" to="https://comark.dev/rendering/react" color="#61dafb"} #title React #description The same component API for React, including React Server Components and Next.js. ::: :::landing-feature-card{icon="i-logos-vue" to="https://comark.dev/rendering/vue" color="#42b883"} #title Vue #description Drop the `Markdown` component in a template. Custom components and plugins are props. ::: :::landing-feature-card{icon="i-logos-svelte-icon" to="https://comark.dev/rendering/svelte" color="#ff3e00"} #title Svelte #description Native Svelte 5 rendering with runes. No wrapper framework, no `{@html}`. ::: :: ::landing-tabs ```yaml [props] items: - icon: i-lucide-radio title: Streaming component description: Chunks land mid-word, mid-token, mid-component. Flip one prop and every frame renders as finished markup, caret and all. - icon: i-lucide-scan-text title: Auto-close description: A stream can stop mid-bold, mid-fence, mid-component. By default, Comark auto-closes what is still open, with any renderer or on its own. ``` #headline Streaming #title Built for AI output #description A model streams Markdown a few characters at a time. Render it as it lands and your users watch raw asterisks appear, code fences hang open, and the layout jump on every token. Comark closes what is still open, so every frame renders as finished markup. #code-0 :::landing-streaming-demo ::: #code-1 :::landing-auto-close-demo ::: :: ::landing-tabs ```yaml [props] reverse: true items: - icon: i-lucide-code title: Shiki description: Syntax highlighting for code blocks, with multi-theme support and on-demand language loading. - icon: i-lucide-braces title: JSON Render description: Declarative UI specs in json-render code blocks, turned into real components. - icon: i-simple-icons-mermaid title: Mermaid description: Flowcharts and diagrams rendered from mermaid code blocks. ``` #headline Plugins #title Use official plugins, or write your own #description Official plugins add syntax highlighting, math, diagrams, table of contents, and more. Write your own against the plugin API or [browse all plugins](https://comark.dev/plugins). #code-0 ```ts [highlight.ts] import { parseMarkdown } from 'comark' import shiki from 'comark/plugins/shiki' import githubDark from '@shikijs/themes/github-dark' import githubLight from '@shikijs/themes/github-light' const document = await parseMarkdown(content, { plugins: [ shiki({ themes: { light: githubLight, dark: githubDark } }) ] }) ``` #code-1 ```tsx [Dashboard.tsx] import { Markdown } from '@comark/react' import jsonRender from '@comark/react/plugins/json-render' export function Dashboard({ content }: { content: string }) { return ( {content} ) } ``` #code-2 ```vue [Diagram.vue] ``` :: ::landing-tabs ```yaml [props] items: - icon: i-lucide-component title: Components description: Block and inline components with props, slots, and nested Markdown. - icon: i-lucide-tag title: Attributes description: Classes, IDs, and data attributes on any native Markdown element. - icon: i-lucide-list title: Frontmatter description: A leading YAML block parsed into metadata you can bind to component props. ``` #headline Syntax #title [Co]{.font-bold}[mponents in]{.text-muted} [Mark]{.font-bold}[down]{.text-muted} #description Discover our opt-in syntax for components, attributes, and frontmatter, handled by default plugins you can turn off. CommonMark and GFM are supported by default, so the Markdown files you already have keep working. #code-0 ```mdc [components.md] ::alert{type="info"} This is an **important** message. :: ::card{title="My Card"} Card content with full **markdown** support. :: Click the :button[Submit]{type="primary"} to continue. ``` #code-1 ```mdc [attributes.md] **bold text**{.highlight #important} [Read the docs](/getting-started/introduction){.button target="_blank"} ![Logo](logo.svg){.responsive width="800" height="600"} Wrap [any inline text]{.text-primary} in a span. ``` #code-2 ```mdc [article.md] --- title: My Article author: Jane Doe tags: - markdown --- # My Article Written by :badge{:label="frontmatter.author"}. ``` :: ::landing-faq ```yaml [props] items: - label: How is Comark different from MDX? content: MDX compiles Markdown to JSX at build time, tying content to a bundler and to React. Comark parses Markdown into serializable data at build time or runtime, then renders it to multiple targets. - label: Does Comark require a specific framework? content: No. The core parser is framework-free. Renderers exist for Vue, React, Svelte, and Angular, plus string output as HTML and ANSI. - label: Can I use markdown-it plugins with Comark? content: Yes. Comark is built on markdown-exit, a TypeScript rewrite of markdown-it that preserves its plugin API. Existing markdown-it plugins work alongside Comark plugins. - label: How does Comark handle streaming AI output? content: The auto-close parser completes unterminated syntax (bold, code fences, components) so every incomplete frame renders correctly. Framework renderers expose this as a streaming prop. - label: Do I have to use components? content: No. Plain CommonMark and GFM parse as standard Markdown, with a few documented differences. Components and attributes are opt-in syntax. - label: Is it free? content: Yes. MIT-licensed open source, maintained by Vercel. Your only costs are your own hosting. ``` #headline FAQ #title Common questions :: ::landing-cta #title From Markdown to UI. #description Install Comark, pick a renderer, and render your first Markdown document in minutes. Read [why Comark](https://comark.dev/kb/why-comark) exists, or start with the [document model](https://comark.dev/getting-started/document-model). #links :::u-button{to="https://comark.dev/getting-started/introduction" trailing-icon="i-lucide-arrow-right" size="lg"} Get started ::: :::u-button ```yaml [props] to: https://comark.dev/kb/why-comark color: neutral variant: outline size: lg ``` Why Comark ::: :: ## Resources for Agents - [API catalog: every service document this site publishes](https://comark.dev/.well-known/api-catalog) - [Sitemap (XML)](https://comark.dev/sitemap.xml) - [Sitemap (Markdown): every page on the site](https://comark.dev/sitemap.md) - [MCP server card: MCP endpoint at https://comark.dev/mcp](https://comark.dev/.well-known/mcp/server-card.json) - [MCP endpoint (streamable HTTP)](https://comark.dev/mcp) - [llms.txt: index of the documentation for LLMs](https://comark.dev/llms.txt) - [llms-full.txt: the full documentation as a single file](https://comark.dev/llms-full.txt) - [Agent skills index: every skill published by this site](https://comark.dev/.well-known/skills/index.json) - [Agent skill: comark](https://comark.dev/.well-known/skills/comark/SKILL.md) - [Agent skill: migrate-mdc-to-comark](https://comark.dev/.well-known/skills/migrate-mdc-to-comark/SKILL.md) - [OpenAPI document: every route this site serves to agents](https://comark.dev/openapi.json) - [RSS feed of the documentation](https://comark.dev/rss.xml) # What Is Comark? Markdown Parser and Renderer > Comark is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal. **Comark** is an open-source Markdown parser and renderer with components, attributes and plugins. It streams AI output to React, Vue, Svelte, Angular, HTML and the terminal. Give it standard CommonMark or GitHub Flavored Markdown (GFM), and render the same source to any of these targets. Parsing and rendering are decoupled through a compact, serializable [`MarkdownDocument`](https://comark.dev/getting-started/document-model). Parse on the server, in the browser, in a worker, during a build, or as content streams in. You can then inspect, transform, cache, serialize, or render the document without tying your content to one output target. Comark is built on [markdown-exit](https://github.com/serkodev/markdown-exit), a TypeScript rewrite of markdown-it that preserves its plugin API. It combines that foundation with streaming support, an extensible plugin system, and optional component and attribute syntax for richer content. Start with the renderer that matches your output: ::code-group ```ts [HTML] import { renderHtml } from '@comark/html' const content = '# Hello **World**' const html = await renderHtml(content) ``` ```ts [ANSI] import { renderAnsi } from '@comark/ansi' const content = '# Hello **World**' const output = await renderAnsi(content) ``` ```vue [Vue] ``` ```tsx [React] import { Markdown } from '@comark/react' const content = '# Hello **World**' export default function App() { return {content} } ``` ```svelte [Svelte] ``` ```typescript [Angular] import { Component } from '@angular/core' import { Markdown } from '@comark/angular' @Component({ selector: 'app-root', standalone: true, imports: [Markdown], template: ``, }) export class AppComponent { content = '# Hello **World**' } ``` :: ::callout{to="https://comark.dev/kb/why-comark" icon="i-lucide-lightbulb"} Learn why we created Comark and the principles behind its design in [Why Comark](https://comark.dev/kb/why-comark). :: ## How it works ::div{.border.border-default.rounded.bg-muted.p-4} ```mermaid {theme="zinc-light" theme-dark="zinc-dark"} graph LR A[Markdown] --> B[Parser] B --> C[MarkdownDocument] C --> D{Renderer} D --> I[ANSI] D --> H[HTML] D --> J[Markdown] D --> F[React] D --> G[Svelte] D --> E[Vue] D --> K[Angular] ``` :: 1. **Parse** - Comark parses Markdown into a compact [`MarkdownDocument`](https://comark.dev/getting-started/document-model) 2. **Transform** - Inspect, transform, cache, or serialize the document 3. **Render** - Convert the document to HTML, ANSI, Markdown, Vue, React, Svelte, or Angular ## Extend Markdown when needed Plain Markdown works without any component syntax. When your content needs richer structure, Comark adds readable components and attributes without embedding verbose HTML or executable framework code: ```mdc # Welcome to my blog This is regular **Markdown** with a custom component: ::alert{type="warning"} This is an important message! :: ``` The `::alert` block supports properties, children, and named slots. The renderer only uses components you explicitly register, so the source remains portable data rather than framework-specific code. Learn more in [Component Syntax](https://comark.dev/syntax/components) and [Attributes](https://comark.dev/syntax/attributes). Editor and browser tooling when working with Comark components: - [Comark for VS Code extension](https://comark.dev/getting-started/installation#ide-extension) for components and attributes syntax highlighting and autocomplete - [Comark for GitHub browser extension](https://comark.dev/getting-started/installation#browser-extension) to visualize Comark syntax directly on GitHub Markdown pages ## When to use Comark Comark is ideal for: - **Content platforms** - Parse Markdown from files, databases, APIs, or a CMS at build time or runtime - **Multi-target publishing** - Reuse one source for applications, static HTML, RSS, email, and terminal output - **AI chat interfaces** - Stream and render incomplete Markdown responses in real time - **Documentation and blogs** - Combine standard Markdown with callouts, embeds, diagrams, and interactive UI - **Content pipelines** - Inspect, transform, cache, or serialize a compact document before rendering ## Comparison with other tools For detailed, honest comparisons, see [Comark vs MDX](https://comark.dev/compare/comark-vs-mdx), [Comark vs react-markdown](https://comark.dev/compare/comark-vs-react-markdown), [Comark vs Streamdown](https://comark.dev/compare/comark-vs-streamdown), and [Comark vs Markdoc](https://comark.dev/compare/comark-vs-markdoc). | Feature | Comark | Streamdown | MDX | Markdoc | | ----------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | | Streaming support | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | | Component syntax | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | | Vue support | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | | React support | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | | Svelte support | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | | Angular support | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | | No build step | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | | Parse to AST | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | | Compact AST | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | | Auto-close for streams | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | | Decoupled parsing & rendering | ::icon{name="i-lucide-check" .text-green-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-x" .text-red-500} :: | ::icon{name="i-lucide-check" .text-green-500} :: | ## Core capabilities ::card-group :::card{icon="i-lucide-file-text" title="CommonMark and GFM" to="https://comark.dev/syntax/markdown"} Parse familiar Markdown syntax, including tables, task lists, code blocks, and more. ::: :::card{icon="i-lucide-layers" title="Parse Once, Render Anywhere" to="https://comark.dev/getting-started/document-model"} Produce a compact document you can inspect, cache, serialize, and render to every supported target. ::: :::card{icon="i-lucide-radio" title="Streaming Support" to="https://comark.dev/rendering/vue#streaming"} Real-time incremental parsing for AI chat interfaces and live content. ::: :::card{icon="i-lucide-puzzle" title="Plugin Ecosystem" to="https://comark.dev/plugins"} Extend parsing with Comark plugins or reuse plugins written for markdown-it. ::: :::card{icon="i-lucide-component" title="Component Syntax" to="https://comark.dev/syntax/components"} Add props, slots, and nested UI while keeping content as readable plain text. ::: :::card{icon="i-lucide-code" title="Dedicated Renderers" to="https://comark.dev/getting-started/installation"} Render to HTML, ANSI, Vue, React, Svelte, and Angular without changing the source. ::: :: ## FAQ ::accordion :::accordion-item{label="How is Comark different from MDX?"} MDX compiles Markdown to JSX at build time, tying content to a bundler and to React. Comark parses Markdown into serializable data at build time or runtime, then renders it to multiple targets. See [Comark vs MDX](https://comark.dev/compare/comark-vs-mdx). ::: :::accordion-item{label="Does Comark require a specific framework?"} No. The core parser is framework-free. Renderers exist for [Vue](https://comark.dev/rendering/vue), [React](https://comark.dev/rendering/react), [Svelte](https://comark.dev/rendering/svelte), and [Angular](https://comark.dev/rendering/angular), plus string output as [HTML](https://comark.dev/rendering/html) and [ANSI](https://comark.dev/rendering/ansi). ::: :::accordion-item{label="Can I use markdown-it plugins with Comark?"} Yes. Comark is built on markdown-exit, a TypeScript rewrite of markdown-it that preserves its plugin API. Existing markdown-it plugins work alongside [Comark plugins](https://comark.dev/plugins). See [using markdown-it plugins](https://comark.dev/plugins/custom/markdown-it). ::: :::accordion-item{label="How does Comark handle streaming AI output?"} The [auto-close](https://comark.dev/reference/auto-close) parser completes unterminated syntax (bold, code fences, components) so every incomplete frame renders correctly. Framework renderers expose this as a `streaming` prop. ::: :::accordion-item{label="Do I have to use components?"} No. Plain CommonMark and GFM parse as standard Markdown, with a few [documented differences](https://comark.dev/syntax/markdown#differences-from-commonmark-and-gfm). Components and [attributes](https://comark.dev/syntax/attributes) are opt-in syntax. ::: :: --- - [Installation](https://comark.dev/getting-started/installation) - [GitHub](https://github.com/comarkdown/comark) # Installation > Install Comark and choose a parser or renderer for HTML, ANSI, Vue, React, Svelte, or Angular to start working with Markdown in minutes. ## Quick start Choose the output you need and render your first Markdown document in minutes. ::card-group{cols="2"} :::card{icon="i-vscode-icons-file-type-html" title="HTML" to="https://comark.dev/rendering/html"} Server-side rendering and static generation without a framework. ::: :::card{icon="i-logos-vue" title="Vue" to="https://comark.dev/rendering/vue"} Use Comark in Vue 3 applications with full async component support and streaming. ::: :::card{icon="i-logos-nuxt-icon" title="Nuxt" to="https://comark.dev/rendering/nuxt"} Zero-config Nuxt module with auto-imported components and Nuxt UI integration. ::: :::card{icon="i-logos-react" title="React" to="https://comark.dev/rendering/react"} Use Comark in React and Next.js applications. ::: :::card{icon="i-logos-svelte-icon" title="Svelte" to="https://comark.dev/rendering/svelte"} Use Comark in Svelte 5 applications with snippets and streaming. ::: :::card{icon="i-logos-angular-icon" title="Angular" to="https://comark.dev/rendering/angular"} Use Comark in Angular 17+ applications with standalone components and streaming. ::: :::card{icon="i-lucide-terminal" title="ANSI" to="https://comark.dev/rendering/ansi"} Render Comark as styled terminal output for CLIs and developer tooling. ::: :: ## Agent skill To give coding agents the Comark skill (syntax, AST, renderers, streaming), install it from production: ```bash [Terminal] npx skills add https://comark.dev ``` ## IDE extension Install the [Comark extension](https://marketplace.visualstudio.com/items?itemName=Nuxt.mdc) in **VS Code** or **Cursor** for syntax highlighting, bracket matching, document folding, and auto-completion in `.md` files using Comark syntax. ::card-group :::card ```yaml [props] title: Comark Extension icon: i-logos-visual-studio-code color: neutral variant: subtle to: https://marketplace.visualstudio.com/items?itemName=Nuxt.mdc target: _blank ``` ::: :: Or within your editor, open Quick Open (`Ctrl+P` / `Cmd+P`) and run: ```bash ext install Nuxt.mdc ``` ## Browser extension Install **Comark for GitHub** to better visualize Markdown files that use Comark components and attributes syntax on GitHub. ::card-group :::card ```yaml [props] title: Chrome Web Store icon: i-logos-chrome color: neutral variant: subtle to: https://chromewebstore.google.com/detail/comark-for-github/mbbnjnblfplfjkakjfjkhhefhcdnhcin target: _blank ``` ::: :::card ```yaml [props] title: Firefox Add-ons icon: i-logos-firefox color: neutral variant: subtle to: https://addons.mozilla.org/en-US/firefox/addon/comark-for-github-gitlab/ target: _blank ``` ::: :::card ```yaml [props] title: View Source / Manual Install icon: i-simple-icons-github color: neutral variant: subtle to: https://github.com/comarkdown/comark-webext target: _blank ``` ::: :: ## Packages Comark packages share one pipeline. The core parser produces a serializable `MarkdownDocument`, while string and UI renderers consume that document. Install the package that matches your output: ### UI frameworks Use these when you need to render Markdown as an interactive document in Vue, React, Nuxt, Svelte, or Angular. ::tabs{.gap-0} :::tab-item{label="Vue" icon="i-logos-vue"} ::::code-group ```bash [pnpm] pnpm add @comark/vue ``` ```bash [npm] npm install @comark/vue ``` ```bash [yarn] yarn add @comark/vue ``` ```bash [bun] bun add @comark/vue ``` :::: ::: :::tab-item{label="React" icon="i-logos-react"} ::::code-group ```bash [pnpm] pnpm add @comark/react ``` ```bash [npm] npm install @comark/react ``` ```bash [yarn] yarn add @comark/react ``` ```bash [bun] bun add @comark/react ``` :::: ::: :::tab-item{label="Nuxt" icon="i-logos-nuxt-icon"} ```bash [Terminal] npx nuxt add comark ``` ::: :::tab-item{label="Svelte" icon="i-logos-svelte-icon"} ::::code-group ```bash [pnpm] pnpm add @comark/svelte ``` ```bash [npm] npm install @comark/svelte ``` ```bash [yarn] yarn add @comark/svelte ``` ```bash [bun] bun add @comark/svelte ``` :::: ::: :::tab-item{label="Angular" icon="i-logos-angular-icon"} ::::code-group ```bash [pnpm] pnpm add @comark/angular ``` ```bash [npm] npm install @comark/angular ``` ```bash [yarn] yarn add @comark/angular ``` ```bash [bun] bun add @comark/angular ``` :::: ::: :: ### String renderers Use these when you need a string output with no UI framework involved like server-side HTML generation, static site builds, or CLI tooling. ::tabs{.gap-0} :::tab-item{label="HTML" icon="i-vscode-icons-file-type-html"} ::::code-group ```bash [pnpm] pnpm add @comark/html ``` ```bash [npm] npm install @comark/html ``` ```bash [yarn] yarn add @comark/html ``` ```bash [bun] bun add @comark/html ``` :::: ::: :::tab-item{label="ANSI" icon="i-lucide-terminal"} ::::code-group ```bash [pnpm] pnpm add @comark/ansi ``` ```bash [npm] npm install @comark/ansi ``` ```bash [yarn] yarn add @comark/ansi ``` ```bash [bun] bun add @comark/ansi ``` :::: ::: :: ### Core `comark` is the parser and document foundation used by every renderer. Use it directly to parse Markdown, inspect or transform the AST, cache or serialize documents, and render them back to Markdown. ::note All framework packages re-export its types, so you only need to install `comark` separately if you're not using a framework renderer. :: ::code-group ```bash [pnpm] pnpm add comark ``` ```bash [npm] npm install comark ``` ```bash [yarn] yarn add comark ``` ```bash [bun] bun add comark ``` :: ```typescript import { parseMarkdown, createMarkdownParser } from 'comark' import { renderMarkdown } from 'comark/render' // Parse to AST const document = await parseMarkdown(source) // Transform nodes, then serialize back to Markdown const output = await renderMarkdown(document) // Reusable parser: initializes plugins once, faster for batch processing const parseDoc = createMarkdownParser({ plugins: [shiki(), toc()] }) ``` See the [Parse API](https://comark.dev/reference/parse) and [Render API](https://comark.dev/reference/render) for full options. ## Learn more Learn more about Comark features and syntax: ::card-group{cols="2"} :::card{icon="i-lucide-code" title="Markdown Syntax" to="https://comark.dev/syntax/markdown"} Learn the standard Markdown syntax and how to use it with Comark. ::: :::card{icon="i-lucide-component" title="Components" to="https://comark.dev/syntax/components"} Learn the component syntax for blocks, inline elements, props, and slots ::: :::card{icon="i-lucide-tag" title="Attributes" to="https://comark.dev/syntax/attributes"} Add classes, IDs, and custom attributes to Markdown elements ::: :::card{icon="i-lucide-plug" title="Plugins" to="https://comark.dev/plugins"} Add syntax highlighting, math, mermaid diagrams, and more ::: :: --- - [Comark Syntax](https://comark.dev/syntax/components) - [Plugins](https://comark.dev/plugins) # Document Model > Understand the serializable Markdown document returned by Comark, including its nodes, frontmatter, metadata and rendering workflow. `parseMarkdown()` returns a `MarkdownDocument`: a plain, serializable object containing parsed nodes, frontmatter, and plugin metadata. ```ts import { parseMarkdown } from 'comark' const document = await parseMarkdown(`--- title: Hello Comark --- # Getting **started**`) // { // frontmatter: { // title: 'Hello Comark' // }, // meta: {}, // nodes: [ // ['h1', { id: 'getting-started' }, 'Getting ', ['strong', {}, 'started']] // ] // } ``` The document is the shared boundary between parsing and rendering. You can parse once, serialize or cache the result, inspect or transform its nodes, and render it with HTML, ANSI, Vue, React, Svelte, or Angular. ## Document structure The root object has three fields: ```ts interface MarkdownDocument { frontmatter: Record meta: Record nodes: Node[] } ``` | Field | Description | | ------------- | ----------------------------------------------------------------------------------------------- | | `frontmatter` | Structured data from the leading YAML frontmatter block | | `meta` | Data collected by [plugins](https://comark.dev/plugins), such as a table of contents or summary | | `nodes` | Parsed Markdown and component content | ### Frontmatter Comark parses a leading YAML block into `document.frontmatter` (see [Frontmatter syntax](https://comark.dev/syntax/frontmatter)): ::code-group ```mdc [Markdown] --- title: Getting Started draft: false --- # Hello ``` ```typescript [Result] document.frontmatter // { title: 'Getting Started', draft: false } ``` :: ### Plugin metadata Plugins store derived document data in `document.meta`. For example, the table-of-contents plugin adds `meta.toc`: ```typescript import { parseMarkdown } from 'comark' import toc from 'comark/plugins/toc' const document = await parseMarkdown('# Guide\n\n## Install', { plugins: [toc()], }) console.log(document.meta.toc) ``` Plugin types flow into `MarkdownDocument`, so metadata and frontmatter can stay strongly typed throughout your application. ## Node model Comark uses compact tuples instead of an object for every syntax node. A `Node` is a text string, an element tuple, or a comment tuple: ```typescript type Node = TextNode | ElementNode | CommentNode type TextNode = string type ElementNode = [string, ElementNodeAttributes, ...Node[]] type CommentNode = [null, ElementNodeAttributes, string] ``` This format follows the shape of rendered markup: the tag is at index `0`, attributes are at index `1`, and child nodes start at index `2`. ### Text nodes Text is stored directly as a string: ```json "Hello, world!" ``` ### Element nodes Elements use `[tag, attributes, ...children]`: ::code-group ```mdc [Markdown] This is **important**. ``` ```json [Nodes] [ "p", {}, "This is ", ["strong", {}, "important"], "." ] ``` :: Markdown elements use familiar HTML tag names such as `h1`, `p`, `strong`, `a`, `ul`, and `li`. Comark components use their component name as the tag: ::code-group ```mdc [Markdown] ::alert{type="warning"} Check your configuration. :: ``` ```json [Node] [ "alert", { "type": "warning" }, "Check your configuration." ] ``` :: ### Attributes Element attributes are stored in a key-value object. The optional `$` field contains parser metadata used by renderers and tooling: ```typescript interface ElementNodeAttributes { [key: string]: unknown $?: { line?: number html?: 0 | 1 block?: 0 | 1 } } ``` Treat `$` as structural metadata. Component props and standard HTML attributes live alongside it. ### Comment nodes Comments use `null` as their tag: ::code-group ```mdc [Markdown] ``` ```json [Node] [null, {}, " Keep this note "] ``` :: ## Parse once, render many Every renderer accepts the same `MarkdownDocument`. Parsing once is useful for server rendering, build pipelines, content APIs, caches, and multiple output targets. ```typescript import { parseMarkdown } from 'comark' import { renderHtmlFromDocument } from '@comark/html' import { renderAnsiFromDocument } from '@comark/ansi' const document = await parseMarkdown(source) const html = await renderHtmlFromDocument(document) const ansi = await renderAnsiFromDocument(document) ``` Framework renderers expose a `MarkdownDocument` component for the same workflow: ::code-group ```vue [Vue] ``` ```tsx [React] ``` ```svelte [Svelte] ``` ```html [Angular] ``` :: These components render a parsed document without importing or running the parser in the client bundle. ## Inspect documents Use the utilities from `comark/utils` instead of writing traversal logic for common operations. ### Extract text `textContent()` returns the plain text inside a node: ```typescript [text-content.ts] import { textContent } from 'comark/utils' const heading = document.nodes[0] console.log(textContent(heading)) // Hello world ``` ### Visit nodes `visit()` walks every node that matches a checker. A visitor can inspect, replace, or remove matching nodes: ```typescript [external-links.ts] import type { ElementNode, Node } from 'comark' import { visit } from 'comark/utils' const isExternalLink = (node: Node): node is ElementNode => { return Array.isArray(node) && node[0] === 'a' && String(node[1].href).startsWith('https://') } visit(document, isExternalLink, (node) => { return [node[0], { ...node[1], target: '_blank', rel: 'noopener' }, ...node.slice(2)] }) ``` The visitor mutates `document.nodes` when a node is replaced or removed. Create a copy first when your application requires immutable updates. ## Serialize and cache documents A `MarkdownDocument` contains only plain arrays, objects, strings, and values contributed by plugins. You can send it over an API boundary or persist it as JSON: ```typescript import type { MarkdownDocument } from 'comark' const document = await parseMarkdown(source) await cache.set(key, JSON.stringify(document)) const cachedDocument = JSON.parse(await cache.get(key)) as MarkdownDocument ``` Use the same Comark and plugin versions when reading persisted documents. Rebuild cached documents after an upgrade that changes node output or plugin metadata. ## Type documents Pass metadata and frontmatter types to `MarkdownDocument` when documents cross application boundaries: ```typescript interface ArticleMeta { readingTime: number } interface ArticleFrontmatter { title: string description?: string } type ArticleDocument = MarkdownDocument ``` Parser plugins can contribute these types automatically when you parse content directly. Use “AST” when discussing the node representation itself. Use “document” for the root object returned by the parser and accepted by renderers. ```tsx const document = await parseMarkdown(markdown) return ``` --- - [Parsing API](https://comark.dev/reference/parse) - [Rendering](https://comark.dev/rendering/html) # Markdown > Comark parses CommonMark and GitHub Flavored Markdown (GFM), including headings, formatting, lists, tables, and code blocks, with a few documented differences. Comark parses CommonMark and GitHub Flavored Markdown (GFM). The parser is built on markdown-exit, which follows the CommonMark spec. Comark then converts the tokens into its own [document model](https://comark.dev/getting-started/document-model), and a few constructs render differently. See [Differences from CommonMark and GFM](#differences-from-commonmark-and-gfm). ## Headings ```md # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ###### Heading 6 ``` All headings automatically get an `id` from their visible text, for linking. Set [`headingIds: false`](https://comark.dev/reference/parse#options) to skip generation. An explicit `{id="..."}` replaces the generated id. ```mdc # Hello World ## 1. Introduction ## The `parse` function ``` The id is the heading's visible text, lowercased, with spaces turned into `-` and other punctuation removed. A leading digit is prefixed with `_`. `**bold**`, ```code```, and link labels contribute their text, not the tag name. An inline component name is omitted (`## Star :icon-star here` → `star-here`). Ids are unique within a document. A heading whose id is already taken gets a numeric suffix, at any level and in any section: ```md ## Npm ### Install ## Yarn ### Install ## Options ## Options ``` Ids are flat by default. Set `headingIds: 'nested'` in the [parse options](https://comark.dev/reference/parse#options) to prefix each id from `h3` down with the nearest `h2` or deeper ancestor (never the `h1`): ```md # Guide ## Ending space ### Slash in title ``` ## Text formatting ::code-preview :::div **Bold text** *Italic text* ***Bold and italic*** ~~Strikethrough~~ `Inline code` ::: #code ```mdc **Bold text** *Italic text* ***Bold and italic*** ~~Strikethrough~~ `Inline code` ``` :: ::callout Text nodes in Comark are plain strings, similar to HTML text nodes. To add custom styles, attributes, or metadata to specific parts of text, use the [Span Attributes](https://comark.dev/syntax/attributes#span-attributes) syntax: `Hello [world]{data-world="earth" style="color: blue"}`. This creates a `` element with custom attributes, so you can style or add metadata to inline text. :: ## Lists ### Unordered ```mdc - Item 1 - Item 2 - Nested item - Another nested item - Item 3 ``` ### Ordered ```mdc 1. First item 2. Second item 1. Nested item 2. Another nested item 3. Third item ``` ## Links and images ```mdc [Link text](https://example.com) [Link with title](https://example.com "Link title") ![Image alt text](https://example.com/image.png) ![Image with title](https://example.com/image.png "Image title") ``` ::callout{icon="i-lucide-info"} Add custom attributes to links and images with the [Attributes](https://comark.dev/syntax/attributes) syntax: `[Link](url){target="_blank"}`. :: Bare URLs like `https://example.com` are automatically converted into clickable links. Disable this with the [`linkify`](https://comark.dev/reference/parse#options) option (`{ linkify: false }`). ## Blockquotes ::code-preview :::div > This is a blockquote > > And contain other markdown elements like **bold** and *italic* ::: #code ```mdc > This is a blockquote > > And contain other markdown elements like **bold** and *italic* ``` :: ### Alerts The [alerts plugin](https://comark.dev/plugins/defaults/alert) is built-in and transforms special blockquotes into styled callout blocks. Place an alert marker on the first line of a blockquote: ```mdc > [!NOTE] > Useful information that users should know, even when skimming content. > [!TIP] > Helpful advice for doing things better or more easily. > [!IMPORTANT] > Key information users need to know to achieve their goal. > [!WARNING] > Urgent info that needs immediate user attention to avoid problems. > [!CAUTION] > Advises about risks or negative outcomes of certain actions. ``` Supported markers: `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`. ### Horizontal rules Any of these create a horizontal rule: ```mdc --- *** ___ ``` --- ## Code blocks Comark provides advanced code block features with metadata support. ### Basic code block ::code-preview ```javascript function hello() { console.log("Hello, World!") } ``` #code ~~~mdc ```javascript function hello() { console.log("Hello, World!") } ``` ~~~ :: ### Filename metadata Add a filename using `[...]` brackets: ::code-preview ```javascript [server.js] const express = require('express') const app = express() ``` #code ~~~mdc ```javascript [server.js] const express = require('express') const app = express() ``` ~~~ :: ### Line highlighting Highlight specific lines using `{...}` syntax: ::code-preview ```javascript {1-3,5} function example() { const a = 1 const b = 2 const c = 3 return a + b + c } ``` #code ~~~mdc ```javascript {1-3,5} function example() { const a = 1 const b = 2 const c = 3 return a + b + c } ``` ~~~ :: | Syntax | Description | | --------------- | ------------------------- | | `{3}` | Single line | | `{1-5}` | Range of lines | | `{1,3,5}` | Multiple specific lines | | `{1-3,7,10-12}` | Combined ranges and lines | ### Combined metadata All metadata can be combined in any order: ::code-preview ```javascript [utils.ts] {1-3} function hello() { console.log("Hello") } ``` #code ~~~mdc ```javascript {1-3} [utils.ts] meta=value function hello() { console.log("Hello") } ``` ~~~ :: ### Special characters in filename Use backslash to escape special characters: ::code-preview ```typescript [@[...slug\\\].ts] // Brackets and special chars are supported ``` #code ~~~mdc ```typescript [@[...slug\].ts] // Brackets and special chars are supported ``` ~~~ :: ### AST structure Code blocks produce this AST structure: ```json [ "pre", { "language": "javascript", "filename": "server.js", "highlights": [1, 2, 3], "meta": "meta=value" }, ["code", { "class": "language-javascript" }, "code content here"] ] ``` --- ## Task lists Comark supports GitHub Flavored Markdown task lists: ::code-preview - [x] Completed task - [ ] Pending task - [ ] - [ ] Nested pending task - [x] Nested completed task #code ```mdc - [x] Completed task - [ ] Pending task - [x] Another completed task - [ ] Nested pending task - [x] Nested completed task ``` :: - `[x]` or `[X]` for completed tasks - `[ ]` for pending tasks - Works in both ordered and unordered lists - Supports nesting --- ## Tables ::code-preview | Header 1 | Header 2 | Header 3 | | -------- | -------- | -------- | | Cell 1 | Cell 2 | Cell 3 | | Cell 4 | Cell 5 | Cell 6 | #code ```mdc | Header 1 | Header 2 | Header 3 | | -------- | -------- | -------- | | Cell 1 | Cell 2 | Cell 3 | | Cell 4 | Cell 5 | Cell 6 | ``` :: ### Aligned tables ::code-preview | Left Aligned | Center Aligned | Right Aligned | | :----------- | :------------: | ------------: | | Left | Center | Right | | Text | Text | Text | #code ```mdc | Left Aligned | Center Aligned | Right Aligned | | :----------- | :------------: | ------------: | | Left | Center | Right | | Text | Text | Text | ``` :: | Syntax | Alignment | | ------- | --------- | | `:---` | Left | | `:---:` | Center | | `---:` | Right | ### Inline Markdown in tables ::code-preview | Feature | Status | Link | | -------- | ---------- | -------------------------------------- | | **Bold** | *Italic* | [Link](https://example) | | `Code` | ~~Strike~~ | ![Image](https://picsum.photos/120/30) | #code ```mdc | Feature | Status | Link | | ------------ | --------------- | ----------------------- | | **Bold** | *Italic* | [Link](https://example) | | `Code` | ~~Strike~~ | ![Image](https://picsum.photos/120/30) | ``` :: ## Comments HTML-style comments are supported and preserved in the AST but not rendered in output: ```mdc ``` Comments can span multiple lines: ```mdc ``` Comments are represented in the [document model](https://comark.dev/getting-started/document-model#comment-nodes) as a tuple with `null` as the tag: ```json [null, {}, " comment text "] ``` ## Emojis Emoji shortcodes use the `:emoji_name:` syntax and require the [emoji plugin](https://comark.dev/plugins/built-in/emoji): ::code-preview Hello 👋 Welcome to our docs! 🚀 #code ```mdc Hello :wave: Welcome to our docs! :rocket: ``` :: ### Popular emojis ::code-preview 😄 ❤️ 🔥 🚀 ✨ 🎉 🤔 👀 💯 ⭐ ⚡ 💡 ⚠️ #code ```mdc :smile: :heart: :fire: :rocket: :sparkles: :tada: :thinking: :eyes: :100: :star: :zap: :bulb: :warning: ``` :: ## Differences from CommonMark and GFM Comark renders most CommonMark and GFM documents the same way as a spec-compliant parser. The differences below come from Comark syntax, from the default options, or from the conversion to the document model. ### Comark syntax - **Shortcut reference links.** `[text]` without a destination is a [span](https://comark.dev/syntax/attributes#span-attributes), so `[foo]` doesn't resolve a `[foo]: /url` definition. Full (`[foo][ref]`) and collapsed (`[foo][]`) reference links work. - **Component and attribute markers.** Text such as `::name`, `:name[...]`, and `{...}` after an element is Comark syntax. Set `registerDefaultPlugins: false` to parse it as plain text. ### Default options - **`autoClose`** is on by default, so unterminated syntax is closed: `*foo bar` renders as emphasis, and an unclosed ````` becomes inline code. Set `autoClose: false` for static content that must follow the spec. See the [Streaming API](https://comark.dev/reference/auto-close). - **`autoUnwrap`** removes the `

` inside a list item or a blockquote that holds a single paragraph. - **`headingIds`** adds an `id` to every heading. ### Document model - **Tight and loose lists** aren't distinguished. List items render the same way with or without blank lines between them. - **Empty link text** removes the link: `[](/url)` renders nothing. - **Image alt text** keeps the raw Markdown: `![foo *bar*](/img.png)` gives the alt text `foo *bar*`. - **Markdown inside an HTML block**, such as a `

` followed by a blank line, renders after the HTML element instead of inside it. - **Fenced code blocks** add `language` and `meta` attributes to the `
` element.

### GFM extensions

- **Email autolinks** such as `foo@bar.baz` stay plain text. URL autolinks such as `www.example.com` work.
- **The tag filter** isn't applied: raw ``, `<style>`, `<xmp>` and the other filtered tags aren't escaped. Set `blockedTags` in the [security plugin](https://comark.dev/plugins/built-in/security) to remove them.

---

- [Frontmatter](https://comark.dev/syntax/frontmatter)
- [Components](https://comark.dev/syntax/components)


# Components in Markdown: Block and Inline Syntax

> Embed custom components directly in your Markdown with block and inline syntax, props, slots, and nested children.

Comark extends standard Markdown with component syntax, letting you embed custom UI directly in your content while keeping it readable. Parsing is provided by the built-in [`components` plugin](https://comark.dev/plugins/defaults/components) (enabled by default).

::callout
Comark also supports JSON-render syntax in code blocks — see the [json-render plugin](https://comark.dev/plugins/built-in/json-render).
::

## Block

Block components use the `::component-name` syntax and occupy their own line:

```mdc
::component-name{prop1="value1" prop2="value2"}
Content inside the component

Can have **markdown** and other elements
::
```

Here are some examples of block components:

::code-group
```mdc [Alert]
::alert{type="info"}
This is an important message!
::
```

```mdc [Card]
::card{title="My Card"}
Card content with **markdown** support
::
```

```mdc [Empty]
::divider
::
```
::

## Inline

Inline components use the `:component-name` syntax and can be placed within text:

```mdc
Check out this :icon-star component in the middle of text.

Click the :button[Submit]{type="primary"} to continue.

The status is :badge[Active]{color="green"} right now.
```

| Syntax                        | Description                                  |
| ----------------------------- | -------------------------------------------- |
| `:icon-check`                 | Standalone inline component                  |
| `:badge[New]`                 | Inline component with content                |
| `:badge[New]{color="blue"}`   | Inline component with content and properties |
| `:tooltip{text="Hover text"}` | Inline component with properties only        |

## Properties

Components support two property syntaxes: inline attributes and YAML frontmatter.

### Inline attributes

Use the `{...}` syntax for scalar properties:

```mdc
::component{prop="value"}
<!-- Standard key-value pair -->
::

::component{bool}
<!-- Boolean property (becomes :bool="true" in AST) -->
::

::component{#custom-id}
<!-- ID attribute -->
::

::component{.class-name}
<!-- CSS class -->
::

::component{.class-one .class-two}
<!-- Multiple CSS classes -->
::

::component{obj='{"key": "value"}'}
<!-- Object/JSON value -->
::

::component{multiple="props" bool #id .class}
<!-- Multiple properties combined -->
::
```

### Block props

For components with many properties, use YAML block syntax inside the component. Two styles are supported:

::code-group
~~~mdc [Codeblock Style]
::component
```yaml [props]
title: My Component
type: info
count: 42
enabled: true
items:
  - First item
  - Second item
config:
  theme: dark
  mode: auto
```
Component content goes here

With full **markdown** support
::
~~~

```mdc [Frontmatter Style]
::component
---
title: My Component
type: info
count: 42
enabled: true
items:
  - First item
  - Second item
config:
  theme: dark
  mode: auto
---
Component content goes here

With full **markdown** support
::
```
::

::callout{color="info" icon="i-lucide-info"}
Objects remain objects, arrays remain arrays, and numbers/booleans are parsed as their respective types rather than strings. Your components receive fully typed props.
::

#### Key rules

- Must be at the very beginning of the component content
- Enclosed by `---` delimiters (frontmatter style) or `````yaml [props]`` fences (codeblock style)
- Merged with inline attributes (YAML props take precedence, `class` values are merged)
- Full support for complex data structures

::tip
The default style can be configured with the [`blockAttributesStyle`](https://comark.dev/reference/render#options) option.
::

### Combine both syntaxes

Inline attributes and YAML props can be combined. YAML props take precedence, except `class` values which are merged:

~~~mdc
::card{.featured}
```yaml [props]
title: Featured Article
author: Jane Doe
tags:
  - markdown
  - documentation
```
This combines inline class `.featured` with YAML props
::
~~~

### Use cases

**Configuration-heavy components:**

~~~mdc
::data-table
```yaml [props]
columns:
  - name: Name
    field: name
    sortable: true
  - name: Email
    field: email
    sortable: true
options:
  striped: true
  pageSize: 10
```
::
~~~

**API documentation:**

~~~mdc
::api-endpoint
```yaml [props]
method: POST
path: /api/users
parameters:
  - name: username
    type: string
    required: true
  - name: email
    type: string
    required: true
response:
  status: 201
  body:
    id: string
    username: string
```
::
~~~

## Data binding

Props prefixed with `:` are JSON-parsed at render time. When the value isn't valid JSON, the renderer looks it up as a dot-path against an ambient **render context**, letting authors reference runtime data, frontmatter, meta, or the enclosing component's props without hardcoding them.

### Scope

The render context exposes four namespaces:

| Namespace     | Source                                               |
| ------------- | ---------------------------------------------------- |
| `frontmatter` | The document's YAML frontmatter                      |
| `meta`        | Plugin-populated metadata on the parsed tree         |
| `data`        | Runtime values passed via the renderer's `data` prop |
| `props`       | The enclosing component's own props                  |

### Frontmatter

Reference any value declared in the document's frontmatter:

```mdc
---
theAnswer: 42
user:
  name: Ada
---

::question{:answer="frontmatter.theAnswer"}
::

Hello, :badge{:label="frontmatter.user.name"}
```

### Runtime data

Pass values from your app via the renderer's `data` prop and reference them under the `data.` namespace:

::code-group
```vue [Vue]
<Markdown :value="content" :data="{ user: { name: 'Ada' } }" />
```

```tsx [React]
<Markdown value={content} data={{ user: { name: 'Ada' } }} />
```

```svelte [Svelte]
<Markdown value={content} data={{ user: { name: 'Ada' } }} />
```

```typescript [Angular]
<comark-markdown [value]="content" [data]="{ user: { name: 'Ada' } }" />
```
::

```mdc
Welcome, :badge{:label="data.user.name"}!
```

### Parent component props

Nested components can read the enclosing component's resolved props through the `props.` namespace. This is useful when a child should mirror something declared once on its parent:

```mdc
::card{title="Hello" variant="primary"}
  :::badge{:color="props.variant" :text="props.title"}
  :::
::
```

### Resolution rules

- If the `:prefixed` value is a valid JSON literal (for example `5`, `true`, `"foo"`, `{"a":1}`), it's used as-is.
- Otherwise, the value is treated as a dot-path and resolved against `{ frontmatter, meta, data, props }`.
- Unknown paths resolve to `undefined`: the prop is passed as `undefined` rather than the raw string.
- Only props authored with the `:` prefix participate in data binding. Plain `prop="value"` attributes are always passed as literal strings.

::callout{color="info" icon="i-lucide-replace"}
Need to interpolate a value directly into text rather than a prop? The [Binding plugin](https://comark.dev/plugins/built-in/binding) adds a `{{ path || default }}` inline shorthand that resolves against the same render context.
::

## Slots

Block components support slots for passing structured content to components.

### Default slot

Content placed directly inside a component goes to the default slot, no prefix needed:

```mdc
::alert{type="info"}
This content goes to the default slot.
::
```

You can also write `#default` explicitly, which is useful when mixing it with named slots:

```mdc
::card
#default
This is the **default** slot content.

#footer
Footer content here.
::
```

### Named slots

```mdc
::card
#header
## Card Title

#content
This is the main content of the card

#footer
Footer text here
::
```

### Combine with props

You can use both YAML block props and named slots in the same component:

~~~mdc
::card{.featured}
```yaml [props]
variant: elevated
color: primary
actions:
  - label: Read More
    url: /article
  - label: Share
    icon: share
```
#header
## Article Title
*By Jane Doe*

#content
This is the main article content with **markdown** support.

#footer
Published on January 15, 2024
::
~~~

::callout{color="warning" icon="i-lucide-triangle-alert"}
YAML block props must come immediately after the opening `::component` line, **before** any slot definitions.
::

**Correct order:**

~~~mdc
::component{inline-attrs}
```yaml [props]
yaml: props
```
#slot-name
Slot content
::
~~~

**Incorrect order:**

~~~mdc
::component
#slot-name
Slot content
```yaml [props]
yaml: props  <!-- This won't work -->
```
::
~~~

## Nested

Components can be nested within each other using additional colons:

```mdc
::outer-component
Content in outer

:::inner-component{variant="compact"}
Content in inner
:::

More content in outer
::
```

**Deep nesting:**

```mdc
::level-1
  :::level-2
    ::::level-3
    Content
    ::::
  :::
::
```

::callout{color="info" icon="i-lucide-info"}
Adding extra colons is not required but is a good practice to keep nesting visually consistent. The parser resolves nesting by matching opening and closing tags.
::

This also works:

```mdc
::level-1
  ::level-2
    ::level-3
    Content
    ::
  ::
::
```

---

- [Attributes](https://comark.dev/syntax/attributes)
- [Components Plugin](https://comark.dev/plugins/defaults/components)


# Attributes

> Add custom classes, IDs, styles, and data attributes to native Markdown elements using the curly brace syntax.

Comark allows adding custom attributes to native Markdown elements using `{...}` syntax after the element. Parsing is provided by the built-in [`attributes` plugin](https://comark.dev/plugins/defaults/attributes) (enabled by default).

## Element attributes

### Strong/bold

```mdc
**bold text**{.highlight #important}
**bold text**{data-value="custom"}
**bold text**{bool}
```

### Italic/emphasis

```mdc
*italic text*{.emphasized}
_italic text_{#custom-id}
```

### Links

```mdc
[Link text](url){target="_blank" rel="noopener"}
[Link text](url){.button .primary}
[External](https://example.com){target="_blank" .external-link}
```

### Images

```mdc
![Alt text](image.png){.responsive width="800" height="600"}
![Logo](logo.svg){.logo #site-logo}
```

### Inline code

~~~mdc
`code snippet`{.language-js}
`variable`{data-type="string"}
~~~

### Attribute types summary

| Syntax                     | Output                | Description           |
| -------------------------- | --------------------- | --------------------- |
| `{bool}`                   | `:bool="true"`        | Boolean attribute     |
| `{#my-id}`                 | `id="my-id"`          | ID attribute          |
| `{.my-class}`              | `class="my-class"`    | Class attribute       |
| `{key="value"}`            | `key="value"`         | Key-value attribute   |
| `{:data='{"key": "val"}'}` | `data={"key": "val"}` | JSON object attribute |

## Span attributes

Wrap inline text in a `<span>` element with custom attributes. This is useful for styling specific parts of text or adding metadata without creating custom components.

### Syntax

```mdc
[text content]{attributes}
```

The text goes in square brackets `[...]` followed by attributes in curly braces `{...}`.

### Examples

**Class:**

::code-group
```mdc [Syntax]
This is [highlighted text]{.highlight} in a paragraph.
```

```html [Output]
<p>This is <span class="highlight">highlighted text</span> in a paragraph.</p>
```
::

**Multiple classes:**

::code-group
```mdc [Syntax]
[Important]{.badge .primary} information here.
```

```html [Output]
<p><span class="badge primary">Important</span> information here.</p>
```
::

**ID:**

::code-group
```mdc [Syntax]
Reference this [specific text]{#ref-1} later.
```

```html [Output]
<p>Reference this <span id="ref-1">specific text</span> later.</p>
```
::

**Inline styles:**

::code-group
```mdc [Syntax]
[Blue text]{style="color: blue; font-weight: bold"}
```

```html [Output]
<p><span style="color: blue; font-weight: bold">Blue text</span></p>
```
::

**Data attributes:**

::code-group
```mdc [Syntax]
[Earth]{data-planet="earth" data-type="terrestrial"}
```

```html [Output]
<p><span data-planet="earth" data-type="terrestrial">Earth</span></p>
```
::

**Combined attributes:**

::code-group
```mdc [Syntax]
[Status: Active]{#status .badge .success data-state="active"}
```

```html [Output]
<p><span id="status" class="badge success" data-state="active">Status: Active</span></p>
```
::

### Common use cases

1. **Styling with utility classes:**
   ```mdc
   [Warning]{.text-red-500 .font-bold}: System maintenance in progress.
   ```
2. **Adding tooltips:**
   ```mdc
   Hover over [this text]{title="Additional information"} for more details.
   ```
3. **Adding metadata for JavaScript:**
   ```mdc
   Click [here]{data-action="open-modal" data-target="login"} to sign in.
   ```
4. **Accessibility attributes:**
   ```mdc
   [Important]{role="alert" aria-label="Critical notice"} security update available.
   ```

### Nested Markdown

Span syntax supports nested markdown formatting:

::code-group
```mdc [Syntax]
[**Bold** and *italic* text]{.highlight}
```

```html [Output]
<p><span class="highlight"><strong>Bold</strong> and <em>italic</em> text</span></p>
```
::

## Block attributes

A trailing `{...}` on the last line of a block-level element attaches the attributes to that block. This works on paragraphs, headings, list items (bullet, ordered, and task list), and blockquote paragraphs.

### Paragraph

::code-group
```mdc [Syntax]
A paragraph {attr="value"}
```

```html [Output]
<p attr="value">A paragraph</p>
```
::

### Headings

`{...}` after a heading attaches to that heading (alongside its auto-generated id when `headingIds` is enabled):

::code-group
```mdc [Syntax]
# Heading 1 {.intro data-section="hero"}

## Heading 2 {attr="value"}
```

```html [Output]
<h1 id="heading-1" class="intro" data-section="hero">Heading 1</h1>
<h2 id="heading-2" attr="value">Heading 2</h2>
```
::

A `{#id}` overrides the auto-generated id. When rendering back to markdown, the id is written out only if it differs from the auto-generated one, so custom ids survive a round trip:

::code-group
```mdc [Syntax]
# Product Documentation {#prod-doc}
```

```html [Output]
<h1 id="prod-doc">Product Documentation</h1>
```
::

### List items

Bullet, ordered, and task-list items all accept trailing attributes:

::code-group
```mdc [Syntax]
- a list item {attr="value"}
- another list item {attr2="value2"}

1. List item {attr="value"}
2. List item {attr2="value2"}

- [ ] Task list item {attr="value"}
- [x] Task list item {attr2="value2"}
```

```html [Output]
<ul>
  <li attr="value">a list item</li>
  <li attr2="value2">another list item</li>
</ul>
<ol>
  <li attr="value">List item</li>
  <li attr2="value2">List item</li>
</ol>
<ul class="contains-task-list">
  <li class="task-list-item" attr="value"><input ... /> Task list item</li>
  <li class="task-list-item" attr2="value2"><input ... /> Task list item</li>
</ul>
```
::

### Blockquote

A single-line blockquote attaches the attributes to the blockquote element:

::code-group
```mdc [Syntax]
> Blockquote {attr="value"}
```

```html [Output]
<blockquote attr="value">Blockquote</blockquote>
```
::

When a blockquote has multiple paragraphs, each paragraph's trailing `{...}` attaches to that paragraph:

::code-group
```mdc [Syntax]
> Blockquote paragraph 1 {attr="value"}
>
> Blockquote paragraph 2 {attr2="value2"}
```

```html [Output]
<blockquote>
  <p attr="value">Blockquote paragraph 1</p>
  <p attr2="value2">Blockquote paragraph 2</p>
</blockquote>
```
::

### Span vs paragraph

Whether the attributes attach to a span or to the surrounding paragraph depends on whether there is a space between the closing `]` and `{`:

::code-group
```mdc [Syntax]
A paragraph [span] {attr="value"}

A paragraph [span]{attr="value"}
```

```html [Output]
<p attr="value">A paragraph <span>span</span></p>
<p>A paragraph <span attr="value">span</span></p>
```
::

### Wrapping a list, table, blockquote, or code block

For attributes that don't have a natural slot in the native syntax (lists, tables, multi-paragraph blockquotes, and fenced code blocks), use the matching `::tag` block component. When a `::ul`, `::ol`, `::table`, `::blockquote`, or `::pre` wraps a single same-tagged child, Comark folds them into one element, and the wrapper's attributes land on the actual list/table/blockquote/pre.

::code-group
~~~mdc [Syntax]
::ul{attr="value"}
- item 1
- item 2
::

::ol{attr="value"}
1. item 1
2. item 2
::

::table{attr="value"}
| col1 | col2 |
| ---- | ---- |
| a    | b    |
::

::blockquote{attr="value"}
> Paragraph 1
>
> Paragraph 2
::

::pre{attr="value"}
```ts
const variable = "value"
```
::
~~~

```html [Output]
<ul attr="value">
  <li>item 1</li>
  <li>item 2</li>
</ul>
<ol attr="value">
  <li>item 1</li>
  <li>item 2</li>
</ol>
<table attr="value">
  <thead><tr><th>col1</th><th>col2</th></tr></thead>
  <tbody><tr><td>a</td><td>b</td></tr></tbody>
</table>
<blockquote attr="value">
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</blockquote>
<pre data-language="ts" attr="value"><code class="language-ts">const variable = "value"</code></pre>
```
::

::callout{color="info" icon="i-lucide-info"}
Block attributes work on the standard Markdown elements above. For larger custom block content, use [Components](https://comark.dev/syntax/components) instead.
::

---

- [Components](https://comark.dev/syntax/components)
- [Attributes Plugin](https://comark.dev/plugins/defaults/attributes)


# Frontmatter

> Add a leading YAML block to declare document metadata that Comark parses into tree.frontmatter.

Comark supports YAML frontmatter at the beginning of documents. Parsing is provided by the built-in [`frontmatter` plugin](https://comark.dev/plugins/defaults/frontmatter) (enabled by default).

## Syntax

```mdc
---
title: My Document Title
author: John Doe
date: 2024-01-15
tags:
  - markdown
  - documentation
depth: 3
searchDepth: 2
custom_field: custom value
---

# Document Content

Your markdown content here...
```

### Rules

- Must be at the **very beginning** of the document (no leading blank lines)
- Enclosed by `---` delimiters
- Body is parsed as YAML
- Empty frontmatter (`---\n---` with no content) is ignored

## Parse result

The `frontmatter` property is available on the parse result:

~~~ts
import { parseMarkdown } from 'comark'

const content = `---
title: My Article
author: Jane Doe
---

# Hello World
`

const { frontmatter, nodes } = await parseMarkdown(content)
/*
frontmatter → { title: 'My Article', author: 'Jane Doe' }
nodes       → [ ['h1', { id: 'hello-world' }, 'Hello World'] ]
*/
~~~

See [Document Model](https://comark.dev/getting-started/document-model#frontmatter) for how frontmatter sits on `MarkdownDocument`.

## Data binding

Frontmatter values can be referenced in component props with the `:prefix` binding syntax:

```mdc
---
theAnswer: 42
user:
  name: Ada
---

::question{:answer="frontmatter.theAnswer"}
::

Hello, :badge{:label="frontmatter.user.name"}
```

Learn more in [Data Binding](https://comark.dev/syntax/components#data-binding).

## Disable

Turn off all default plugins so the `---` block is treated as regular markdown:

```ts
import { parseMarkdown } from 'comark'

const result = await parseMarkdown(content, {
  registerDefaultPlugins: false,
})
// result.frontmatter → {}
```

See the [frontmatter plugin](https://comark.dev/plugins/defaults/frontmatter) for registration and options.

---

- [Frontmatter Plugin](https://comark.dev/plugins/defaults/frontmatter)
- [Document Model](https://comark.dev/getting-started/document-model)


# Markdown to HTML in JavaScript, No Framework

> Convert Markdown to HTML strings in JavaScript without a framework, for server rendering, static sites, RSS feeds, and emails.

The `@comark/html` package parses Markdown or renders an existing `MarkdownDocument` to an HTML string without a framework dependency. Use it for server-side rendering, static site generation, RSS feeds, and emails.

## Installation

::code-group
```bash [pnpm]
pnpm add @comark/html
```

```bash [npm]
npm install @comark/html
```

```bash [yarn]
yarn add @comark/html
```

```bash [bun]
bun add @comark/html
```
::

## `renderHtml()`

The quickest way to parse markdown and get an HTML string in one call.

### Usage

::code-group
~~~typescript [Code]
import { renderHtml } from '@comark/html'

const html = await renderHtml(`
# Getting Started

This is a **bold** statement with a [link](https://example.com).

- Item 1
- Item 2
`)
~~~

```html [Output]
<h1 id="getting-started">Getting Started</h1>
<p>This is a <strong>bold</strong> statement with a <a href="https://example.com">link</a>.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
```
::

### Options

| Option                                     | Type                            | Default     | Description                                                                                        |
| ------------------------------------------ | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- |
| [`plugins`](#render-options-plugins)       | `ComarkPlugin[]`                | `[]`        | Array of plugins                                                                                   |
| [`components`](#render-options-components) | `Record<string, fn>`            | `{}`        | Custom component renderers                                                                         |
| [`data`](#render-options-data)             | `Record<string, any>`           | `undefined` | Data passed to component renderers                                                                 |
| `autoClose`                                | `boolean`                       | `true`      | Close incomplete Markdown and components before parsing                                            |
| `autoUnwrap`                               | `boolean`                       | `true`      | Remove a single paragraph wrapper inside components                                                |
| `linkify`                                  | `boolean`                       | `true`      | Convert URL-like text into links                                                                   |
| `registerDefaultPlugins`                   | `boolean`                       | `true`      | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) |
| `unwrap`                                   | `boolean \| string \| string[]` | `false`     | Remove selected wrapper tags from the parsed document                                              |

`renderHtml()` accepts all [`ParserOptions`](https://comark.dev/reference/parse#options) in addition to the renderer options above.

#### `plugins`

See [ComarkPlugin](https://comark.dev/plugins) for available plugins.

```typescript
import { renderHtml } from '@comark/html'
import shiki from '@comark/html/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const html = await renderHtml('```js\nconsole.log("hi")\n```', {
  plugins: [
    shiki({
      themes: { light: githubLight, dark: githubDark }
    })
  ],
})
```

#### `components`

Map component names to async render functions. Each function receives the element as `[tag, attrs, ...children]` and a context with `render` to process nested content:

::code-group
~~~typescript [Code]
import { renderHtml } from '@comark/html'

const html = await renderHtml(`
::alert{type="warning"}
This is a warning message!
::
`, {
  components: {
    alert: async ([, attrs, ...children], { render }) => {
      return `<div class="alert alert-${attrs.type}" role="alert">${await render(children)}</div>`
    }
  }
})
~~~

```html [Output]
<div class="alert alert-warning" role="alert"><p>This is a warning message!</p></div>
```
::

#### `data`

Pass external data to every component renderer via the context object:

::code-group
~~~typescript
import { renderHtml } from '@comark/html'

const html = await renderHtml(`
::header
Welcome!
::
`, {
  data: { siteName: 'My Blog' },
  components: {
    header: async ([, , ...children], { render, data }) => {
      return `<header><h1>${data?.siteName}</h1>${await render(children)}</header>`
    }
  }
})
~~~

```html [Output]
<header><h1>My Blog</h1><p>Welcome!</p></header>
```
::

### `createHtmlRenderer()`

Creates a reusable parse+render function. The underlying parser is initialized once and reused on every call, which is more efficient when rendering many documents.

#### Usage

```typescript
import { createHtmlRenderer } from '@comark/html'
import shiki from '@comark/html/plugins/shiki'

const renderHtml = createHtmlRenderer({
  plugins: [shiki()],
})

// Reuse the same configured parser
const html1 = await renderHtml('# Document 1\n\n...')
const html2 = await renderHtml('# Document 2\n\n...')
```

#### Options

Same as [`renderHtml()`](#renderhtml-options).

---

## `renderHtmlFromDocument()`

Renders a pre-parsed `MarkdownDocument` directly, with no parsing step. Use it when you already have a document from a prior parse, build step, or API call.

### Integration

Parse on the server and render in a separate step. No parser or plugin code needed at render time:

```typescript [server/api/content/[slug\\].ts]
import { createMarkdownParser } from 'comark'
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'

const parse = createMarkdownParser()

export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, 'slug')
  const markdown = await readFile(join('content', `${slug}.md`), 'utf-8')
  return parse(markdown)
})
```

Pass the pre-parsed document to `renderHtmlFromDocument`:

```typescript [render.ts]
import { renderHtmlFromDocument } from '@comark/html'

const document = await $fetch(`/api/content/${slug}`)
const html = await renderHtmlFromDocument(document)
```

### Options

| Option                                     | Type                  | Description                        |
| ------------------------------------------ | --------------------- | ---------------------------------- |
| [`components`](#render-options-components) | `Record<string, fn>`  | Custom component renderers         |
| [`data`](#render-options-data)             | `Record<string, any>` | Data passed to component renderers |

---

## Overriding HTML elements

Pass native HTML tag names as keys in `components` to override how standard elements render:

```typescript
import { createHtmlRenderer } from '@comark/html'

const renderHtml = createHtmlRenderer({
  components: {
    h1: async ([, attrs, ...children], { render }) => {
      const anchor = attrs.id ? `<a href="#${attrs.id}">#</a>` : ''
      return `<h1 id="${attrs.id}" class="heading">${anchor}${await render(children)}</h1>`
    },
    a: async ([, attrs, ...children], { render }) => {
      const external = attrs.href?.startsWith('http') ? ' target="_blank" rel="noopener"' : ''
      return `<a href="${attrs.href}"${external}>${await render(children)}</a>`
    }
  }
})
```

---

## TypeScript support

```typescript
import type { ElementNode, Node } from 'comark'
import { createHtmlRenderer } from '@comark/html'

type RenderContext = {
  render: (nodes: Node[]) => Promise<string>
  data?: Record<string, any>
}

type ComponentRenderer = (element: ElementNode, ctx: RenderContext) => Promise<string>

const components: Record<string, ComponentRenderer> = {
  alert: async ([, attrs, ...children], { render }) => {
    return `<div class="alert alert-${attrs.type}">${await render(children)}</div>`
  }
}

const renderHtml = createHtmlRenderer({ components })
```

---

## Use cases

### Static site generation

~~~typescript [build.ts]
import { readFile, writeFile } from 'node:fs/promises'
import { createHtmlRenderer } from '@comark/html'
import shiki from '@comark/html/plugins/shiki'

const renderHtml = createHtmlRenderer({ plugins: [shiki()] })

async function buildPage(filePath: string) {
  const source = await readFile(filePath, 'utf-8')
  const html = await renderHtml(source)

  await writeFile('out/index.html', `
    <!DOCTYPE html>
    <html>
      <head><title>My Page
      ${html}
    
  `)
}
~~~

### RSS feed

~~~typescript [rss.ts]
import { createHtmlRenderer } from '@comark/html'

const renderHtml = createHtmlRenderer()

async function generateRSSItem(source: string) {
  const html = await renderHtml(source)
  return `
    
      
    
  `
}
~~~

### API response

```typescript [server.ts]
import { createHtmlRenderer } from '@comark/html'

const renderHtml = createHtmlRenderer()

async function handleRequest(markdownContent: string) {
  return Response.json({ html: await renderHtml(markdownContent) })
}
```

---

- [Plugins](https://comark.dev/plugins)
- [Streaming API](https://comark.dev/reference/auto-close)


# Vue Markdown Component with Streaming and Custom Components

> Render Markdown in Vue 3 with the Markdown component: map elements to your own components, add plugins, and stream AI output as it arrives.

The `@comark/vue` package provides Vue components for rendering Comark content with full support for custom components, plugins, and streaming.

## Installation

::code-group
```bash [pnpm]
pnpm add @comark/vue
```

```bash [npm]
npm install @comark/vue
```

```bash [yarn]
yarn add @comark/vue
```

```bash [bun]
bun add @comark/vue
```
::

## ``

The `` component is the quickest way to render markdown in Vue. It handles parsing and rendering automatically.

::warning
`` is an **async** component and must always be wrapped in ``
::

For server-side rendering, use the [`MarkdownDocument`](#code-markdowndocument) component combined with parsing on the server instead.

~~~vue [App.vue]



~~~

### Usage

Pass markdown content via the default slot or the `value` prop. `value` accepts a markdown **string** or a pre-parsed [`MarkdownDocument`](https://comark.dev/getting-started/document-model):

::code-group
```vue [Default Slot]

```

```vue [String]

```

```vue [MarkdownDocument]



```
::

::callout{icon="i-lucide-package" color="warning"}
Passing a document to `` skips parsing at runtime, but the **parser is still bundled** because `Markdown` imports it. To keep the client bundle free of the parser, use [``](#code-markdowndocument) instead.
::

### Props

| Prop                                                                 | Type                            | Default     | Description                                                                                                                                      |
| -------------------------------------------------------------------- | ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `value`                                                              | `string \| MarkdownDocument`    | `undefined` | Markdown string or pre-parsed document (alternative to default slot)                                                                             |
| [`options`](#code-markdown-props-code-options)                       | `ParserOptions`                 | `{}`        | Parser options (autoUnwrap, autoClose, etc.)                                                                                                     |
| [`plugins`](#code-markdown-props-code-plugins)                       | `ComarkPlugin[]`                | `[]`        | Array of plugins                                                                                                                                 |
| `unwrap`                                                             | `boolean \| string \| string[]` | `false`     | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `

`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom Vue component mappings | | [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | `summary` | `boolean` | `false` | Only render content before `` | | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `{}` | Runtime values referenced from markdown via `:prop="data.path"` | #### `options` See [ParserOptions](https://github.com/comarkdown/comark/blob/main/packages/comark/src/types.ts#L247) for available options. ```vue [App.vue] ``` #### `plugins` See [ComarkPlugin](https://comark.dev/plugins) for available plugins. ```vue [App.vue] ``` For math and mermaid plugins, also pass the companion components: ```vue [App.vue] ``` #### `components` Use this prop to map custom Vue components to Comark elements and use them in your markdown. ::steps{level="4"} #### Create a Vue component Save a component such as `components/Alert.vue`: ```vue [components/Alert.vue] ``` #### Map the tag to your component Pass the `components` prop to `Markdown`: ```vue [App.vue] ``` :::note Components receive props from the markdown and render children via slots. ::: #### Use it in your Markdown content ```mdc ::alert{type="warning"} This is a warning message! :: ``` :: ::tip See [Component Bindings](#component-bindings) for how props and slots map to your Vue component, or [Component Syntax](https://comark.dev/syntax/components) for the full Comark syntax API: nested components, inline syntax, and more. :: #### `componentsManifest` For lazy-loading components on demand. Components are resolved once and cached. Works with both `` and ``: ```vue [App.vue] ``` #### `data` Expose runtime values to markdown authors. Any prop written with a `:` prefix is resolved against the render context `{ frontmatter, meta, data, props }` when its value isn't valid JSON. See [Data Binding](https://comark.dev/syntax/components#data-binding) for the full scope. ```vue [App.vue] ``` ### `defineMarkdownComponent` Creates a pre-configured `` component with default options, plugins, and components baked in. #### Usage ::steps{level="4"} #### Expose your configured component ```typescript [markdown.ts] import { defineMarkdownComponent } from '@comark/vue' import shiki from '@comark/vue/plugins/shiki' import math, { Math } from '@comark/vue/plugins/math' import mermaid, { Mermaid } from '@comark/vue/plugins/mermaid' import githubLight from '@shikijs/themes/github-light' import githubDark from '@shikijs/themes/github-dark' import CustomAlert from './components/CustomAlert.vue' export const AppMarkdown = defineMarkdownComponent({ name: 'AppMarkdown', plugins: [ math(), mermaid(), shiki({ themes: { light: githubLight, dark: githubDark }, }), ], components: { Math, Mermaid, alert: CustomAlert, }, }) ``` #### Use it in your templates ```vue [App.vue] ``` :: #### Options | Option | Type | Default | Description | | --------------------------------------------------------------------- | -------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | [`extends`](#code-markdown-code-definemarkdowncomponent-code-extends) | `ReturnType` | `undefined` | Inherit plugins and components from another component | | `name` | `string` | `undefined` | Component name for debugging | | `autoUnwrap` | `boolean` | `true` | Automatically unwrap single block elements | | `autoClose` | `boolean` | `true` | Auto-close incomplete markdown syntax | | `linkify` | `boolean` | `true` | Auto-convert URL-like text into links | | `registerDefaultPlugins` | `boolean` | `true` | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) | | [`plugins`](#code-markdown-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom Vue component mappings | | `class` | `string` | `undefined` | Additional CSS classes for the wrapper div | #### `extends` Inherit plugins and components from another component, then layer your own on top: ```typescript [markdown.ts] import { defineMarkdownComponent } from '@comark/vue' import shiki from '@comark/vue/plugins/shiki' import githubLight from '@shikijs/themes/github-light' import githubDark from '@shikijs/themes/github-dark' import toc from '@comark/vue/plugins/toc' import math, { Math } from '@comark/vue/plugins/math' import ProsePre from './components/ProsePre.vue' // Base: highlight + prose components, used everywhere export const BaseMarkdown = defineMarkdownComponent({ name: 'BaseMarkdown', plugins: [ shiki({ themes: { light: githubLight, dark: githubDark } }), ], components: { ProsePre }, }) // Article: extends Base, adds TOC and math export const ArticleMarkdown = defineMarkdownComponent({ name: 'ArticleMarkdown', extends: BaseMarkdown, plugins: [toc({ depth: 3 }), math()], components: { Math }, }) // Comment: extends Base only, no TOC, no math export const CommentMarkdown = defineMarkdownComponent({ name: 'CommentMarkdown', extends: BaseMarkdown, }) ``` #### Merging behavior - `plugins`: Arrays are concatenated (config plugins + prop plugins) - `components`: Component mappings override global configuration - Other `options`: Component options override global configuration --- ## `` Renders a pre-parsed `MarkdownDocument` without any parsing. Use it when you parse on the server, in a build step, or via an API, so no parser or plugin code is shipped to the browser. ### Parsing ::steps{level="4"} #### Parse on the server ```typescript [server.ts] import { createMarkdownParser } from 'comark' import { readFile } from 'node:fs/promises' const parse = createMarkdownParser() // In your server handler export async function getContentDocument(slug: string) { const markdown = await readFile(`content/${slug}.md`, 'utf-8') return parse(markdown) } ``` #### Render with `MarkdownDocument` ```vue [ContentPage.vue] ``` :: ### Renderer props | Prop | Type | Default | Description | | -------------------------------------------------------------------- | ------------------------------ | ----------- | --------------------------------------------------------------- | | `value` | `MarkdownDocument` | — | **Required.** The parsed document returned by `parseMarkdown()` | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom Vue component mappings | | [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver for lazy-loaded components | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `{}` | Runtime values referenced from markdown via `:prop="data.path"` | ### `defineMarkdownDocumentComponent` Creates a pre-configured `` with baked-in component mappings. #### Setup ::steps{level="4"} #### Expose your configured renderer ```typescript [markdown.ts] import { defineMarkdownDocumentComponent } from '@comark/vue' import CustomAlert from './components/Alert.vue' import ProsePre from './components/ProsePre.vue' export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({ name: 'ArticleMarkdownDocument', components: { alert: CustomAlert, ProsePre, }, }) ``` #### Use it in your templates ```vue [ArticlePage.vue] ``` :: #### Renderer options | Option | Type | Default | Description | | ------------------------------------------------------------------------------------ | ---------------------------------------------------- | ----------- | ------------------------------------------------ | | [`extends`](#code-markdowndocument-code-definemarkdowndocumentcomponent-inheritance) | `ReturnType` | `undefined` | Inherit component mappings from another renderer | | `name` | `string` | `undefined` | Component name for debugging | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom Vue component mappings | | `class` | `string` | `undefined` | Additional CSS classes for the wrapper div | #### Inheritance Inherit component mappings from another renderer, then layer your own on top: ```typescript [markdown.ts] import { defineMarkdownDocumentComponent } from '@comark/vue' import ProsePre from './components/ProsePre.vue' import ProseA from './components/ProseA.vue' import CustomAlert from './components/Alert.vue' import CommentAlert from './components/CommentAlert.vue' const BaseMarkdownDocument = defineMarkdownDocumentComponent({ name: 'BaseMarkdownDocument', components: { ProsePre, ProseA }, }) export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({ name: 'ArticleMarkdownDocument', extends: BaseMarkdownDocument, components: { alert: CustomAlert }, }) export const CommentMarkdownDocument = defineMarkdownDocumentComponent({ name: 'CommentMarkdownDocument', extends: BaseMarkdownDocument, components: { alert: CommentAlert }, }) ``` --- ## Live documents `MarkdownDocument` can subscribe to an ambient **context** so external sources can drive a mounted renderer: an HMR signal, a collaboration socket, an agent editing the document while you chat with it, or devtools. The listen key is the document's own `meta.key` (set by a plugin) or the `document-key` prop, so a parsed document can carry its own identity. If `globalThis.comarkContext` exists the renderer listens for updates on that key and re-renders; on unmount it cleans up. With no context, the key is ignored at zero cost. ```vue ``` A **driver** installs the context once and pushes updates by key with `set()` (replace the whole document) or `patch()` (surgical node edits, with structural sharing so only the changed branch re-renders): ```ts import { createComarkContext, parseMarkdown } from 'comark' const ctx = createComarkContext() // installs globalThis.comarkContext const doc = ctx.get('page', await parseMarkdown('# Hello')) // seed on first access doc.set(await parseMarkdown('# Replaced')) doc.patch({ op: 'insert', path: [1], node: ['p', {}, 'inserted'] }) ``` A `path` is a node-index path into `document.nodes`: the first segment indexes the top-level nodes, each later segment indexes into that element's children. Patch operations are `replace`, `insert`, `remove` (each takes a `path`), plus `meta`, `frontmatter`, and `data` merges. The same context API powers `@comark/react`, `@comark/svelte`, and `@comark/angular`: websocket handlers, agents, devtools, and HMR all drive it the same way. --- ## Component bindings Comark automatically bridges the gap between Comark syntax and your component's interface. ### Prop binding Attributes in Comark syntax are passed as props to your component. Use the `:` prefix to pass typed values: | Markdown | Prop value | | --------------------------- | ------------------------- | | `{type="warning"}` | `"warning"` (string) | | `{:count="5"}` | `5` (number) | | `{:active="true"}` | `true` (boolean) | | `{:config='{"key":"val"}'}` | `{ key: 'val' }` (object) | ### Named slots Named slots in Comark (`#slotname`) map to Vue named slots: - **Default slot** → `` - **Named slots** → `` (for example, `#footer` → ``) ::code-group ```vue [Vue] ``` ```mdc [Comark] ::card{title="My Card"} Default slot content. #footer Footer slot content. :: ``` :: --- ## Overriding HTML elements Override how native HTML elements render by mapping a component to their tag name via the `components` prop in both the `Markdown` and `MarkdownDocument` components. ::steps{level="3"} ### Create overridden version ```vue [components/Heading.vue] ``` ### Map Pass the component to the `components` prop: ```vue [App.vue] ``` :: ### Resolution order When Comark encounters a tag, it looks for a matching Vue component in this order, stopping at the first match: 1. **`Prose{PascalTag}`**: for example, `ProseH1` for `h1`. Follows the Nuxt Content prose component convention. 2. **`{PascalTag}`**: for example, `Alert` for `alert`. PascalCase version of the tag name. 3. **`{tag}`**: for example, `alert`. Exact tag name as-is. 4. **Global**: any component registered via `app.component()`. If none match, the tag renders as a native HTML element. --- ## Nuxt UI integration When [`@nuxt/ui`](https://ui.nuxt.com) is installed, Comark automatically uses Nuxt UI's prose components for enhanced styling. ### Vite setup Enable prose components by setting `prose: true` in the Nuxt UI Vite plugin: ```typescript [vite.config.ts] import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import ui from '@nuxt/ui/vite' export default defineConfig({ plugins: [ vue(), ui({ prose: true }) ] }) ``` ### CSS setup ```css [src/assets/css/main.css] @import "tailwindcss"; @import "@nuxt/ui"; ``` --- ## Streaming Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews. ### Setup Set `streaming` to `true` while content is being received, then `false` when done: ```vue [components/AiChat.vue] ``` ::callout{icon="i-lucide-info" color="info"} `autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `:options="{ autoClose: false }"`. :: ### Caret The `caret` prop appends a blinking cursor to the last text node while `streaming` is `true`: ```vue [App.vue] {{ content }} {{ content }} ``` ```css .my-caret { display: inline-block; width: 2px; height: 1em; background: currentColor; animation: blink 1s step-end infinite; vertical-align: text-bottom; } @keyframes blink { 50% { opacity: 0; } } ``` --- ## TypeScript support Use `ComarkPlugin` from `comark` to type plugin arrays, and `ElementNode` to type the `__node` prop in components that override HTML elements: ```vue [ComarkWrapper.vue] ``` ```vue [components/Heading.vue] ``` --- --- - [Plugins](https://comark.dev/plugins) - [Streaming API](https://comark.dev/reference/auto-close) # Nuxt Markdown Rendering with Components > Render Markdown in Nuxt with the @comark/nuxt module: auto-imported components, prose overrides, Nuxt UI integration, and streaming. The `@comark/nuxt` module provides **zero-config** Comark setup for Nuxt: - Auto-imported components. - [`~/components/prose`](#overriding-html-elements) directory for overriding HTML elements. - [Nuxt UI](https://ui.nuxt.com) integration. - [SSR/SSG support](#server-side-rendering) out of the box. ## Installation ### Automatic This will automatically install the dependency and register the module in `nuxt.config.ts`: ```bash [Terminal] npx nuxt add comark ``` ### Manual Add `@comark/nuxt` to your dependencies: ::code-group ```bash [pnpm] pnpm add @comark/nuxt ``` ```bash [npm] npm install @comark/nuxt ``` ```bash [yarn] yarn add @comark/nuxt ``` ```bash [bun] bun add @comark/nuxt ``` :: Then add it to the `modules` in your `nuxt.config`: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@comark/nuxt'] }) ``` --- ## `` The `` component is automatically available in all your templates, no imports needed. It handles parsing and rendering in one step. ### Usage Pass markdown via the default slot or the `value` prop: ::code-group ```vue [Default Slot] ``` ```vue [Markdown Prop] ``` :: ### Props | Prop | Type | Default | Description | | -------------------------------------------------------------------- | ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `value` | `string \| MarkdownDocument` | `undefined` | Markdown string or pre-parsed document (alternative to default slot) | | [`options`](#code-markdown-props-code-options) | `ParserOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) | | [`plugins`](#code-markdown-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins | | `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `

`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom Vue component mappings | | [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | `summary` | `boolean` | `false` | Only render content before `` | | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | #### `options` See [ParserOptions](https://github.com/comarkdown/comark/blob/main/packages/comark/src/types.ts#L247) for available options. ```vue [app.vue] ``` #### `plugins` See [ComarkPlugin](https://comark.dev/plugins) for available plugins. ```vue [app.vue] ``` For math and mermaid plugins, also pass the companion components: ```vue [app.vue] ``` #### `components` Use this prop to map custom Vue components to Comark elements and use them in your markdown. ::steps{level="4"} #### Create a Vue component Save a component such as `components/Alert.vue`: ```vue [components/Alert.vue] ``` #### Map the tag to your component Pass the `components` prop to ``: ```vue [app.vue] ``` :::note Components receive props from the markdown and render children via slots. ::: #### Use it in your Markdown content ```mdc ::alert{type="warning"} This is a warning message! :: ``` :: ::tip To override how native HTML elements render (such as `h1` or `pre`), place components in `~/components/prose` instead. See [Overriding HTML elements](#overriding-html-elements). :: #### `componentsManifest` For lazy-loading components on demand. Components are resolved once and cached. ```vue [app.vue] ``` --- ## Server-side rendering Comark fully supports multiple rendering modes in Nuxt: ### Static generation ~~~vue [pages/index.vue] ~~~ ### Dynamic SSR ```vue [pages/blog/[slug\\].vue] ``` ### Prerendering ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@comark/nuxt'], nitro: { prerender: { routes: ['/blog', '/docs'] } } }) ``` --- ## Overriding HTML elements Place Vue components in `~/components/prose` to override how native HTML elements render. They are automatically registered, no imports or `components` prop needed. ### Directory structure ``` ~/components/ prose/ ProseH1.vue ProseH2.vue ProsePre.vue ProseA.vue ``` ### Example ```vue [components/prose/ProseH1.vue] ``` ### Resolution order When Comark encounters a tag, it looks for a matching component in this order, stopping at the first match: 1. **`Prose{PascalTag}`**: for example, `ProseH1` for `h1`. Nuxt auto-registers all components from `~/components/prose` matching this convention. 2. **`{PascalTag}`**: for example, `Alert` for `alert`. PascalCase version of the tag name. 3. **`{tag}`**: for example, `alert`. Exact tag name as-is. 4. **Global**: any component registered via `app.component()` or Nuxt auto-imports. If none match, the tag renders as a native HTML element. --- ## Component bindings Comark automatically bridges the gap between Comark syntax and your component's interface. ### Prop binding Attributes in Comark syntax are passed as props to your component. Use the `:` prefix to pass typed values: | Markdown | Prop value | | --------------------------- | ------------------------- | | `{type="warning"}` | `"warning"` (string) | | `{:count="5"}` | `5` (number) | | `{:active="true"}` | `true` (boolean) | | `{:config='{"key":"val"}'}` | `{ key: 'val' }` (object) | ### Named slots Named slots in Comark (`#slotname`) map to Vue named slots: - **Default slot** → `` - **Named slots** → `` (for example, `#footer` → ``) ::code-group ```vue [Vue] ``` ```mdc [Comark] ::card{title="My Card"} Default slot content. #footer Footer slot content. :: ``` :: --- ## Nuxt UI integration When [`@nuxt/ui`](https://ui.nuxt.com) is installed, Comark automatically uses Nuxt UI's prose components for enhanced styling. ::callout{icon="i-lucide-sparkles" color="info"} No extra configuration needed. The `@comark/nuxt` module detects `@nuxt/ui` in your dependencies and configures prose components automatically. :: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@comark/nuxt', '@nuxt/ui'] }) ``` ### CSS setup ```css [app/assets/css/main.css] @import "tailwindcss"; @import "@nuxt/ui"; ``` ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@comark/nuxt', '@nuxt/ui'], css: ['~/assets/css/main.css'] }) ``` --- ## Streaming Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews. ### Setup Set `streaming` to `true` while content is being received, then `false` when done: ```vue [app.vue] ``` ::callout{icon="i-lucide-info" color="info"} `autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `:options="{ autoClose: false }"`. :: ### Caret The `caret` prop appends a blinking cursor to the last text node while `streaming` is `true`: ```vue [app.vue] {{ content }} {{ content }} ``` ```css .my-caret { display: inline-block; width: 2px; height: 1em; background: currentColor; animation: blink 1s step-end infinite; vertical-align: text-bottom; } @keyframes blink { 50% { opacity: 0; } } ``` --- ## TypeScript support Use `ComarkPlugin` from `comark` to type plugin arrays, and `ElementNode` to type the `__node` prop in components that override HTML elements: ```vue [components/ComarkWrapper.vue] ``` ```vue [components/prose/ProseH1.vue] ``` --- --- - [Plugins](https://comark.dev/plugins) - [Streaming API](https://comark.dev/reference/auto-close) # React Markdown Renderer with Streaming and Components > Render Markdown in React and Next.js with custom components, plugins, React Server Components, and streaming for AI chat output. The `@comark/react` package provides React components for rendering Comark content with full support for custom components, plugins, and streaming. ## Installation ::code-group ```bash [pnpm] pnpm add @comark/react ``` ```bash [npm] npm install @comark/react ``` ```bash [yarn] yarn add @comark/react ``` ```bash [bun] bun add @comark/react ``` :: ## `` The `` component is the quickest way to render markdown in React. It handles parsing and rendering automatically. ::warning{to="#code-markdowndocument"} `` is an **async** component. You can also use the `` component to handle parsing yourself. :: ~~~tsx [App.tsx] import { Markdown } from '@comark/react' const content = `# Hello World This is **markdown** with Comark components. ` export default function App() { return {content} } ~~~ ### Usage Pass markdown content via `children` or the `value` prop. `value` accepts a markdown **string** or a pre-parsed [`MarkdownDocument`](https://comark.dev/getting-started/document-model): ::code-group ```tsx [Children] {content} ``` ```tsx [String] ``` ```tsx [MarkdownDocument] import type { MarkdownDocument } from 'comark' export default function Article({ document }: { document: MarkdownDocument }) { return } ``` :: ::callout{icon="i-lucide-package" color="warning"} Passing a document to `` skips parsing at runtime, but the **parser is still bundled** because `Markdown` imports it. To keep the client bundle free of the parser, use [``](#code-markdowndocument) instead. :: ### Props | Prop | Type | Default | Description | | -------------------------------------------------------------------- | -------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `children` | `React.ReactNode` | - | Markdown content to parse and render | | `value` | `string \| MarkdownDocument` | `''` | Markdown string or pre-parsed document (alternative to children) | | [`options`](#code-markdown-props-code-options) | `ParserOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) | | [`plugins`](#code-markdown-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins | | `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `

`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom React component mappings | | [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `(name: string) => Promise` | `undefined` | Dynamic component resolver | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `className` | `string` | `undefined` | CSS class for wrapper element | | `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `

` | #### `options` See [ParserOptions](https://github.com/comarkdown/comark/blob/main/packages/comark/src/types.ts#L247) for available options. ```tsx [App.tsx] {content} ``` #### `plugins` See [ComarkPlugin](https://comark.dev/plugins) for available plugins. ```tsx [App.tsx] import { Markdown } from '@comark/react' import shiki from '@comark/react/plugins/shiki' import githubLight from '@shikijs/themes/github-light' import githubDark from '@shikijs/themes/github-dark' const plugins = [ shiki({ themes: { light: githubLight, dark: githubDark } }) ] export default function App() { return {content} } ``` For math and mermaid plugins, also pass the companion components: ```tsx [App.tsx] import { Markdown } from '@comark/react' import math, { Math } from '@comark/react/plugins/math' import mermaid, { Mermaid } from '@comark/react/plugins/mermaid' import 'katex/dist/katex.min.css' export default function App() { return ( ) } ``` #### `components` Use this prop to map custom React components to Comark elements and use them in your markdown. ::steps{level="4"} #### Create a component ```tsx [components/Alert.tsx] interface AlertProps { type?: 'info' | 'warning' | 'error' | 'success' children: React.ReactNode } export default function Alert({ type = 'info', children }: AlertProps) { return (
{children}
) } ``` #### Map the tag to your component ```tsx [App.tsx] import { Markdown } from '@comark/react' import Alert from './components/Alert' import Card from './components/Card' const components = { alert: Alert, card: Card } export default function App() { return {content} } ``` #### Use it in your Markdown ```mdc ::alert{type="warning"} This is a warning message! :: ``` :: ::tip See [Component Bindings](#component-bindings) for how props and slots map to your React component, or [Component Syntax](https://comark.dev/syntax/components) for the full Comark syntax API: nested components, inline syntax, and more. :: #### `componentsManifest` For lazy-loading components on demand. Components are resolved via `React.lazy()` and wrapped in `` automatically. Works with both `` and ``: ```tsx [App.tsx] import { Markdown } from '@comark/react' const manifest = (name: string) => { return import(`./components/prose/${name}.tsx`) } export default function App() { return {content} } ``` #### `data` Expose runtime values to markdown authors. Any prop written with a `:` prefix is resolved against the render context `{ frontmatter, meta, data, props }` when its value isn't valid JSON. See [Data Binding](https://comark.dev/syntax/components#data-binding) for the full scope. ```tsx [App.tsx] import { Markdown } from '@comark/react' const user = { name: 'Ada', role: 'admin' } const content = `Hello, :badge{:label="data.user.name"}!` export default function App() { return } ``` ### `defineMarkdownComponent` Creates a pre-configured `` component with default options, plugins, and components baked in. #### Usage ::steps{level="4"} #### Expose your configured component ```typescript [markdown.ts] import { defineMarkdownComponent } from '@comark/react' import shiki from '@comark/react/plugins/shiki' import math, { Math } from '@comark/react/plugins/math' import mermaid, { Mermaid } from '@comark/react/plugins/mermaid' import githubLight from '@shikijs/themes/github-light' import githubDark from '@shikijs/themes/github-dark' import CustomAlert from './components/CustomAlert' export const AppMarkdown = defineMarkdownComponent({ name: 'AppMarkdown', plugins: [ math(), mermaid(), shiki({ themes: { light: githubLight, dark: githubDark }, }), ], components: { Math, Mermaid, alert: CustomAlert, }, }) ``` #### Use it in your templates ```tsx [App.tsx] import { AppMarkdown } from './markdown' export default function App() { return ( <> {/* All configuration is already included */} {content} {/* Can still override per-instance */} {content} ) } ``` :: #### Options | Option | Type | Default | Description | | --------------------------------------------------------------------- | -------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | [`extends`](#code-markdown-code-definemarkdowncomponent-code-extends) | `ReturnType` | `undefined` | Inherit plugins and components from another component | | `name` | `string` | `undefined` | Component name for debugging | | `autoUnwrap` | `boolean` | `true` | Automatically unwrap single block elements | | `autoClose` | `boolean` | `true` | Auto-close incomplete markdown syntax | | `linkify` | `boolean` | `true` | Auto-convert URL-like text into links | | `registerDefaultPlugins` | `boolean` | `true` | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) | | [`plugins`](#code-markdown-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom React component mappings | | `className` | `string` | `undefined` | Additional CSS classes for the wrapper div | #### `extends` Inherit plugins and components from another component, then layer your own on top: ```typescript [markdown/index.ts] import { defineMarkdownComponent } from '@comark/react' import shiki from '@comark/react/plugins/shiki' import githubLight from '@shikijs/themes/github-light' import githubDark from '@shikijs/themes/github-dark' import toc from '@comark/react/plugins/toc' import math, { Math } from '@comark/react/plugins/math' import CodeBlock from './components/CodeBlock' // Base: highlight + shared prose overrides, used everywhere const BaseMarkdown = defineMarkdownComponent({ name: 'BaseMarkdown', plugins: [shiki({ themes: { light: githubLight, dark: githubDark } })], components: { pre: CodeBlock }, }) // Article: extends Base, adds TOC and math export const ArticleMarkdown = defineMarkdownComponent({ name: 'ArticleMarkdown', extends: BaseMarkdown, plugins: [toc({ depth: 3 }), math()], components: { Math }, }) // Comment: extends Base only, no TOC, no math export const CommentMarkdown = defineMarkdownComponent({ name: 'CommentMarkdown', extends: BaseMarkdown, }) ``` #### Merging behavior - `plugins`: Arrays are concatenated (config plugins + prop plugins) - `components`: Props override config (prop components take precedence) - Other `options`: Props override config #### Usage with Next.js App Router ```tsx [markdown.ts] 'use client' import { defineMarkdownComponent } from '@comark/react' import math, { Math } from '@comark/react/plugins/math' export const DocsMarkdown = defineMarkdownComponent({ name: 'DocsMarkdown', plugins: [math()], components: { Math }, }) ``` ```tsx [app/docs/[slug\\]/page.tsx] import { DocsMarkdown } from '@/components/markdown' export default async function Page({ params }: { params: { slug: string } }) { const content = await getDocContent(params.slug) return {content} } ``` --- ## `` Renders a pre-parsed `MarkdownDocument` without any parsing. Use it when you parse on the server, in a build step, or via an API, so no parser or plugin code is shipped to the browser. ### Parsing Parse your markdown content and pass the document directly in a React Server Component: ```tsx [app/docs/[slug\\]/page.tsx] import { createMarkdownParser } from 'comark' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { MarkdownDocument } from '@comark/react' import Alert from '@/components/Alert' const parse = createMarkdownParser() export default async function DocsPage({ params }: { params: { slug: string } }) { const markdown = await readFile(join('content', `${params.slug}.md`), 'utf-8') const document = await parseMarkdown(markdown) return } ``` ### Renderer props | Prop | Type | Default | Description | | -------------------------------------------------------------------- | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- | | `value` | `MarkdownDocument` | — | **Required.** The parsed document returned by `parseMarkdown()` | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom React component mappings | | [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver for lazy-loaded components | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `className` | `string` | `undefined` | CSS class for the wrapper `
` | | `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `
` | ### `defineMarkdownDocumentComponent` Creates a pre-configured `` with baked-in component mappings. #### Setup ::steps{level="4"} #### Expose your configured renderer ```tsx [markdown/index.ts] import { defineMarkdownDocumentComponent } from '@comark/react' import Alert from './components/Alert' import CodeBlock from './components/CodeBlock' export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({ name: 'ArticleMarkdownDocument', components: { alert: Alert, pre: CodeBlock, }, }) ``` #### Page integration ```tsx [app/docs/[slug\\]/page.tsx] import { parseMarkdown } from 'comark' import { ArticleMarkdownDocument } from '@/markdown' export default async function Page({ params }: { params: { slug: string } }) { const markdown = await getContent(params.slug) const document = await parseMarkdown(markdown) return } ``` :: #### Renderer options | Option | Type | Default | Description | | ------------------------------------------------------------------------------------ | ---------------------------------------------------- | ----------- | ------------------------------------------------ | | [`extends`](#code-markdowndocument-code-definemarkdowndocumentcomponent-inheritance) | `ReturnType` | `undefined` | Inherit component mappings from another renderer | | `name` | `string` | `undefined` | Component name for debugging | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom React component mappings | | `className` | `string` | `undefined` | Additional CSS classes for the wrapper div | #### Inheritance Inherit component mappings from another renderer, then layer your own on top: ```tsx [markdown/index.ts] import { defineMarkdownDocumentComponent } from '@comark/react' import CodeBlock from './components/CodeBlock' import ProseA from './components/ProseA' import Alert from './components/Alert' import CommentAlert from './components/CommentAlert' const BaseMarkdownDocument = defineMarkdownDocumentComponent({ name: 'BaseMarkdownDocument', components: { pre: CodeBlock, a: ProseA }, }) export const ArticleMarkdownDocument = defineMarkdownDocumentComponent({ name: 'ArticleMarkdownDocument', extends: BaseMarkdownDocument, components: { alert: Alert }, }) export const CommentMarkdownDocument = defineMarkdownDocumentComponent({ name: 'CommentMarkdownDocument', extends: BaseMarkdownDocument, components: { alert: CommentAlert }, }) ``` --- ## Live documents `MarkdownLive` is a client wrapper around `MarkdownDocument` that subscribes to an ambient **context** so external sources can drive a mounted renderer: an HMR signal, a collaboration socket, an agent editing the document while you chat with it, or devtools. It accepts the same props as `MarkdownDocument` plus a `documentKey`. The listen key is the document's own `meta.key` (set by a plugin) or the `documentKey` prop; if `globalThis.comarkContext` exists the renderer listens for updates on that key and re-renders, cleaning up on unmount. `MarkdownDocument` itself stays free of client-only hooks so it can render in a React Server Component. With no context, the key is ignored at zero cost. ```tsx import { MarkdownLive } from '@comark/react' ``` A **driver** installs the context once and pushes updates by key with `set()` (replace the whole document) or `patch()` (surgical node edits, with structural sharing so only the changed branch re-renders): ```ts import { createComarkContext, parseMarkdown } from 'comark' const ctx = createComarkContext() // installs globalThis.comarkContext const doc = ctx.get('page', await parseMarkdown('# Hello')) // seed on first access doc.set(await parseMarkdown('# Replaced')) doc.patch({ op: 'insert', path: [1], node: ['p', {}, 'inserted'] }) ``` A `path` is a node-index path into `document.nodes`: the first segment indexes the top-level nodes, each later segment indexes into that element's children. Patch operations are `replace`, `insert`, `remove` (each takes a `path`), plus `meta`, `frontmatter`, and `data` merges. The same context API powers `@comark/vue`, `@comark/svelte`, and `@comark/angular`: websocket handlers, agents, devtools, and HMR all drive it the same way. --- ## Component bindings Comark automatically bridges the gap between Comark syntax and your component's interface. ### Prop binding Attributes in Comark syntax are passed as props to your component. Use the `:` prefix to pass typed values. HTML attribute names are automatically converted to their React equivalents: | Markdown | React prop | | --------------------------- | ---------------------------------- | | `{type="warning"}` | `type="warning"` (string) | | `{:count="5"}` | `count={5}` (number) | | `{:active="true"}` | `active={true}` (boolean) | | `{:config='{"key":"val"}'}` | `config={{ key: 'val' }}` (object) | | `{class="highlight"}` | `className="highlight"` | | `{tabindex="0"}` | `tabIndex={0}` | | `{style="color: red"}` | `style={{ color: 'red' }}` | ### Named slots Named slots in Comark (`#slotname`) map to `slot{Name}` props in React: - **Default slot** → `children` - **Named slots** → `slot{Name}` (for example, `#footer` → `slotFooter`) ::code-group ```tsx [React] interface CardProps { title?: string children?: React.ReactNode slotFooter?: React.ReactNode } export default function Card({ title, children, slotFooter }: CardProps) { return (
{title &&

{title}

}
{children}
{slotFooter &&
{slotFooter}
}
) } ``` ```mdc [Comark] ::card{title="My Card"} Default slot content. #footer Footer slot content. :: ``` :: --- ## Overriding HTML elements Override how native HTML elements render by mapping a component to their tag name via the `components` prop. ::steps{level="3"} ### Create overridden version ```tsx [components/Heading.tsx] interface HeadingProps { __node?: ElementNode id?: string children: React.ReactNode } export default function Heading({ __node, id, children }: HeadingProps) { const Tag = __node?.[0] || 'h2' return ( {id && #} {children} ) } ``` ### Map ```tsx [App.tsx] {content} ``` :: ### Resolution order Components are resolved in this order: 1. **`Prose{PascalTag}`**: for example, `ProseH1` for `h1` 2. **`{PascalTag}`**: for example, `Alert` for `alert` 3. **`{tag}`**: for example, `alert` If no custom component matches, the tag renders as a native HTML element. --- ## Streaming Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews. ### Setup Set `streaming` to `true` while content is being received, then `false` when done: ```tsx [components/AiChat.tsx] import { useState } from 'react' import { Markdown } from '@comark/react' export default function AiChat() { const [content, setContent] = useState('') const [isStreaming, setIsStreaming] = useState(false) async function askAI(prompt: string) { setContent('') setIsStreaming(true) const response = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ prompt }), }) const reader = response.body!.getReader() const decoder = new TextDecoder() let accumulated = '' while (true) { const { done, value } = await reader.read() if (done) break accumulated += decoder.decode(value, { stream: true }) setContent(accumulated) } setIsStreaming(false) } return ( {content} ) } ``` ::callout{icon="i-lucide-info" color="info"} `autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `options={{ autoClose: false }}`. :: ### Caret The `caret` prop appends a blinking cursor to the last text node while `streaming` is `true`: ```tsx [App.tsx] {/* Default caret */} {content} {/* Custom caret class */} {content} ``` ```css .my-caret { display: inline-block; width: 2px; height: 1em; background: currentColor; animation: blink 1s step-end infinite; vertical-align: text-bottom; } @keyframes blink { 50% { opacity: 0; } } ``` --- ## TypeScript support Use `ComarkPlugin` from `comark` to type plugin arrays, and `ElementNode` to type the `__node` prop in components that override HTML elements: ```tsx [ComarkWrapper.tsx] import type { ComponentType } from 'react' import type { ComarkPlugin } from 'comark' import { Markdown } from '@comark/react' interface Props { content: string components?: Record> plugins?: ComarkPlugin[] } export default function ComarkWrapper({ content, components, plugins }: Props) { return {content} } ``` ```tsx [components/Heading.tsx] import type { ElementNode } from 'comark' interface HeadingProps { __node?: ElementNode id?: string children: React.ReactNode } export default function Heading({ __node, id, children }: HeadingProps) { const Tag = __node?.[0] || 'h2' return {children} } ``` --- --- - [Plugins](https://comark.dev/plugins) - [Streaming API](https://comark.dev/reference/auto-close) # Svelte 5 Markdown Renderer with Streaming > Render Markdown in Svelte 5 with native components, custom components, plugins, and streaming for AI output, including SvelteKit SSR. The `@comark/svelte` package provides Svelte 5 components for rendering Comark content with full support for custom components, plugins, and streaming. ## Installation ::code-group ```bash [pnpm] pnpm add @comark/svelte ``` ```bash [npm] npm install @comark/svelte ``` ```bash [yarn] yarn add @comark/svelte ``` ```bash [bun] bun add @comark/svelte ``` :: ## `` The `` component is the quickest way to render markdown in Svelte 5. It handles parsing and rendering automatically using `$state` and `$effect`. ::warning `` uses `$effect` internally and **will not render during SSR**. For SvelteKit, parse in your `load()` function and use [``](#code-markdowndocument) instead. :: ### Usage Pass markdown content via the `value` prop. `value` accepts a markdown **string** or a pre-parsed [`MarkdownDocument`](https://comark.dev/getting-started/document-model): ::code-group ```svelte [String] ``` ```svelte [MarkdownDocument] ``` :: ::callout{icon="i-lucide-package" color="warning"} Passing a document to `` skips parsing at runtime, but the **parser is still bundled** because `Markdown` imports it. To keep the client bundle free of the parser, use [``](#code-markdowndocument) instead. :: ### Props | Prop | Type | Default | Description | | -------------------------------------------------------------------- | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `value` | `string \| MarkdownDocument` | `''` | Markdown string or pre-parsed document | | [`options`](#code-markdown-props-code-options) | `ParserOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) | | [`plugins`](#code-markdown-props-code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins | | `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `

`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom Svelte component mappings | | [`componentsManifest`](#code-markdown-props-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `class` | `string` | `''` | CSS class for wrapper element | | `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `

` | #### `options` ```svelte [App.svelte] ``` #### `plugins` See [ComarkPlugin](https://comark.dev/plugins) for available plugins. ```svelte [App.svelte] ``` For math and mermaid plugins, also pass the companion components: ```svelte [App.svelte] ``` #### `components` Map custom Svelte components to Comark elements. Components receive props from the markdown and children as a Svelte `children` snippet. ::steps{level="4"} #### Create a Svelte component ```svelte [src/components/comark/Alert.svelte] ``` #### Map the tag to your component ```svelte [App.svelte] ``` #### Use it in your Markdown content ```mdc ::alert{type="warning"} This is a warning message! :: ``` :: ::tip See [Component Bindings](#component-bindings) for how props and snippets map to your Svelte component. :: #### `componentsManifest` A function that resolves component names to Svelte components at runtime. It can return a module (or `Promise`) for lazy loading, or the component directly for synchronous resolution. ::tip Keep components that are rendered from Markdown in a dedicated folder such as `src/components/comark/` or, in SvelteKit, `$lib/components/comark/`. This keeps Comark-rendered components separate from normal app UI components and makes `componentsManifest` globs easier to audit. :: **Lazy loading**: components are imported on demand when first rendered. This works with `` on the client: ```svelte [App.svelte] ``` **Lazy loading with SvelteKit SSR**: use `` and return dynamic imports from `componentsManifest`. SvelteKit awaits async SSR work, so only rendered components are loaded and included in the server HTML: ```svelte [routes/+page.svelte] ``` You can also use `import.meta.glob` when you want the manifest to cover every Svelte component in a folder: ```svelte [routes/+page.svelte] ``` ::warning If you add a `pending` snippet to ``, SSR renders that fallback instead of waiting for the lazy component HTML. Omit `pending` when you want the resolved components in the initial server HTML. :: **Stable SSR without experimental async**: use `import.meta.glob` with `eager: true` so `` can render components synchronously: ```svelte [routes/+page.svelte] ``` ::tip An explicit `componentMap` is easiest to audit and lets you choose public Markdown tags one by one. `import.meta.glob` is useful when you want a whole folder to become available by convention; Vite resolves matching files at **build time**. With `eager: true`, modules are imported statically. Without it, Vite generates dynamic `import()` calls that load each module lazily at runtime. :: #### `data` Expose runtime values to markdown authors. Any prop written with a `:` prefix is resolved against the render context `{ frontmatter, meta, data, props }` when its value isn't valid JSON. See [Data Binding](https://comark.dev/syntax/components#data-binding) for the full scope. ```svelte [App.svelte] ``` --- ## `` (experimental) `` uses Svelte's experimental `await` in `$derived` for a more declarative approach. It also awaits async `componentsManifest` entries during SSR, so lazy component imports render into SvelteKit server HTML. Requires `experimental.async` in your Svelte config: ```js [svelte.config.js] const config = { compilerOptions: { experimental: { async: true, }, }, } export default config ``` SvelteKit 3 no longer reads `svelte.config.js`; pass the same options to the `sveltekit()` plugin instead: ```ts [vite.config.ts] import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ sveltekit({ compilerOptions: { experimental: { async: true, }, }, }), ], }) ``` ```svelte [App.svelte] {#snippet pending()}

Loading...

{/snippet} {#snippet failed(error, reset)}

Error: {error.message}

{/snippet}
``` ::callout{icon="i-lucide-triangle-alert" color="warning"} The `experimental.async` feature is still experimental in Svelte 5. For production SSR without experimental async, prefer `` with eager/static components. :: Use `` when you need SSR HTML for non-eager, lazy-loaded Svelte components. Use `` with eager/static components when you want stable, non-experimental SSR. See the [SvelteKit example](https://comark.dev/examples/frameworks/sveltekit) for a complete local app with lazy SSR and stable SSR routes. --- ## `` Renders a pre-parsed `MarkdownDocument` without any parsing. Use it when you parse on the server (for example, in a SvelteKit `load` function), so no parser or plugin code is shipped to the browser. ### Integration ::steps{level="4"} #### Fetch the document in your load function ```typescript [src/routes/docs/[slug\\]/+page.ts] import type { PageLoad } from './$types' export const load: PageLoad = async ({ params, fetch }) => { const res = await fetch(`/api/content/${params.slug}`) const document = await res.json() return { document } } ``` #### Render with `MarkdownDocument` ```svelte [src/routes/docs/[slug\\]/+page.svelte] ``` :: ### Renderer props | Prop | Type | Default | Description | | ---------------------------------------------------------------------- | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- | | `value` | `MarkdownDocument` | — | **Required.** The parsed document returned by `parseMarkdown()` | | [`components`](#code-markdown-props-code-components) | `Record` | `{}` | Custom Svelte component mappings | | [`componentsManifest`](#code-markdowndocument-code-componentsmanifest) | `ComponentManifest` | `undefined` | Dynamic component resolver for lazy-loaded components | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node | | [`data`](#code-markdown-props-code-data) | `Record` | `undefined` | Runtime values referenced from markdown via `:prop="data.path"` | | `class` | `string` | `''` | CSS class for wrapper element | | `...attributes` | `HTMLAttributes` | — | Any other HTML attribute (`id`, `style`, `role`, `data-*`, `aria-*`, ...) is set on the wrapper `
` | ### `componentsManifest` For lazy-loading components on demand: ```svelte [App.svelte] ``` ::warning During SSR, `` can only render manifest entries synchronously. Use eager/static components for SSR HTML, or use [``](#code-markdownasync-code-experimental) when the manifest returns dynamic imports. :: --- ## Live documents `MarkdownDocument` can subscribe to an ambient **context** so external sources can drive a mounted renderer: an HMR signal, a collaboration socket, an agent editing the document while you chat with it, or devtools. Pass a `documentKey`, and if `globalThis.comarkContext` exists the renderer listens for updates on that key and re-renders; on unmount it cleans up. The key falls back to the document's own `meta.key` when a plugin sets it. With no context, it's a no-op at zero cost. ```svelte ``` A **driver** installs the context once and pushes updates by key with `set()` (replace the whole document) or `patch()` (surgical node edits, with structural sharing so only the changed branch re-renders): ```ts import { createComarkContext, parseMarkdown } from 'comark' const ctx = createComarkContext() // installs globalThis.comarkContext const doc = ctx.get('page', await parseMarkdown('# Hello')) // seed on first access doc.set(await parseMarkdown('# Replaced')) doc.patch({ op: 'insert', path: [1], node: ['p', {}, 'inserted'] }) ``` A `path` is a node-index path into `document.nodes`: the first segment indexes the top-level nodes, each later segment indexes into that element's children. Patch operations are `replace`, `insert`, `remove` (each takes a `path`), plus `meta`, `frontmatter`, and `data` merges. The same context API powers `@comark/vue`, `@comark/react`, and `@comark/angular`: websocket handlers, agents, devtools, and HMR all drive it the same way. --- ## Component bindings Comark automatically bridges the gap between Comark syntax and your component's interface. ### Prop binding Attributes in Comark syntax are passed as props to your component. Use the `:` prefix to pass typed values: | Markdown | Prop value | | --------------------------- | ------------------------- | | `{type="warning"}` | `"warning"` (string) | | `{:count="5"}` | `5` (number) | | `{:active="true"}` | `true` (boolean) | | `{:config='{"key":"val"}'}` | `{ key: 'val' }` (object) | ### Named snippets Named slots in Comark (`#slotname`) map to Svelte 5 snippets: - **Default content** → `children` snippet - **Named snippets** → named snippet prop (for example, `#footer` → `footer` snippet) ::code-group ```svelte [Svelte]

{title}

{@render children?.()}
{@render footer?.()}
``` ```mdc [Comark] ::card{title="My Card"} Default slot content. #footer Footer slot content. :: ``` :: --- ## Overriding HTML elements Use the `Prose` prefix to override how native HTML elements render: ::steps{level="3"} ### Create an override component ```svelte [ProseH1.svelte]

{@render children?.()}

``` ### Map it via the `components` prop ```svelte [App.svelte] ``` :: ### Resolution order Components are resolved in this order: 1. `Prose{PascalTag}`: for example, `ProseH1` for `h1` 2. `{PascalTag}`: for example, `Alert` for `alert` 3. `{tag}`: for example, `alert` If no custom component matches, the tag renders as a native HTML element (via ``). --- ## Streaming Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews. `Markdown` and `MarkdownAsync` reuse completed blocks while `streaming` is true. Set it to false when the stream ends to run a final full parse. Changes to `options`, `plugins`, or `unwrap` create a new parser. Heading tails and reference definitions use a full parse to preserve heading IDs and links. Set `streaming` to `true` while content is being received, then `false` when done: ```svelte [components/AiChat.svelte] ``` ::callout{icon="i-lucide-info" color="info"} `autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `options={{ autoClose: false }}`. :: ### Caret The `caret` prop appends a blinking cursor to the last text node while `streaming` is `true`: ```svelte [App.svelte] ``` ```css .my-caret { display: inline-block; width: 2px; height: 1em; background: currentColor; animation: blink 1s step-end infinite; vertical-align: text-bottom; } @keyframes blink { 50% { opacity: 0; } } ``` --- ## TypeScript support ```svelte [ComarkWrapper.svelte] ``` ```svelte [src/components/comark/ProseH1.svelte] ``` --- --- - [Plugins](https://comark.dev/plugins) - [Streaming API](https://comark.dev/reference/auto-close) # Angular Markdown Component with Streaming (v17+) > Render Markdown in Angular 17+ with standalone components, custom components, plugins, and streaming for AI output. The `@comark/angular` package provides standalone components for rendering Comark content in Angular with full support for custom components, plugins, and streaming. ## Requirements | Dependency | Version | | -------------- | ------------------ | | **Angular** | `>=17.0.0 <22.0.0` | | **TypeScript** | `>=5.5.0` | ::callout{icon="i-lucide-info" color="info"} `@comark/angular` is **bundler-agnostic**: it works with the Angular CLI, Vite (via [`@analogjs/vite-plugin-angular`](https://analogjs.org)), Webpack, esbuild, or any other build tool that supports Angular. :: ## Installation ::code-group ```bash [pnpm] pnpm add @comark/angular ``` ```bash [npm] npm install @comark/angular ``` ```bash [yarn] yarn add @comark/angular ``` ```bash [bun] bun add @comark/angular ``` :: ## `` The `` component is the quickest way to render markdown in Angular. It handles parsing and rendering automatically. The `value` input accepts a markdown **string** or a pre-parsed [`MarkdownDocument`](https://comark.dev/getting-started/document-model). ~~~typescript [app.component.ts] import { Component } from '@angular/core' import { Markdown } from '@comark/angular' @Component({ selector: 'app-root', standalone: true, imports: [Markdown], template: ``, }) export class AppComponent { content = `# Hello World This is **markdown** with Comark components. ` } ~~~ ::callout{icon="i-lucide-package" color="warning"} Passing a document to `` skips parsing at runtime, but the **parser is still bundled** because `Markdown` imports it. To keep the client bundle free of the parser, use [``](#code-comark-markdown-document) instead. :: ### Props (inputs) | Input | Type | Default | Description | | --------------------------- | ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `value` | `string \| MarkdownDocument` | `''` | Markdown string or pre-parsed document | | [`options`](#code-options) | `ParserOptions` | `{}` | Parser options (autoUnwrap, autoClose, etc.) | | [`plugins`](#code-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins | | `unwrap` | `boolean \| string \| string[]` | `false` | Strip wrapper tags (MDC `unwrap`) — `true` unwraps `

`; a comma/space-separated string or array peels tags sequentially, for example `"ul li"` | | [`components`](#components) | `Record>` | `{}` | Custom Angular component mappings | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | `summary` | `boolean` | `false` | Only render content before `` | | [`caret`](#caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | | [`data`](#data) | `Record` | `{}` | Runtime values referenced from markdown via `:prop="data.path"` | #### `options` See [ParserOptions](https://github.com/comarkdown/comark/blob/main/packages/comark/src/types.ts#L247) for available options. ```typescript [app.component.ts] @Component({ template: ``, }) ``` #### `plugins` See [ComarkPlugin](https://comark.dev/plugins) for available plugins. ```typescript [app.component.ts] import { Component } from '@angular/core' import { Markdown } from '@comark/angular' import shiki from '@comark/angular/plugins/shiki' import githubLight from '@shikijs/themes/github-light' import githubDark from '@shikijs/themes/github-dark' @Component({ selector: 'app-root', standalone: true, imports: [Markdown], template: ``, }) export class AppComponent { content = '```js\nconsole.log("hello")\n```' plugins = [ shiki({ themes: { light: githubLight, dark: githubDark }, }), ] } ``` For math and mermaid plugins, also pass the companion components: ~~~typescript [app.component.ts] import { Component } from '@angular/core' import { Markdown } from '@comark/angular' import math, { Math } from '@comark/angular/plugins/math' import mermaid, { Mermaid } from '@comark/angular/plugins/mermaid' @Component({ selector: 'app-root', standalone: true, imports: [Markdown], template: ` `, }) export class AppComponent { content = '$E = mc^2$' plugins = [math(), mermaid()] components = { Math, Mermaid } } ~~~ #### `components` Use this input to map custom Angular components to Comark elements and use them in your markdown. ::steps{level="4"} #### Create a component ~~~typescript [components/alert.component.ts] import { Component, Input } from '@angular/core' @Component({ selector: 'app-alert', standalone: true, template: `

`, }) export class AlertComponent { @Input() type: 'info' | 'warning' | 'error' | 'success' = 'info' } ~~~ #### Map the tag to your component ```typescript [app.component.ts] import { Component } from '@angular/core' import { Markdown } from '@comark/angular' import { AlertComponent } from './components/alert.component' import { CardComponent } from './components/card.component' @Component({ selector: 'app-root', standalone: true, imports: [Markdown], template: ``, }) export class AppComponent { content = '...' components = { alert: AlertComponent, card: CardComponent } } ``` #### Use it in your Markdown ```mdc ::alert{type="warning"} This is a warning message! :: ``` :: ::tip See [Component Bindings](#component-bindings) for how props map to your Angular component inputs, or [Component Syntax](https://comark.dev/syntax/components) for the full Comark syntax API: nested components, inline syntax, and more. :: #### `data` Expose runtime values to markdown authors. Any prop written with a `:` prefix is resolved against the render context `{ frontmatter, meta, data, props }` when its value isn't valid JSON. See [Data Binding](https://comark.dev/syntax/components#data-binding) for the full scope. ```typescript [app.component.ts] import { Component } from '@angular/core' import { Markdown } from '@comark/angular' @Component({ selector: 'app-root', standalone: true, imports: [Markdown], template: ``, }) export class AppComponent { data = { user: { name: 'Ada', role: 'admin' } } content = `Hello, :badge{:label="data.user.name"}!` } ``` --- ## `` Renders a pre-parsed `MarkdownDocument` without any parsing. Use it when you parse on the server, in a build step, or via an API, so no parser or plugin code is shipped to the browser. ### Parsing Parse your markdown content and pass the document directly: ~~~typescript [docs.component.ts] import { Component, OnInit } from '@angular/core' import { MarkdownDocument } from '@comark/angular' import { parseMarkdown } from 'comark' import type { MarkdownDocument as MarkdownDocumentType } from 'comark' @Component({ selector: 'app-docs', standalone: true, imports: [MarkdownDocument], template: ` @if (document) { } `, }) export class DocsComponent implements OnInit { document: MarkdownDocumentType | null = null components = {} async ngOnInit() { const markdown = await fetch('/api/content').then((r) => r.text()) this.document = await parseMarkdown(markdown) } } ~~~ ### Renderer props (inputs) | Input | Type | Default | Description | | -------------------------------- | ------------------------------ | ------- | --------------------------------------------------------------- | | `value` | `MarkdownDocument` | — | **Required.** The parsed document returned by `parseMarkdown()` | | [`components`](#code-components) | `Record>` | `{}` | Custom Angular component mappings | | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | [`caret`](#caret) | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node | | [`data`](#data) | `Record` | `{}` | Runtime values referenced from markdown via `:prop="data.path"` | --- ## Component bindings Comark automatically bridges the gap between Comark syntax and your Angular component's interface. ### Prop binding Attributes in Comark syntax are passed as `@Input()` values to your component. Use the `:` prefix to pass typed values: | Markdown | Angular Input value | | --------------------------- | ------------------------- | | `{type="warning"}` | `"warning"` (string) | | `{:count="5"}` | `5` (number) | | `{:active="true"}` | `true` (boolean) | | `{:config='{"key":"val"}'}` | `{ key: 'val' }` (object) | ### Content projection Default content in Comark maps to Angular's content projection (``): ::code-group ~~~typescript [Angular] @Component({ selector: 'app-card', standalone: true, template: `

{{ title }}

`, }) export class CardComponent { @Input() title = '' } ~~~ ```mdc [Comark] ::card{title="My Card"} Default content goes here. :: ``` :: --- ## Overriding HTML elements Use the `Prose` prefix to override how native HTML elements render: ::steps{level="3"} ### Create an override component ~~~typescript [components/prose-h1.component.ts] import { Component, Input } from '@angular/core' @Component({ selector: 'prose-h1', standalone: true, template: `

`, }) export class ProseH1Component { @Input() id?: string } ~~~ ### Map it via the `components` input ```typescript [app.component.ts] @Component({ template: ``, }) export class AppComponent { components = { ProseH1: ProseH1Component } } ``` :: ### Resolution order Components are resolved in this order: 1. `Prose{PascalTag}`: for example, `ProseH1` for `h1` 2. `{PascalTag}`: for example, `Alert` for `alert` 3. `{tag}`: for example, `alert` If no custom component matches, the tag renders as a native HTML element. --- ## Streaming Enable real-time rendering as content arrives, ideal for AI chat interfaces and live previews. Set `streaming` to `true` while content is being received, then `false` when done: ~~~typescript [components/ai-chat.component.ts] import { Component } from '@angular/core' import { Markdown } from '@comark/angular' @Component({ selector: 'app-ai-chat', standalone: true, imports: [Markdown], template: ` `, }) export class AiChatComponent { content = '' isStreaming = false async askAI(prompt: string) { this.content = '' this.isStreaming = true const response = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ prompt }), }) const reader = response.body!.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break this.content += decoder.decode(value, { stream: true }) } this.isStreaming = false } } ~~~ ::callout{icon="i-lucide-info" color="info"} `autoClose` is enabled by default: incomplete syntax like `**bold text` is automatically closed on every parse. Disable with `[options]="{ autoClose: false }"`. :: ### Caret The `caret` input appends a blinking cursor to the last text node while `streaming` is `true`: ```html ``` ```css .my-caret { display: inline-block; width: 2px; height: 1em; background: currentColor; animation: blink 1s step-end infinite; vertical-align: text-bottom; } @keyframes blink { 50% { opacity: 0; } } ``` --- ## TypeScript support Use `ComarkPlugin` from `comark` to type plugin arrays, and `ElementNode` to type the `__node` input in components that override HTML elements: ```typescript [comark-wrapper.component.ts] import { Component, Input, Type } from '@angular/core' import { Markdown } from '@comark/angular' import type { ComarkPlugin } from 'comark' @Component({ selector: 'app-comark-wrapper', standalone: true, imports: [Markdown], template: ``, }) export class ComarkWrapperComponent { @Input() content = '' @Input() components: Record> = {} @Input() plugins: ComarkPlugin[] = [] } ``` ~~~typescript [components/heading.component.ts] import { Component, Input } from '@angular/core' import type { ElementNode } from 'comark' @Component({ selector: 'app-heading', standalone: true, template: `

`, }) export class HeadingComponent { @Input() __node?: ElementNode @Input() id?: string } ~~~ --- ## Pre-configured components Use `defineMarkdownComponent` and `defineMarkdownDocumentComponent` to create pre-configured wrappers with default plugins, components, and styling baked in, so there is no need to repeat the same config on every instance. ### `defineMarkdownComponent` ```typescript [components/docs-markdown.ts] import { defineMarkdownComponent } from '@comark/angular' import shiki from '@comark/angular/plugins/shiki' import math, { Math } from '@comark/angular/plugins/math' import mermaid, { Mermaid } from '@comark/angular/plugins/mermaid' export const DocsMarkdown = defineMarkdownComponent({ plugins: [shiki(), math(), mermaid()], components: { Math, Mermaid }, class: 'prose dark:prose-invert', }) ``` Then use it like any other component. The returned component always uses the `comark-markdown-defined` selector: ```typescript [page.component.ts] import { Component } from '@angular/core' import { DocsMarkdown } from './components/docs-markdown' @Component({ selector: 'app-page', standalone: true, imports: [DocsMarkdown], template: ``, }) export class PageComponent { content = '# Hello\n\n$E = mc^2$' } ``` Instance-level `[plugins]` and `[components]` inputs are merged with (and override) the config-level defaults. ### `defineMarkdownDocumentComponent` Same idea, but for the low-level renderer. The returned component always uses the `comark-markdown-document-defined` selector: ```typescript [components/docs-markdown-document.ts] import { defineMarkdownDocumentComponent } from '@comark/angular' import { Math } from '@comark/angular/plugins/math' export const DocsMarkdownDocument = defineMarkdownDocumentComponent({ components: { Math }, class: 'prose', }) ``` --- --- - [Plugins](https://comark.dev/plugins) - [Streaming API](https://comark.dev/reference/auto-close) # Render Markdown in the Terminal (ANSI) for Node CLIs > Render Markdown as styled ANSI output in the terminal for Node.js CLIs, scripts, and coding agents, with syntax highlighting. The `@comark/ansi` package renders Markdown to ANSI-styled strings for terminal output. Install it separately: ## Installation ::code-group ```bash [pnpm] pnpm add @comark/ansi ``` ```bash [npm] npm install @comark/ansi ``` ```bash [yarn] yarn add @comark/ansi ``` ```bash [bun] bun add @comark/ansi ``` :: ## `renderAnsi()` The quickest way to parse Markdown and get an ANSI-styled string in one call. ### Usage ~~~typescript import { renderAnsi } from '@comark/ansi' const output = await renderAnsi(` # Getting Started This is a **bold** statement with a [link](https://example.com). - Item 1 - Item 2 `) process.stdout.write(output) ~~~ ```text [Terminal] # Getting Started ← bold + underline This is a bold statement with a link (https://example.com). • Item 1 • Item 2 ``` ### Options | Option | Type | Default | Description | | ------------------------------------------ | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | [`plugins`](#render-options-plugins) | `ComarkPlugin[]` | `[]` | Array of plugins | | [`components`](#render-options-components) | `Record` | `{}` | Custom component renderers | | [`data`](#render-options-data) | `Record` | `undefined` | Data passed to component renderers | | `colors` | `boolean` | `true`* | Emit ANSI escape codes | | `width` | `number` | `80` | Terminal width for HR and code block headers | | `autoClose` | `boolean` | `true` | Close incomplete Markdown and components before parsing | | `autoUnwrap` | `boolean` | `true` | Remove a single paragraph wrapper inside components | | `linkify` | `boolean` | `true` | Convert URL-like text into links | | `registerDefaultPlugins` | `boolean` | `true` | Register default plugins (`frontmatter`, `html`, `alert`, `task-list`, `components`, `attributes`) | | `unwrap` | `boolean \| string \| string[]` | `false` | Remove selected wrapper tags from the parsed document | \*Automatically set to `false` when the `NO_COLOR` env var is present. `renderAnsi()` accepts all [`ParserOptions`](https://comark.dev/reference/parse#options) in addition to the ANSI renderer options above. #### `plugins` See [ComarkPlugin](https://comark.dev/plugins) for available plugins. ```ts import { renderAnsi } from '@comark/ansi' import shiki from '@comark/ansi/plugins/shiki' const output = await renderAnsi('```typescript\nconsole.log("hello")\n```', { plugins: [shiki()], }) ``` #### `components` Map component names to async render functions. Each function receives the element as `[tag, attrs, ...children]` and a context with `render` to process nested content: ~~~ts import { renderAnsi } from '@comark/ansi' const output = await renderAnsi(` ::badge{type="success"} Build passed :: `, { components: { badge: async ([, attrs, ...children], { render }) => { return `[${String(attrs.type).toUpperCase()}] ${await render(children)}` }, }, }) // → [SUCCESS] Build passed ~~~ #### `data` Pass external data to every component renderer via the context object: ~~~ts import { renderAnsi } from '@comark/ansi' const output = await renderAnsi(` ::status All systems operational. :: `, { data: { env: 'production' }, components: { status: async ([, , ...children], { render, data }) => { return `[${data?.env}] ${await render(children)}` }, }, }) // → [production] All systems operational ~~~ ### `createAnsiRenderer()` Creates a reusable parse+render function. The underlying parser is initialized once and reused on every call, which is more efficient when rendering many documents. #### Usage ```typescript import { createAnsiRenderer } from '@comark/ansi' import shiki from '@comark/ansi/plugins/shiki' const render = createAnsiRenderer({ plugins: [shiki()], width: 120, }) // Reuse the same configured parser const out1 = await render('# Document 1\n\n...') const out2 = await render('# Document 2\n\n...') ``` #### Options Same as [`renderAnsi()`](#renderansi-options). --- ## `printAnsi()` Parse and print markdown directly to `stdout` in one call. ::note `printAnsi()` replaces `writeAnsi()`, which is deprecated and will be removed in the next major version. :: ### Usage ~~~ts import { printAnsi } from '@comark/ansi' await printAnsi(` # Hello World This is **bold**, _italic_, and \`inline code\`. > [!NOTE] > @comark/ansi renders GitHub-style alerts with color. `) ~~~ ### Options Pass options to configure the parser, renderer, or output destination using `writer`: ```ts import { printAnsi } from '@comark/ansi' import math, { Math } from '@comark/ansi/plugins/math' await printAnsi('Inline $E = mc^2$', { plugins: [math()], components: { Math }, width: 100, writer: (s) => process.stderr.write(s), }) ``` They are the same options as [`renderAnsi()`](#renderansi-options) plus the `writer?: (string: string) => void` option. ### `createAnsiPrinter()` Creates a reusable printer with pre-configured options. The underlying parser is initialized once and reused on every call, which is more efficient when printing many documents. ::note `createAnsiPrinter()` replaces `createAnsiWriter()`, which is deprecated and will be removed in the next major version. :: #### Usage ```ts import { createAnsiPrinter } from '@comark/ansi' import math, { Math } from '@comark/ansi/plugins/math' import shiki from '@comark/ansi/plugins/shiki' const print = createAnsiPrinter({ plugins: [math(), shiki()], components: { Math }, width: 120, }) // Reuse the same configured parser & printer await print('# Document 1\n\n...') await print('# Document 2\n\n...') ``` #### Options Same as [`printAnsi()`](#printansi-options). --- ## `renderAnsiFromDocument()` Render a pre-parsed `MarkdownDocument` to an ANSI string, with no parsing step. Use this when you already have a document and want to avoid re-parsing. ### Integration ~~~typescript import { parseMarkdown } from 'comark' import { renderAnsiFromDocument } from '@comark/ansi' const document = await parseMarkdown(` # Getting Started This is a **bold** statement with a [link](https://example.com). - Item 1 - Item 2 `) const output = await renderAnsiFromDocument(document) process.stdout.write(output) ~~~ ### Options | Option | Type | Default | Description | | ------------------------------------------ | --------------------- | ------- | -------------------------------------------- | | [`components`](#render-options-components) | `Record` | `{}` | Custom component renderers | | [`data`](#render-options-data) | `Record` | — | Data passed to component renderers | | `colors` | `boolean` | `true`* | Emit ANSI escape codes | | `width` | `number` | `80` | Terminal width for HR and code block headers | \*Automatically set to `false` when the `NO_COLOR` env var is present. --- ## Overriding terminal output Pass native markdown tag names as keys in `components` to override how standard elements render in the terminal: ```typescript import { createAnsiRenderer } from '@comark/ansi' const renderAnsi = createAnsiRenderer({ components: { h1: async ([, , ...children], { render }) => { return `\x1b[1;4;35m★ ${await render(children)}\x1b[0m\n` }, a: async ([, attrs, ...children], { render }) => { const label = await render(children) return `\x1b[36m${label}\x1b[0m (\x1b[2m${attrs.href}\x1b[0m)` }, }, }) ``` --- ## Syntax support ### Headings Headings are styled by level: bold + underline for `h1`, with distinct colors per level down to `h6`. ### GitHub alerts Blockquotes with `[!TYPE]` markers render as colored alerts matching GitHub's style: ```markdown > [!NOTE] > Informational message. > [!TIP] > Helpful suggestion. > [!IMPORTANT] > Crucial information. > [!WARNING] > Potential risk. > [!CAUTION] > Danger ahead. ``` Each type has its own color: NOTE → blue, TIP → green, IMPORTANT → magenta, WARNING → yellow, CAUTION → red. ### Code blocks Code blocks show the language and filename in a header line. When the `shiki` plugin is used, tokens are rendered with true-color ANSI (`\x1b[38;2;R;G;Bm`) derived from Shiki's dark theme: ```typescript import { createAnsiPrinter } from '@comark/ansi' import shiki from '@comark/ansi/plugins/shiki' const printAnsi = createAnsiPrinter({ plugins: [shiki()] }) await printAnsi('```typescript [app.ts]\nconsole.log("hello")\n```') // typescript app.ts // console.log("hello") ← syntax highlighted ``` ### Math Math expressions from the `math` plugin render as colored LaTeX source: inline in yellow, block in magenta: ```typescript import { createAnsiPrinter } from '@comark/ansi' import math, { Math } from '@comark/ansi/plugins/math' const printAnsi = createAnsiPrinter({ plugins: [math()], components: { Math } }) await printAnsi('Inline $E = mc^2$ and block:\n\n$$\n\\frac{a}{b}\n$$') ``` ### Tables Tables render with box-drawing characters: ``` ┌─────────────┬─────────┐ │ Feature │ Status │ ├─────────────┼─────────┤ │ Headings │ ✅ │ │ Code blocks │ ✅ │ └─────────────┴─────────┘ ``` --- ## TypeScript support ```typescript import type { AnsiRendererOptions } from '@comark/ansi' import { createAnsiRenderer } from '@comark/ansi' import type { NodeHandler } from 'comark' const components: Record = { badge: async ([, attrs, ...children], { render }) => { return `[${String(attrs.type).toUpperCase()}] ${await render(children)}` }, } const options: AnsiRendererOptions = { colors: true, width: 100, components } const renderAnsi = createAnsiRenderer(options) ``` --- - [Plugins](https://comark.dev/plugins) - [Streaming API](https://comark.dev/reference/auto-close) # Alerts > Built-in plugin that transforms GitHub-style blockquote alerts into styled callout blocks with icons and colors. The alerts plugin transforms GitHub-flavored alert blockquotes into styled callout blocks with icons and colors, identical to how GitHub renders them in READMEs and issues. The plugin is **built-in and enabled by default**. No installation or registration required. ## Usage Alerts use standard blockquote syntax with a special marker on the first line: ```mdc > [!NOTE] > Useful information that users should know, even when skimming content. > [!TIP] > Helpful advice for doing things better or more easily. > [!IMPORTANT] > Key information users need to know to achieve their goal. > [!WARNING] > Urgent info that needs immediate user attention to avoid problems. > [!CAUTION] > Advises about risks or negative outcomes of certain actions. ``` The plugin tags the blockquote in the AST and leaves the visuals to your renderer: map each alert type to your own component ([see below](#custom-components)) or style the plain output with CSS. On this site, alert types are mapped to callout components and render as: > [!NOTE] > Useful information that users should know, even when skimming content. > [!TIP] > Helpful advice for doing things better or more easily. > [!IMPORTANT] > Key information users need to know to achieve their goal. > [!WARNING] > Urgent info that needs immediate user attention to avoid problems. > [!CAUTION] > Advises about risks or negative outcomes of certain actions. --- ## Features ### Alert types | Marker | Color | Use for | | -------------- | ------ | ------------------------------------------- | | `[!NOTE]` | Blue | Supplementary information | | `[!TIP]` | Green | Helpful hints and best practices | | `[!IMPORTANT]` | Purple | Critical information for success | | `[!WARNING]` | Yellow | Potential issues and gotchas | | `[!CAUTION]` | Red | Dangerous actions or destructive operations | ### Multi-line content Alerts can span multiple lines and contain inline markdown: ```mdc > [!WARNING] > **Breaking change** in v2.0: the `parseMarkdown()` function is now async. > Update all call sites to use `await parseMarkdown(...)`. ``` --- ## How it works The plugin transforms the alert blockquote in the AST: the `[!NOTE]` marker is removed and an `as` attribute is added to the `blockquote` node. ```ts import { parseMarkdown } from 'comark' const tree = await parseMarkdown(`> [!NOTE] > Useful information for users.`) console.log(tree.nodes) // [ // ['blockquote', { as: 'note' }, 'Useful information for users.'] // ] ``` The Vue, React, and Angular renderers resolve the `as` attribute against your components map first, then fall back to the tag name (`blockquote`). This means you can render each alert type with its own component. In the HTML renderer, register a `blockquote` component and read the alert type from `attrs.as`. ## Custom components Register a component for each alert type you want to customize: ::code-group ```vue [Vue] ``` ```tsx [React] import { Markdown } from '@comark/react' import { Note } from './Note' import { Warning } from './Warning' const markdown = `> [!NOTE] > Useful information for users.` export default function Page() { return {markdown} } ``` ```ts [HTML] import { renderHtml } from '@comark/html' const html = await renderHtml(`> [!NOTE] > Useful information for users.`, { components: { // The HTML renderer resolves by tag name: read the alert type from `attrs.as` blockquote: async ([, attrs, ...children], { render }) => { if (!attrs.as) return `
${await render(children)}
` return `` }, }, }) // ``` :: Alert types without a registered component render as a regular `
` element, so you can also target them with CSS: `blockquote[as="note"]`. --- ## Nuxt UI integration When using the [Nuxt module](https://comark.dev/rendering/nuxt) alongside `@nuxt/ui`, alert blocks are automatically mapped to Nuxt UI's ``, ``, ``, and `` components. No extra configuration needed. --- - [Markdown Syntax](https://comark.dev/syntax/markdown) - [Plugins](https://comark.dev/plugins) # Attributes > Built-in plugin that parses inline attributes into AST props. The `comark/plugins/attributes` plugin enables Comark **inline attributes** (`{...}` after a token): - Props after a token: `Hello {.cls}` → paragraph gets `class="cls"` - Props on links, emphasis, headings, list items, and components - Props on span wrappers from the [components plugin](https://comark.dev/plugins/defaults/components): `[world]{.accent}` The plugin is **enabled by default** via `registerDefaultPlugins`. No installation or registration required. ::tip For the full authoring guide (shorthands, binding prefixes, limits), see [Attribute Syntax](https://comark.dev/syntax/attributes). :: ## Usage ```mdc # Title {.hero} Hello [world]{class="accent"}. [Docs](https://comark.dev){target="_blank"} ``` ```typescript import { parseMarkdown } from 'comark' const result = await parseMarkdown('Hello {.cls}') // → [ ['p', { class: 'cls' }, 'Hello'] ] const span = await parseMarkdown('[world]{.accent}') // → [ ['p', {}, ['span', { class: 'accent' }, 'world']] ] ``` ### Explicit registration When default plugins are off, opt in with the plugin directly: ```typescript import { parseMarkdown } from 'comark' import attributes from 'comark/plugins/attributes' const result = await parseMarkdown(content, { registerDefaultPlugins: false, plugins: [attributes()], }) ``` With framework packages: ::code-group ```vue [Vue] ``` ```tsx [React] import { Markdown } from '@comark/react' import attributes from '@comark/react/plugins/attributes' {content} ``` :: ### Disable attribute syntax Turn off all defaults (including attributes): ```typescript const result = await parseMarkdown(content, { registerDefaultPlugins: false }) ``` With attributes disabled, `{...}` is left as plain text. Bare `[text]` span wrappers still require the [components plugin](https://comark.dev/plugins/defaults/components). ## markdown-it / markdown-exit adapter ```typescript import { markdownItAttributes } from 'comark/plugins/attributes' md.use(markdownItAttributes) ``` Pair with [components](https://comark.dev/plugins/defaults/components) when you also need `::name` / `:name` syntax: ```typescript import { markdownItComponents } from 'comark/plugins/components' import { markdownItAttributes } from 'comark/plugins/attributes' md.use(markdownItComponents) md.use(markdownItAttributes) ``` ## How it works The plugin registers `comark_inline_props` for `{class="highlight" id="intro"}` after a preceding token. Hidden `mdc_inline_props` tokens are merged onto the previous token (or a wrapping span for bare text). Parent-level props on headings, paragraphs, and list items are lifted during `md.parse`. Span openers (`[text]`) are provided by the [components plugin](https://comark.dev/plugins/defaults/components); attributes only attach the trailing `{...}`. --- - [Attribute Syntax](https://comark.dev/syntax/attributes) - [Plugins](https://comark.dev/plugins) # Components > Built-in plugin that parses block and inline Comark component syntax into AST nodes. The `comark/plugins/components` plugin enables Comark **block** and **inline** component syntax, plus span wrappers: - Block: `::name` … `::` (and single-line shorthand `:name[content]{props}`) - Inline: `:name`, `:name[content]`, `:name[content]{props}` - Spans: `[text]` (add `{attrs}` with the [attributes plugin](https://comark.dev/plugins/defaults/attributes)) The plugin is **enabled by default** via `registerDefaultPlugins`. No installation or registration required. ::tip For the full authoring guide (props, slots, nesting), see [Component Syntax](https://comark.dev/syntax/components). :: ## Usage ```mdc ::alert{type="info"} Hello **world** :: Inline :badge[New]{color="blue"} component. ``` ~~~typescript import { parseMarkdown } from 'comark' const result = await parseMarkdown(` ::alert{type="info"} Hello :: `) // → [ ['alert', { type: 'info' }, 'Hello'] ] ~~~ ### Explicit registration When default plugins are off, opt in with the plugin directly: ```typescript import { parseMarkdown } from 'comark' import components from 'comark/plugins/components' const result = await parseMarkdown(content, { registerDefaultPlugins: false, plugins: [components()], }) ``` With framework packages, use the same plugin path under the framework scope (plain re-export): ::code-group ```vue [Vue] ``` ```tsx [React] import { Markdown } from '@comark/react' import components from '@comark/react/plugins/components' {content} ``` :: ### Disable component syntax Turn off all defaults (including components): ```typescript const result = await parseMarkdown(content, { registerDefaultPlugins: false }) ``` When the components plugin is not active, `autoClose` also skips component-fence completion (`::` closers). ## markdown-it / markdown-exit adapter For hosts that use markdown-it or markdown-exit directly (for example VitePress), use the exported adapter: ```typescript import { markdownItComponents } from 'comark/plugins/components' import { markdownItAttributes } from 'comark/plugins/attributes' md.use(markdownItComponents) md.use(markdownItAttributes) // optional; props on components need attributes too ``` ## How it works The plugin registers markdown-exit block and inline rules: | Rule | Syntax | | ----------------------------------------- | ----------------------------------------- | | `comark_block` / `comark_block_shorthand` | `::name` blocks and one-line shorthands | | `comark_block_yaml` | YAML / fenced props blocks on components | | `comark_block_slots` | `#slot` named slots | | `comark_inline_span` | `[text]` span wrappers | | `comark_inline_component` | `:name[content]{props}` inline components | Token → AST conversion is handled by the core token processor (`mdc_block_*`, `mdc_inline_span`, `mdc_inline_component`). --- - [Component Syntax](https://comark.dev/syntax/components) - [Plugins](https://comark.dev/plugins) # Frontmatter > Built-in plugin that parses a leading YAML frontmatter block into tree.frontmatter. The `comark/plugins/frontmatter` plugin extracts a leading YAML frontmatter block (`---`) into `tree.frontmatter` and removes it from the markdown body before tokenization. The plugin is **enabled by default** via `registerDefaultPlugins`. No installation or registration required. ## Usage ```mdc --- title: Hello World description: A sample document --- # Hello World ``` ~~~typescript import { parseMarkdown } from 'comark' const result = await parseMarkdown(`--- title: Hello World --- # Hello World `) console.log(result.frontmatter) // → { title: 'Hello World' } console.log(result.nodes) // → [ ['h1', { id: 'hello-world' }, 'Hello World'] ] ~~~ ### Explicit registration When default plugins are off, opt in with the plugin directly: ```typescript import { parseMarkdown } from 'comark' import frontmatter from 'comark/plugins/frontmatter' const result = await parseMarkdown(content, { registerDefaultPlugins: false, plugins: [frontmatter()], }) ``` ### Disable frontmatter parsing Turn off all defaults (including frontmatter) so the `---` block is treated as regular markdown: ```typescript const result = await parseMarkdown(content, { registerDefaultPlugins: false }) // result.frontmatter → {} ``` ## How it works In the `pre` hook (before markdown-exit tokenization), the plugin: 1. Detects a leading `---` … `---` block via `parseFrontmatter` 2. Parses the YAML body into `state.frontmatter` 3. Strips the frontmatter from `state.markdown` 4. Adjusts `state.parsedLines` so later nodes get correct line numbers The core parser then copies `state.frontmatter` onto `tree.frontmatter`. --- - [Frontmatter Syntax](https://comark.dev/syntax/frontmatter) - [Plugins](https://comark.dev/plugins) # HTML > Built-in plugin that parses embedded HTML tags into Comark AST nodes. The `comark/plugins/html` plugin enables embedded HTML block and inline tags in Comark/markdown content. Tags are tokenized and converted into AST nodes that can be mixed with Comark components and markdown syntax. The plugin is **enabled by default** via `registerDefaultPlugins`. No installation or registration required. ## Usage ```mdc
::alert{type="info"} Hello world ::
``` ~~~typescript import { parseMarkdown } from 'comark' const result = await parseMarkdown(` Hello _world_ `) // → [ // ['p', {}, // ['strong', { class: 'bold', $: { html: 1, block: 0 } }, 'Hello'], // ' ', // ['em', {}, 'world'] // ] // ] ~~~ ### Explicit registration When default plugins are off, opt in with the plugin directly: ```typescript import { parseMarkdown } from 'comark' import html from 'comark/plugins/html' const result = await parseMarkdown(content, { registerDefaultPlugins: false, plugins: [html()], }) ``` ### Disable HTML parsing Turn off all defaults (including HTML) so tags are treated as plain text: ```typescript const result = await parseMarkdown(content, { registerDefaultPlugins: false }) ``` ::note `ParserOptions.html` is **deprecated** and logs a warning. Prefer `registerDefaultPlugins: false` (and register `html()` only when you need it). `html: false` still skips the default html plugin for compatibility. :: ## Options ```typescript import html from 'comark/plugins/html' await parseMarkdown(content, { plugins: [html({ markdown: false })], }) ``` | Option | Default | Description | | ---------- | ------- | ----------------------------------------------------------------------------------- | | `markdown` | `true` | Expand text inside closed HTML as inline markdown. `false` keeps that text literal. | `markdown: false` does not turn blank-line bodies into literal text. A blank line ends a CommonMark HTML block, so the following paragraphs are normal markdown and nest under the still-open tag either way. ```mdc
Hello **World**
Hello **World**
Hello **World**
``` ` ``` ```tsx [React] import { Markdown } from '@comark/react' import shiki from '@comark/react/plugins/shiki' import githubLight from '@shikijs/themes/github-light' import githubDark from '@shikijs/themes/github-dark' {content} ``` :: --- ## Features ### Dual-theme support Highlight code with different themes for light and dark modes. Both palettes are embedded as CSS custom properties, so there is no flash on theme switch. See all [available themes →](https://shiki.style/themes) ```typescript shiki({ themes: { light: githubLight, dark: githubDark } }) ``` ### Language detection Comark reads the language from the code fence info string and highlights accordingly. On the standard entry, the default language set is pre-registered; pass extra grammars via `languages` (from `@shikijs/langs`). On `core`, only the languages you pass are available. See all [180+ supported languages →](https://shiki.style/languages) ~~~markdown ```typescript const x: number = 42 ``` ~~~ ### Inline code Inline code is highlighted when it declares a language with the attributes syntax. `lang` wins over `language`: ```markdown The type is `Ref`{lang="ts-type"} and the component is ``{lang="vue-html"}. ``` Two fragment languages ship out of the box: `ts-type` tokenizes through the TypeScript grammar seeded with `let a:`, and `vue-html` through the Vue grammar seeded with `