返回 DeepSeek-Reasonix
highlight.ts
根目录 / desktop / frontend / src / lib / highlight.ts
1 // Syntax highlighting via highlight.js core with a hand-picked language set
2 // (registering only what a coding agent surfaces keeps the bundle lean). This is
3 // the engine behind the editor seam's HljsCode / HljsDiff; token colors are
4 // themed in styles.css (.hljs-*) to match the app palette rather than a stock CSS.
5
6 import hljs from "highlight.js/lib/core";
7 import apache from "highlight.js/lib/languages/apache";
8 import bash from "highlight.js/lib/languages/bash";
9 import c from "highlight.js/lib/languages/c";
10 import clojure from "highlight.js/lib/languages/clojure";
11 import cmake from "highlight.js/lib/languages/cmake";
12 import coffeescript from "highlight.js/lib/languages/coffeescript";
13 import cpp from "highlight.js/lib/languages/cpp";
14 import csharp from "highlight.js/lib/languages/csharp";
15 import css from "highlight.js/lib/languages/css";
16 import dart from "highlight.js/lib/languages/dart";
17 import diff from "highlight.js/lib/languages/diff";
18 import dockerfile from "highlight.js/lib/languages/dockerfile";
19 import elixir from "highlight.js/lib/languages/elixir";
20 import erlang from "highlight.js/lib/languages/erlang";
21 import fsharp from "highlight.js/lib/languages/fsharp";
22 import go from "highlight.js/lib/languages/go";
23 import gradle from "highlight.js/lib/languages/gradle";
24 import graphql from "highlight.js/lib/languages/graphql";
25 import groovy from "highlight.js/lib/languages/groovy";
26 import haskell from "highlight.js/lib/languages/haskell";
27 import ini from "highlight.js/lib/languages/ini";
28 import java from "highlight.js/lib/languages/java";
29 import javascript from "highlight.js/lib/languages/javascript";
30 import json from "highlight.js/lib/languages/json";
31 import julia from "highlight.js/lib/languages/julia";
32 import kotlin from "highlight.js/lib/languages/kotlin";
33 import latex from "highlight.js/lib/languages/latex";
34 import less from "highlight.js/lib/languages/less";
35 import lua from "highlight.js/lib/languages/lua";
36 import makefile from "highlight.js/lib/languages/makefile";
37 import markdown from "highlight.js/lib/languages/markdown";
38 import matlab from "highlight.js/lib/languages/matlab";
39 import nginx from "highlight.js/lib/languages/nginx";
40 import objectivec from "highlight.js/lib/languages/objectivec";
41 import perl from "highlight.js/lib/languages/perl";
42 import php from "highlight.js/lib/languages/php";
43 import powershell from "highlight.js/lib/languages/powershell";
44 import properties from "highlight.js/lib/languages/properties";
45 import protobuf from "highlight.js/lib/languages/protobuf";
46 import python from "highlight.js/lib/languages/python";
47 import r from "highlight.js/lib/languages/r";
48 import ruby from "highlight.js/lib/languages/ruby";
49 import rust from "highlight.js/lib/languages/rust";
50 import scala from "highlight.js/lib/languages/scala";
51 import scss from "highlight.js/lib/languages/scss";
52 import sql from "highlight.js/lib/languages/sql";
53 import swift from "highlight.js/lib/languages/swift";
54 import typescript from "highlight.js/lib/languages/typescript";
55 import vbnet from "highlight.js/lib/languages/vbnet";
56 import vim from "highlight.js/lib/languages/vim";
57 import xml from "highlight.js/lib/languages/xml";
58 import yaml from "highlight.js/lib/languages/yaml";
59
60 import { ALIASES } from "./lang";
61
62 hljs.registerLanguage("apache", apache);
63 hljs.registerLanguage("bash", bash);
64 hljs.registerLanguage("c", c);
65 hljs.registerLanguage("clojure", clojure);
66 hljs.registerLanguage("cmake", cmake);
67 hljs.registerLanguage("coffeescript", coffeescript);
68 hljs.registerLanguage("cpp", cpp);
69 hljs.registerLanguage("csharp", csharp);
70 hljs.registerLanguage("css", css);
71 hljs.registerLanguage("dart", dart);
72 hljs.registerLanguage("diff", diff);
73 hljs.registerLanguage("dockerfile", dockerfile);
74 hljs.registerLanguage("elixir", elixir);
75 hljs.registerLanguage("erlang", erlang);
76 hljs.registerLanguage("fsharp", fsharp);
77 hljs.registerLanguage("go", go);
78 hljs.registerLanguage("gradle", gradle);
79 hljs.registerLanguage("graphql", graphql);
80 hljs.registerLanguage("groovy", groovy);
81 hljs.registerLanguage("haskell", haskell);
82 hljs.registerLanguage("ini", ini);
83 hljs.registerLanguage("java", java);
84 hljs.registerLanguage("javascript", javascript);
85 hljs.registerLanguage("json", json);
86 hljs.registerLanguage("julia", julia);
87 hljs.registerLanguage("kotlin", kotlin);
88 hljs.registerLanguage("latex", latex);
89 hljs.registerLanguage("less", less);
90 hljs.registerLanguage("lua", lua);
91 hljs.registerLanguage("makefile", makefile);
92 hljs.registerLanguage("markdown", markdown);
93 hljs.registerLanguage("matlab", matlab);
94 hljs.registerLanguage("nginx", nginx);
95 hljs.registerLanguage("objectivec", objectivec);
96 hljs.registerLanguage("perl", perl);
97 hljs.registerLanguage("php", php);
98 hljs.registerLanguage("powershell", powershell);
99 hljs.registerLanguage("properties", properties);
100 hljs.registerLanguage("protobuf", protobuf);
101 hljs.registerLanguage("python", python);
102 hljs.registerLanguage("r", r);
103 hljs.registerLanguage("ruby", ruby);
104 hljs.registerLanguage("rust", rust);
105 hljs.registerLanguage("scala", scala);
106 hljs.registerLanguage("scss", scss);
107 hljs.registerLanguage("sql", sql);
108 hljs.registerLanguage("swift", swift);
109 hljs.registerLanguage("typescript", typescript);
110 hljs.registerLanguage("vbnet", vbnet);
111 hljs.registerLanguage("vim", vim);
112 hljs.registerLanguage("xml", xml);
113 hljs.registerLanguage("yaml", yaml);
114
115 export function escapeHtml(s: string): string {
116 return s.replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"));
117 }
118
119 export const MAX_HIGHLIGHT_BYTES = 512 * 1024;
120 export const MAX_HIGHLIGHT_LINES = 20_000;
121 // Above this size a highlightable block renders as full plain text first and
122 // swaps in highlighted HTML from an idle callback — a several-hundred-KB
123 // highlight.js walk must not block the frame that mounts the block.
124 export const IDLE_HIGHLIGHT_MIN_BYTES = 32 * 1024;
125
126 export function shouldHighlightCode(sourceSize: number, lineCount: number): boolean {
127 return sourceSize <= MAX_HIGHLIGHT_BYTES && lineCount <= MAX_HIGHLIGHT_LINES;
128 }
129
130 // Apply one shared syntax-highlighting budget to chat blocks, tool output, and
131 // workspace previews. Callers with an authoritative byte size or an existing
132 // line split can pass those values to avoid duplicate work; ordinary chat
133 // blocks are measured here without allocating another copy of the source.
134 export function shouldHighlightSource(
135 source: string,
136 sourceSize?: number,
137 lineCount?: number,
138 ): boolean {
139 if (sourceSize != null && sourceSize > MAX_HIGHLIGHT_BYTES) return false;
140 if (lineCount != null && lineCount > MAX_HIGHLIGHT_LINES) return false;
141 if (sourceSize != null && lineCount != null) return true;
142
143 let measuredBytes = sourceSize ?? 0;
144 let measuredLines = lineCount ?? 1;
145 for (let i = 0; i < source.length; i += 1) {
146 const codeUnit = source.charCodeAt(i);
147 if (lineCount == null && codeUnit === 10) {
148 measuredLines += 1;
149 if (measuredLines > MAX_HIGHLIGHT_LINES) return false;
150 }
151 if (sourceSize != null) continue;
152
153 if (codeUnit < 0x80) {
154 measuredBytes += 1;
155 } else if (codeUnit < 0x800) {
156 measuredBytes += 2;
157 } else if (
158 codeUnit >= 0xd800
159 && codeUnit <= 0xdbff
160 && i + 1 < source.length
161 && source.charCodeAt(i + 1) >= 0xdc00
162 && source.charCodeAt(i + 1) <= 0xdfff
163 ) {
164 measuredBytes += 4;
165 i += 1;
166 } else {
167 measuredBytes += 3;
168 }
169 if (measuredBytes > MAX_HIGHLIGHT_BYTES) return false;
170 }
171 return true;
172 }
173
174 // resolveLang maps a markdown fence tag or guessed name to a registered language,
175 // or "" when we can't highlight it (caller renders escaped plain text).
176 export function resolveLang(lang?: string): string {
177 if (!lang) return "";
178 const l = lang.toLowerCase();
179 const resolved = ALIASES[l] ?? l;
180 return hljs.getLanguage(resolved) ? resolved : "";
181 }
182
183 // LRU cache for highlighted output. The same code block can re-render many
184 // times (Re-renders due to streaming updates that don't change this block,
185 // React's StrictMode double-invoke in dev, hover-reveals of the toolbar's
186 // child elements). highlight.highlight() is a real lexer walk that shows
187 // up in the profile for large blocks; a 200-entry LRU keyed on the
188 // resolved language plus a fast hash of the code keeps the steady-state
189 // cost at a Map.get(). The Map is a plain LRU, not a WeakMap, so a large
190 // (non-streaming) transcript will eventually evict; the size of 200 is
191 // chosen to cover the visible viewport plus a small overshoot — most
192 // transcripts re-render the same ~30 visible blocks.
193 const HL_CACHE_MAX = 200;
194
195 // djb2 hash for the cache key. We only use the hash inside the key
196 // (Map<number, string>), not as a security primitive; collisions are fine
197 // because we ALSO store the original code alongside the entry and verify
198 // before serving the cached value. The hash is what makes the key
199 // constant-time to compare for Map.get(); comparing the full source
200 // string would be O(n) on every render and dwarf the savings.
201 function hashCode(s: string): number {
202 let h = 5381;
203 for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
204 return h;
205 }
206
207 interface CacheEntry {
208 code: string;
209 html: string;
210 }
211 const hlCache = new Map<number, CacheEntry>();
212
213 function cacheGet(code: string, lang: string): string | null {
214 const key = hashCode(lang + "\0" + code);
215 const e = hlCache.get(key);
216 if (!e) return null;
217 // Defend against the (rare) hash collision: the stored code must match
218 // the queried code exactly. We move the entry to the end of the Map to
219 // mark it most-recently-used.
220 if (e.code !== code) return null;
221 hlCache.delete(key);
222 hlCache.set(key, e);
223 return e.html;
224 }
225
226 function cachePut(code: string, lang: string, html: string): void {
227 const key = hashCode(lang + "\0" + code);
228 if (hlCache.has(key)) hlCache.delete(key); // refresh
229 hlCache.set(key, { code, html });
230 while (hlCache.size > HL_CACHE_MAX) {
231 // Map iteration is insertion-order; the first key is the oldest.
232 const oldest = hlCache.keys().next().value;
233 if (oldest === undefined) break;
234 hlCache.delete(oldest);
235 }
236 }
237
238 // highlightToHtml returns highlighted HTML (token <span>s) for the given code, or
239 // escaped plain text when the language is unknown. ignoreIllegals so partial /
240 // streaming snippets never throw. The LRU cache shaves the bulk of the work
241 // when a transcript re-renders the same blocks (most common: a streaming
242 // update changes the *next* block, not this one).
243 export function highlightToHtml(code: string, lang?: string): string {
244 const resolved = resolveLang(lang);
245 if (!resolved) return escapeHtml(code);
246 const cached = cacheGet(code, resolved);
247 if (cached !== null) return cached;
248 let html: string;
249 try {
250 html = hljs.highlight(code, { language: resolved, ignoreIllegals: true }).value;
251 } catch {
252 return escapeHtml(code);
253 }
254 cachePut(code, resolved, html);
255 return html;
256 }
257
257 lines TYPESCRIPT