@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
362 lines (353 loc) • 12.7 kB
JavaScript
import { hastToFallback } from "./fallbackFormat.mjs";
import { getLanguageFromExtension } from "../pipeline/loaderUtils/getLanguageFromExtension.mjs";
import { getVariantFileLineCounts } from "../useCode/sourceLineCounts.mjs";
/** Per-variant → per-file line metadata threaded for the fallback. */
/**
* Resolve a `language-{language}` hint for a file from its extension, used to
* scope the fallback `<code>` styling the same way the post-load tree is.
* Returns `undefined` when the name has no extension or it isn't recognized.
*/
function getLanguageFromFileName(fileName) {
if (!fileName) {
return undefined;
}
const dotIndex = fileName.lastIndexOf('.');
if (dotIndex === -1) {
return undefined;
}
return getLanguageFromExtension(fileName.slice(dotIndex));
}
/**
* Resolve the compact fallback for a file. Prefers the pre-extracted variant
* `fallback` (always emitted by the loader as a root fallback) and only derives
* one from the source for live/dev trees that never went through the loader.
*
* A plain-string source (an unparsed code block, e.g. `<CodeHighlighter>{code}`)
* is wrapped in a single focus frame so the fallback always has the same frame
* structure as the highlighted render — never a bare text node. `hastCompressed`
* payloads can't be decoded here (no DEFLATE dictionary), so without a variant
* `fallback` they yield `undefined`.
*/
function sourceToFallback(source, fallback) {
if (fallback) {
return fallback;
}
if (!source) {
return undefined;
}
if (typeof source === 'string') {
// Wrap the raw code in a single focus frame so the fallback always carries a
// frame, matching the highlighted render (`buildRootFallback` likewise emits
// frames with text children) — never a bare text node. The whole source is
// the visible window; collapse-to-empty demotes it like any other focus frame.
return [['span', 'frame', {
dataFrameType: 'focus'
}, source]];
}
if ('type' in source && source.type === 'root') {
return hastToFallback(source);
}
if ('hastJson' in source) {
return hastToFallback(JSON.parse(source.hastJson));
}
// hastCompressed cannot be decoded here without the dictionary
return undefined;
}
/**
* Derive the compact `source`/`extraSource` fallbacks and the `language` hint
* for a single variant. Prefers the pre-extracted per-file fallbacks in
* `variantHasts` (server path) and otherwise derives them from the variant's
* own `source`/`fallback` (dev path). `language` comes from the variant's
* explicit `language` or the main file's extension and is only set when a
* `source` is present, mirroring how consumers gate the `language-{language}`
* class behind a rendered source.
*/
function deriveVariantSources(variantCode, variantHasts, variantLineCounts) {
const fileNames = [variantCode.fileName, ...Object.keys(variantCode.extraFiles || {})].filter(name => Boolean(name));
const mainFile = variantCode.fileName || fileNames[0];
// Per-file line counts: prefer render-time windowing (`variantLineCounts`), else
// the counts the loader stored on the code (`VariantCode` / extra-file `totalLines`
// / `focusedLines`). So every file/variant carries its window, not just the main one.
const fileCounts = fileName => {
const threaded = variantLineCounts?.[fileName];
if (threaded) {
return threaded;
}
const file = variantCode.fileName === fileName ? variantCode : variantCode.extraFiles?.[fileName];
if (file && typeof file !== 'string' && file.totalLines !== undefined) {
return {
totalLines: file.totalLines,
focusedLines: file.focusedLines ?? file.totalLines,
collapsible: file.collapsible === true
};
}
// Last resort: count lines off the source (a plain string with no enhancers ⇒
// `focusedLines === totalLines`, matching `<Pre>`'s `getSourceLineCounts`). Guarded
// because a `hastCompressed` source can't be decoded without its dictionary (which
// the server strips before this runs) — the server passes `variantLineCounts`
// instead, so this branch only really fires client-side where the dictionary is on
// the code.
try {
const counts = getVariantFileLineCounts(variantCode, fileName);
// `totalLines === 0` means a hast with no `root.data` counts (not a real count).
return counts && counts.totalLines > 0 ? counts : undefined;
} catch {
return undefined;
}
};
let source;
let extraSource;
if (variantHasts) {
// Pre-extracted fallback data (server path).
if (mainFile && variantHasts[mainFile]) {
source = variantHasts[mainFile];
}
const extra = {};
for (const [fName, nodes] of Object.entries(variantHasts)) {
if (fName !== mainFile) {
extra[fName] = {
source: nodes,
...fileCounts(fName)
};
}
}
if (Object.keys(extra).length > 0) {
extraSource = extra;
}
} else {
// No pre-extracted fallback data (e.g. dev mode). Prefer the variant's own
// `fallback`, falling back to deriving one from the source directly.
source = sourceToFallback(variantCode.source, variantCode.fallback);
const extra = {};
for (const [fName, fData] of Object.entries(variantCode.extraFiles || {})) {
if (typeof fData === 'object' && fData.source) {
const fb = sourceToFallback(fData.source, fData.fallback);
if (fb) {
extra[fName] = {
source: fb,
...fileCounts(fName)
};
}
}
}
if (Object.keys(extra).length > 0) {
extraSource = extra;
}
}
const language = source ? variantCode.language ?? getLanguageFromFileName(mainFile) : undefined;
const mainCounts = source && mainFile ? fileCounts(mainFile) : undefined;
return {
fileNames,
source,
totalLines: mainCounts?.totalLines,
focusedLines: mainCounts?.focusedLines,
collapsible: mainCounts?.collapsible,
extraSource,
language
};
}
export function codeToFallbackProps(variant, code,
// `fallbackUsesExtraFiles` / the selected file name are threaded in by both
// call sites for signature parity, but the per-file gating now happens
// upstream in `stripFallbackHastsFromCode` (which only hoists the allowed
// files into `allFallbackHasts`), so the derivation below reads them off the
// already-gated `allFallbackHasts` rather than re-applying the flags here.
_fileName, _needsAllFiles = false, needsAllVariants = false, allFallbackHasts, allLineCounts) {
const variantCode = code?.[variant];
if (!variantCode || typeof variantCode === 'string') {
return {};
}
const {
fileNames,
source,
totalLines,
focusedLines,
collapsible,
extraSource,
language
} = deriveVariantSources(variantCode, allFallbackHasts?.[variant], allLineCounts?.[variant]);
if (needsAllVariants) {
const extraVariants = Object.entries(code || {}).reduce((acc, [name, vCode]) => {
if (name !== variant && vCode && typeof vCode !== 'string') {
const {
fileNames: evFileNames,
source: evSource,
totalLines: evTotalLines,
focusedLines: evFocusedLines,
collapsible: evCollapsible,
extraSource: evExtraSource,
language: evLanguage
} = deriveVariantSources(vCode, allFallbackHasts?.[name], allLineCounts?.[name]);
acc[name] = {
fileNames: evFileNames,
...(evSource ? {
source: evSource
} : undefined),
...(evTotalLines !== undefined ? {
totalLines: evTotalLines
} : undefined),
...(evFocusedLines !== undefined ? {
focusedLines: evFocusedLines
} : undefined),
...(evCollapsible !== undefined ? {
collapsible: evCollapsible
} : undefined),
...(evLanguage ? {
language: evLanguage
} : undefined),
...(evExtraSource ? {
extraSource: evExtraSource
} : undefined)
};
}
return acc;
}, {});
return {
fileNames,
...(source ? {
source
} : undefined),
...(totalLines !== undefined ? {
totalLines
} : undefined),
...(focusedLines !== undefined ? {
focusedLines
} : undefined),
...(collapsible !== undefined ? {
collapsible
} : undefined),
...(language ? {
language
} : undefined),
...(extraSource ? {
extraSource
} : undefined),
extraVariants
};
}
return {
fileNames,
...(source ? {
source
} : undefined),
...(totalLines !== undefined ? {
totalLines
} : undefined),
...(focusedLines !== undefined ? {
focusedLines
} : undefined),
...(collapsible !== undefined ? {
collapsible
} : undefined),
...(language ? {
language
} : undefined),
...(extraSource ? {
extraSource
} : undefined)
};
}
/**
* Read a variant's per-file fallbacks straight off its `VariantCode` `fallback`
* fields (main + extra files), returning a `Fallbacks` map keyed by file name.
*
* The fallback crosses the server→client boundary exactly once: either on the
* `VariantCode` (no `ContentLoading`) or — after `stripFallbackHastsFromCode`
* moves it — on the `ContentLoading` props. This reads the former location, so
* the client can resolve the DEFLATE dictionary for `hastCompressed` without a
* hoist when there's no `ContentLoading`. Returns `undefined` when the variant
* carries no fallback (a string variant, a live-HAST source, or one whose
* fallbacks were stripped for a `ContentLoading` component) — in which case the
* hoisted copy is used instead.
*/
export function deriveFallbacksFromCode(code, variantName) {
const variant = code?.[variantName];
if (!variant || typeof variant === 'string') {
return undefined;
}
const fallbacks = {};
if (variant.fallback && variant.fileName) {
fallbacks[variant.fileName] = variant.fallback;
}
if (variant.extraFiles) {
for (const [fileName, fileData] of Object.entries(variant.extraFiles)) {
if (typeof fileData === 'object' && fileData.fallback) {
fallbacks[fileName] = fileData.fallback;
}
}
}
return Object.keys(fallbacks).length > 0 ? fallbacks : undefined;
}
/**
* Strip `fallback` entries from a `Code` object and return the
* stripped Code alongside the extracted fallbacks grouped by variant → fileName.
*
* Used on the server to separate the fallback data from the Code
* so Code is sent to CodeHighlighterClient without fallbacks, and
* the data is passed to ContentLoading as source/extraSource props.
*/
export function stripFallbackHastsFromCode(code, variantName, fallbackUsesExtraFiles, fallbackUsesAllVariants) {
if (!code) {
return {
strippedCode: {},
allFallbackHasts: {}
};
}
const allFallbackHasts = {};
const strippedCode = {};
const variantsToProcess = fallbackUsesAllVariants ? Object.keys(code) : [variantName];
const variantsToProcessSet = new Set(variantsToProcess);
for (const [key, variant] of Object.entries(code)) {
if (!variant || typeof variant === 'string' || !variantsToProcessSet.has(key)) {
strippedCode[key] = variant;
continue;
}
const hasts = {};
let changed = false;
// Main file
if (variant.fallback && variant.fileName) {
hasts[variant.fileName] = variant.fallback;
changed = true;
}
// Extra files
let strippedExtraFiles = variant.extraFiles;
if ((fallbackUsesExtraFiles || fallbackUsesAllVariants) && variant.extraFiles) {
const newExtraFiles = {
...variant.extraFiles
};
for (const [fName, fData] of Object.entries(variant.extraFiles)) {
if (typeof fData !== 'string' && fData.fallback) {
hasts[fName] = fData.fallback;
const {
fallback: omittedFallback,
...rest
} = fData;
newExtraFiles[fName] = rest;
changed = true;
}
}
if (changed) {
strippedExtraFiles = newExtraFiles;
}
}
if (changed) {
const {
fallback: omittedFallback,
...restVariant
} = variant;
strippedCode[key] = {
...restVariant,
extraFiles: strippedExtraFiles
};
} else {
strippedCode[key] = variant;
}
if (Object.keys(hasts).length > 0) {
allFallbackHasts[key] = hasts;
}
}
return {
strippedCode,
allFallbackHasts
};
}