GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Documentation
  • Plugins
  • Use cases
  • Examples
  • Playground
  • Ecosystem
  • GitHub
  • Installation
  • Document
  • Markdown
  • Components
  • Attributes
  • Frontmatter
  • HTML

This view

  • Original page
  • New address

GeezerVison is showing comark.dev — Parse API

API Reference

Parse API

Parse Markdown into a compact, serializable document with parseMarkdown() or createMarkdownParser(), on the server, in the browser, or from a stream.

parseMarkdown(source, options?)

Parses Markdown from a string and returns a complete MarkdownDocument. Default plugins add frontmatter, alerts, task lists, HTML, components, and attributes to the standard Markdown parser.

Parameters:

  • source - The Markdown content as a string
  • options? - Parser options including plugins

Returns: MarkdownDocument object containing:

  • nodes - The parsed Markdown AST nodes
  • frontmatter - Frontmatter data parsed from YAML
  • meta - Additional metadata from plugins (for example, toc, summary)

Example:

import { parseMarkdown } from 'comark'

const content = `---
title: Hello World
---

This is a simple example
`

const result = await parseMarkdown(content)

console.log(result)
{
  "nodes": [
    ["p", {}, "This is a simple example"]
  ],
  "frontmatter": {
    "title": "Hello World"
  },
  "meta": {}
}
For the complete parsed document and node types, see the Document Model.

Frontmatter

The parse function automatically extracts and parses YAML frontmatter:

const content = `---
title: My Document
tags:
  - javascript
  - markdown
author:
  name: John Doe
  email: [email protected]
---

# Content here
`

const result = await parseMarkdown(content)
console.log(result.frontmatter)
{
  "title": "My Document",
  "tags": ["javascript", "markdown"],
  "author": { "name": "John Doe", "email": "[email protected]" }
}

Table of contents

Register the toc plugin to generate a table of contents based on headings:

import toc from 'comark/plugins/toc'

const content = `# Main Title

## Section 1

Some content here.

### Subsection 1.1

More content.

## Section 2

Final content.
`

const result = await parseMarkdown(content, { plugins: [toc()] })
console.log(result.meta.toc)
{
  "title": "Main Title",
  "depth": 2,
  "searchDepth": 2,
  "links": [
    { "id": "section-1", "text": "Section 1", "depth": 2, "children": [
      { "id": "subsection-11", "text": "Subsection 1.1", "depth": 3 }
    ]},
    { "id": "section-2", "text": "Section 2", "depth": 2 }
  ]
}

HTML parsing

HTML tags embedded in Comark content are parsed into AST nodes by default and can be mixed freely with Comark components and markdown syntax.

const content = `
<div class="note">
  ::alert{type="info"}
  Hello <strong class="text-red-500">world</strong>
  ::
</div>
`

const result = await parseMarkdown(content)
console.log(result.nodes)
[
  ["div", { "class": "note" },
    ["alert", { "type": "info" },
      "Hello ",
      ["strong", { "class": "text-red-500" }, "world"]
    ]
  ]
]

HTML parsing is provided by the built-in html plugin.

Summary

Summary extraction requires the summary plugin.

Content before the <!-- more --> comment is extracted as a summary when using the summary plugin:

parse.ts
import { parseMarkdown } from 'comark'
import summary from 'comark/plugins/summary'

const content = `# Article Title

This is the introduction paragraph that will be used as a summary.

<!-- more -->

This is the full article content that won't appear in the summary.
`

const result = await parseMarkdown(content, {
  plugins: [summary()]
})

console.log(result.meta.summary)
// Node[] with only the content before <!-- more -->

createMarkdownParser(options?)

Creates a reusable parser function with pre-configured options. Unlike parseMarkdown() which creates a new parser instance on each call, createMarkdownParser() returns a parser function that can be called multiple times with the same configuration.

Parameters:

  • options? - Parser options (same as parseMarkdown())

Returns: An async parser function (source: string) => Promise<MarkdownDocument>

Example:

parse.ts
import { createMarkdownParser } from 'comark'
import shiki from 'comark/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import emoji from 'comark/plugins/emoji'
import toc from 'comark/plugins/toc'

// Create a parser with specific configuration
const parse = createMarkdownParser({
  autoUnwrap: true,
  autoClose: true,
  plugins: [
    shiki({
      themes: { light: githubLight, dark: githubDark }
    }),
    emoji(),
    toc()
  ]
})

// Reuse the parser for multiple documents
const doc1 = await parse('# Document 1\n\nContent...')
const doc2 = await parse('# Document 2\n\nMore content...')
const doc3 = await parse('# Document 3\n\nEven more...')

Use cases

Here are some use cases for createMarkdownParser():

Static site generator

Use Promise.all to parse all files in parallel. Since createMarkdownParser() initializes the parser once, the returned function is safe to call concurrently.

