| 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 === "&" ? "&" : c === "<" ? "<" : ">")); |
| 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 |