Skip to content

Commit 4eedb19

Browse files
heiskrCopilot
andauthored
Convert REST and webhook code highlighting to React (lowlight) (#61772)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ecd8bed commit 4eedb19

8 files changed

Lines changed: 133 additions & 202 deletions

File tree

package-lock.json

Lines changed: 32 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
"github-slugger": "^2.0.0",
213213
"glob": "13.0.2",
214214
"hast-util-from-parse5": "^8.0.3",
215+
"hast-util-to-jsx-runtime": "^2.3.6",
215216
"hast-util-to-string": "^3.0.1",
216217
"hastscript": "^9.0.1",
217218
"helmet": "^8.1.0",

src/automated-pipelines/components/AutomatedPage.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { ArticleGridLayout } from '@/frame/components/article/ArticleGridLayout'
88
import { ArticleInlineLayout } from '@/frame/components/article/ArticleInlineLayout'
99
import { MiniTocs } from '@/frame/components/ui/MiniTocs'
1010
import { useAutomatedPageContext } from '@/automated-pipelines/components/AutomatedPageContext'
11-
import { ClientSideHighlight } from '@/frame/components/ClientSideHighlight'
1211
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
1312
import { JourneyTrackCard, JourneyTrackNav } from '@/journeys/components'
1413

@@ -60,8 +59,6 @@ export const AutomatedPage = ({ children, rawChildren, fullWidth }: Props) => {
6059

6160
return (
6261
<DefaultLayout>
63-
<ClientSideHighlight />
64-
6562
{currentLayout === 'inline' ? (
6663
<>
6764
<ArticleInlineLayout

src/frame/components/ClientSideHighlight.tsx

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/frame/components/ClientSideHighlightJS.tsx

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
import type { ReactNode } from 'react'
3+
import { Fragment, jsx, jsxs } from 'react/jsx-runtime'
4+
import { toJsxRuntime } from 'hast-util-to-jsx-runtime'
5+
import { createLowlight } from 'lowlight'
6+
import json from 'highlight.js/lib/languages/json'
7+
import javascript from 'highlight.js/lib/languages/javascript'
8+
import hljsCurl from 'highlightjs-curl'
9+
import cx from 'classnames'
10+
11+
// React-native replacement for the imperative ClientSideHighlightJS enhancer
12+
// (#6619). The old enhancer scanned the document for `[data-highlight] code` and
13+
// called `hljs.highlightElement`, which REPLACES the `<code>`'s innerHTML —
14+
// destructive on a React-owned node. Instead, components that render code
15+
// (RestCodeSamples, Webhook) use <HighlightedCode>, which highlights with
16+
// `lowlight` (the hast-based highlighter behind rehype-highlight) and renders the
17+
// tokens as real React elements via `toJsxRuntime`. No innerHTML, no DOM scan.
18+
//
19+
// Highlighting (the per-block compute) is deferred until the block scrolls into
20+
// view, preserving the old IntersectionObserver *compute* laziness so large REST
21+
// reference pages don't highlight every off-screen sample at once. Note: unlike
22+
// the old `dynamic(..., { ssr: false })` chunk, the highlighter libraries are now
23+
// statically bundled into these pages, so *load* laziness is not preserved. These
24+
// pages always contain code samples, so the chunk would have loaded anyway.
25+
26+
// Keep the language set tight: highlight.js can pull in everything, which is huge.
27+
const lowlight = createLowlight({ json, javascript, curl: hljsCurl })
28+
const SUPPORTED_LANGUAGES = new Set(['json', 'javascript', 'curl'])
29+
30+
function highlightToReact(language: string, code: string): ReactNode {
31+
const tree = lowlight.highlight(language, code)
32+
return toJsxRuntime(tree, { Fragment, jsx, jsxs })
33+
}
34+
35+
type HighlightedCodeProps = {
36+
language: string
37+
code: string
38+
className?: string
39+
}
40+
41+
export function HighlightedCode({ language, code, className }: HighlightedCodeProps) {
42+
const ref = useRef<HTMLElement>(null)
43+
const [highlighted, setHighlighted] = useState<ReactNode>(null)
44+
45+
useEffect(() => {
46+
setHighlighted(null)
47+
if (!SUPPORTED_LANGUAGES.has(language)) return
48+
49+
const element = ref.current
50+
// No IntersectionObserver (or no element): just highlight right away.
51+
if (!element || typeof window === 'undefined' || !window.IntersectionObserver) {
52+
setHighlighted(highlightToReact(language, code))
53+
return
54+
}
55+
56+
const observer = new IntersectionObserver((entries) => {
57+
for (const entry of entries) {
58+
if (entry.isIntersecting) {
59+
setHighlighted(highlightToReact(language, code))
60+
observer.disconnect()
61+
break
62+
}
63+
}
64+
})
65+
observer.observe(element)
66+
return () => observer.disconnect()
67+
}, [language, code])
68+
69+
// `hljs` provides the base theme; the same token classes (`hljs-*`) that
70+
// highlight.js produced are emitted by lowlight, so styling is unchanged.
71+
return (
72+
<code ref={ref} className={cx('hljs', `language-${language}`, className)}>
73+
{highlighted ?? code}
74+
</code>
75+
)
76+
}

0 commit comments

Comments
 (0)