UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

254 lines (235 loc) 9.52 kB
import { createFrame } from "./createFrame.mjs"; import { isFrameSpan } from "./isFrameSpan.mjs"; import { redistributeFrameFallbacks } from "./redistributeFrameFallbacks.mjs"; /** * Represents a line element and its trailing newline text node (if any). */ /** * A collapsed-lines placeholder span (`<span class="collapse" data-lines={n}>`) * along with its anchor line numbers. Tracking both the preceding and * following line lets the emitter re-attach the placeholder to whichever * neighbor survives the new frame ranges, instead of silently dropping it * when the preceding line is filtered out. */ function isCollapsedLinesPlaceholder(node) { return node.type === 'element' && node.tagName === 'span' && node.properties != null && node.properties.className === 'collapse'; } /** * Flattens all line elements and collapsed-lines placeholders from existing * frames. This is a flat iteration over root.children (frames) and their * direct children (lines + newline text nodes + placeholders). Not a deep * recursive traversal. */ function flattenLineEntries(root) { const lineEntries = []; const placeholderEntries = []; // Placeholders that have seen their `prevLine` (or none) but not yet // their `nextLine`. Resolved when the next `.line` appears. let pendingNextAnchor = []; let lastLineNumber; for (const frame of root.children) { if (frame.type !== 'element' || frame.tagName !== 'span') { continue; } const children = frame.children ?? []; for (let i = 0; i < children.length; i += 1) { const child = children[i]; if (child.type === 'element' && child.tagName === 'span' && child.properties?.className === 'line' && typeof child.properties.dataLn === 'number') { const lineNumber = child.properties.dataLn; // Resolve any placeholders waiting on a following line. for (const pending of pendingNextAnchor) { pending.nextLine = lineNumber; } pendingNextAnchor = []; const entry = { lineNumber, element: child }; // Check if the next child is a trailing newline text node const next = children[i + 1]; if (next && next.type === 'text' && /^[\r\n]+$/.test(next.value)) { entry.trailingNewline = next; i += 1; // Skip the newline in the next iteration } lineEntries.push(entry); lastLineNumber = lineNumber; } else if (isCollapsedLinesPlaceholder(child)) { const entry = { element: child, prevLine: lastLineNumber }; placeholderEntries.push(entry); pendingNextAnchor.push(entry); } } } return { lineEntries, placeholderEntries }; } /** * Collects the per-frame fallback node arrays from the existing frames, paired * with the inclusive 1-based line range each frame covers (derived from its * `.line` children's `data-ln`). * * Returns `null` when any line-bearing frame is missing a `data.fallback`, so * the caller can safely skip fallback redistribution rather than emitting an * incomplete result. */ function collectFrameFallbacks(root) { const frames = []; for (const frame of root.children) { if (frame.type !== 'element' || !isFrameSpan(frame)) { continue; } let startLine = Infinity; let endLine = -Infinity; for (const child of frame.children) { if (child.type === 'element' && child.properties?.className === 'line' && typeof child.properties.dataLn === 'number') { const lineNumber = child.properties.dataLn; if (lineNumber < startLine) { startLine = lineNumber; } if (lineNumber > endLine) { endLine = lineNumber; } } } if (endLine < startLine) { // Frame carries no line elements (e.g. placeholder-only); nothing to map. continue; } const fallback = frame.data?.fallback; if (!fallback) { return null; } frames.push({ startLine, endLine, nodes: fallback }); } return frames; } /** * Restructures the HAST frame tree based on computed frame ranges. * * This function flattens all existing frame children into a single ordered array, * then redistributes them into new frames based on the provided frame ranges. * It's a flat iteration (not a deep recursive traversal) since the HAST structure * is always Root → Frame(s) → Line(s). * * Per-frame `data.fallback` text is kept in sync: it is redistributed onto the * new frames by shifting only the lines that cross a frame boundary (see * `redistributeFrameFallbacks`), so enhancers that re-chunk frames don't * invalidate the precomputed fallback. * * @param root - The HAST root node to restructure (mutated in place) * @param frameRanges - Ordered array of frame ranges * @param regionIndentLevels - Map of highlighted region index to indent level */ export function restructureFrames(root, frameRanges, regionIndentLevels) { // Step 0: Capture existing per-frame fallbacks before the tree is rebuilt so // they can be redistributed onto the new frames. const oldFrameFallbacks = collectFrameFallbacks(root); // Step 1: Flatten all lines + placeholders from existing frames. const { lineEntries, placeholderEntries } = flattenLineEntries(root); // Build a lookup from line number to entry for O(1) access const lineEntryMap = new Map(); for (const entry of lineEntries) { lineEntryMap.set(entry.lineNumber, entry); } // Step 2: Pre-compute the set of kept line numbers so each placeholder // can decide where it belongs without re-scanning the ranges. const keptLines = new Set(); for (const range of frameRanges) { for (let line = range.startLine; line <= range.endLine; line += 1) { keptLines.add(line); } } // Track which placeholders have been emitted to avoid duplicates when // both anchors are kept across separate ranges. const emittedPlaceholders = new Set(); // Index placeholders by their anchor line for O(1) lookup during emission. const placeholdersAfterLine = new Map(); const placeholdersBeforeLine = new Map(); for (const placeholder of placeholderEntries) { if (placeholder.prevLine !== undefined && keptLines.has(placeholder.prevLine)) { // Prefer attaching to the preceding kept line (source-order stable). const list = placeholdersAfterLine.get(placeholder.prevLine) ?? []; list.push(placeholder); placeholdersAfterLine.set(placeholder.prevLine, list); } else if (placeholder.nextLine !== undefined && keptLines.has(placeholder.nextLine)) { // Preceding anchor was dropped; fall back to the next kept line. const list = placeholdersBeforeLine.get(placeholder.nextLine) ?? []; list.push(placeholder); placeholdersBeforeLine.set(placeholder.nextLine, list); } // Otherwise: neither anchor survives → placeholder is dropped. } // Step 3: Build new frames. const newFrames = []; // Redistribute the captured per-frame fallbacks onto the new ranges. The // result is aligned to `frameRanges` by index; entries for ranges that end // up empty are simply never read. const redistributedFallbacks = oldFrameFallbacks ? redistributeFrameFallbacks(oldFrameFallbacks, frameRanges) : null; for (let rangeIndex = 0; rangeIndex < frameRanges.length; rangeIndex += 1) { const range = frameRanges[rangeIndex]; const children = []; for (let line = range.startLine; line <= range.endLine; line += 1) { const entry = lineEntryMap.get(line); if (!entry) { continue; } // Emit any placeholders whose preceding anchor was dropped and that // are anchored before this line. const before = placeholdersBeforeLine.get(line); if (before) { for (const placeholder of before) { if (!emittedPlaceholders.has(placeholder)) { children.push(placeholder.element); emittedPlaceholders.add(placeholder); } } } children.push(entry.element); // Always add trailing newline when present to preserve original line breaks if (entry.trailingNewline) { children.push(entry.trailingNewline); } // Re-emit placeholders that originally followed this line. const after = placeholdersAfterLine.get(line); if (after) { for (const placeholder of after) { if (!emittedPlaceholders.has(placeholder)) { children.push(placeholder.element); emittedPlaceholders.add(placeholder); } } } } // Only create frame if it has children if (children.length > 0) { const indentLevel = range.regionIndex !== undefined ? regionIndentLevels.get(range.regionIndex) : undefined; const frame = createFrame(children, range.type, indentLevel, range.truncated); const fallbackNodes = redistributedFallbacks?.[rangeIndex]; if (fallbackNodes && fallbackNodes.length > 0) { // Cast to `ElementData` because `hast-util-from-parse5` augments it // with a required `position` field (upstream bug — should be // optional). We never run through that parser here, so the field // never exists at runtime. if (!frame.data) { frame.data = {}; } frame.data.fallback = fallbackNodes; } newFrames.push(frame); } } // Step 4: Replace root children root.children = newFrames; }