mirror of
https://github.com/sern-handler/website
synced 2026-06-11 10:22:20 +00:00
192 lines
7.5 KiB
JavaScript
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
|