build.ts
import { createMarkdownParser } from 'comark'
import { readdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { renderHtmlFromDocument } from '@comark/html'
import shiki from 'comark/plugins/shiki'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'
import toc from 'comark/plugins/toc'
import emoji from 'comark/plugins/emoji'

async function buildSite(contentDir: string, outDir: string) {
  // Create parser once with all desired plugins
  const parse = createMarkdownParser({
    plugins: [
      shiki({
        themes: { light: githubLight, dark: githubDark }
      }),
      toc({ depth: 3 }),
      emoji()
    ]
  })

  const files = await readdir(contentDir)
  const mdFiles = files.filter(f => f.endsWith('.md'))

  // Parse all files in parallel with the same parser instance
  await Promise.all(
    mdFiles.map(async (file) => {
      const content = await readFile(join(contentDir, file), 'utf-8')
      const doc = await parse(content)
      const html = await renderHtmlFromDocument(doc)
      await writeFile(join(outDir, file.replace('.md', '.html')), html)
    })
  )

  console.log(`Built ${mdFiles.length} pages`)
}

await buildSite('./content', './dist')

API server

server.ts
import { createMarkdownParser } from 'comark'
import security from 'comark/plugins/security'

// Create parser once when server starts
const parse = createMarkdownParser({
  plugins: [
    security() // Sanitize user-generated content
  ]
})

// Reuse parser for every request
app.post('/api/markdown', async (req, res) => {
  try {
    const tree = await parse(req.body.content)
    res.json({ success: true, tree })
  } catch (error) {
    res.status(400).json({ error: 'Invalid markdown' })
  }
})

Benchmark

Using createMarkdownParser() has several benefits over calling parseMarkdown() multiple times:

  • Performance: Parser and plugins are initialized once, not on every parse
  • Consistency: All documents parsed with the same configuration
  • Memory efficiency: Single parser instance handles multiple documents
  • Ideal for batch processing: Perfect when parsing many files
benchmark.ts
import { parseMarkdown, createMarkdownParser } from 'comark'
import shiki from 'comark/plugins/shiki'

const content = '```js\nconsole.log("hello")\n```'

// ❌ Slow: Creates new parser + highlighter for each parse
console.time('parse x1000')
for (let i = 0; i < 1000; i++) {
  await parseMarkdown(content, {
    plugins: [shiki()]
  })
}
console.timeEnd('parse x1000')
// → ~8000ms (parser + highlighter recreated 1000 times)

// ✅ Fast: Reuses same parser + highlighter instance
console.time('createMarkdownParser x1000')
const parse = createMarkdownParser({
  plugins: [shiki()]
})
for (let i = 0; i < 1000; i++) {
  await parse(content)
}
console.timeEnd('createMarkdownParser x1000')
// → ~800ms (10x faster! parser + highlighter created once)

Entry points

ImportContents
comarkparseMarkdown(), createMarkdownParser(), createSerializedMarkdownParser() with the default plugins and auto-close, plus autoCloseMarkdown(), createComarkContext(), defineComarkPlugin(), parseFrontmatter()
comark/coreThe same three parser functions with no default plugins and no built-in auto-closer
comark/auto-closeautoCloseMarkdown() and its placeholders, without the parser
comark/contextcreateComarkContext() and the live-update types, without the parser
comark/parseDeprecated. Alias of comark. Import from comark instead

comark/core

Use comark/core when you want to choose every plugin yourself and keep the default plugins out of your bundle. It accepts the same options as comark, with two differences:

  • Only the plugins in plugins run. registerDefaultPlugins is not accepted; add linkify() to link bare URLs.
  • autoClose is a function or undefined (the default, which skips auto-closing). Pass autoCloseMarkdown from comark/auto-close to opt in.
core.ts
import { createMarkdownParser } from 'comark/core'
import { autoCloseMarkdown } from 'comark/auto-close'
import components from 'comark/plugins/components'

const parse = createMarkdownParser({
  plugins: [components()],
  autoClose: (markdown, streaming) => autoCloseMarkdown(markdown, { dropTrailingOpeners: streaming }),
})

const tree = await parse('::alert\n**Streaming')

Options

Both parseMarkdown() and createMarkdownParser() accept the same ParserOptions:

OptionTypeDefaultDescription
autoUnwrapbooleantrueRemove unnecessary <p> wrappers from single-element containers
autoCloseboolean | (markdown: string, streaming?: boolean) => stringtrueAuto-close incomplete markdown syntax, or use a custom completion function. streaming is true when the parse was called with { streaming: true }.
unwrapboolean | string | string[]falseRemove wrapper tags from the tree, hoisting their children (MDC unwrap behaviour). true unwraps p; a comma/whitespace-separated string or array unwraps the listed tags; '*' matches any tag. Tags apply sequentially (each descends one level), and adjacent text is merged into a single string.

| linkify | boolean | true | Deprecated. Use registerDefaultPlugins: false and register linkify() only when needed. Still honored (over registerDefaultPlugins) and logs a warning | | headingIds | boolean \| 'nested' \| 'flat' | true | Auto-generate id attributes for h1–h6 from visible heading text (not tag or component names). true/'flat' uses the heading's own text only (install); 'nested' prefixes each id with its parent heading's id (setup-install); false disables auto ids. Unicode letters, marks, and numbers are kept (## Café → café); punctuation and symbols are dropped, and a heading that slugifies to nothing gets no id. Ids are unique per document: duplicates get a -1, -2, ... suffix (options, options-1, options-2). An explicit id attribute wins. See Headings. | | registerDefaultPlugins | boolean | true | Register the built-in default plugins (linkify, frontmatter, html, alert, task-list, components, attributes). Set false to disable them. | | plugins | ComarkPlugin[] | [] | Ordered plugins to run after the defaults. A same-name plugin replaces its default; duplicate explicit names keep the first instance. See Default plugins. | | tracer | ComarkTracer | undefined | Timing recorder for the parse pipeline — see Timing the parse |

Timing the parse

Pass a tracer to time each phase of the pipeline and every plugin hook, so you can see where parse time goes (for example, a slow post shiki hook). The contract is a structural subset of OpenTelemetry Tracer — startSpan and startActiveSpan — so a real OTel tracer works as-is:

import { trace } from '@opentelemetry/api'
import shiki from 'comark/plugins/shiki'

const parse = createMarkdownParser({
  // Uses the OpenTelemetry provider registered by your app or hosting platform.
  tracer: trace.getTracer('comark'),
  plugins: [shiki()],
})
@opentelemetry/api defines the tracing API but does not export spans by itself. trace.getTracer() uses the globally registered OpenTelemetry provider; without one, it returns a no-op tracer. Hosting platforms often register and configure that provider for you. Otherwise, initialize an OpenTelemetry SDK and exporter before creating the parser.

See the complete Node.js example, including an OTLP/HTTP exporter and commands for viewing traces locally with otel-front.

Or a minimal recorder:

const spans: { name: string, duration: number }[] = []
const tracer = {
  startSpan(name) {
    const start = performance.now()
    return { end: () => spans.push({ name, duration: performance.now() - start }) }
  },
  startActiveSpan(name, optionsOrFn, maybeFn) {
    const fn = typeof optionsOrFn === 'function' ? optionsOrFn : maybeFn
    const span = tracer.startSpan(name)
    // Nested startActiveSpan/startSpan calls become children via your context/stack.
    return fn(span) // caller (comark) ends the span
  },
}

const parse = createMarkdownParser({ tracer, plugins: [shiki()] })
await parse(markdown)
// spans → comark:parse
//           ├─ comark:autoclose
//           ├─ comark:pre:frontmatter
//           ├─ comark:tokenize
//           ├─ comark:nodes
//           ├─ comark:post:alert
//           └─ comark:post:shiki
//           …

Recorded spans: a root comark:parse active span enclosing comark:autoclose, comark:tokenize (markdown parsing), comark:nodes (token → AST conversion and unwrapping), and comark:pre:<name> / comark:post:<name> for each plugin hook. Nested startActiveSpan calls form the parent → child hierarchy (OTel active context, or a stack in a simple recorder). There is no timing overhead when tracer is omitted, and no Node-specific API is used — it works in the browser too.

Inline rendering

Use unwrap: 'p' (or unwrap: true) to render markdown without the wrapping <p>, which is handy for buttons, badges, and other inline hosts. Adjacent text is merged into a single string, matching MDC's unwrap:

await parseMarkdown('Hello **world**', { unwrap: 'p' })
// nodes: ['Hello ', ['strong', {}, 'world']]

await parseMarkdown('a\n\nb', { unwrap: true })
// nodes: ['ab']   (paragraphs merged, no separator)

Tags are applied sequentially, each descending one level into the result of the previous one — so a space-separated (or comma-separated) list peels nested wrappers. '*' matches any tag:

// Unwrap the <ul>, then the <li> inside it
await parseMarkdown('- Buy milk', { unwrap: 'ul li' })
// nodes: ['Buy milk']

On the framework components this is exposed as an unwrap shorthand prop:

<UButton><Markdown :value="text" unwrap /></UButton>
<UButton><Markdown :value="text" unwrap="ul li" /></UButton>
<UButton><Markdown :value="text" :options="{ unwrap: 'p' }" /></UButton>

Render API

Convert a Markdown AST back to markdown, preserving frontmatter and component syntax.

Streaming API

Render incomplete Markdown correctly during streaming. autoCloseMarkdown() closes unterminated syntax so AI output displays at every frame.

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.