返回 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 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
122 export function shouldHighlightCode(sourceSize: number, lineCount: number): boolean {
123 return sourceSize <= MAX_HIGHLIGHT_BYTES && lineCount <= MAX_HIGHLIGHT_LINES;
124 }
125
126 // Apply one shared syntax-highlighting budget to chat blocks, tool output, and
127 // workspace previews. Callers with an authoritative byte size or an existing
128 // line split can pass those values to avoid duplicate work; ordinary chat
129 // blocks are measured here without allocating another copy of the source.
130 export function shouldHighlightSource(
131 source: string,
132 sourceSize?: number,
133 lineCount?: number,
134 ): boolean {
135 if (sourceSize != null && sourceSize > MAX_HIGHLIGHT_BYTES) return false;
136 if (lineCount != null && lineCount > MAX_HIGHLIGHT_LINES) return false;
137 if (sourceSize != null && lineCount != null) return true;
138
139 let measuredBytes = sourceSize ?? 0;
140 let measuredLines = lineCount ?? 1;
141 for (let i = 0; i < source.length; i += 1) {
142 const codeUnit = source.charCodeAt(i);
143 if (lineCount == null && codeUnit === 10) {
144 measuredLines += 1;
145 if (measuredLines > MAX_HIGHLIGHT_LINES) return false;
146 }
147 if (sourceSize != null) continue;
148
149 if (codeUnit < 0x80) {
150 measuredBytes += 1;
151 } else if (codeUnit < 0x800) {
152 measuredBytes += 2;
153 } else if (
154 codeUnit >= 0xd800
155 && codeUnit <= 0xdbff
156 && i + 1 < source.length
157 && source.charCodeAt(i + 1) >= 0xdc00
158 && source.charCodeAt(i + 1) <= 0xdfff
159 ) {
160 measuredBytes += 4;
161 i += 1;
162 } else {
163 measuredBytes += 3;
164 }
165 if (measuredBytes > MAX_HIGHLIGHT_BYTES) return false;
166 }
167 return true;
168 }
169
170 // resolveLang maps a markdown fence tag or guessed name to a registered language,
171 // or "" when we can't highlight it (caller renders escaped plain text).
172 export function resolveLang(lang?: string): string {
173 if (!lang) return "";
174 const l = lang.toLowerCase();
175 const resolved = ALIASES[l] ?? l;
176 return hljs.getLanguage(resolved) ? resolved : "";
177 }
178
179 // LRU cache for highlighted output. The same code block can re-render many
180 // times (Re-renders due to streaming updates that don't change this block,
181 // React's StrictMode double-invoke in dev, hover-reveals of the toolbar's
182 // child elements). highlight.highlight() is a real lexer walk that shows
183 // up in the profile for large blocks; a 200-entry LRU keyed on the
184 // resolved language plus a fast hash of the code keeps the steady-state
185 // cost at a Map.get(). The Map is a plain LRU, not a WeakMap, so a large
186 // (non-streaming) transcript will eventually evict; the size of 200 is
187 // chosen to cover the visible viewport plus a small overshoot — most
188 // transcripts re-render the same ~30 visible blocks.
189 const HL_CACHE_MAX = 200;
190
191 // djb2 hash for the cache key. We only use the hash inside the key
192 // (Map<number, string>), not as a security primitive; collisions are fine
193 // because we ALSO store the original code alongside the entry and verify
194 // before serving the cached value. The hash is what makes the key
195 // constant-time to compare for Map.get(); comparing the full source
196 // string would be O(n) on every render and dwarf the savings.
197 function hashCode(s: string): number {
198 let h = 5381;
199 for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
200 return h;
201 }
202
203 interface CacheEntry {
204 code: string;
205 html: string;
206 }
207 const hlCache = new Map<number, CacheEntry>();
208
209 function cacheGet(code: string, lang: string): string | null {
210 const key = hashCode(lang + "\0" + code);
211 const e = hlCache.get(key);
212 if (!e) return null;
213 // Defend against the (rare) hash collision: the stored code must match
214 // the queried code exactly. We move the entry to the end of the Map to
215 // mark it most-recently-used.
216 if (e.code !== code) return null;
217 hlCache.delete(key);
218 hlCache.set(key, e);
219 return e.html;
220 }
221
222 function cachePut(code: string, lang: string, html: string): void {
223 const key = hashCode(lang + "\0" + code);
224 if (hlCache.has(key)) hlCache.delete(key); // refresh
225 hlCache.set(key, { code, html });
226 while (hlCache.size > HL_CACHE_MAX) {
227 // Map iteration is insertion-order; the first key is the oldest.
228 const oldest = hlCache.keys().next().value;
229 if (oldest === undefined) break;
230 hlCache.delete(oldest);
231 }
232 }
233
234 // highlightToHtml returns highlighted HTML (token <span>s) for the given code, or
235 // escaped plain text when the language is unknown. ignoreIllegals so partial /
236 // streaming snippets never throw. The LRU cache shaves the bulk of the work
237 // when a transcript re-renders the same blocks (most common: a streaming
238 // update changes the *next* block, not this one).
239 export function highlightToHtml(code: string, lang?: string): string {
240 const resolved = resolveLang(lang);
241 if (!resolved) return escapeHtml(code);
242 const cached = cacheGet(code, resolved);
243 if (cached !== null) return cached;
244 let html: string;
245 try {
246 html = hljs.highlight(code, { language: resolved, ignoreIllegals: true }).value;
247 } catch {
248 return escapeHtml(code);
249 }
250 cachePut(code, resolved, html);
251 return html;
252 }
253
253 lines TYPESCRIPT