---
title: "markdown"
description: "Highlight code in rehype, remark and markdown-it."
source: https://twinkleplop.pngwn.at/docs/markdown
---
# markdown

Highlight code in rehype, remark and markdown-it.

Use the plugin for your markdown processor. All three share the options described below.

## setup

### markdown-it

```ts title="markdown-it.ts"
import markdown_it from "markdown-it";
import markdown_it_twinkleplop from "@twinkleplop/markdown-it";
import { language as typescript } from "@twinkleplop/typescript";
import { language as css } from "@twinkleplop/css";

const md = markdown_it().use(markdown_it_twinkleplop, {
  languages: { ts: typescript(), js: "ts", css: css() },
});

md.render(source);
```

The plugin replaces the `fence` renderer rule, and the `code_inline` rule when inline code is enabled. Code blocks that the plugin does not highlight use the previous renderer rule.

### remark

```ts title="remark.ts"
import { unified } from "unified";
import remark_parse from "remark-parse";
import remark_rehype from "remark-rehype";
import rehype_stringify from "rehype-stringify";
import remark_twinkleplop from "@twinkleplop/remark";
import { language as typescript } from "@twinkleplop/typescript";

const pipeline = unified()
  .use(remark_parse)
  .use(remark_twinkleplop, { languages: { ts: typescript(), js: "ts" } })
  .use(remark_rehype, { allowDangerousHtml: true })
  .use(rehype_stringify, { allowDangerousHtml: true });
```

> The remark plugin emits mdast `html` nodes. If your pipeline continues into rehype, enable `allowDangerousHtml` on both `remark-rehype` and `rehype-stringify` to preserve the code blocks.

### rehype

```ts title="rehype.ts"
import rehype_twinkleplop from "@twinkleplop/rehype";
import { language as typescript } from "@twinkleplop/typescript";

.use(rehype_twinkleplop, {
  languages: { ts: typescript(), js: "ts" },
});
```

The plugin replaces each `pre > code` element with the highlighted block. By default, it parses the highlighted HTML into hast nodes. This works without `allowDangerousHtml`.

Parsing the HTML adds processing time. Set `output: "raw"` to return the highlighted string as a raw node.

```ts title="raw.ts"
// skip the hast re-parse and emit a raw node
.use(rehype_twinkleplop, { languages, output: "raw" });
```

`"raw"` needs `allowDangerousHtml` on `rehype-stringify` (or `rehype-raw` before it), otherwise the stringifier escapes the block and displays the HTML as text.

Both modes render the same code. In the default mode, the pipeline's stringifier may use different HTML entities, such as `&#x3C;` for `&lt;`. Raw mode preserves Twinkleplop's HTML string, as the markdown-it and remark plugins do.

Parsing decodes the entities written by `render.escape`, so setting it throws unless `output` is `"raw"`.

The language comes from the `code` element's `language-<name>` class, and the meta string from `data.meta` (which `remark-rehype` sets) or a `metastring` attribute for a tree parsed from HTML.

## language registry

`languages` maps a fence name to a highlighter. A value is a highlight function, an entry with a second highlighter for twoslash fences, or the name of another entry.

```ts title="registry.ts"
languages: {
  // a highlight function
  ts: typescript(),
  // an entry with a second highlighter for twoslash fences
  tsx: { highlight: tsx_lang(), twoslash: create_highlighter({ lang: "tsx" }) },
  // an alias — resolves transitively, once, at setup
  js: "ts",
  mjs: "js",
}
```

Aliases can refer to other aliases and are resolved during setup. Circular aliases, missing entries and an unregistered `default_language` cause setup errors.

Configure [fidelity](https://twinkleplop.pngwn.at/docs/fidelity) and [directives](https://twinkleplop.pngwn.at/docs/directives) when calling `language(...)` to create each highlighter.

Matching is exact and case sensitive: `TS` is not `ts`. A fence with no language uses `default_language`, and with none set is left exactly as the toolchain rendered it.

> In rehype and remark, `default_language` applies to both indented code blocks and fences without a language. In markdown-it, it applies only to fences. Indented blocks use markdown-it's existing `code_block` rule.

## options

These are shared by all three plugins. `@twinkleplop/rehype` extends them with one more, `output`, covered [above](https://twinkleplop.pngwn.at/docs/markdown#rehype).

| option | default | meaning |
| --- | --- | --- |
| languages | required | Fence name to highlighter, entry, or alias. |
| default_language | none | Used by a fence that names no language. |
| on_unknown_language | "throw" | `"plain"` renders the fence as escaped text instead. |
| line_numbers | false | Site default; the fence metadata overrides it. |
| inline | false | `"tailing-curly-colon"` highlights `` `code{:lang}` ``. |
| twoslash | "meta" | `"always"` routes every fence through the entry's twoslash highlighter. |
| parse_meta | none | `(raw, parsed) => render`, for custom metadata. |
| render | {} | Default render options. Individual fences can override them. `escape` also covers titles, captions and the language name. |

## fence metadata

The plugins support Shiki, VitePress and rehype-pretty-code metadata. Use `parse_meta` for custom metadata.

| convention | source | effect |
| --- | --- | --- |
| {1,3-4} | shiki / VitePress | `highlight` on those lines |
| {1,3-4}#id | rehype-pretty-code | plus `data-highlighted-line-id` |
| /word/ | shiki | `highlighted-word` on every occurrence |
| /word/3-5 | rehype-pretty-code | only the 3rd to 5th occurrences |
| :line-numbers=N | VitePress | numbers on, starting at N |
| showLineNumbers{N} | rehype-pretty-code | the same |
| [title] | VitePress | title caption |
| title="…" | rehype-pretty-code | title caption |
| caption="…" | rehype-pretty-code | caption below the block |
| twoslash | shiki | route through the entry's twoslash highlighter |

A pattern may hold spaces (`/two words/`) and escape a slash (`/a\/b/`). `[!code …]` inside the fence body is not a meta convention — it belongs to the registered language's [directives](https://twinkleplop.pngwn.at/docs/directives).

## output

A fence without a title or caption renders as a code block:

```html title="output.html"
<pre class="twinkleplop language-ts has-highlight" data-language="ts"><code>...</code></pre>
```

A title or caption wraps it in a figure:

```html title="figure.html"
<figure class="twinkleplop-block" data-language="ts">
  <figcaption class="twinkleplop-title">math.ts</figcaption>
  <pre class="twinkleplop language-ts" data-language="ts"><code>...</code></pre>
  <figcaption class="twinkleplop-caption">the running total</figcaption>
</figure>
```

Inline code renders as `<code class="twinkleplop-inline language-ts">` around the inline structure — no block, no line elements, `<br>` between lines.

The registered highlighter renders the block. Its `has-*` classes, line classes and hidden ranges work as they do when calling it directly. `class_name` replaces `twinkleplop`; `language-<name>` and `data-language` use the language name written in the fence, including aliases.
