|
| 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