Files
2025-07-24 18:46:24 +02:00

110 lines
4.3 KiB
JavaScript

import {
createCssVariablesTheme,
createHighlighter,
isSpecialLang
} from "shiki";
let _cssVariablesTheme;
const cssVariablesTheme = () => _cssVariablesTheme ?? (_cssVariablesTheme = createCssVariablesTheme({
variablePrefix: "--astro-code-"
}));
async function createShikiHighlighter({
langs = [],
theme = "github-dark",
themes = {},
langAlias = {}
} = {}) {
theme = theme === "css-variables" ? cssVariablesTheme() : theme;
const highlighter = await createHighlighter({
langs: ["plaintext", ...langs],
langAlias,
themes: Object.values(themes).length ? Object.values(themes) : [theme]
});
async function highlight(code, lang = "plaintext", options, to) {
const resolvedLang = langAlias[lang] ?? lang;
const loadedLanguages = highlighter.getLoadedLanguages();
if (!isSpecialLang(lang) && !loadedLanguages.includes(resolvedLang)) {
try {
await highlighter.loadLanguage(resolvedLang);
} catch (_err) {
const langStr = lang === resolvedLang ? `"${lang}"` : `"${lang}" (aliased to "${resolvedLang}")`;
console.warn(`[Shiki] The language ${langStr} doesn't exist, falling back to "plaintext".`);
lang = "plaintext";
}
}
code = code.replace(/(?:\r\n|\r|\n)$/, "");
const themeOptions = Object.values(themes).length ? { themes } : { theme };
const inline = options?.inline ?? false;
return highlighter[to === "html" ? "codeToHtml" : "codeToHast"](code, {
...themeOptions,
defaultColor: options.defaultColor,
lang,
// NOTE: while we can spread `options.attributes` here so that Shiki can auto-serialize this as rendered
// attributes on the top-level tag, it's not clear whether it is fine to pass all attributes as meta, as
// they're technically not meta, nor parsed from Shiki's `parseMetaString` API.
meta: options?.meta ? { __raw: options?.meta } : void 0,
transformers: [
{
pre(node) {
if (inline) {
node.tagName = "code";
}
const {
class: attributesClass,
style: attributesStyle,
...rest
} = options?.attributes ?? {};
Object.assign(node.properties, rest);
const classValue = (normalizePropAsString(node.properties.class) ?? "") + (attributesClass ? ` ${attributesClass}` : "");
const styleValue = (normalizePropAsString(node.properties.style) ?? "") + (attributesStyle ? `; ${attributesStyle}` : "");
node.properties.class = classValue.replace(/shiki/g, "astro-code");
node.properties.dataLanguage = lang;
if (options.wrap === false || options.wrap === void 0) {
node.properties.style = styleValue + "; overflow-x: auto;";
} else if (options.wrap === true) {
node.properties.style = styleValue + "; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;";
}
},
line(node) {
if (resolvedLang === "diff") {
const innerSpanNode = node.children[0];
const innerSpanTextNode = innerSpanNode?.type === "element" && innerSpanNode.children?.[0];
if (innerSpanTextNode && innerSpanTextNode.type === "text") {
const start = innerSpanTextNode.value[0];
if (start === "+" || start === "-") {
innerSpanTextNode.value = innerSpanTextNode.value.slice(1);
innerSpanNode.children.unshift({
type: "element",
tagName: "span",
properties: { style: "user-select: none;" },
children: [{ type: "text", value: start }]
});
}
}
}
},
code(node) {
if (inline) {
return node.children[0];
}
}
},
...options.transformers ?? []
]
});
}
return {
codeToHast(code, lang, options = {}) {
return highlight(code, lang, options, "hast");
},
codeToHtml(code, lang, options = {}) {
return highlight(code, lang, options, "html");
}
};
}
function normalizePropAsString(value) {
return Array.isArray(value) ? value.join(" ") : value;
}
export {
createShikiHighlighter
};