@nuxtjs/mdc
Version:
Nuxt MDC module
181 lines (180 loc) • 6.68 kB
JavaScript
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
export function createShikiHighlighter({
langs = [],
themes = [],
bundledLangs = {},
bundledThemes = {},
getMdcConfigs,
options: shikiOptions,
engine
} = {}) {
let shiki;
let configs;
async function _getShiki() {
const { createHighlighterCore, addClassToHast, isSpecialLang, isSpecialTheme } = await import("shiki/core");
const { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight } = await import("@shikijs/transformers");
const shiki2 = await createHighlighterCore({
langs,
themes,
engine: engine || createJavaScriptRegexEngine()
});
for await (const config of await getConfigs()) {
await config.shiki?.setup?.(shiki2);
}
return {
shiki: shiki2,
addClassToHast,
isSpecialLang,
isSpecialTheme,
transformers: [
transformerNotationDiff(),
transformerNotationErrorLevel(),
transformerNotationFocus(),
transformerNotationHighlight()
]
};
}
async function getShiki() {
if (!shiki) {
shiki = _getShiki();
}
return shiki;
}
async function getConfigs() {
if (!configs) {
configs = Promise.resolve(getMdcConfigs?.() || []);
}
return configs;
}
const highlighter = async (code, lang, theme, options = {}) => {
const {
shiki: shiki2,
addClassToHast,
isSpecialLang,
isSpecialTheme,
transformers: baseTransformers
} = await getShiki();
const codeToHastOptions = {
defaultColor: false,
meta: {
__raw: options.meta
}
};
if (lang === "ts-type" || lang === "typescript-type") {
lang = "typescript";
codeToHastOptions.grammarContextCode = "let a:";
} else if (lang === "vue-html" || lang === "vue-template") {
lang = "vue";
codeToHastOptions.grammarContextCode = "<template>";
}
const themesObject = { ...typeof theme === "string" ? { default: theme } : theme || {} };
const loadedThemes = shiki2.getLoadedThemes();
const loadedLanguages = shiki2.getLoadedLanguages();
if (typeof lang === "string" && !loadedLanguages.includes(lang) && !isSpecialLang(lang)) {
if (bundledLangs[lang]) {
await shiki2.loadLanguage(bundledLangs[lang]);
} else {
if (process.dev) {
console.warn(`[@nuxtjs/mdc] Language "${lang}" is not loaded to the Shiki highlighter, fallback to plain text. Add the language to "mdc.highlight.langs" to fix this.`);
}
lang = "text";
}
}
for (const [color, theme2] of Object.entries(themesObject)) {
if (typeof theme2 === "string" && !loadedThemes.includes(theme2) && !isSpecialTheme(theme2)) {
if (bundledThemes[theme2]) {
await shiki2.loadTheme(bundledThemes[theme2]);
} else {
if (process.dev) {
console.warn(`[@nuxtjs/mdc] Theme "${theme2}" is not loaded to the Shiki highlighter. Add the theme to "mdc.highlight.themes" to fix this.`);
}
themesObject[color] = "none";
}
}
}
const transformersMap = /* @__PURE__ */ new Map();
for (const transformer of baseTransformers) {
transformersMap.set(transformer.name || `transformer:${Math.random()}-${transformer.constructor.name}`, transformer);
}
for (const config of await getConfigs()) {
const newTransformers = typeof config.shiki?.transformers === "function" ? await config.shiki?.transformers(code, lang, theme, options) : config.shiki?.transformers || [];
for (const transformer of newTransformers) {
transformersMap.set(transformer.name || `transformer:${Math.random()}-${transformer.constructor.name}`, transformer);
}
}
const root = shiki2.codeToHast(code.trimEnd(), {
lang,
...codeToHastOptions,
themes: themesObject,
transformers: [
...transformersMap.values(),
{
name: "mdc:highlight",
line(node, line) {
if (options.highlights?.includes(line))
addClassToHast(node, "highlight");
node.properties.line = line;
}
},
{
name: "mdc:newline",
line(node) {
if (code?.includes("\n")) {
if (node.children.length === 0 || node.children.length === 1 && node.children[0].type === "element" && node.children[0].children.length === 1 && node.children[0].children[0].type === "text" && node.children[0].children[0].value === "") {
node.children = [{
type: "element",
tagName: "span",
properties: {
emptyLinePlaceholder: true
},
children: [{ type: "text", value: "\n" }]
}];
return;
}
const last = node.children.at(-1);
if (last?.type === "element" && last.tagName === "span") {
const text = last.children.at(-1);
if (text?.type === "text")
text.value += "\n";
}
}
}
}
]
});
const preEl = root.children[0];
const codeEl = preEl.children[0];
const wrapperStyle = shikiOptions?.wrapperStyle;
preEl.properties.style = wrapperStyle ? typeof wrapperStyle === "string" ? wrapperStyle : preEl.properties.style : "";
const styles = [];
Object.keys(themesObject).forEach((color) => {
const colorScheme = color !== "default" ? `.${color}` : "";
styles.push(
wrapperStyle ? `${colorScheme} .shiki,` : "",
`html .${color} .shiki span {`,
`color: var(--shiki-${color});`,
`background: var(--shiki-${color}-bg);`,
`font-style: var(--shiki-${color}-font-style);`,
`font-weight: var(--shiki-${color}-font-weight);`,
`text-decoration: var(--shiki-${color}-text-decoration);`,
"}"
);
styles.push(
`html${colorScheme} .shiki span {`,
`color: var(--shiki-${color});`,
`background: var(--shiki-${color}-bg);`,
`font-style: var(--shiki-${color}-font-style);`,
`font-weight: var(--shiki-${color}-font-weight);`,
`text-decoration: var(--shiki-${color}-text-decoration);`,
"}"
);
});
return {
tree: codeEl.children,
className: Array.isArray(preEl.properties.class) ? preEl.properties.class.join(" ") : preEl.properties.class,
inlineStyle: preEl.properties.style,
style: styles.join("")
};
};
return highlighter;
}