import type { ShikiConfig } from 'astro'; import type * as shiki from 'shiki'; import { getHighlighter } from 'shiki'; import { visit } from 'unist-util-visit'; /** * getHighlighter() is the most expensive step of Shiki. Instead of calling it on every page, * cache it here as much as possible. Make sure that your highlighters can be cached, state-free. * We make this async, so that multiple calls to parse markdown still share the same highlighter. */ const highlighterCacheAsync = new Map>(); const remarkShiki = async ({ langs = [], theme = 'github-dark', wrap = false }: ShikiConfig) => { const cacheID: string = typeof theme === 'string' ? theme : theme.name; let highlighterAsync = highlighterCacheAsync.get(cacheID); if (!highlighterAsync) { highlighterAsync = getHighlighter({ theme }); highlighterCacheAsync.set(cacheID, highlighterAsync); } const highlighter = await highlighterAsync; // NOTE: There may be a performance issue here for large sites that use `lang`. // Since this will be called on every page load. Unclear how to fix this. for (const lang of langs) { await highlighter.loadLanguage(lang); } return () => (tree: any) => { visit(tree, 'code', (node) => { let lang: string; if (typeof node.lang === 'string') { const langExists = highlighter.getLoadedLanguages().includes(node.lang); if (langExists) { lang = node.lang; } else { // eslint-disable-next-line no-console console.warn(`The language "${node.lang}" doesn't exist, falling back to plaintext.`); lang = 'plaintext'; } } else { lang = 'plaintext'; } let html = highlighter!.codeToHtml(node.value, { lang }); // Q: Couldn't these regexes match on a user's inputted code blocks? // A: Nope! All rendered HTML is properly escaped. // Ex. If a user typed `([\+|\-])/g, '$2' ); } // Handle code wrapping // if wrap=null, do nothing. if (wrap === false) { html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"'); } else if (wrap === true) { html = html.replace( /style="(.*?)"/, 'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"' ); } node.type = 'html'; node.value = html; node.children = []; }); }; }; export default remarkShiki;