|
1 | 1 | import type { Element, Root, Text } from 'hast' |
2 | 2 | import { fromHtml } from 'hast-util-from-html' |
3 | 3 | import { toHtml } from 'hast-util-to-html' |
| 4 | +import type { Raw } from 'mdast-util-to-hast' |
4 | 5 |
|
5 | 6 | import type { MdxCodeHighlighter } from './highlight' |
6 | 7 |
|
7 | | -export function applyShikiHighlighting( |
| 8 | +/** Applies the configured highlighter to code blocks and inline code. */ |
| 9 | +export async function applyCodeHighlighting( |
8 | 10 | root: Root, |
9 | 11 | highlighter: MdxCodeHighlighter, |
10 | 12 | ) { |
11 | | - const visit = (node: Root | Element) => { |
| 13 | + const visit = async (node: Root | Element) => { |
12 | 14 | const children = node.children ?? [] |
13 | 15 | for (let index = 0; index < children.length; index += 1) { |
14 | 16 | const child = children[index] |
15 | 17 | if (child.type !== 'element') continue |
16 | 18 | if (child.tagName === 'pre') { |
17 | | - const highlighted = highlightPre(child, highlighter) |
| 19 | + const highlighted = await highlightCodeBlock(child, highlighter) |
18 | 20 | if (highlighted) { |
19 | 21 | children[index] = highlighted |
20 | 22 | continue |
21 | 23 | } |
22 | 24 | } |
23 | 25 | if (child.tagName === 'code' && !isPre(node)) { |
24 | | - const highlighted = highlightInlineCode(child, highlighter) |
| 26 | + const highlighted = await highlightInlineCode(child, highlighter) |
25 | 27 | if (highlighted) { |
26 | 28 | children[index] = highlighted |
27 | 29 | continue |
28 | 30 | } |
29 | 31 | } |
30 | 32 | if (child.children && child.children.length > 0) { |
31 | | - visit(child) |
| 33 | + await visit(child) |
32 | 34 | } |
33 | 35 | } |
34 | 36 | } |
35 | | - visit(root) |
| 37 | + await visit(root) |
36 | 38 | } |
37 | 39 |
|
38 | | -function highlightPre( |
| 40 | +async function highlightCodeBlock( |
39 | 41 | pre: Element, |
40 | 42 | highlighter: MdxCodeHighlighter, |
41 | | -): Element | undefined { |
| 43 | +): Promise<Element | Raw | undefined> { |
42 | 44 | const code = pre.children?.find( |
43 | 45 | (child) => child.type === 'element' && child.tagName === 'code', |
44 | 46 | ) as Element | undefined |
45 | 47 | if (!code) return undefined |
46 | 48 | const language = resolveLanguage(code) ?? resolveLanguage(pre) |
47 | 49 | const text = collectText(code).trimEnd() |
48 | 50 | if (text.length === 0) return undefined |
49 | | - return renderHighlightedPre(text, language, highlighter) |
| 51 | + return renderHighlightedCodeBlock(text, language, highlighter) |
50 | 52 | } |
51 | 53 |
|
52 | | -function highlightInlineCode( |
| 54 | +async function highlightInlineCode( |
53 | 55 | code: Element, |
54 | 56 | highlighter: MdxCodeHighlighter, |
55 | | -): Element | undefined { |
| 57 | +): Promise<Element | undefined> { |
56 | 58 | const language = resolveLanguage(code) |
57 | 59 | const text = collectText(code).trimEnd() |
58 | 60 | if (text.length === 0) return undefined |
59 | | - const html = highlighter.codeToHtml(text, language) |
| 61 | + const html = await highlighter.codeToHtml(text, language) |
60 | 62 | const parsed = fromHtml(html, { fragment: true }) |
61 | 63 | const replacement = parsed.children?.find( |
62 | 64 | (child) => child.type === 'element' && child.tagName === 'pre', |
@@ -86,38 +88,49 @@ function highlightInlineCode( |
86 | 88 | return code |
87 | 89 | } |
88 | 90 |
|
89 | | -function renderHighlightedPre( |
| 91 | +/** Preserves arbitrary block HTML; a single pre uses the site's code styling. */ |
| 92 | +async function renderHighlightedCodeBlock( |
90 | 93 | text: string, |
91 | 94 | language: string | undefined, |
92 | 95 | highlighter: MdxCodeHighlighter, |
93 | | -): Element | undefined { |
94 | | - const html = highlighter.codeToHtml(text, language) |
| 96 | +): Promise<Element | Raw> { |
| 97 | + const html = await highlighter.codeToHtml(text, language) |
95 | 98 | const parsed = fromHtml(html, { fragment: true }) |
96 | | - const replacement = parsed.children?.find( |
97 | | - (child) => child.type === 'element' && child.tagName === 'pre', |
98 | | - ) as Element | undefined |
99 | | - if (!replacement) return undefined |
100 | | - normalizeShikiPre(replacement) |
| 99 | + const replacement = parsed.children[0] |
| 100 | + if ( |
| 101 | + parsed.children.length !== 1 || |
| 102 | + replacement?.type !== 'element' || |
| 103 | + replacement.tagName !== 'pre' |
| 104 | + ) { |
| 105 | + return { type: 'raw', value: html } |
| 106 | + } |
| 107 | + normalizeHighlightedPre(replacement) |
101 | 108 | return replacement |
102 | 109 | } |
103 | 110 |
|
104 | | -export function renderHighlightedPreHtml( |
| 111 | +/** Renders a code block inside component markup, keeping pre content literal. */ |
| 112 | +export async function renderHighlightedCodeBlockHtml( |
105 | 113 | text: string, |
106 | 114 | language: string | undefined, |
107 | 115 | highlighter: MdxCodeHighlighter, |
108 | | -): string | undefined { |
109 | | - const replacement = renderHighlightedPre(text, language, highlighter) |
110 | | - if (!replacement) return undefined |
| 116 | +): Promise<string> { |
| 117 | + const replacement = await renderHighlightedCodeBlock( |
| 118 | + text, |
| 119 | + language, |
| 120 | + highlighter, |
| 121 | + ) |
| 122 | + if (replacement.type === 'raw') return replacement.value |
111 | 123 | replacement.properties['r-pre'] = '' |
112 | 124 | return toHtml(replacement, { allowDangerousHtml: true }) |
113 | 125 | } |
114 | 126 |
|
115 | | -export function renderHighlightedInlineCodeHtml( |
| 127 | +/** Extracts inline code from the highlighter's pre/code output. */ |
| 128 | +export async function renderHighlightedInlineCodeHtml( |
116 | 129 | text: string, |
117 | 130 | language: string | undefined, |
118 | 131 | highlighter: MdxCodeHighlighter, |
119 | | -): string | undefined { |
120 | | - const html = highlighter.codeToHtml(text, language) |
| 132 | +): Promise<string | undefined> { |
| 133 | + const html = await highlighter.codeToHtml(text, language) |
121 | 134 | const parsed = fromHtml(html, { fragment: true }) |
122 | 135 | const replacement = parsed.children?.find( |
123 | 136 | (child) => child.type === 'element' && child.tagName === 'pre', |
@@ -184,7 +197,8 @@ function isPre(node: Root | Element) { |
184 | 197 | return node.type === 'element' && node.tagName === 'pre' |
185 | 198 | } |
186 | 199 |
|
187 | | -function normalizeShikiPre(pre: Element) { |
| 200 | +/** The site's shared code styles use the shiki class for every highlighter. */ |
| 201 | +function normalizeHighlightedPre(pre: Element) { |
188 | 202 | if (!pre.properties) pre.properties = {} |
189 | 203 | pre.properties.className = mergeClassNames(pre.properties.className, 'shiki') |
190 | 204 | const style = |
|
0 commit comments