Files
website/node_modules/@expressive-code/plugin-shiki/dist/index.js
2024-05-06 17:15:30 -04:00

192 lines
7.5 KiB
JavaScript

// src/index.ts
import { ExpressiveCodeTheme as ExpressiveCodeTheme2, InlineStyleAnnotation } from "@expressive-code/core";
// src/highlighter.ts
import { getHighlighter, isSpecialLang, bundledLanguages } from "shiki";
import { getStableObjectHash } from "@expressive-code/core";
var highlighterPromiseByConfig = /* @__PURE__ */ new Map();
var promisesByHighlighter = /* @__PURE__ */ new WeakMap();
var themeCacheKeys = /* @__PURE__ */ new WeakMap();
async function getCachedHighlighter(config = {}) {
const configCacheKey = getStableObjectHash(config);
let highlighterPromise = highlighterPromiseByConfig.get(configCacheKey);
if (highlighterPromise === void 0) {
const langs = [];
if (config.langs?.length) {
langs.push(...config.langs);
}
langs.push(...Object.keys(bundledLanguages));
highlighterPromise = getHighlighter({
themes: [],
langs
});
highlighterPromiseByConfig.set(configCacheKey, highlighterPromise);
}
return highlighterPromise;
}
async function ensureThemeIsLoaded(highlighter, theme) {
const existingCacheKey = themeCacheKeys.get(theme);
const cacheKey = existingCacheKey ?? `${theme.name}-${getStableObjectHash({ bg: theme.bg, fg: theme.fg, settings: theme.settings })}`;
if (!existingCacheKey)
themeCacheKeys.set(theme, cacheKey);
if (!highlighter.getLoadedThemes().includes(cacheKey)) {
await memoizeHighlighterTask(highlighter, `loadTheme:${cacheKey}`, () => {
const themeUsingCacheKey = { ...theme, name: cacheKey, settings: theme.settings ?? [] };
return highlighter.loadTheme(themeUsingCacheKey);
});
}
return cacheKey;
}
async function ensureLanguageIsLoaded(highlighter, language) {
const loadedLanguages = new Set(highlighter.getLoadedLanguages());
const isLoaded = loadedLanguages.has(language);
const isSpecial = isSpecialLang(language);
const isBundled = Object.keys(bundledLanguages).includes(language);
const isAvailable = isLoaded || isSpecial || isBundled;
if (!isAvailable)
return "txt";
if (isLoaded || isSpecial)
return language;
const loadedLanguage = await memoizeHighlighterTask(highlighter, `loadLanguage:${language}`, async () => {
await highlighter.loadLanguage(language);
return language;
});
return loadedLanguage;
}
function memoizeHighlighterTask(highlighter, taskId, taskFn) {
let promises = promisesByHighlighter.get(highlighter);
if (!promises) {
promises = /* @__PURE__ */ new Map();
promisesByHighlighter.set(highlighter, promises);
}
let promise = promises.get(taskId);
if (promise === void 0) {
promise = taskFn();
promises.set(taskId, promise);
}
return promise;
}
// src/index.ts
import { bundledThemes } from "shiki";
async function loadShikiTheme(bundledThemeName) {
const shikiTheme = (await bundledThemes[bundledThemeName]()).default;
return new ExpressiveCodeTheme2(shikiTheme);
}
function pluginShiki(options = {}) {
const { langs } = options;
return {
name: "Shiki",
hooks: {
performSyntaxAnalysis: async ({ codeBlock, styleVariants, config: { logger } }) => {
const codeLines = codeBlock.getLines();
let code = codeBlock.code;
if (isTerminalLanguage(codeBlock.language)) {
code = code.replace(/<([^>]*[^>\s])>/g, "X$1X");
}
let highlighter;
try {
highlighter = await getCachedHighlighter({ langs });
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
throw new Error(`Failed to load syntax highlighter. Please ensure that the configured langs are supported by Shiki.
Received error message: "${error.message}"`, {
cause: error
});
}
const loadedLanguageName = await ensureLanguageIsLoaded(highlighter, codeBlock.language);
if (loadedLanguageName !== codeBlock.language) {
logger.warn(
`Found unknown code block language "${codeBlock.language}" in ${codeBlock.parentDocument?.sourceFilePath ? `document "${codeBlock.parentDocument?.sourceFilePath}"` : "markdown/MDX document"}. Using "${loadedLanguageName}" instead. You can add custom languages using the "langs" config option.`
);
}
for (let styleVariantIndex = 0; styleVariantIndex < styleVariants.length; styleVariantIndex++) {
const theme = styleVariants[styleVariantIndex].theme;
const loadedThemeName = await ensureThemeIsLoaded(highlighter, theme);
let tokenLines;
try {
tokenLines = highlighter.codeToTokensBase(code, {
// @ts-expect-error: We took care that the language is loaded
lang: loadedLanguageName,
// @ts-expect-error: We took care that the theme is loaded
theme: loadedThemeName,
includeExplanation: false
});
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
throw new Error(`Shiki failed to highlight code block with language "${codeBlock.language}" and theme "${theme.name}".
Received error message: "${error.message}"`, {
cause: error
});
}
tokenLines.forEach((line, lineIndex) => {
if (codeBlock.language === "ansi" && styleVariantIndex === 0)
removeAnsiSequencesFromCodeLine(codeLines[lineIndex], line);
let charIndex = 0;
line.forEach((token) => {
const tokenLength = token.content.length;
const tokenEndIndex = charIndex + tokenLength;
const fontStyle = token.fontStyle || 0 /* None */;
codeLines[lineIndex]?.addAnnotation(
new InlineStyleAnnotation({
styleVariantIndex,
color: token.color || theme.fg,
italic: (fontStyle & 1 /* Italic */) === 1 /* Italic */,
bold: (fontStyle & 2 /* Bold */) === 2 /* Bold */,
underline: (fontStyle & 4 /* Underline */) === 4 /* Underline */,
inlineRange: {
columnStart: charIndex,
columnEnd: tokenEndIndex
},
renderPhase: "earliest"
})
);
charIndex = tokenEndIndex;
});
});
}
}
}
};
}
function isTerminalLanguage(language) {
return ["shellscript", "shell", "bash", "sh", "zsh", "nu", "nushell"].includes(language);
}
function removeAnsiSequencesFromCodeLine(codeLine, lineTokens) {
const newLine = lineTokens.map((token) => token.content).join("");
const rangesToRemove = getRemovedRanges(codeLine.text, newLine);
for (let index = rangesToRemove.length - 1; index >= 0; index--) {
const [start, end] = rangesToRemove[index];
codeLine.editText(start, end, "");
}
}
function getRemovedRanges(original, edited) {
const ranges = [];
let from = -1;
let orgIdx = 0;
let edtIdx = 0;
while (orgIdx < original.length && edtIdx < edited.length) {
if (original[orgIdx] !== edited[edtIdx]) {
if (from === -1)
from = orgIdx;
orgIdx++;
} else {
if (from > -1) {
ranges.push([from, orgIdx]);
from = -1;
}
orgIdx++;
edtIdx++;
}
}
if (edtIdx < edited.length)
throw new Error(`Edited string contains characters not present in original (${JSON.stringify({ original, edited })})`);
if (orgIdx < original.length)
ranges.push([orgIdx, original.length]);
return ranges;
}
export {
loadShikiTheme,
pluginShiki
};
//# sourceMappingURL=index.js.map