UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

422 lines (405 loc) 15.1 kB
import { COLLAPSED_VISIBLE_FRAME_TYPES } from "../pipeline/parseSource/frameVisibility.mjs"; import { isFrameSpan } from "../pipeline/parseSource/isFrameSpan.mjs"; /** * Compact serialization format for fallback HAST trees. * * A `FallbackNode` is either: * - a plain string (text node), or * - a variable-length tuple whose meaning is determined by length: * - `[tagName, children]` – no class, no props * - `[tagName, className, children]` – has class, no props * - `[tagName, className, properties, children]` – has class and props * - `[tagName, className, properties, children, extra]` – full * * Where: * - `tagName` – HTML element name (e.g. `'span'`, `'a'`) * - `className` – space-joined class string * - `properties` – remaining HTML properties (without `className`) * - `children` – a single text string **or** an array of child `FallbackNode`s * - `extra` – optional bag for anything else (data attributes, etc.) * * This eliminates the repeated `type`, `tagName`, `properties`, `children`, * and `value` keys that make raw HAST costly in RSC payloads. */ /** * A residual fallback that has been DEFLATE-compressed on its own. Mirrors the * `{ hastCompressed }` shape of `VariantSource`. * * Used for the parts of a variant's fallback that the `ContentLoading` * component never renders (extra files when the loading UI shows a single file, * extra variants when it shows a single variant). Those residual fallbacks * exist only as the DEFLATE dictionary for decompressing `hastCompressed`, so * shipping them as plain `FallbackNode[]` text would be dead weight in the * initial payload. They are compressed standalone — no preset text dictionary, * so the blob is self-contained — and decompressed back to `FallbackNode[]` * (via `decompressFallback`) only when the full content swaps in. */ /** * Convert a HAST root into the compact `FallbackNode[]` format. */ export function hastToFallback(root) { return convertChildren(root.children); } function convertChildren(children) { return children.map(convertNode); } function convertNode(node) { if (node.type === 'text') { return node.value; } if (node.type === 'element') { const el = node; const { className, ...restProps } = el.properties || {}; let classStr = ''; if (className != null) { classStr = Array.isArray(className) ? className.join(' ') : String(className); } const props = Object.keys(restProps).length > 0 ? restProps : null; const kids = el.children ?? []; // Optimize single-text-child to inline string let childValue; if (kids.length === 1 && kids[0].type === 'text') { childValue = kids[0].value; } else { childValue = convertChildren(kids); } // Variable-length tuple: omit trailing empty fields if (props !== null) { return [el.tagName, classStr, props, childValue]; } if (classStr) { return [el.tagName, classStr, childValue]; } return [el.tagName, childValue]; } // For any other node types (comment, doctype, etc.), skip return ''; } /** * Convert the compact `FallbackNode[]` format back into a HAST root. */ export function fallbackToHast(nodes) { // `ElementContent` (text/element) is a subset of `RootContent`, so the mapped // children satisfy `Root.children` directly. return { type: 'root', children: nodes.map(nodeToHast) }; } function nodeToHast(node) { if (typeof node === 'string') { return { type: 'text', value: node }; } let tagName; let classStr; let props; let children; let extra; if (node.length === 2) { [tagName, children] = node; } else if (node.length === 3) { [tagName, classStr, children] = node; } else if (node.length === 5) { [tagName, classStr, props, children, extra] = node; } else { [tagName, classStr, props, children] = node; } const properties = { ...props, ...extra }; if (classStr) { properties.className = classStr.split(' '); } const childNodes = typeof children === 'string' ? [{ type: 'text', value: children }] : children.map(nodeToHast); return { type: 'element', tagName, // The fallback tuple stores element props as `unknown`; assert they are // valid HAST property values at this boundary (the one untyped seam). properties: properties, children: childNodes }; } /** * Extract the text content from compact `FallbackNode[]` without * converting back to HAST. Used to build DEFLATE dictionaries. */ export function fallbackToText(nodes) { return nodes.map(nodeText).join(''); } function nodeText(node) { if (typeof node === 'string') { return node; } // children is always the last element (or second-to-last when extra is present) const children = node.length === 5 ? node[3] : node[node.length - 1]; if (typeof children === 'string') { return children; } return children.map(nodeText).join(''); } /** * Collects the plain text of a frame from its `.line` spans and the newline * text nodes between them. Used only as a fallback when a frame is missing a * precomputed `data.fallback` (which `addLineGutters` normally provides); * walks the frame's direct children once without recursing into highlighting * spans beyond their text. */ function collectFrameText(frame) { let out = ''; const visit = nodes => { for (const node of nodes) { if (node.type === 'text') { out += node.value; } else if (node.type === 'element') { visit(node.children); } } }; visit(frame.children); return out; } /** * The frame's precomputed plain-text fallback (`frame.data.fallback`, set by * `addLineGutters`), falling back to walking the frame's text when absent. This * is the exact text the highlighted children render, so it doubles as the * DEFLATE dictionary contribution for the frame. */ function framePlainText(frame) { const fallbackNodes = frame.data?.fallback; return fallbackNodes && fallbackNodes.length > 0 ? fallbackNodes.map(node => node.type === 'text' ? node.value : '').join('') : collectFrameText(frame); } /** * Builds the variant-level root fallback from a final (post-enhancer) HAST * root. Each `span.frame` becomes a compact frame element whose single text * child is the frame's precomputed plain text (`frame.data.fallback`), so the * result is directly renderable as the pre-hydration code block **and** can be * redistributed back onto the decoded HAST's frames (see * `redistributeRootFallback`). * * The frame's `data-lined` attribute is dropped (line spans don't exist in the * fallback) while other frame attributes (e.g. `data-frame-type`) are kept so * the fallback's layout matches the highlighted render. Non-frame top-level * nodes (e.g. whitespace text between frames) are preserved in place. */ export function buildRootFallback(root) { const syntheticChildren = []; for (const child of root.children) { if (child.type === 'element' && isFrameSpan(child)) { const frame = child; const { dataLined, ...properties } = frame.properties || {}; syntheticChildren.push({ type: 'element', tagName: 'span', properties, children: [{ type: 'text', value: framePlainText(frame) }] }); } else { syntheticChildren.push(child); } } return convertChildren(syntheticChildren); } /** A frame node in compact `FallbackNode` form — `['span', 'frame', …]`, in any of the * 3/4/5-element shapes (`className` is always the second element). */ function isFrameFallbackNode(node) { return Array.isArray(node) && node.length >= 3 && node[0] === 'span' && typeof node[1] === 'string' && node[1].split(' ').includes('frame'); } /** * Whether a compact `fallback` already carries highlighting — i.e. at least one frame * keeps nested token spans (array children) instead of flat plain text. True exactly for * the promoted {@link promoteCriticalFallback} form; a plain {@link buildRootFallback} has * a string child on every frame. `<Pre>` uses this to defer the decompressing decode ONLY * when the first paint is already highlighted, so it never flashes plain → highlighted. */ export function fallbackIsHighlighted(fallback) { return fallback.some(node => { if (!isFrameFallbackNode(node)) { return false; } // `children` is the last tuple slot, except the 5-element form keeps `extra` last // (matches `nodeText`). An array means nested token spans, i.e. highlighted. const children = node.length === 5 ? node[3] : node[node.length - 1]; return Array.isArray(children); }); } /** * The **sparse** highlighted-visible fallback: a map from frame index to the * highlighted `FallbackNode` for ONLY the frames visible on the initial collapsed * render (`visibleFrames`). Off-screen frames are omitted — they flatten to exactly * {@link buildRootFallback}'s plain output, so storing them would just duplicate * `fallback` in the precompute. {@link promoteCriticalFallback} splices these back * over the plain fallback for `highlightAt: 'init'` (paint highlighted on the first * render, zero decompression). Frame indices count `span.frame` children only, * matching `getInitialVisibleFrames`. * * The decoded `root` is shared/read-only; this only reads its frames (the synthetic * visible frame reuses the frame's `children` array without mutating it, and * `data-lined` is dropped via destructuring, not deletion). */ export function buildCriticalFallback(root, visibleFrames) { const critical = {}; let frameIndex = 0; for (const child of root.children) { if (child.type === 'element' && isFrameSpan(child)) { if (visibleFrames[frameIndex]) { const frame = child; const { dataLined, ...properties } = frame.properties || {}; const [node] = convertChildren([{ type: 'element', tagName: 'span', properties, children: frame.children }]); critical[frameIndex] = node; } frameIndex += 1; } } return critical; } /** * Splice a sparse {@link buildCriticalFallback} diff back onto a plain `fallback`, * replacing each visible frame (matched by index in document order) with its * highlighted node. The result is the full highlighted-visible fallback; its text is * byte-identical to `fallback` (the highlight spans only wrap the same characters), so * it stays a valid DEFLATE dictionary — asserted by tests. Returns a new array; the * input is not mutated. */ export function promoteCriticalFallback(fallback, critical) { let frameIndex = 0; return fallback.map(node => { if (!isFrameFallbackNode(node)) { return node; } const replacement = critical[frameIndex]; frameIndex += 1; return replacement ?? node; }); } /** * Redistributes a root fallback (built by `buildRootFallback`) back onto the * frames of a decoded HAST root, setting each frame's `data.fallback` to the * corresponding fallback frame's text nodes (as HAST, not the compact form). * * Frames align 1:1 by position because the root fallback and the decoded HAST * are both derived from the same final tree. Non-frame fallback entries (e.g. * inter-frame whitespace) advance the cursor without being assigned. Mutates * `root` in place and returns it. */ export function redistributeRootFallback(root, fallback) { let fallbackIndex = 0; for (const child of root.children) { // Advance past any non-frame fallback entries so frames stay aligned. while (fallbackIndex < fallback.length && !isFallbackFrame(fallback[fallbackIndex])) { fallbackIndex += 1; } if (child.type !== 'element' || !isFrameSpan(child)) { continue; } if (fallbackIndex >= fallback.length) { break; } const frameFallback = fallback[fallbackIndex]; fallbackIndex += 1; const frame = child; // The fallback frame's children are the frame's plain text. const childValue = frameFallback.length === 5 ? frameFallback[3] : frameFallback[frameFallback.length - 1]; const nodes = typeof childValue === 'string' ? [{ type: 'text', value: childValue }] : childValue.map(node => ({ type: 'text', value: nodeText(node) })); if (!frame.data) { frame.data = {}; } frame.data.fallback = nodes; } return root; } function isFallbackFrame(node) { if (!Array.isArray(node) || node[0] !== 'span' || node.length < 3) { return false; } const classStr = node[1]; return typeof classStr === 'string' && (classStr === 'frame' || classStr.split(' ').includes('frame')); } function fallbackFrameType(frame) { // Properties (carrying `dataFrameType`) live at index 2 of the 4- and // 5-element tuple forms; the shorter forms have no properties. if (frame.length >= 4) { const frameType = frame[2]?.dataFrameType; return typeof frameType === 'string' ? frameType : undefined; } return undefined; } /** * Reduce a root fallback to the frames visible while the code block is * collapsed — the contiguous focused window (`padding-top`, `highlighted` / * `focus`, `padding-bottom`). Inter-frame nodes inside that window are kept so * the slice renders with the same spacing as the full fallback. * * Matches the runtime rule in `Pre.tsx`: when a block has no emphasis frames * (the whole source is the focused window) the first frame stands in. Returns * the input unchanged when it has no frames at all. * * When `collapsesToEmpty` is `true` the source records `focusedLines === 0` * (the `oversizedFocus: 'hide'` collapse-to-nothing case): the collapsed window * is intentionally empty, so the first-frame fallback is skipped and an empty * array is returned. Mirrors the runtime rule in `Pre.tsx` / * `getInitialVisibleSourceLines`. * * Used by `fallbackCollapsed` to paint only the on-screen lines while the * file's full fallback rides along compressed (see the prop-compression * pattern's "Splitting the Fallback by Visibility"). */ export function collapsedVisibleFallback(fallback, collapsesToEmpty = false) { if (collapsesToEmpty) { return []; } let firstFrame = -1; let firstVisible = -1; let lastVisible = -1; for (let index = 0; index < fallback.length; index += 1) { const node = fallback[index]; if (!isFallbackFrame(node)) { continue; } if (firstFrame === -1) { firstFrame = index; } const frameType = fallbackFrameType(node); if (frameType && COLLAPSED_VISIBLE_FRAME_TYPES.has(frameType)) { if (firstVisible === -1) { firstVisible = index; } lastVisible = index; } } if (firstVisible === -1) { // No emphasis frames: the first frame is the collapsed window. return firstFrame === -1 ? fallback : [fallback[firstFrame]]; } return fallback.slice(firstVisible, lastVisible + 1); }