2022-02-22 16:46:04 +00:00
|
|
|
import type * as shiki from 'shiki';
|
|
|
|
import { getHighlighter } from 'shiki';
|
2022-01-31 22:14:07 +00:00
|
|
|
import { visit } from 'unist-util-visit';
|
2022-04-11 23:01:12 +00:00
|
|
|
import type { ShikiConfig } from './types.js';
|
2022-02-07 16:31:02 +00:00
|
|
|
|
2022-03-02 22:09:18 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
2022-03-24 16:49:54 +00:00
|
|
|
* We make this async, so that multiple calls to parse markdown still share the same highlighter.
|
2022-03-02 22:09:18 +00:00
|
|
|
*/
|
2022-03-24 16:49:54 +00:00
|
|
|
const highlighterCacheAsync = new Map<string, Promise<shiki.Highlighter>>();
|
2022-01-31 22:14:07 +00:00
|
|
|
|
2022-04-02 20:15:41 +00:00
|
|
|
const remarkShiki = async (
|
2022-04-11 23:01:12 +00:00
|
|
|
{ langs, theme, wrap }: ShikiConfig,
|
2022-04-02 20:15:41 +00:00
|
|
|
scopedClassName?: string | null
|
|
|
|
) => {
|
2022-03-02 22:09:18 +00:00
|
|
|
const cacheID: string = typeof theme === 'string' ? theme : theme.name;
|
2022-03-24 16:49:54 +00:00
|
|
|
let highlighterAsync = highlighterCacheAsync.get(cacheID);
|
|
|
|
if (!highlighterAsync) {
|
|
|
|
highlighterAsync = getHighlighter({ theme });
|
|
|
|
highlighterCacheAsync.set(cacheID, highlighterAsync);
|
2022-03-02 22:09:18 +00:00
|
|
|
}
|
2022-03-24 16:49:54 +00:00
|
|
|
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.
|
2022-02-07 16:31:02 +00:00
|
|
|
for (const lang of langs) {
|
|
|
|
await highlighter.loadLanguage(lang);
|
|
|
|
}
|
2022-03-24 16:49:54 +00:00
|
|
|
|
2022-01-31 22:14:07 +00:00
|
|
|
return () => (tree: any) => {
|
|
|
|
visit(tree, 'code', (node) => {
|
2022-03-02 22:09:18 +00:00
|
|
|
let html = highlighter!.codeToHtml(node.value, { lang: node.lang ?? 'plaintext' });
|
2022-01-31 22:14:07 +00:00
|
|
|
|
2022-03-18 21:29:51 +00:00
|
|
|
// 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 `<span class="line"` into a code block,
|
|
|
|
// It would become this before hitting our regexes:
|
|
|
|
// <span class="line"
|
|
|
|
|
2022-03-03 17:34:36 +00:00
|
|
|
// Replace "shiki" class naming with "astro" and add "is:raw".
|
2022-04-02 20:15:41 +00:00
|
|
|
html = html.replace(
|
|
|
|
'<pre class="shiki"',
|
|
|
|
`<pre is:raw class="astro-code${scopedClassName ? ' ' + scopedClassName : ''}"`
|
|
|
|
);
|
2022-01-31 22:14:07 +00:00
|
|
|
// Replace "shiki" css variable naming with "astro".
|
2022-04-02 20:15:41 +00:00
|
|
|
html = html.replace(
|
|
|
|
/style="(background-)?color: var\(--shiki-/g,
|
|
|
|
'style="$1color: var(--astro-code-'
|
|
|
|
);
|
2022-02-07 16:31:02 +00:00
|
|
|
// Handle code wrapping
|
|
|
|
// if wrap=null, do nothing.
|
|
|
|
if (wrap === false) {
|
|
|
|
html = html.replace(/style="(.*?)"/, 'style="$1; overflow-x: auto;"');
|
|
|
|
} else if (wrap === true) {
|
2022-04-02 20:15:41 +00:00
|
|
|
html = html.replace(
|
|
|
|
/style="(.*?)"/,
|
|
|
|
'style="$1; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"'
|
|
|
|
);
|
2022-02-07 16:31:02 +00:00
|
|
|
}
|
2022-01-31 22:14:07 +00:00
|
|
|
|
2022-03-18 21:29:51 +00:00
|
|
|
// Apply scopedClassName to all nested lines
|
|
|
|
if (scopedClassName) {
|
|
|
|
html = html.replace(/\<span class="line"\>/g, `<span class="line ${scopedClassName}"`);
|
|
|
|
}
|
|
|
|
|
2022-01-31 22:14:07 +00:00
|
|
|
node.type = 'html';
|
|
|
|
node.value = html;
|
|
|
|
node.children = [];
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export default remarkShiki;
|