738 lines
24 KiB
JavaScript
738 lines
24 KiB
JavaScript
|
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 };
|