@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
462 lines (440 loc) • 17.8 kB
JavaScript
/**
* 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;
}