@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
167 lines (154 loc) • 6.52 kB
JavaScript
'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);
}