Syntax Highlighting
The highlight option in parseMarkdown and markdownToDomphy lets you plug any syntax highlighter into fenced code block rendering. The highlighter receives the raw code and language identifier per block, and can return either a string of HTML or a Domphy element.
The Highlight type
type Highlight = (
code: string,
language: string,
) => string | DomphyElement | null | undefined| Parameter | Description |
|---|---|
code | Raw source text of the block. Not HTML-escaped — that is the highlighter's job. |
language | Language identifier from the fence (e.g. "ts", "css", "sh"). Empty string when no language is given. |
| Return value | What happens |
|---|---|
| Non-empty string | Wrapped in rawHtml() as the content of the <code> element. TextNode then runs sanitizeHTMLString (strips <script>, on* handlers, javascript: URLs) — defense in depth, not a sanitizer for untrusted input. |
DomphyElement | Replaces the entire block — no pre > code wrapper is added around it. |
Falsy (null, undefined, empty string) | Falls back to plain escaped text. |
String return — rawHtml + sanitize
Most highlighters emit token markup as an HTML string. Return that string and @domphy/press wraps it in rawHtml() (core's explicit HTML opt-in). Do not interpolate raw code into an HTML string yourself — pass the source to a highlighter (Shiki, highlight.js, …) and return its output:
import { parseMarkdown } from "@domphy/press"
const { body } = parseMarkdown("```ts\nconst x = 1\n```", {
highlight(code, language) {
// Highlighter output only — never `...${code}...` as HTML.
return myHighlighter(code, language)
},
})
// body[0] ->
// { pre: [{ code: rawHtml('…token markup…'), dataLanguage: "ts", class: "language-ts" }] }The dataLanguage and class properties on the <code> element are always emitted by the walker when a language identifier is present, regardless of whether a highlighter is supplied.
DomphyElement return — stay in the element tree
Return a DomphyElement when you want the highlighted output to remain as a Domphy tree node rather than a raw HTML string. The returned element replaces the whole block — there is no pre > code wrapper, so include your own if you need one:
import type { DomphyElement } from "@domphy/core"
import { parseMarkdown } from "@domphy/press"
const { body } = parseMarkdown("```js\nfoo()\n```", {
highlight(code): DomphyElement {
return { span: code, class: "code-block" }
},
})
// body[0] ->
// { span: "foo()\n", class: "code-block" }Falling back for unknown languages
Return null or undefined to skip highlighting for a specific block. The walker falls back to plain escaped text:
import { parseMarkdown } from "@domphy/press"
const knownLanguages = new Set(["ts", "js", "css", "html", "sh"])
const { body } = parseMarkdown(source, {
highlight(code, language) {
if (!language || !knownLanguages.has(language)) return null
return myHighlighter(code, language)
},
})Integrating Shiki
Shiki emits HTML strings — a natural fit for the string-return path. Create the highlighter once outside the parse call to avoid re-initialising it per document:
import { createHighlighter } from "shiki"
import { parseMarkdown } from "@domphy/press"
// Initialise once at app startup or build time.
const shiki = await createHighlighter({
themes: ["github-light"],
langs: ["ts", "js", "css", "html", "sh"],
})
function renderDoc(source: string) {
return parseMarkdown(source, {
highlight(code, language) {
if (!language) return null
try {
// codeToHtml wraps its output in <pre><code>. Extract only the
// inner content so we don't double-wrap with the walker's own <pre>.
const full = shiki.codeToHtml(code, { lang: language, theme: "github-light" })
// Shiki's output: <pre ...><code ...>...tokens...</code></pre>
// We want only what's inside <code>...</code>.
const match = full.match(/<code[^>]*>([\s\S]*)<\/code>/)
return match ? match[1] : null
} catch {
return null // unknown language: fall back to plain text
}
},
})
}Integrating highlight.js
highlight.js emits annotated HTML strings directly without the outer wrapper:
import hljs from "highlight.js"
import { parseMarkdown } from "@domphy/press"
const { body } = parseMarkdown(source, {
highlight(code, language) {
if (!language) return null
const registered = hljs.getLanguage(language)
if (!registered) return null
return hljs.highlight(code, { language }).value
},
})Then include the corresponding highlight.js CSS theme on the page. The <code> element already carries class="language-{lang}" from the walker, which most themes target.
Building a custom token-based highlighter
Return a DomphyElement when you want full control over the output structure without raw HTML strings:
import type { DomphyElement } from "@domphy/core"
import { parseMarkdown } from "@domphy/press"
// A tiny highlighter that wraps comment lines in a distinct span.
function commentHighlight(code: string, language: string): DomphyElement {
const parts: (string | DomphyElement)[] = []
const lines = code.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (line.trimStart().startsWith("//")) {
parts.push({ span: line, class: "code-comment" })
} else {
parts.push(line)
}
// Add a newline between lines, but not after the last one.
if (i < lines.length - 1) parts.push("\n")
}
return { span: parts, class: `language-${language}` }
}
const { body } = parseMarkdown("```ts\n// setup\nconst x = 1\n```", {
highlight(code, language) {
if (language !== "ts" && language !== "js") return null
return commentHighlight(code, language)
},
})CSS-only highlighting without a highlighter option
Skip the highlight option entirely to let a client-side CSS library handle colouring. The walker already emits the right attributes:
<!-- rendered output for a fenced ts block -->
<pre>
<code class="language-ts" data-language="ts">const x = 1;</code>
</pre>Load a CSS theme on the page and the language-* class provides the hook:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll()</script>Using highlight with walkMdast
The highlight option is also accepted by walkMdast when you build your own remark pipeline:
import { remark } from "remark"
import { splitFrontmatter, walkMdast, createUniqueSlugger, defaultSlugify } from "@domphy/press"
const processor = remark()
const { content } = splitFrontmatter(source)
const tree = processor.parse(content)
processor.runSync(tree, content)
const body = walkMdast(tree, {
highlight(code, info) {
return myHighlighter(code, info)
},
slug: createUniqueSlugger(defaultSlugify),
toc: [],
})