-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[MDX] Switch from Shiki Twoslash -> Astro Markdown highlighter (#4292)
* freat: twoslash -> Astro shiki parser * test: update shiki style check * feat: always apply rehypeRaw * deps: move remark-shiki-twoslash to dev * test: add shiki-twoslash test * docs: update readme with twoslash example * chore: changeset * nit: remove "describe('disabled')"
- Loading branch information
1 parent
3889a7f
commit f1a52c1
Showing
7 changed files
with
156 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@astrojs/mdx': minor | ||
--- | ||
|
||
Switch from Shiki Twoslash to Astro's Shiki Markdown highlighter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
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<string, Promise<shiki.Highlighter>>(); | ||
|
||
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 `<span class="line"` into a code block, | ||
// It would become this before hitting our regexes: | ||
// <span class="line" | ||
|
||
// Replace "shiki" class naming with "astro". | ||
html = html.replace('<pre class="shiki"', `<pre class="astro-code"`); | ||
// Replace "shiki" css variable naming with "astro". | ||
html = html.replace( | ||
/style="(background-)?color: var\(--shiki-/g, | ||
'style="$1color: var(--astro-code-' | ||
); | ||
// Add "user-select: none;" for "+"/"-" diff symbols | ||
if (node.lang === 'diff') { | ||
html = html.replace( | ||
/<span class="line"><span style="(.*?)">([\+|\-])/g, | ||
'<span class="line"><span style="$1"><span style="user-select: none;">$2</span>' | ||
); | ||
} | ||
// 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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.