import { getHighlighter } from 'shiki'; import { visit } from 'unist-util-visit'; import { toString } from 'hast-util-to-string'; import rangeParser2 from 'parse-numeric-range'; import { unified } from 'unified'; import rehypeParse from 'rehype-parse'; // src/index.ts function isJSONTheme(value) { return value ? Object.hasOwn(value, "tokenColors") : false; } function isElement(value) { return value ? value.type === "element" : false; } function isText(value) { return value ? value.type === "text" : false; } function isInlineCode(element, parent) { return element.tagName === "code" && isElement(parent) && parent.tagName !== "pre" || element.tagName === "inlineCode"; } function isBlockCode(element) { return element.tagName === "pre" && Array.isArray(element.children) && element.children.length === 1 && isElement(element.children[0]) && element.children[0].tagName === "code"; } function getInlineCodeLang(meta, defaultFallbackLang) { const placeholder = "\0"; let temp = meta.replace(/\\\\/g, placeholder); temp = temp.replace(/\\({:[a-zA-Z.-]+})$/, "$1"); const lang = temp.match(/{:([a-zA-Z.-]+)}$/)?.[1]; return lang?.replace(new RegExp(placeholder, "g"), "\\") || defaultFallbackLang; } function parseBlockMetaString(element, filter, defaultFallback) { let meta = filter( // @ts-expect-error: TODO handle this element.data?.meta ?? element.properties?.metastring ?? "" ); const titleMatch = meta.match(/title="([^"]*)"/); const title = titleMatch?.[1] ?? null; meta = meta.replace(titleMatch?.[0] ?? "", ""); const captionMatch = meta.match(/caption="([^"]*)"/); const caption = captionMatch?.[1] ?? null; meta = meta.replace(captionMatch?.[0] ?? "", ""); let lang = defaultFallback; if (element.properties && Array.isArray(element.properties.className) && typeof element.properties.className[0] === "string" && element.properties.className[0].startsWith("language-")) { lang = element.properties.className[0].replace("language-", ""); } return { title, caption, lang, meta }; } function getThemeNames(theme) { if (isJSONTheme(theme)) { return [theme.name]; } if (typeof theme === "string") { return [theme]; } return Object.values(theme).map( (theme2) => typeof theme2 === "string" ? theme2 : theme2.name ); } function replaceLineClass(element) { if (Array.isArray(element.properties?.className) && element.properties.className.includes("line")) { const className = element.properties.className.filter((c) => c !== "line"); element.properties.className = className.length > 0 ? className : void 0; element.properties["data-line"] = ""; } } function getLineId(lineNumber, meta) { const segments = meta.match(/\{[^}]+\}#[a-zA-Z0-9]+/g); if (!segments) return null; for (const segment of segments) { const [range, id] = segment.split("#"); if (!range || !id) continue; const match = range.match(/\{(.*?)\}/); const capture = match?.[1]; if (capture && rangeParser2(capture).includes(lineNumber)) { return id; } } return null; } // src/chars/splitElement.ts function splitElement({ elements, elementToWrap, innerString, rightString, leftString, rest, nextElementContinues, index, ignoreChars }) { if (isElement(elementToWrap) && elementToWrap.children?.[0]?.type !== "text" || ignoreChars) { return [elementToWrap, index]; } let newIndex = index; const textElement = elementToWrap.children[0]; if (isText(textElement)) { textElement.value = innerString; } let rightStr = rightString; const leftStr = leftString; if (rest.length > 0) { rightStr += rest.map((s) => s === "" ? innerString : innerString + s).join(""); } if (leftStr.length > 0) { elements.splice(newIndex, 0, { ...elementToWrap, properties: { ...elementToWrap.properties }, children: [{ type: "text", value: leftStr }] }); } if (rightStr.length > 0 && !nextElementContinues) { newIndex = leftStr.length > 0 ? newIndex + 2 : newIndex + 1; elements.splice(newIndex, 0, { ...elementToWrap, properties: { ...elementToWrap.properties }, children: [{ type: "text", value: rightStr }] }); } return [elementToWrap, index + 1]; } function nextElementMaybeContinuesChars({ elements, nextIndex, remainingPart }) { if (remainingPart === "") { return false; } const nextNode = elements[nextIndex]; const content = getContent(nextNode); if (!content) { return false; } const includesNext = content.startsWith(remainingPart) || remainingPart.startsWith(content); const overlap = findOverlap(content, remainingPart); if (overlap === remainingPart && content.startsWith(remainingPart)) { return true; } if (includesNext) { return nextElementMaybeContinuesChars({ elements, nextIndex: nextIndex + 1, remainingPart: remainingPart.replace(content, "") }); } return false; } function getContent(node) { if (!node) return; return toString(node); } function findOverlap(a, b) { if (b.length === 0) { return ""; } if (a.endsWith(b)) { return b; } if (a.indexOf(b) >= 0) { return b; } return findOverlap(a, b.substring(0, b.length - 1)); } function reverseString(s) { return s.split("").reverse().join(""); } // src/chars/getElementsToHighlight.ts function getElementsToHighlight(element, chars, startIndex = 0, ignoreChars = false) { const toWrap = []; let charsSoFar = ""; if (element.children) { const elements = element.children; for (let i = startIndex; i < elements.length; i++) { const remaining = charsSoFar ? chars.replace(charsSoFar, "") : chars; if (remaining === "") { return toWrap; } const maybeElement = elements[i]; if (!maybeElement || maybeElement.type !== "element" || // ignore any previously matched chars within Object.hasOwn( maybeElement.properties ?? {}, "rehype-pretty-code-visited" )) { continue; } const content = getContent(maybeElement) || ""; if (content === chars || charsSoFar + content === chars) { toWrap.push({ element: maybeElement, index: i }); return toWrap; } if (chars.startsWith(charsSoFar + content)) { if (nextElementMaybeContinuesChars({ elements, nextIndex: i + 1, remainingPart: remaining.replace(content, "") })) { toWrap.push({ element: elements[i], index: i }); charsSoFar += content; continue; } } const overlap = findOverlap(content, remaining); const partialMatch = overlap && remaining.startsWith(overlap); if (partialMatch) { const nextPart = remaining.replace(overlap, ""); if (nextPart !== "" && getContent(elements[i + 1]) && !nextElementMaybeContinuesChars({ elements, nextIndex: i + 1, remainingPart: nextPart })) { continue; } const splitParts = content.split(overlap); const [leftPart, rightPart, ...rest] = splitParts; if (rightPart || leftPart || rest.length > 0) { const withNextNode = content + (getContent(elements[i + 1]) ? getContent(elements[i + 1]) : ""); const nextNodeOverlap = findOverlap(withNextNode, remaining); const splitIndex = withNextNode.indexOf(nextNodeOverlap); if (chars.endsWith(overlap) || chars.startsWith(overlap)) { const rightString = rightPart.replace(overlap, ""); const innerString = overlap; const leftString = content.substring(0, splitIndex); const nextElementContinues = nextElementMaybeContinuesChars({ elements, nextIndex: i + 1, remainingPart: nextPart }); const [newElement, updatedIndex] = splitElement({ elements, elementToWrap: elements[i], innerString, rightString, leftString, rest, nextElementContinues, index: i, ignoreChars }); charsSoFar += overlap; toWrap.push({ element: newElement, index: updatedIndex }); } } } } } return toWrap; } // src/chars/wrapHighlightedChars.ts function wrapHighlightedChars(parentElement, elementsToWrap, options, ignoreWord, onVisitHighlightedChars) { if (!elementsToWrap || elementsToWrap.length === 0) { return; } const [{ element }] = elementsToWrap; if (ignoreWord) { if (element.properties) { element.properties["rehype-pretty-code-visited"] = ""; } return; } if (elementsToWrap.length > 1) { parentElement.children.splice( elementsToWrap[0].index, elementsToWrap.length, { type: "element", tagName: "mark", properties: { "data-highlighted-chars-mark": "" }, children: elementsToWrap.map(({ element: element3 }) => element3) } ); const element2 = parentElement.children[elementsToWrap[0].index]; if (!isElement(element2)) { return; } const wordStr = element2.children.reduce((acc, node) => { const textElement = isElement(node) ? node.children[0] : null; if (isText(textElement)) { return acc + textElement.value; } return acc; }, ""); const id = options.idsMap.get(wordStr); element2.properties = element2.properties || {}; element2.properties["data-highlighted-chars"] = ""; element2.properties["data-chars-id"] = id; element2.tagName = "mark"; onVisitHighlightedChars?.(element2, id); } else { const [{ element: element2 }] = elementsToWrap; const textElement = element2.children[0]; if (!isText(textElement)) { return; } const id = options.idsMap.get(textElement.value); element2.properties = element2.properties || {}; element2.properties["rehype-pretty-code-visited"] = ""; element2.properties["data-highlighted-chars"] = ""; element2.properties["data-chars-id"] = id; element2.tagName = "mark"; element2.children = [ { type: "element", tagName: "span", properties: { style: element2.properties.style }, children: element2.children } ]; element2.properties.style = void 0; onVisitHighlightedChars?.(element2, id); } } function charsHighlighter(element, charsList, options, onVisitHighlightedChars) { const { ranges = [] } = options; const textContent = toString(element); charsList.forEach((chars, index) => { if (chars && textContent?.includes(chars)) { let textContent2 = toString(element); let startIndex = 0; while (textContent2.includes(chars)) { const currentCharsRange = ranges[index] || []; const id = `${chars}-${index}`; options.counterMap.set(id, (options.counterMap.get(id) || 0) + 1); const ignoreChars = currentCharsRange.length > 0 && !currentCharsRange.includes(options.counterMap.get(id) ?? -1); const elementsToWrap = getElementsToHighlight( element, chars, startIndex, ignoreChars ); if (elementsToWrap.length === 0) break; wrapHighlightedChars( element, elementsToWrap, options, ignoreChars, onVisitHighlightedChars ); startIndex = Math.max( elementsToWrap[elementsToWrap.length - 1].index - 2, 0 ); textContent2 = element.children.map((childNode) => { const props = isElement(childNode) ? childNode.properties : {}; if (props && !Object.hasOwn(props, "rehype-pretty-code-visited") && !Object.hasOwn(props, "data-highlighted-chars-mark")) { return toString(childNode); } }).join(""); } } }); element.children.forEach((childNode) => { if (!isElement(childNode)) return; if (Object.hasOwn(childNode.properties, "rehype-pretty-code-visited")) { childNode.properties["rehype-pretty-code-visited"] = void 0; } }); } // src/index.ts function apply(element, { tree, lang, title, caption, inline = false, keepBackground = true, grid = true, lineNumbersMaxDigits = 1, theme, onVisitTitle, onVisitCaption }) { element.tagName = inline ? "span" : "figure"; element.properties["data-rehype-pretty-code-figure"] = ""; const codeData = element.children[0]?.data; element.children = [tree].map((tree2) => { const pre = tree2.children[0]; const themeNames = getThemeNames(theme); const themeNamesString = themeNames.join(" "); if (!isElement(pre) || !pre.properties) { return []; } const code = pre.children[0]; if (Array.isArray(pre.properties.className) && pre.properties.className.includes("shiki")) { const className = pre.properties.className.filter( (c) => c !== "shiki" && c !== "shiki-themes" && (typeof c === "string" ? !themeNames.includes(c) : true) ); pre.properties.className = className.length > 0 ? className : void 0; } if (!keepBackground) { pre.properties.style = void 0; } pre.properties["data-language"] = lang; pre.properties["data-theme"] = themeNamesString; if (!isElement(code) || !code.properties) { return []; } code.properties["data-language"] = lang; code.properties["data-theme"] = themeNamesString; code.data = codeData; if (inline) { if (keepBackground) { code.properties.style = pre.properties.style; } return code; } if (grid) { if (code.properties.style) { code.properties.style += "display: grid;"; } else { code.properties.style = "display: grid;"; } } if (Object.hasOwn(code.properties, "data-line-numbers")) { code.properties["data-line-numbers-max-digits"] = lineNumbersMaxDigits.toString().length; } const fragments = []; if (title) { const elementContent = { type: "element", tagName: caption ? "div" : "figcaption", properties: { "data-rehype-pretty-code-title": "", "data-language": lang, "data-theme": themeNamesString }, children: [{ type: "text", value: title }] }; onVisitTitle?.(elementContent); fragments.push(elementContent); } fragments.push(pre); if (caption) { const elementContent = { type: "element", tagName: "figcaption", properties: { "data-rehype-pretty-code-caption": "", "data-language": lang, "data-theme": themeNamesString }, children: [{ type: "text", value: caption }] }; onVisitCaption?.(elementContent); fragments.push(elementContent); } return fragments; }).flatMap((c) => c); } var globalHighlighterCache = /* @__PURE__ */ new WeakMap(); var hastParser = unified().use(rehypeParse, { fragment: true }); function rehypePrettyCode(options = {}) { const { grid = true, theme = "github-dark-dimmed", keepBackground = true, defaultLang = "", tokensMap = {}, filterMetaString = (v) => v, getHighlighter: getHighlighter$1 = getHighlighter, transformers, onVisitLine, onVisitHighlightedLine, onVisitHighlightedChars, onVisitTitle, onVisitCaption } = options; let cachedHighlighter = globalHighlighterCache.get(options); if (!cachedHighlighter) { cachedHighlighter = getHighlighter$1({ themes: isJSONTheme(theme) || typeof theme === "string" ? [theme] : Object.values(theme), langs: ["plaintext"] }); globalHighlighterCache.set(options, cachedHighlighter); } const defaultCodeBlockLang = typeof defaultLang === "string" ? defaultLang : defaultLang.block || ""; const defaultInlineCodeLang = typeof defaultLang === "string" ? defaultLang : defaultLang.inline || ""; function getOptions(lang, meta) { const multipleThemes = !isJSONTheme(theme) && typeof theme === "object" ? theme : null; const singleTheme = isJSONTheme(theme) || typeof theme === "string" ? theme : null; return { lang, meta: { __raw: meta }, transformers, defaultColor: typeof theme === "string" ? theme : false, ...multipleThemes ? { themes: multipleThemes } : { theme: singleTheme } }; } return async (tree) => { const langsToLoad = /* @__PURE__ */ new Set(); const highlighter = await cachedHighlighter; if (!highlighter) return; visit(tree, "element", (element, _, parent) => { if (isInlineCode(element, parent)) { const textElement = element.children[0]; if (!isText(textElement)) return; const value = textElement.value; if (!value) return; const lang = getInlineCodeLang(value, defaultInlineCodeLang); if (lang && lang[0] !== ".") { langsToLoad.add(lang); } } if (isBlockCode(element)) { const codeElement = element.children[0]; if (!isElement(codeElement)) return; const { lang } = parseBlockMetaString( codeElement, filterMetaString, defaultCodeBlockLang ); if (lang) { langsToLoad.add(lang); } } }); try { await Promise.allSettled( Array.from(langsToLoad).map((lang) => { try { return highlighter.loadLanguage( lang ); } catch (e) { return Promise.reject(e); } }) ); } catch (e) { console.error(e); } visit(tree, "element", (element, _, parent) => { if (isInlineCode(element, parent)) { const textElement = element.children[0]; if (!isText(textElement)) return; const value = textElement.value; if (!value) return; const keepLangPart = /\\{:[a-zA-Z.-]+}$/.test(value); const strippedValue = keepLangPart ? value.replace(/\\({:[a-zA-Z.-]+})$/, "$1") : value.replace(/{:[a-zA-Z.-]+}$/, ""); textElement.value = strippedValue; const lang = keepLangPart ? "" : getInlineCodeLang(value, defaultInlineCodeLang); const isLang = lang[0] !== "."; if (!lang) return; let codeTree; if (!isLang) { const themeNames = getThemeNames(theme); const isMultiTheme = typeof theme === "object" && !isJSONTheme(theme); const themeKeys = isMultiTheme ? Object.keys(theme) : null; const colorsByTheme = themeNames.map( (name) => name ? highlighter.getTheme(name).settings.find( ({ scope }) => scope?.includes(tokensMap[lang.slice(1)] ?? lang.slice(1)) )?.settings.foreground ?? "inherit" : "inherit" ); if (isMultiTheme && themeKeys) { codeTree = hastParser.parse( `
${strippedValue}
` ); } else { codeTree = hastParser.parse( `
${strippedValue}
` ); } } else { try { codeTree = hastParser.parse( highlighter.codeToHtml(strippedValue, getOptions(lang)) ); } catch (e) { codeTree = hastParser.parse( highlighter.codeToHtml(strippedValue, getOptions("plaintext")) ); } } visit(codeTree, "element", replaceLineClass); apply(element, { tree: codeTree, lang: isLang ? lang : ".token", inline: true, keepBackground, theme }); } if (isBlockCode(element)) { const codeElement = element.children[0]; if (!isElement(codeElement)) return; const textElement = codeElement.children[0]; const { title, caption, meta, lang } = parseBlockMetaString( codeElement, filterMetaString, defaultCodeBlockLang ); if (!lang || lang === "math") return; const lineNumbers = []; if (meta) { const matches = meta.matchAll(/\{(.*?)\}/g); for (const match of matches) { if (match[1]) { lineNumbers.push(...rangeParser2(match[1])); } } } let lineNumbersMaxDigits = 0; const lineIdMap = /* @__PURE__ */ new Map(); const charsList = []; const charsListNumbers = []; const charsListIdMap = /* @__PURE__ */ new Map(); const charsMatches = meta ? [ ...meta.matchAll( /(?["/])(?.*?)\k(?\S*)/g ) ] : void 0; lineNumbers.forEach((lineNumber) => { const id = getLineId(lineNumber, meta); id && lineIdMap.set(lineNumber, id); }); if (Array.isArray(charsMatches)) { charsMatches.forEach((name) => { const { chars, charsIdAndOrRange } = name.groups; charsList.push(chars); if (charsIdAndOrRange === "") { charsListNumbers.push([]); } else { const [range, id] = charsIdAndOrRange.split("#"); range && charsListNumbers.push(rangeParser2(range)); id && charsListIdMap.set(chars, id); } }); } if (!isText(textElement)) return; const strippedValue = textElement.value.replace(/\n$/, ""); let codeTree; try { codeTree = hastParser.parse( highlighter.codeToHtml(strippedValue, getOptions(lang, meta)) ); } catch (e) { codeTree = hastParser.parse( highlighter.codeToHtml( strippedValue, getOptions("plaintext", meta) ) ); } let lineCounter = 0; const charsHighlighterOptions = { ranges: charsListNumbers, idsMap: charsListIdMap, counterMap: /* @__PURE__ */ new Map() }; visit(codeTree, "element", (element2) => { if (element2.tagName === "code" && /srebmuNeniLwohs(?!(.*)(\/))/.test(reverseString(meta))) { if (element2.properties) { element2.properties["data-line-numbers"] = ""; } const lineNumbersStartAtMatch = reverseString(meta).match( /(?:\}(\d+){)?srebmuNeniLwohs(?!(.*)(\/))/ ); const startNumberString = lineNumbersStartAtMatch?.[1]; if (startNumberString) { const startAt = startNumberString ? Number(reverseString(startNumberString)) - 1 : 0; lineNumbersMaxDigits = startAt; if (element2.properties) { element2.properties.style = `counter-set: line ${startAt};`; } } } if (Array.isArray(element2.properties?.className) && element2.properties?.className?.[0] === "line") { if (grid && toString(element2) === "") { element2.children = [{ type: "text", value: " " }]; } replaceLineClass(element2); onVisitLine?.(element2); lineCounter++; if (lineNumbers.includes(lineCounter)) { element2.properties["data-highlighted-line"] = ""; const lineId = lineIdMap.get(lineCounter); if (lineId) { element2.properties["data-highlighted-line-id"] = lineId; } onVisitHighlightedLine?.(element2, lineId); } charsHighlighter( element2, charsList, charsHighlighterOptions, onVisitHighlightedChars ); lineNumbersMaxDigits++; } }); apply(element, { tree: codeTree, lang, title, caption, keepBackground, grid, lineNumbersMaxDigits, theme, onVisitTitle, onVisitCaption }); } }); }; } export { rehypePrettyCode as default };