-
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathsyntaxHighlighting.ts
More file actions
118 lines (103 loc) · 3.8 KB
/
syntaxHighlighting.ts
File metadata and controls
118 lines (103 loc) · 3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { DiffsHighlighter, getSharedHighlighter, SupportedLanguages } from "@pierre/diffs";
import { LRUCache } from "./lruCache";
import { fnv1a32, resolveDiffThemeName, type DiffThemeName } from "./diffRendering";
const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;
/**
* Map VSCode language identifiers that don't match Shiki's bundled language names.
* VSCode uses e.g. "typescriptreact" / "javascriptreact" while Shiki expects "tsx" / "jsx".
*/
const VSCODE_TO_SHIKI_LANG: Record<string, string> = {
typescriptreact: "tsx",
javascriptreact: "jsx",
};
/** Normalise a language identifier so Shiki can resolve it. */
function normalizeLanguage(language: string): string {
return VSCODE_TO_SHIKI_LANG[language] ?? language;
}
const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;
const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;
const highlightedCodeCache = new LRUCache<string>(
MAX_HIGHLIGHT_CACHE_ENTRIES,
MAX_HIGHLIGHT_CACHE_MEMORY_BYTES,
);
const highlighterPromiseCache = new Map<string, Promise<DiffsHighlighter>>();
export function extractFenceLanguage(className: string | undefined): string {
const match = className?.match(CODE_FENCE_LANGUAGE_REGEX);
const raw = match?.[1] ?? "text";
// Shiki doesn't bundle a gitignore grammar; ini is a close match (#685)
return raw === "gitignore" ? "ini" : raw;
}
export function extractHighlightedCodeInnerHtml(html: string): string {
const match = html.match(/<pre[^>]*>\s*<code[^>]*>([\s\S]*)<\/code>\s*<\/pre>/i);
return match?.[1] ?? html;
}
function estimateHighlightedSize(html: string, code: string): number {
return Math.max(html.length * 2, code.length * 3);
}
function createHighlightCacheKey(
code: string,
language: string,
themeName: DiffThemeName,
scope: string,
): string {
return `${scope}:${code.length}:${fnv1a32(code).toString(36)}:${language}:${themeName}`;
}
export function getCachedHighlightedHtml(
code: string,
language: string,
themeName: DiffThemeName,
scope: string,
): string | null {
return highlightedCodeCache.get(createHighlightCacheKey(code, language, themeName, scope));
}
export function setCachedHighlightedHtml(
code: string,
language: string,
themeName: DiffThemeName,
scope: string,
html: string,
): void {
highlightedCodeCache.set(
createHighlightCacheKey(code, language, themeName, scope),
html,
estimateHighlightedSize(html, code),
);
}
export function getHighlighterPromise(language: string): Promise<DiffsHighlighter> {
const normalized = normalizeLanguage(language);
const cached = highlighterPromiseCache.get(normalized);
if (cached) return cached;
const promise = getSharedHighlighter({
themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")],
langs: [normalized as SupportedLanguages],
preferredHighlighter: "shiki-js",
}).catch((err) => {
highlighterPromiseCache.delete(normalized);
if (normalized === "text") {
// "text" itself failed — Shiki cannot initialize at all, surface the error
throw err;
}
// Language not supported by Shiki — fall back to "text"
return getHighlighterPromise("text");
});
highlighterPromiseCache.set(normalized, promise);
return promise;
}
export function renderHighlightedCodeHtml(
highlighter: DiffsHighlighter,
code: string,
language: string,
themeName: DiffThemeName,
): string {
const normalized = normalizeLanguage(language);
try {
return highlighter.codeToHtml(code, { lang: normalized, theme: themeName });
} catch (error) {
// Log highlighting failures for debugging while falling back to plain text
console.warn(
`Code highlighting failed for language "${normalized}", falling back to plain text.`,
error instanceof Error ? error.message : error,
);
return highlighter.codeToHtml(code, { lang: "text", theme: themeName });
}
}