Rendering

ANSI Terminal Rendering

Render Comark content as styled terminal output using ANSI escape codes — perfect for CLIs, scripts, and developer tooling.

The @comark/ansi package renders Comark AST to ANSI-styled strings for terminal output. Install it separately:

Installation

pnpm add @comark/ansi

render()

The quickest way to parse markdown and get an ANSI-styled string in one call.

Usage

import { render } from '@comark/ansi'

const output = await render(`
# Getting Started

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

- Item 1
- Item 2
`)

process.stdout.write(output)

Options

OptionTypeDefaultDescription
pluginsComarkPlugin[][]Array of plugins
componentsRecord<string, fn>{}Custom component renderers
dataRecord<string, any>undefinedData passed to component renderers
colorsbooleantrue*Emit ANSI escape codes
widthnumber80Terminal width for HR and code block headers

*Automatically set to false when the NO_COLOR env var is present.

plugins

See ComarkPlugin for available plugins.

render.ts
import { render } from '@comark/ansi'
import highlight from '@comark/ansi/plugins/highlight'

const output = await render('```typescript\nconsole.log("hello")\n```', {
  plugins: [highlight()],
})

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:

render.ts
import { render } from '@comark/ansi'

const output = await render(`
::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:

render.ts
import { render } from '@comark/ansi'

const output = await render(`
::status
All systems operational.
::
`, {
  data: { env: 'production' },
  components: {
    status: async ([, , ...children], { render, data }) => {
      return `[${data?.env}] ${await render(children)}`
    },
  },
})

createRender()

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

Usage

import { createRender } from '@comark/ansi'
import highlight from '@comark/ansi/plugins/highlight'

const render = createRender({
  plugins: [highlight()],
  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 render().


log()

Parse and print markdown directly to stdout in one call.

import { log } from '@comark/ansi'

await log(`
# Hello World

This is **bold**, _italic_, and \`inline code\`.

> [!NOTE]
> @comark/ansi renders GitHub-style alerts with color.
`)

Pass options to configure the parser, renderer, or output destination:

import { log } from '@comark/ansi'
import math, { Math } from '@comark/ansi/plugins/math'

await log('Inline $E = mc^2$', {
  plugins: [math()],
  components: { Math },
  width: 100,
  write: (s) => process.stderr.write(s),
})

createLog()

Creates a reusable log function with pre-configured options. The underlying parser is initialized once and reused on every call — more efficient when logging many documents.

Usage

import { createLog } from '@comark/ansi'
import math, { Math } from '@comark/ansi/plugins/math'
import highlight from '@comark/ansi/plugins/highlight'

const log = createLog({
  plugins: [math(), highlight()],
  components: { Math },
  width: 120,
})

// Reuse the same configured parser
await log('# Document 1\n\n...')
await log('# Document 2\n\n...')

Options

Same as log().


renderANSI()

Render a pre-parsed Comark tree to an ANSI string — no parsing step. Use this when you already have a parsed tree and want to avoid re-parsing.

Integration

import { parse } from 'comark'
import { renderANSI } from '@comark/ansi'

const tree = await parse(`
# Getting Started

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

- Item 1
- Item 2
`)

const output = await renderANSI(tree)
process.stdout.write(output)

Options

OptionTypeDefaultDescription
componentsRecord<string, fn>{}Custom component renderers
dataRecord<string, any>Data passed to component renderers
colorsbooleantrue*Emit ANSI escape codes
widthnumber80Terminal 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:

import { createRender } from '@comark/ansi'

const render = createRender({
  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:

> [!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 highlight plugin is used, tokens are rendered with true-color ANSI (\x1b[38;2;R;G;Bm) derived from Shiki's dark theme:

import { createLog } from '@comark/ansi'
import highlight from '@comark/ansi/plugins/highlight'

const log = createLog({ plugins: [highlight()] })

await log('```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:

import { createLog } from '@comark/ansi'
import math, { Math } from '@comark/ansi/plugins/math'

const log = createLog({ plugins: [math()], components: { Math } })

await log('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

import type { ComarkElement, ComarkNode } from 'comark'
import { createRender } from '@comark/ansi'

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

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

const components: Record<string, ComponentRenderer> = {
  badge: async ([, attrs, ...children], { render }) => {
    return `[${String(attrs.type).toUpperCase()}] ${await render(children)}`
  },
}

const render = createRender({ components })