UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

462 lines (440 loc) 17.8 kB
/** * Metadata for an emphasized line. */ /** * A range of lines that forms a frame in the output. */ /** * A contiguous region of highlighted lines. */ /** * Options for the enhance code emphasis factory. */ /** Default max number of lines kept in focus when not explicitly configured. */ export const DEFAULT_FOCUS_FRAMES_MAX_SIZE = 12; /** * Groups consecutive emphasized line numbers into highlight regions. * * @param emphasizedLines - Map of line numbers to their emphasis metadata * @returns Sorted array of highlight regions */ function groupHighlightRegions(emphasizedLines) { if (emphasizedLines.size === 0) { return []; } const sortedLines = Array.from(emphasizedLines.keys()).sort((a, b) => a - b); const regions = []; // Track overrides in three tiers (highest to lowest priority): // 1. explicit focus — per-line focus directive (propagatedOverride !== true) // 2. propagated focus — multiline focus range (propagatedOverride === true) // 3. non-focus — highlight directives without focus // Within each tier, first-in-region wins (??=). function emptyChannels() { return { explicitFocusPadding: undefined, propagatedFocusPadding: undefined, nonFocusPadding: undefined, explicitFocusMaxSize: undefined, propagatedFocusMaxSize: undefined, nonFocusMaxSize: undefined }; } function accumulateOverrides(channels, meta) { if (meta?.paddingFrameMaxSize !== undefined) { if (meta.focus) { if (meta.propagatedOverride) { channels.propagatedFocusPadding ??= meta.paddingFrameMaxSize; } else { channels.explicitFocusPadding ??= meta.paddingFrameMaxSize; } } else { channels.nonFocusPadding ??= meta.paddingFrameMaxSize; } } if (meta?.focusFramesMaxSize !== undefined) { if (meta.focus) { if (meta.propagatedOverride) { channels.propagatedFocusMaxSize ??= meta.focusFramesMaxSize; } else { channels.explicitFocusMaxSize ??= meta.focusFramesMaxSize; } } else { channels.nonFocusMaxSize ??= meta.focusFramesMaxSize; } } } function resolvePadding(channels) { return channels.explicitFocusPadding ?? channels.propagatedFocusPadding ?? channels.nonFocusPadding; } function resolveMaxSize(channels) { return channels.explicitFocusMaxSize ?? channels.propagatedFocusMaxSize ?? channels.nonFocusMaxSize; } let regionStart = sortedLines[0]; let regionEnd = sortedLines[0]; const firstMeta = emphasizedLines.get(sortedLines[0]); let hasFocus = firstMeta?.focus ?? false; let hasLineHighlight = firstMeta?.lineHighlight ?? false; let allLinesHighlighted = firstMeta?.lineHighlight ?? false; let channels = emptyChannels(); accumulateOverrides(channels, firstMeta); for (let i = 1; i < sortedLines.length; i += 1) { const line = sortedLines[i]; if (line === regionEnd + 1) { // Consecutive line, extend current region regionEnd = line; const meta = emphasizedLines.get(line); if (meta?.focus) { hasFocus = true; } if (meta?.lineHighlight) { hasLineHighlight = true; } else { allLinesHighlighted = false; } accumulateOverrides(channels, meta); } else { // Gap found, close current region and start a new one regions.push({ startLine: regionStart, endLine: regionEnd, index: regions.length, focused: hasFocus, hasLineHighlight, allLinesHighlighted, paddingFrameMaxSize: resolvePadding(channels), focusFramesMaxSize: resolveMaxSize(channels) }); regionStart = line; regionEnd = line; const meta = emphasizedLines.get(line); hasFocus = meta?.focus ?? false; hasLineHighlight = meta?.lineHighlight ?? false; allLinesHighlighted = meta?.lineHighlight ?? false; channels = emptyChannels(); accumulateOverrides(channels, meta); } } // Close the last region regions.push({ paddingFrameMaxSize: resolvePadding(channels), startLine: regionStart, endLine: regionEnd, index: regions.length, focused: hasFocus, hasLineHighlight, allLinesHighlighted, focusFramesMaxSize: resolveMaxSize(channels) }); return regions; } /** * Determines the focused region index. * Returns the region explicitly marked with `focus: true`, or the first region. * * @param regions - Highlight regions * @returns The index of the focused region */ function determineFocusedRegionIndex(regions) { const focusedIndex = regions.findIndex(r => r.focused); return focusedIndex >= 0 ? focusedIndex : 0; } /** * Calculates padding sizes for the focused highlight region. * * @param region - The focused highlight region * @param prevRegionEnd - End line of the previous highlight region (or 0) * @param nextRegionStart - Start line of the next highlight region (or totalLines + 1) * @param paddingFrameMaxSize - Per-region padding size (or from global options if undefined) * @param focusFramesMaxSize - Global focus frames max size option * @returns Padding sizes [paddingTop, paddingBottom] */ function calculatePadding(region, prevRegionEnd, nextRegionStart, paddingFrameMaxSize, focusFramesMaxSize) { // Use per-region padding, fallback to 0 if not specified const padding = paddingFrameMaxSize ?? 0; if (padding <= 0) { return [0, 0]; } const highlightSize = region.endLine - region.startLine + 1; let paddingTop = padding; let paddingBottom = padding; // Apply focusFramesMaxSize constraint if (focusFramesMaxSize !== undefined) { const remaining = focusFramesMaxSize - highlightSize; if (remaining <= 0) { return [0, 0]; } paddingTop = Math.min(paddingTop, Math.floor(remaining / 2)); paddingBottom = Math.min(paddingBottom, Math.ceil(remaining / 2)); } // Clamp to available lines before the highlight (don't overlap previous region) const availableBefore = region.startLine - 1 - prevRegionEnd; paddingTop = Math.min(paddingTop, Math.max(0, availableBefore)); // Clamp to available lines after the highlight (don't overlap next region) const availableAfter = nextRegionStart - 1 - region.endLine; paddingBottom = Math.min(paddingBottom, Math.max(0, availableAfter)); return [paddingTop, paddingBottom]; } /** * When the focused region exceeds focusFramesMaxSize, determines the * sub-window from the start of the region that stays focused. * * @returns [focusStart, focusEnd] (1-based, inclusive) or null if no split needed */ function calculateFocusWindow(region, focusFramesMaxSize) { if (focusFramesMaxSize === undefined || focusFramesMaxSize < 1) { return null; } const regionSize = region.endLine - region.startLine + 1; if (regionSize <= focusFramesMaxSize) { return null; } const focusStart = region.startLine; const focusEnd = focusStart + focusFramesMaxSize - 1; return [focusStart, focusEnd]; } /** * Splits an inclusive line range into `normal` frames, chunked by * `normalFrameMaxSize` when provided. Returns an empty array when the range * is empty (`start > end`). */ function splitIntoNormalFrames(start, end, normalFrameMaxSize) { if (start > end) { return []; } if (normalFrameMaxSize === undefined || normalFrameMaxSize < 1) { return [{ startLine: start, endLine: end, type: 'normal' }]; } const frames = []; let cursor = start; while (cursor <= end) { const frameEnd = Math.min(cursor + normalFrameMaxSize - 1, end); frames.push({ startLine: cursor, endLine: frameEnd, type: 'normal' }); cursor = frameEnd + 1; } return frames; } /** * Calculates frame ranges for the code block based on emphasized lines. * * This is a pure function that operates on line numbers — no HAST traversal. * It groups consecutive highlighted lines into regions, determines the focused * region (first by default, or the one with `focus: true`), computes padding * for the focused region, and returns an ordered array of frame ranges covering * all lines 1 through totalLines. * * @param emphasizedLines - Map of line numbers to their emphasis metadata * @param totalLines - Total number of lines in the code block * @param options - Optional padding configuration * @param normalFrameMaxSize - Maximum lines per normal frame. Read from `hast.data.frameSize` * (set by `starryNightGutter` when it splits a tree into multiple frames) so that emphasis * reframing matches the original gutter split size. * @returns Ordered array of frame ranges covering all lines */ export function calculateFrameRanges(emphasizedLines, totalLines, options = {}, normalFrameMaxSize) { const effectiveFocusFramesMaxSize = options.focusFramesMaxSize ?? DEFAULT_FOCUS_FRAMES_MAX_SIZE; if (options.focusFramesMaxSize !== undefined && (!Number.isFinite(options.focusFramesMaxSize) || options.focusFramesMaxSize < 1)) { throw new Error(`focusFramesMaxSize must be a finite number >= 1, got ${options.focusFramesMaxSize}`); } if (options.paddingFrameMaxSize !== undefined && (!Number.isFinite(options.paddingFrameMaxSize) || options.paddingFrameMaxSize < 0)) { throw new Error(`paddingFrameMaxSize must be a finite number >= 0, got ${options.paddingFrameMaxSize}`); } if (options.oversizedFocus !== undefined && options.oversizedFocus !== 'truncate' && options.oversizedFocus !== 'hide') { throw new Error(`oversizedFocus must be 'truncate' or 'hide', got ${options.oversizedFocus}`); } // Indent shifting replaces padding as the way to convey surrounding context, so the // two options can't be combined — fail fast on a contradictory config. A per-region // `@padding` directive in the source is NOT an error (it rides in `emphasizedLines`, // not `options`); it is simply ignored below while `emitFrameIndent` is set. if (options.emitFrameIndent && (options.paddingFrameMaxSize ?? 0) > 0) { throw new Error('emitFrameIndent cannot be combined with the paddingFrameMaxSize option: indent ' + 'shifting replaces padding. Configure one or the other. (A per-region `@padding` ' + 'directive in the source is still allowed — it is ignored while emitFrameIndent is set.)'); } if (normalFrameMaxSize !== undefined && (!Number.isFinite(normalFrameMaxSize) || normalFrameMaxSize < 1)) { throw new Error(`normalFrameMaxSize must be a finite number >= 1, got ${normalFrameMaxSize}`); } if (totalLines <= 0) { return []; } const regions = groupHighlightRegions(emphasizedLines); if (regions.length === 0) { // Auto-focus: when no emphasis directives exist, focus from line 1. // If focusFramesMaxSize is set and the code exceeds it, truncate. const autoFocusMax = effectiveFocusFramesMaxSize; if (autoFocusMax !== undefined && totalLines > autoFocusMax) { if (options.oversizedFocus === 'hide') { // No focus window is produced for an oversized source: emit normal // frames covering everything. The block collapses to nothing (the // enhancer marks it collapsible with focusedLines === 0). return splitIntoNormalFrames(1, totalLines, normalFrameMaxSize); } const autoFrames = [{ startLine: 1, endLine: autoFocusMax, type: 'focus', regionIndex: 0, truncated: 'visible' }]; // Split the trailing normal frame if normalFrameMaxSize is set autoFrames.push(...splitIntoNormalFrames(autoFocusMax + 1, totalLines, normalFrameMaxSize)); return autoFrames; } return [{ startLine: 1, endLine: totalLines, type: 'focus', regionIndex: 0 }]; } const focusedIndex = determineFocusedRegionIndex(regions); // Calculate focus window split (for oversized regions) const focusedRegion = regions[focusedIndex]; const focusFramesMaxSize = focusedRegion.focusFramesMaxSize ?? effectiveFocusFramesMaxSize; // When `oversizedFocus: 'hide'` is set and the focused region is larger than // the focus window, suppress focus entirely: no window split, no padding, // and the region is rendered with its unfocused frame type. The enhancer // then sees focusedLines === 0 and collapses the block to nothing. const focusedRegionSize = focusedRegion.endLine - focusedRegion.startLine + 1; const oversizedFocusHidden = options.oversizedFocus === 'hide' && focusedRegionSize > focusFramesMaxSize; const focusWindow = oversizedFocusHidden ? null : calculateFocusWindow(focusedRegion, focusFramesMaxSize); // Calculate padding for the focused region (0 when region is split or focus // is suppressed) const prevRegionEnd = focusedIndex > 0 ? regions[focusedIndex - 1].endLine : 0; const nextRegionStart = focusedIndex < regions.length - 1 ? regions[focusedIndex + 1].startLine : totalLines + 1; // `emitFrameIndent` replaces padding with a horizontal shift, so it produces no // padding frames. The global `paddingFrameMaxSize` option can't be combined with it // (validated above), so the only padding that can reach here under `emitFrameIndent` // is a per-region `@padding` directive from the source — which is intentionally // tolerated and suppressed rather than rejected. const [paddingTop, paddingBottom] = oversizedFocusHidden || options.emitFrameIndent ? [0, 0] : calculatePadding(focusedRegion, prevRegionEnd, nextRegionStart, focusedRegion.paddingFrameMaxSize ?? options.paddingFrameMaxSize, focusFramesMaxSize); // Build frame ranges by iterating through all regions const frames = []; let currentLine = 1; for (let i = 0; i < regions.length; i += 1) { const region = regions[i]; const isFocused = i === focusedIndex; if (isFocused && paddingTop > 0) { // Normal lines before padding-top const paddingTopStart = region.startLine - paddingTop; if (currentLine < paddingTopStart) { frames.push({ startLine: currentLine, endLine: paddingTopStart - 1, type: 'normal' }); } // Padding-top frame frames.push({ startLine: paddingTopStart, endLine: region.startLine - 1, type: 'padding-top' }); } else if (currentLine < region.startLine) { // Normal lines before this region frames.push({ startLine: currentLine, endLine: region.startLine - 1, type: 'normal' }); } if (isFocused && focusWindow) { // Split oversized focused region into unfocused-top + focused-center + unfocused-bottom const [focusStart, focusEnd] = focusWindow; const isHighlightFrame = region.hasLineHighlight && (!region.focused || region.allLinesHighlighted); const unfocusedType = isHighlightFrame ? 'highlighted-unfocused' : 'focus-unfocused'; const focusedType = isHighlightFrame ? 'highlighted' : 'focus'; if (region.startLine < focusStart) { frames.push({ startLine: region.startLine, endLine: focusStart - 1, type: unfocusedType, regionIndex: i, truncated: 'hidden' }); } frames.push({ startLine: focusStart, endLine: focusEnd, type: focusedType, regionIndex: i, truncated: 'visible' }); if (focusEnd < region.endLine) { frames.push({ startLine: focusEnd + 1, endLine: region.endLine, type: unfocusedType, regionIndex: i, truncated: 'hidden' }); } } else { // Frame type depends on whether the region's lines are highlighted. // When all lines have data-hl (e.g. @highlight-start @focus), use "highlighted". // When only some lines are highlighted (e.g. @focus with inner @highlight), use "focus". // When focus is suppressed for this oversized region, render it with its // unfocused type so it stays hidden when collapsed (collapse-to-nothing). const renderFocused = isFocused && !oversizedFocusHidden; let frameType; if (region.hasLineHighlight && (!region.focused || region.allLinesHighlighted)) { frameType = renderFocused ? 'highlighted' : 'highlighted-unfocused'; } else { frameType = renderFocused ? 'focus' : 'focus-unfocused'; } frames.push({ startLine: region.startLine, endLine: region.endLine, type: frameType, regionIndex: i }); } currentLine = region.endLine + 1; if (isFocused && paddingBottom > 0) { // Padding-bottom frame frames.push({ startLine: currentLine, endLine: currentLine + paddingBottom - 1, type: 'padding-bottom' }); currentLine = currentLine + paddingBottom; } } // Remaining normal lines after all regions if (currentLine <= totalLines) { frames.push({ startLine: currentLine, endLine: totalLines, type: 'normal' }); } // Split oversized normal frames if normalFrameMaxSize is configured if (normalFrameMaxSize !== undefined && normalFrameMaxSize >= 1) { const maxSize = normalFrameMaxSize; const splitFrames = []; for (const frame of frames) { const frameSize = frame.endLine - frame.startLine + 1; if (frame.type === 'normal' && frameSize > maxSize) { let start = frame.startLine; while (start <= frame.endLine) { const end = Math.min(start + maxSize - 1, frame.endLine); splitFrames.push({ startLine: start, endLine: end, type: 'normal' }); start = end + 1; } } else { splitFrames.push(frame); } } return splitFrames; } return frames; }