// 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