@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
154 lines (137 loc) • 6.22 kB
JavaScript
import { createStarryNight } from '@wooorm/starry-night';
import { visit } from 'unist-util-visit';
import { grammars } from "../parseSource/grammars.mjs";
import { extensionMap } from "../parseSource/grammarMaps.mjs";
import { extendSyntaxTokens } from "../parseSource/extendSyntaxTokens.mjs";
import { getLanguageCapabilitiesFromScope } from "../parseSource/languageCapabilities.mjs";
import { getHastTextContent } from "../hastUtils/index.mjs";
import { removePrefixFromHighlightedNodes } from "./removePrefixFromHighlightedNodes.mjs";
import { removeSuffixFromHighlightedNodes } from "./removeSuffixFromHighlightedNodes.mjs";
const STARRY_NIGHT_KEY = '__docs_infra_starry_night_instance__';
/**
* Options for the transformHtmlCodeInline plugin.
*/
/**
* Ensures Starry Night is initialized and returns the instance.
* Uses a global singleton for efficiency across multiple plugin invocations.
*/
async function getStarryNight() {
if (!globalThis[STARRY_NIGHT_KEY]) {
globalThis[STARRY_NIGHT_KEY] = await createStarryNight(grammars);
}
return globalThis[STARRY_NIGHT_KEY];
}
/**
* A rehype plugin that applies inline syntax highlighting to code elements.
* Unlike transformHtmlCodeBlock, this does NOT add line gutters or precomputed data.
* It's meant for inline code snippets that should be highlighted but remain lightweight.
*
* Processes code elements and replaces their text content with syntax-highlighted HAST nodes.
*
* @param options - Configuration options for the plugin
* @returns A unified transformer function
*/
export default function transformHtmlCodeInline(options = {}) {
const {
includePreElements = false
} = options;
return async tree => {
const starryNight = await getStarryNight();
visit(tree, 'element', (node, _index, parent) => {
// Only process code elements (inline code or code blocks without special handling)
if (node.tagName !== 'code') {
return;
}
// Check if this is inside a pre element
const isInsidePre = parent && parent.type === 'element' && parent.tagName === 'pre';
// Skip if this is inside a pre element (unless includePreElements is enabled)
if (isInsidePre && !includePreElements) {
return;
}
// Skip if it has no children
if (!node.children || node.children.length === 0) {
return;
}
// Extract all text content from children (handles multiple text nodes and newlines)
const source = node.children.map(child => getHastTextContent(child)).join('');
if (!source) {
return;
}
// Check if there's a highlighting prefix in the data attributes
const highlightingPrefix = typeof node.properties?.dataHighlightingPrefix === 'string' ? node.properties.dataHighlightingPrefix : undefined;
// Temporarily prepend the prefix for proper syntax highlighting
let sourceToHighlight = highlightingPrefix ? `${highlightingPrefix}${source}` : source;
// Inline JS-family snippets that look like a bare object literal (e.g. `{ height: 400 }`)
// are tokenized by starry-night as a block statement with labeled statements, which makes
// the keys appear as `pl-en` (entity name) tokens rather than property names. Wrap them
// in `(...)` so the snippet parses as an expression and `extendSyntaxTokens` can split
// out the keys via `splitObjectKeys`. The wrapping characters are stripped after highlighting.
let objectWrap = false;
// Determine language from className (e.g., 'language-ts')
const className = node.properties?.className;
let fileType;
if (Array.isArray(className)) {
const langClass = className.find(c => typeof c === 'string' && c.startsWith('language-'));
if (langClass && typeof langClass === 'string') {
const lang = langClass.replace('language-', '');
// Map common language names to file extensions
const langToExt = {
ts: '.ts',
typescript: '.ts',
js: '.js',
javascript: '.js',
jsx: '.jsx',
tsx: '.tsx',
css: '.css',
html: '.html',
json: '.json',
md: '.md',
markdown: '.md',
sh: '.sh',
shell: '.sh',
bash: '.sh',
yaml: '.yaml',
yml: '.yaml'
};
fileType = langToExt[lang] || `.${lang}`;
}
}
// Skip if no language specified or unsupported type
if (!fileType || !extensionMap[fileType]) {
return;
}
const grammarScope = extensionMap[fileType];
if (!highlightingPrefix && getLanguageCapabilitiesFromScope(grammarScope).semantics === 'js') {
const trimmed = sourceToHighlight.trim();
if (trimmed.length >= 2 && trimmed.startsWith('{') && trimmed.endsWith('}')) {
sourceToHighlight = `(${sourceToHighlight})`;
objectWrap = true;
}
}
// Apply syntax highlighting
const highlighted = starryNight.highlight(sourceToHighlight, extensionMap[fileType]);
extendSyntaxTokens(highlighted, extensionMap[fileType]);
// Replace the code element's children with the highlighted nodes
if (highlighted.type === 'root' && highlighted.children) {
node.children = highlighted.children;
// If we added a prefix for highlighting, remove it from the output
if (highlightingPrefix && node.children.length > 0) {
removePrefixFromHighlightedNodes(node.children, highlightingPrefix.length);
}
if (objectWrap && node.children.length > 0) {
removePrefixFromHighlightedNodes(node.children, 1);
removeSuffixFromHighlightedNodes(node.children, 1);
}
}
// Mark this code element as inline highlighted (only for inline code, not pre>code)
if (!isInsidePre) {
node.properties = node.properties || {};
node.properties.dataInline = '';
}
// Remove the dataHighlightingPrefix property after processing
if (node.properties?.dataHighlightingPrefix) {
delete node.properties.dataHighlightingPrefix;
}
});
};
}