mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 00:11:39 +08:00
feat(tui): per-language syntax highlighting in markdown code fences
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.
This commit is contained in:
117
ui-tui/src/lib/syntax.ts
Normal file
117
ui-tui/src/lib/syntax.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user