[READ-ONLY] a fast, modern browser for the npm registry
at main 145 lines 5.2 kB view raw
1import type { ThemeRegistration } from 'shiki' 2import { createHighlighterCore, type HighlighterCore } from 'shiki/core' 3import { createJavaScriptRegexEngine } from 'shiki/engine/javascript' 4 5let highlighter: HighlighterCore | null = null 6 7function replaceThemeColors( 8 theme: ThemeRegistration, 9 replacements: Record<string, string>, 10): ThemeRegistration { 11 let themeString = JSON.stringify(theme) 12 for (const [oldColor, newColor] of Object.entries(replacements)) { 13 themeString = themeString.replaceAll(oldColor, newColor) 14 themeString = themeString.replaceAll(oldColor.toLowerCase(), newColor) 15 themeString = themeString.replaceAll(oldColor.toUpperCase(), newColor) 16 } 17 return JSON.parse(themeString) 18} 19 20export async function getShikiHighlighter(): Promise<HighlighterCore> { 21 if (!highlighter) { 22 highlighter = await createHighlighterCore({ 23 themes: [ 24 import('@shikijs/themes/github-dark'), 25 import('@shikijs/themes/github-light').then(t => 26 replaceThemeColors(t.default ?? t, { 27 '#22863A': '#227436', // green 28 '#E36209': '#BA4D02', // orange 29 '#D73A49': '#CD3443', // red 30 '#B31D28': '#AC222F', // red 31 }), 32 ), 33 ], 34 langs: [ 35 // Core web languages 36 import('@shikijs/langs/javascript'), 37 import('@shikijs/langs/typescript'), 38 import('@shikijs/langs/json'), 39 import('@shikijs/langs/jsonc'), 40 import('@shikijs/langs/html'), 41 import('@shikijs/langs/css'), 42 import('@shikijs/langs/scss'), 43 import('@shikijs/langs/less'), 44 45 // Frameworks 46 import('@shikijs/langs/vue'), 47 import('@shikijs/langs/jsx'), 48 import('@shikijs/langs/tsx'), 49 import('@shikijs/langs/svelte'), 50 import('@shikijs/langs/astro'), 51 import('@shikijs/langs/glimmer-js'), 52 import('@shikijs/langs/glimmer-ts'), 53 54 // Shell/CLI 55 import('@shikijs/langs/bash'), 56 import('@shikijs/langs/shell'), 57 58 // Config/Data formats 59 import('@shikijs/langs/yaml'), 60 import('@shikijs/langs/toml'), 61 import('@shikijs/langs/xml'), 62 import('@shikijs/langs/markdown'), 63 64 // Other languages 65 import('@shikijs/langs/diff'), 66 import('@shikijs/langs/sql'), 67 import('@shikijs/langs/graphql'), 68 import('@shikijs/langs/python'), 69 import('@shikijs/langs/rust'), 70 import('@shikijs/langs/go'), 71 ], 72 langAlias: { 73 gjs: 'glimmer-js', 74 gts: 'glimmer-ts', 75 }, 76 engine: createJavaScriptRegexEngine(), 77 }) 78 } 79 return highlighter 80} 81 82/** 83 * Synchronously highlight a code block using an already-initialized highlighter. 84 * Use this when you have already awaited getShikiHighlighter() and need to 85 * highlight multiple blocks without async overhead (e.g., in marked renderers). 86 * 87 * @param shiki - The initialized Shiki highlighter instance 88 * @param code - The code to highlight 89 * @param language - The language identifier (e.g., 'typescript', 'bash') 90 * @returns HTML string with syntax highlighting 91 */ 92export function highlightCodeSync(shiki: HighlighterCore, code: string, language: string): string { 93 const loadedLangs = shiki.getLoadedLanguages() 94 95 if (loadedLangs.includes(language as never)) { 96 try { 97 let html = shiki.codeToHtml(code, { 98 lang: language, 99 themes: { light: 'github-light', dark: 'github-dark' }, 100 defaultColor: 'dark', 101 }) 102 // Remove inline style from <pre> tag so CSS can control appearance 103 html = html.replace(/<pre([^>]*) style="[^"]*"/, '<pre$1') 104 // Shiki doesn't encode > in text content (e.g., arrow functions =>) 105 // We need to encode them for HTML validation 106 return escapeRawGt(html) 107 } catch { 108 // Fall back to plain 109 } 110 } 111 112 // Plain code block for unknown languages 113 const escaped = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') 114 return `<pre><code class="language-${language}">${escaped}</code></pre>\n` 115} 116 117/** 118 * Highlight a code block with syntax highlighting (async convenience wrapper). 119 * Initializes the highlighter if needed, then delegates to highlightCodeSync. 120 * 121 * @param code - The code to highlight 122 * @param language - The language identifier (e.g., 'typescript', 'bash') 123 * @returns HTML string with syntax highlighting 124 */ 125export async function highlightCodeBlock(code: string, language: string): Promise<string> { 126 const shiki = await getShikiHighlighter() 127 return highlightCodeSync(shiki, code, language) 128} 129 130/** 131 * Escape raw > characters in HTML text content. 132 * Shiki outputs > without encoding in constructs like arrow functions (=>). 133 * This replaces > that appear in text content (after >) but not inside tags. 134 * 135 * @internal Exported for testing 136 */ 137export function escapeRawGt(html: string): string { 138 // Match > that appears after a closing tag or other > (i.e., in text content) 139 // Pattern: after </...> or after >, match any > that isn't starting a tag 140 return html.replace(/>([^<]*)/g, (match, textContent) => { 141 // Encode any > in the text content portion 142 const escapedText = textContent.replace(/>/g, '&gt;') 143 return `>${escapedText}` 144 }) 145}