Chroma 语法高亮器解析:纯 Go 实现的通用语法高亮库及其在 Loki 生态中的应用
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
本文系统讲解随当前仓库(Grafana Loki)一起 vendored 的 Chroma v2——一个基于 Pygments 理念、以纯 Go 编写的通用语法高亮库,覆盖其词法分析器(Lexer)、格式化器(Formatter)与配色风格(Style)三大核心模型、Go 库 API、HTML 格式化选项与命令行工具,并结合仓库源码说明它在 Loki 工具链(如lokitool)中的真实落地方式。读完本文,你将能够独立使用 Chroma 将任意源码或结构化文本高亮为 HTML、ANSI 终端彩色文本等格式,并能在自己的 Go 项目中复刻 Loki 的高亮输出方案。
Chroma 是什么
Chroma 是一个用纯 Go 编写的通用语法高亮器,它接收源代码及其他结构化文本,并将其转换为语法高亮的 HTML、ANSI 彩色文本等输出。Chroma 在很大程度上基于 Pygments 的设计思想构建,并且内置了将 Pygments 的词法分析器(lexers)和样式(styles)转换为 Chroma 格式的翻译工具。
在当前仓库中,Chroma v2 以依赖形式被 vendor 在 vendor/github.com/alecthomas/chroma/v2 目录下,作为 Loki 项目内部工具链的语法高亮引擎使用(详见下文"Chroma 在 Loki 仓库中的实际使用"一节)。
Chroma 的核心抽象与 Pygments 一一对应,由三个相互协作的概念组成:
- 词法分析器(Lexers):将源文本转换为 Token 流;
- 样式(Styles):定义 Token 类型如何映射为颜色;
- 格式化器(Formatters):将 Token 与样式转换为最终的格式化输出。
这三个概念在仓库中各自有独立包:lexers、formatters、styles。每个包内部都持有一个全局Registry变量,保存所有已注册的实现;同时提供辅助函数用于按名称查找词法分析器、按文件名匹配、格式化输出等操作。在所有场景中,如果无法确定某个 lexer、formatter 或 style,对应 API 会返回nil,此时可以回退到各包中的Fallback值——它提供了合理的默认实现。
支持的编程语言
Chroma 内置了数量庞大的词法分析器,覆盖主流及小众编程语言、配置文件格式、标记语言与 DSL。下表按语言名称首字母分组列出(以当前 vendor 目录中的版本为准):
| 前缀 | 语言 |
|---|---|
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, AMPL, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, Arturo, ATL, AutoHotkey, AutoIt, Awk |
| B | Ballerina, Bash, Bash Session, Batchfile, Beef, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck |
| C | C, C#, C++, C3, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Core, Crystal, CSS, CSV, CUE, Cython |
| D | D, Dart, Dax, Desktop file, Devicetree, Diff, Django/Jinja, dns, Docker, DTD, Dylan |
| E | EBNF, Elixir, Elm, EmacsLisp, ERB, Erlang |
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp |
| G | GAS, GDScript, GDScript3, Gemtext, Genshi, Genshi HTML, Genshi Text, Gettext, Gherkin, Gleam, GLSL, Gnuplot, Go, Go HTML Template, Go Template, Go Text Template, GraphQL, Groff, Groovy |
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy |
| I | Idris, Igor, INI, Io, ISCdhcpd |
| J | J, Janet, Java, JavaScript, JSON, JSONata, Jsonnet, Julia, Jungle |
| K | Kakoune, KDL, Kotlin |
| L | Lateralus, Lean4, Lighttpd configuration file, LilyPond, LLVM, lox, Lua, Luau |
| M | Makefile, Mako, markdown, Markless, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, microcad, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonBit, MoonScript, MorrowindScript, Myghty, MySQL |
| N | NASM, Natural, NDISASM, Newspeak, Nginx configuration file, Nim, Nix, NSIS, Nu |
| O | Objective-C, ObjectPascal, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode |
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, Promela, PromQL, properties, Protocol Buffer, Protocol Buffer Text Format, PRQL, PSL, Puppet, Python, Python 2 |
| Q | QBasic, QML |
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, Rego, reStructuredText, Rexx, RGBDS Assembly, Ring, RPGLE, RPMSpec, Ruby, Rust |
| S | SAS, Sass, Scala, scdoc, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, SNBT, Snobol, Solidity, SourcePawn, Spade, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog |
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData, Typst |
| U | ucode |
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue |
| W | WDTE, WebAssembly Text Format, WebGPU Shading Language, WebVTT, Whiley |
| X | XML, Xorg |
| Y | YAML, YANG |
| Z | Z80 Assembly, Zed, Zig |
上表可能滞后于实际实现,最权威的列表可以通过命令行工具输出:
chroma --list。
值得关注的是,表中包含多个与可观测性领域高度相关的语言:PromQL(Prometheus 查询语言,Loki 的 LogQL 姊妹语言)、YAML(几乎所有配置文件)以及Go Template / Go Text Template等。这些正是 Loki 工具链在输出告警规则、配置文件时实际使用的高亮目标(见下文)。
Chroma 在 Loki 仓库中的实际使用
Chroma 并非仅作为"理论上可用的依赖"存在于本仓库,而是真实服务于 Loki 的命令行工具链。在 pkg/tool/printer/printer.go 中,Loki 的lokitool使用github.com/alecthomas/chroma/v2/quick包为各类 YAML/JSON/模板输出添加终端彩色高亮:
import "github.com/alecthomas/chroma/v2/quick" err := quick.Highlight(os.Stdout, config, "yaml", "terminal", "swapoff")从源码结构看,Printer提供以下高亮输出场景:
PrintAlertmanagerConfig:以"yaml"词法器高亮 Alertmanager 配置,并以"go-text-template"高亮其中的模板文件;PrintRuleGroups/PrintRuleGroup:将规则组 YAML 编码后以"yaml"高亮输出;PrintRuleSet:在json/yaml两种输出格式下分别以对应词法器高亮。
上述调用统一使用"terminal"格式化器与"swapoff"风格——这正是quick.Highlight一行式 API 的典型用法:无需手动组装 lexer、formatter、style,全部交给 quick 包"尽力而为"地解析。同时Printer通过disableColor标志在关闭彩色输出时直接回退到普通fmt.Println,体现了"高亮是增强、可降级"的工程实践。
使用 Chroma Go 库
Chroma 当前是第 2 个大版本,导入路径为:
import "github.com/alecthomas/chroma/v2"快速开始:一行完成高亮
quick包提供了一个免配置的便捷函数Highlight,可以零成本地把一段源码直接格式化输出到任意io.Writer:
err := quick.Highlight(os.Stdout, someSourceCode, "go", "html", "monokai")结合 vendor/github.com/alecthomas/chroma/v2/quick/quick.go 的实现可以看到它的"尽力而为"逻辑:
- 先用
lexers.Get(lexer)按名称查找词法分析器,找不到则用lexers.Analyse(source)根据内容探测,再找不到则回退到lexers.Fallback; - 对词法分析器调用
chroma.Coalesce做合并优化; - 分别用
formatters.Get与styles.Get解析格式化器和风格,失败时回退到各自Fallback; - 最后执行
l.Tokenise(nil, source)生成 Token 迭代器,交给f.Format(w, s, it)完成输出。
也就是说,即使你传空的 lexer/formatter/style 名称,quick.Highlight也会以最佳努力给出可用的高亮结果。
识别语言:三种方式
要高亮代码,首先需要确定这段代码是什么语言。Chroma 提供三种主要途径,返回nil表示无法识别:
1. 根据文件名探测语言
lexer := lexers.Match("foo.go")2. 显式指定语言的 Chroma 语法 ID(完整列表可通过lexers.Names()获取)
lexer := lexers.Get("go")3. 根据内容分析语言
lexer := lexers.Analyse("package main\n\nfunc main()\n{\n}\n")无论哪种方式,识别失败都会返回nil,此时建议回退到默认值:
if lexer == nil { lexer = lexers.Fallback }另外,某些词法分析器输出的 Token 可能极其零碎("chatty")。可以用 coalescing lexer 把连续的相同 Token 类型合并为单个 Token,从而得到更干净的输出:
lexer = chroma.Coalesce(lexer)格式化输出:组装 Style 与 Formatter
语言确定后,需要挑选一个 formatter 和一个 style(主题):
style := styles.Get("swapoff") if style == nil { style = styles.Fallback } formatter := formatters.Get("html") if formatter == nil { formatter = formatters.Fallback }然后对源文本执行 Token 化,获得 Token 迭代器:
contents, err := ioutil.ReadAll(r) iterator, err := lexer.Tokenise(nil, string(contents))最后把迭代器中的 Token 交给 formatter 渲染:
err := formatter.Format(w, style, iterator)HTML 格式化器的高级选项
默认情况下,html注册的格式化器生成内嵌 CSS 的独立(standalone)HTML 文档;若要获得更高的灵活性,应使用 formatters/html 包。可以通过构造选项定制输出行为:
Standalone()——生成内嵌 CSS 的独立 HTML;WithClasses()——使用 CSS 类名而非内联 style 属性;ClassPrefix(prefix)——为生成的每个 CSS 类添加前缀;TabWidth(width)——设置渲染时的 Tab 宽度(字符数);WithLineNumbers()——渲染行号(用LineNumbers样式修饰);WithLinkableLineNumbers()——使行号可链接,且链接指向自身;HighlightLines(ranges)——高亮指定范围内的行(用LineHighlight样式修饰);LineNumbersInTable()——用表格而非 span 来布局行号与代码。
当启用WithClasses()后,可通过WriteCSS导出对应 CSS:
formatter := html.New(html.WithClasses(true)) err := formatter.WriteCSS(w, style)从 vendor/github.com/alecthomas/chroma/v2/formatters/api.go 可以看到,注册表内名为html的默认格式化器其实正是html.New(html.Standalone(true), html.WithClasses(true))的产物——即"独立 HTML + CSS 类"组合,这解释了为什么默认输出自带样式。
深入三大组件
Lexers:词法分析器
Chroma 的词法分析器实现方式与 Pygments 高度一致。若需了解如何实现 lexer,可参考 Pygments 的 lexer 开发文档(Chroma 的大多数概念与之直接对应),更直观的做法是直接阅读仓库中已有的 lexer 实现作为真实范例。
在多数情况下,lexer 可以直接通过自带的 Python 3 脚本pygments2chroma_xml.py从 Pygments 自动转换而来,转换命令形如:
uv run --script _tools/pygments2chroma_xml.py \ pygments.lexers.jvm.KotlinLexer \ > lexers/embedded/kotlin.xml即:输入 Pygments 的 lexer 全限定类名,输出 Chroma 的 XML 定义文件。这也是 Chroma 能迅速覆盖大量语言的原因——绝大多数 lexer 由 Pygments 生态自动移植。Chroma 目前对 lexer 的定义策略是:除需要自定义逻辑的场景外,所有 lexer 都用 XML 定义(见 vendor/github.com/alecthomas/chroma/v2/lexers/README.md)。
Formatters:格式化器
Chroma 支持以下输出形式:
- HTML 输出:用于 Web 页面渲染;
- 终端输出:支持 8 色、256 色以及 true-colour(真彩)三种级别,对应
terminal、terminal256、terminal16m等格式化器(源码位于 vendor/github.com/alecthomas/chroma/v2/formatters,其中 tty_indexed.go 与 tty_truecolour.go 分别实现索引色与真彩输出); noop格式化器:仅输出 Token 文本本身,不做任何修饰;tokens格式化器:输出原始 Token 流,是调试 lexer 的利器;svg格式化器:可输出 SVG 图形格式(源码在 vendor/github.com/alecthomas/chroma/v2/formatters/svg)。
Fallback格式化器是noop(见 api.go),保证即使找不到格式化器,也能原样输出文本。
Styles:配色风格
Chroma 的样式以 XML 定义,其条目语法与 Pygments 相同,全部 Pygments 风格均已通过_tools/style.py脚本转换为 Chroma 格式。样式名称不区分大小写,例如monokai与Monokai被视为同一样式。
当前仓库 vendor/github.com/alecthomas/chroma/v2/styles 目录中内置了 70+ 套风格,涵盖经典与流行主题,例如:monokai、monokailight、dracula、solarized-dark、solarized-light、github、github-dark、nord、onedark、tokyonight-*系列、catppuccin-*系列、gruvbox、xcode、swapoff等。
使用风格时有两个关键机制需要理解:
1.BackgroundToken 提供默认样式
Backgroundtoken 类型通过定义前景色与背景色,为未显式指定的 Token 提供默认样式。例如下面这条 XML 给所有未定义的 Token 名设置默认前景色#f8f8f2,并让高亮代码块的背景色为#000000:
<entry type="Background" style="#f8f8f2 bg:#000000"/>2. Token 类型具有层级继承
样式文件中的 Token 类型是分层的。例如当CommentSpecial未定义时,Chroma 会使用Comment的样式。因此当多个注释类 Token 使用同一颜色时,只需定义Comment一次,再单独覆盖颜色不同的那一个即可,大幅精简样式文件。
命令行接口
Chroma 附带了一个命令行工具chroma,可用于:
- 直接对文件进行语法高亮输出;
- 作为
less(1)的着色预处理器,配合LESSOPEN环境变量使用; - 生成支持语言的权威清单(
chroma --list)。
--fail标志用于抑制输出并以退出码 1 返回,方便在 chroma 无法为给定文件解析出词法分析器时回退到其他预处理器。例如:
export LESSOPEN='| p() { chroma --fail "$1" || cat "$1"; }; p "%s"'把其中的cat换成你偏好的回退预处理器即可。
当可执行文件被以.lessfilter名称调用时,--fail标志会在底层自动开启,以便与 Debian 及其衍生发行版自带的 lesspipe 集成(用于其"用户自定义过滤器"机制)。对于该场景,只需把chroma可执行文件软链接到~/.lessfilter即可。
测试词法分析器
如果修改了某个 lexer 并想本地验证效果,可以进入cmd/chromad目录启动本地调试服务:
go run . --csrf-key=securekey启动后会打印一个链接,在浏览器中打开它,即可在 Playground 上使用本地修改进行测试。
若要运行词法分析器测试,在项目根目录执行:
go test ./lexers根据 vendor/github.com/alecthomas/chroma/v2/lexers/README.md 的说明,lexer 测试的机制是:把testdata/<name>.actual中的已知输入喂给对应解析器,然后校验输出是否与<name>.expected一致;同一个 parser 也可以通过testdata/<name>/目录存放多个*.actual输入来执行多组测试。新增或更新 lexer 时,官方要求补充对应测试。
当新增测试数据文件(*.actual)后,需要重新生成全部测试期望文件,使用RECORD环境变量:
RECORD=true go test ./lexers该命令先设置RECORD=true,再运行测试;该环境变量告知 Chroma 输出测试数据,跑完后即可移除或重置该变量。Windows 用户在标准命令提示符与 PowerShell 下需分两步执行:先用set RECORD=true(cmd)或$env:RECORD = 'true'(PowerShell)设置环境变量,再单独运行go test ./lexers。
与 Pygments 相比还缺什么
Chroma 虽然移植了大量 Pygments 能力,但官方文档也坦承存在如下差距:
- 仍有不少 lexer 未移植,原因包括:Pygments 中复杂语言的 lexer 往往包含大量自定义代码来处理特殊语法(例如 Raku 在正则表达式内嵌套代码的能力),转换需要大量时间与精力;同时为控制移植成本,最初只转换了作者本人听说过的语言(欢迎提交 pull request 补充);
- Pygments 部分更偏门的功能被有意省略,以保持实现简洁;
- 内容探测(Analyse)支持较弱:虽然 Chroma API 支持基于内容的语言检测,但目前只有极少数语言实现了这一能力;官方有计划引入统计算法器,但尚未落地。
对使用者而言,这意味着:以文件名匹配(lexers.Match)或显式指定 ID(lexers.Get)的方式通常最可靠,而lexers.Analyse仅在少数语言上有效——Loki 工具链之所以总是显式传入"yaml"、"json"、"go-text-template"等词法器名称,正是对这一限制的工程化规避。
小结
Chroma v2 为 Go 生态提供了一套完整、纯原生、无需 CGO 依赖的语法高亮方案:以 Pygments 为蓝本的 Lexer/Style/Formatter 三元模型、内置数百种语言与数十套主题、quick一行式 API、可高度定制的 HTML 输出,以及可作为 less 预处理器的 CLI。在本仓库中,它已实际服务于 Loki 的lokitool输出高亮,是"仓库内可复用的通用基础组件"的典型代表。无论是构建自己的代码展示工具、终端日志着色器,还是为 CLI 增加配置预览能力,Chroma 都能以极小的接入成本提供专业的语法着色体验。
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考