UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

167 lines (154 loc) 6.52 kB
'use client'; import * as React from 'react'; import { decompressHast, hastToJsx } from "../pipeline/hastUtils/index.mjs"; import { useCodeComponents } from "../useCode/CodeComponentsContext.mjs"; import { fallbackToHast, fallbackToText } from "../pipeline/hastUtils/fallbackFormat.mjs"; import { requestIdle } from "../useCoordinated/scheduleTasks.mjs"; /** * Find the children of the first `<code>` element in a parsed HAST tree. */ function findCodeChildren(node) { if (node.type === 'element' && node.tagName === 'code') { return node.children; } for (const child of node.children) { if (child.type === 'element') { const found = findCodeChildren(child); if (found) { return found; } } } return null; } /** * Renders a links-only fallback on the server and replaces it with the * fully syntax-highlighted version on the client at the configured time. * * When `fallback` is provided, it is converted to HAST and rendered for the * initial display. Its text content is derived (via `fallbackToText`) to serve * as the DEFLATE dictionary for decompressing `hastCompressed`. * * - `'hydration'`: parse immediately on mount. * - `'idle'`: defer to `requestIdleCallback` regardless of visibility. * - `'visible'`: wait until the element enters the viewport (IntersectionObserver), * then defer to `requestIdleCallback` to avoid blocking scroll or paint. */ export function TypeCode({ hastJson, hastCompressed, highlightAt, fallback, codeProps }) { const components = useCodeComponents(); // Determine the effective mode: fall back to 'idle' when IntersectionObserver // is unavailable (progressive enhancement for older browsers/runtimes). const effectiveMode = highlightAt === 'visible' && (typeof IntersectionObserver === 'undefined' || typeof ResizeObserver === 'undefined') ? 'idle' : highlightAt; const [hast, setHast] = React.useState(null); const [isVisible, setIsVisible] = React.useState(effectiveMode !== 'visible'); const [codeElement, setCodeElement] = React.useState(null); // Re-seed visibility state during render when the effective mode changes // (the 'store previous prop value, set state during render' pattern). The // IntersectionObserver effect still owns runtime true/false toggling because // it runs after this render-time seed. const [prevEffectiveMode, setPrevEffectiveMode] = React.useState(effectiveMode); if (prevEffectiveMode !== effectiveMode) { setPrevEffectiveMode(effectiveMode); setIsVisible(effectiveMode !== 'visible'); } // Convert compact fallback to HAST for rendering. const fallbackHastRoot = React.useMemo(() => fallback ? fallbackToHast(fallback) : undefined, [fallback]); // Derive text dictionary from fallback for decompression. const textDictionary = React.useMemo(() => fallback ? fallbackToText(fallback) : undefined, [fallback]); // Render fallback HAST as JSX for initial display. const fallbackJsx = React.useMemo(() => fallbackHastRoot ? hastToJsx(fallbackHastRoot, components) : null, [fallbackHastRoot, components]); // Observe visibility for 'visible' mode: decompress when scrolled into view, // release expanded HAST when scrolled away to reduce memory pressure. // // Three complementary observers cover different visibility triggers: // - IntersectionObserver: scroll-based viewport entry/exit. // - ResizeObserver: ancestor layout changes (CSS-based tabs, accordions) // that resize the element without necessarily triggering IO. // - Document 'toggle' listener (capture phase): native <details> elements // whose toggle event does not bubble and may not trigger IO or RO. React.useEffect(() => { if (effectiveMode !== 'visible' || !codeElement) { return undefined; } const updateVisibility = inViewport => { if (inViewport) { setIsVisible(true); } else { setIsVisible(false); setHast(null); } }; const io = new IntersectionObserver(([entry]) => { updateVisibility(entry.isIntersecting); }); io.observe(codeElement); // Force IO to re-evaluate without a synchronous getBoundingClientRect call. const nudgeObserver = () => { io.unobserve(codeElement); io.observe(codeElement); }; const ro = new ResizeObserver(nudgeObserver); ro.observe(codeElement); // Native <details> toggle events don't bubble, but capture-phase // listeners on the document still intercept them. Re-check visibility // whenever any <details> on the page opens or closes. document.addEventListener('toggle', nudgeObserver, true); return () => { io.disconnect(); ro.disconnect(); document.removeEventListener('toggle', nudgeObserver, true); }; }, [effectiveMode, codeElement]); // Parse and decompress once visible. React.useEffect(() => { if (!isVisible) { return undefined; } const parse = () => { if (hastCompressed == null && hastJson == null) { return; } try { const raw = hastCompressed ? decompressHast(hastCompressed, textDictionary) : hastJson; const parsed = JSON.parse(raw); // Extract code element's children from the full tree. const root = parsed.type === 'root' ? parsed : { type: 'root', children: Array.isArray(parsed) ? parsed : [parsed] }; const codeChildren = findCodeChildren(root); const hastRoot = { type: 'root', children: codeChildren ?? root.children }; setHast(hastRoot); } catch (error) { console.warn('Failed to parse highlighted code HAST; rendering fallback instead.', error); } }; if (effectiveMode === 'hydration') { parse(); return undefined; } // 'idle' and 'visible' both defer to idle time to avoid blocking the main thread. return requestIdle(parse, { timeout: 2000 }); }, [isVisible, hastJson, hastCompressed, effectiveMode, textDictionary]); const highlighted = React.useMemo(() => hast !== null ? hastToJsx(hast, components) : null, [hast, components]); const content = highlighted ?? fallbackJsx; // 'hydration' and 'idle' parse without visibility gating — no observer needed. if (effectiveMode !== 'visible') { return /*#__PURE__*/React.createElement('code', codeProps, content); } return /*#__PURE__*/React.createElement('code', { ...codeProps, ref: setCodeElement }, content); }