site/node_modules/rehype-pretty-code/dist/index.js

738 lines
24 KiB
JavaScript
Raw Permalink Normal View History

2024-10-14 06:09:33 +00:00
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(
`<pre><code><span style="${themeKeys.map((key, i) => `--shiki-${key}:${colorsByTheme[i]}`).join(";")}">${strippedValue}</span></code></pre>`
);
} else {
codeTree = hastParser.parse(
`<pre><code><span style="color:${colorsByTheme[0]}">${strippedValue}</span></code></pre>`
);
}
} 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(
/(?<delimiter>["/])(?<chars>.*?)\k<delimiter>(?<charsIdAndOrRange>\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 };