mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 08:21:50 +08:00
Adds a minimal hand-rolled highlighter for ts/js/jsx/tsx, py, sh/bash, go, rust, json, yaml, sql. Recognizes whole-line comments, single/double/backtick strings, numbers, and per-language keyword sets. Unknown langs fall through to the current plain rendering; the existing diff-specific colorization is preserved. Closes the §8 "Markdown syntax highlighting is missing (only diff gets colored)" finding from the TUI v2 audit without pulling in a highlighter library.
118 lines
3.4 KiB
TypeScript
118 lines
3.4 KiB
TypeScript
import type { Theme } from '../theme.js'
|
|
|
|
export type Token = [string, string]
|
|
|
|
interface LangSpec {
|
|
comment: null | string
|
|
keywords: Set<string>
|
|
}
|
|
|
|
const KW = (s: string) => new Set(s.split(/\s+/).filter(Boolean))
|
|
|
|
const TS = KW(`
|
|
abstract as async await break case catch class const continue debugger default delete do else enum export extends
|
|
false finally for from function get if implements import in instanceof interface is let new null of package private
|
|
protected public readonly return set static super switch this throw true try type typeof undefined var void while
|
|
with yield
|
|
`)
|
|
|
|
const PY = KW(`
|
|
False None True and as assert async await break class continue def del elif else except finally for from global if
|
|
import in is lambda nonlocal not or pass raise return try while with yield
|
|
`)
|
|
|
|
const SH = KW(`
|
|
if then else elif fi for in do done while until case esac function return break continue local export readonly
|
|
declare typeset
|
|
`)
|
|
|
|
const GO = KW(`
|
|
break case chan const continue default defer else fallthrough for func go goto if import interface map package range
|
|
return select struct switch type var nil true false
|
|
`)
|
|
|
|
const RUST = KW(`
|
|
as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut
|
|
pub ref return self Self static struct super trait true type unsafe use where while yield
|
|
`)
|
|
|
|
const SQL = KW(`
|
|
select from where and or not in is null as by group order limit offset insert into values update set delete create
|
|
table drop alter add column primary key foreign references join left right inner outer on
|
|
`)
|
|
|
|
const LANGS: Record<string, LangSpec> = {
|
|
go: { comment: '//', keywords: GO },
|
|
json: { comment: null, keywords: KW('true false null') },
|
|
py: { comment: '#', keywords: PY },
|
|
rust: { comment: '//', keywords: RUST },
|
|
sh: { comment: '#', keywords: SH },
|
|
sql: { comment: '--', keywords: SQL },
|
|
ts: { comment: '//', keywords: TS },
|
|
yaml: { comment: '#', keywords: KW('true false null yes no on off') }
|
|
}
|
|
|
|
const ALIAS: Record<string, string> = {
|
|
bash: 'sh',
|
|
javascript: 'ts',
|
|
js: 'ts',
|
|
jsx: 'ts',
|
|
python: 'py',
|
|
rs: 'rust',
|
|
shell: 'sh',
|
|
tsx: 'ts',
|
|
typescript: 'ts',
|
|
yml: 'yaml',
|
|
zsh: 'sh'
|
|
}
|
|
|
|
const resolve = (lang: string): LangSpec | null => LANGS[ALIAS[lang] ?? lang] ?? null
|
|
|
|
export const isHighlightable = (lang: string): boolean => resolve(lang) !== null
|
|
|
|
const TOKEN_RE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`|\b\d+(?:\.\d+)?\b|[A-Za-z_$][\w$]*/g
|
|
|
|
export function highlightLine(line: string, lang: string, t: Theme): Token[] {
|
|
const spec = resolve(lang)
|
|
|
|
if (!spec) {
|
|
return [['', line]]
|
|
}
|
|
|
|
if (spec.comment && line.trimStart().startsWith(spec.comment)) {
|
|
return [[t.color.dim, line]]
|
|
}
|
|
|
|
const tokens: Token[] = []
|
|
let last = 0
|
|
|
|
for (const m of line.matchAll(TOKEN_RE)) {
|
|
const start = m.index ?? 0
|
|
|
|
if (start > last) {
|
|
tokens.push(['', line.slice(last, start)])
|
|
}
|
|
|
|
const tok = m[0]
|
|
const ch = tok[0]!
|
|
|
|
if (ch === '"' || ch === "'" || ch === '`') {
|
|
tokens.push([t.color.amber, tok])
|
|
} else if (ch >= '0' && ch <= '9') {
|
|
tokens.push([t.color.cornsilk, tok])
|
|
} else if (spec.keywords.has(tok)) {
|
|
tokens.push([t.color.bronze, tok])
|
|
} else {
|
|
tokens.push(['', tok])
|
|
}
|
|
|
|
last = start + tok.length
|
|
}
|
|
|
|
if (last < line.length) {
|
|
tokens.push(['', line.slice(last)])
|
|
}
|
|
|
|
return tokens
|
|
}
|