|
|
@@ -18,21 +18,26 @@ import langBash from '@shikijs/langs/shellscript'
|
|
|
import langJson from '@shikijs/langs/json'
|
|
|
import type { HighlighterCore } from 'shiki/core'
|
|
|
|
|
|
-/** Language ids (and aliases) the singleton registers; everything else renders plain. */
|
|
|
-const LANG_ALIASES: Record<string, string> = {
|
|
|
- typescript: 'typescript',
|
|
|
- ts: 'typescript',
|
|
|
- tsx: 'typescript',
|
|
|
- javascript: 'typescript',
|
|
|
- js: 'typescript',
|
|
|
- shellscript: 'shellscript',
|
|
|
- bash: 'shellscript',
|
|
|
- sh: 'shellscript',
|
|
|
- shell: 'shellscript',
|
|
|
- zsh: 'shellscript',
|
|
|
- json: 'json',
|
|
|
- jsonc: 'json',
|
|
|
-}
|
|
|
+/**
|
|
|
+ * Language ids (and aliases) the singleton registers; everything else renders
|
|
|
+ * plain. A Map, not an object: fence info strings are assistant-authored, so
|
|
|
+ * a label like `constructor` or `__proto__` must miss instead of resolving an
|
|
|
+ * inherited property and crashing the renderer inside shiki.
|
|
|
+ */
|
|
|
+const LANG_ALIASES = new Map<string, string>([
|
|
|
+ ['typescript', 'typescript'],
|
|
|
+ ['ts', 'typescript'],
|
|
|
+ ['tsx', 'typescript'],
|
|
|
+ ['javascript', 'typescript'],
|
|
|
+ ['js', 'typescript'],
|
|
|
+ ['shellscript', 'shellscript'],
|
|
|
+ ['bash', 'shellscript'],
|
|
|
+ ['sh', 'shellscript'],
|
|
|
+ ['shell', 'shellscript'],
|
|
|
+ ['zsh', 'shellscript'],
|
|
|
+ ['json', 'json'],
|
|
|
+ ['jsonc', 'json'],
|
|
|
+])
|
|
|
|
|
|
/** All token colors resolve through `--shiki-*` custom properties (theme package sheets). */
|
|
|
const cssVariablesTheme = createCssVariablesTheme({
|
|
|
@@ -43,7 +48,7 @@ const cssVariablesTheme = createCssVariablesTheme({
|
|
|
|
|
|
let singleton: HighlighterCore | undefined
|
|
|
|
|
|
-/** The lazily-created synchronous highlighter (one instance per document). */
|
|
|
+/** The synchronous highlighter (one instance per document); pre-warmed below, lazy as the fallback. */
|
|
|
function highlighter(): HighlighterCore {
|
|
|
singleton ??= createHighlighterCoreSync({
|
|
|
themes: [cssVariablesTheme],
|
|
|
@@ -53,6 +58,15 @@ function highlighter(): HighlighterCore {
|
|
|
return singleton
|
|
|
}
|
|
|
|
|
|
+// Engine + grammar construction costs a long task (~120-175ms); building it
|
|
|
+// during the first finalized fence's render would jank exactly when a stream
|
|
|
+// completes. Warm the singleton in a deferred task at module load (= plugin
|
|
|
+// boot) instead; the lazy path above stays as the correctness fallback for a
|
|
|
+// fence that renders before the timer fires. `unref` (Node-only) keeps a
|
|
|
+// non-browser import from pinning the event loop.
|
|
|
+const warmupTimer = setTimeout(() => { highlighter() }, 0)
|
|
|
+;(warmupTimer as { unref?: () => void }).unref?.()
|
|
|
+
|
|
|
/**
|
|
|
* Highlight `code` into shiki's HTML (a single `<pre class="shiki">` tree)
|
|
|
* when `lang` maps to a registered grammar; `undefined` means the caller
|
|
|
@@ -62,7 +76,7 @@ function highlighter(): HighlighterCore {
|
|
|
* @returns the highlighted HTML, or `undefined` for unknown languages.
|
|
|
*/
|
|
|
export function highlightToHtml(code: string, lang: string | undefined): string | undefined {
|
|
|
- const resolved = lang === undefined ? undefined : LANG_ALIASES[lang.toLowerCase()]
|
|
|
+ const resolved = lang === undefined ? undefined : LANG_ALIASES.get(lang.toLowerCase())
|
|
|
if (resolved === undefined) return undefined
|
|
|
return highlighter().codeToHtml(code, { lang: resolved, theme: 'css-variables' })
|
|
|
}
|