UNPKG

@mui/internal-docs-infra

Version:

MUI Infra - internal documentation creation tools.

154 lines (137 loc) 6.22 kB
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; } }); }; }