KaTeX 贡献实战指南:为开源数学排版引擎添加符号、函数与宏
【免费下载链接】KaTeXFast math typesetting for the web.项目地址: https://gitcode.com/GitHub_Trending/ka/KaTeX
导读
本文是 KaTeX(面向 Web 的快速 TeX 数学排版引擎)的贡献者实战指南,基于仓库根目录的 CONTRIBUTING.md 展开,并深入结合src/源码进行印证。KaTeX 目前仍有许多 LaTeX 符号与函数尚未支持,通过阅读本文,你将掌握在src/symbols.ts中添加单个符号、在src/functions/中通过defineFunction注册新函数、在src/macros.ts中用defineMacro定义宏的完整流程,同时学会使用 Jest 单元测试、截图测试、构建与代码风格检查工具链,最终以符合规范的 Pull Request 将改动合入主分支。
说明:本文提到的所有源码路径均以仓库根目录为基准。原贡献文档写作时引用的
src/symbols.js、src/defineFunction.js等文件,在当前仓库中已统一迁移为 TypeScript 版本(.ts后缀),下文一律使用仓库内实际存在的路径。
一、从哪里开始:先确认“值得贡献”的方向
KaTeX 欢迎各种形式的 Pull Request。在动手之前,建议先判断自己的改动属于哪一类:
- 新增符号(symbol):大量 LaTeX 单个字符或命令(如
\neq、\equiv)尚未被支持,改动量小、风险低,是最佳入门选择。 - 新增函数(function):像
\phantom、\bigl这类带参数、带排版语义的命令,需要同时提供解析器、HTML 与 MathML 构建逻辑。 - 新增宏(macro):纯文本替换类的命令,例如
\hphantom、\@ifstar,直接在宏表中注册即可。
仓库内有两份与支持范围直接相关的文档,可用于核对“某个命令是否已被支持”:
- docs/supported.md:KaTeX 已支持的功能列表;
- docs/support_table.md:同时列出已支持与不支持的功能对照表。
此外,static/index.html对应交互式演示页面(本地开发时可通过pnpm start启动),可以在真实环境中输入目标命令,直观判断 KaTeX 是否支持、渲染效果如何。原文档还提示社区 wiki 中有“Examining-TeX”页面,介绍如何分析 TeX 命令的排版规则,这对理解字符分组(group)非常有帮助。
二、添加单个符号:从src/symbols.ts开始
KaTeX 的符号表集中在 src/symbols.ts。该文件顶部注释明确说明了符号的三要素:
| 属性 | 是否必填 | 含义 |
|---|---|---|
font | 必填 | 该符号使用的字体,取值为"main"(主字体)或"ams"(AMS 字体) |
group | 必填 | 符号所属的 ParseNode 分组类型,如textord、mathord、rel、bin、open、close、punct、inner等 |
replace | 视情况 | 该符号被替换成的字符,例如\phi的replace值为\u03d5(主字体中的 phi 字符) |
符号表的最外层映射还按模式(mode)区分:"math"(数学模式)与"text"(文本模式),同一命令在不同模式下的字体和分组可以不同。
2.1 使用defineSymbol注册
文件中的注册入口是defineSymbol函数(src/symbols.ts):
export function defineSymbol( mode: Mode, font: SymbolFont, group: Group, replace: string, name: string, acceptUnicodeChar?: boolean, ) { symbols[mode][name] = {font, group, replace}; if (acceptUnicodeChar && replace) { symbols[mode][replace] = symbols[mode][name]; } }最后一个参数acceptUnicodeChar为true时,会同时把replace对应的 Unicode 字符注册为同一符号,方便用户直接输入 Unicode 字符而非 LaTeX 命令。典型的注册语句如:
defineSymbol(math, main, rel, "\u2261", "\\equiv", true); defineSymbol(math, main, punct, "\u002e", "\\ldotp");2.2 三步确定新符号的注册参数
- 确定 Unicode 字符:把目标命令放到 MathJax 等渲染器中跑一次,观察其输出的 Unicode 码点,以此作为
replace值。 - 确定分组(group):在 src/symbols.ts 的符号表中寻找同类符号。例如要添加
\neq,就去找=所属的分组;若找不到相似参考,可以把新符号与不同类型的符号混排,观察间距是否符合 TeX 的间距规则来推断其分组(关系符、二元运算符、标点等在 TeX 中有不同的自动间距)。 - 渲染验证:符号可渲染后,打开浏览器 JavaScript 控制台,确认没有
No character metrics for '_'之类的警告。该警告表示当前字体度量数据中缺少该字符,需要重新生成字体与度量文件——相关工具位于 dockers/fonts(包含buildFonts.sh与buildMetrics.sh等脚本,可参考 dockers/fonts/README.md)。
三、添加新函数:defineFunction的完整生命周期
比单个符号更复杂的是带参数、带排版逻辑的命令。这类命令统一放在 src/functions 目录下,通过defineFunction注册。原文档特别指出:这是 KaTeX 正在推行的“新式”定义方式——把过去分散在 src/functions.ts、src/buildHTML.ts、src/buildMathML.ts 三个文件中的函数名注册、HTML 构建、MathML 构建集中到单个文件内,目标是让所有函数最终都迁移到这套系统。
3.1defineFunction的规格字段
src/defineFunction.ts 中定义了完整的FunctionSpec类型,各字段及其默认值如下:
| 字段 | 默认值 | 说明 |
|---|---|---|
type | —(必填) | 唯一的字符串,用于区分解析节点(ParseNode),也决定handler返回值类型 |
names | —(必填) | 函数名列表;列表中的多个命令共享同一份实现 |
numArgs | —(必填) | 函数必选参数个数 |
numOptionalArgs | 0 | 可选参数个数;找不到可选参数时以null传给 handler |
argTypes | 无 | 对应每个参数的类型数组,长度为numOptionalArgs + numArgs,可选参数类型在前 |
allowedInArgument | false | 是否展开为单个 token 或花括号包裹的一组 token;若可被包裹,就能作为\sqrt(无可选参数形式)或上/下标的参数 |
allowedInText | false | 是否允许在文本模式中使用 |
allowedInMath | true | 是否允许在数学模式中使用 |
infix | 未设置 | 是否为中缀运算符,必须显式置true |
primitive | 未设置 | 是否为 TeX 原语 |
handler | —(通常必填) | 解析回调,接收(context, args, optArgs),返回一个ParseNode |
构建器则通过可选的htmlBuilder与mathmlBuilder提供,分别返回表示 DOM 结构的HtmlDomNode与 MathML 结构的MathDomNode,二者不应修改传入的ParseNode。注册时defineFunction会把函数名写入_functions表(供 Parser 查找),把构建器写入_htmlGroupBuilders与_mathmlGroupBuilders(供 HTML/MathML 构建阶段使用)。
3.2 经典示例一:\phantom
src/functions/phantom.ts 是原文档钦点的入门范例,完整展示了“解析 + HTML + MathML”三段式结构:
defineFunction({ type: "phantom", names: ["\\phantom"], numArgs: 1, allowedInText: true, handler: ({parser}, args) => { const body = args[0]; return { type: "phantom", mode: parser.mode, body: ordargument(body), }; }, htmlBuilder: (group, options) => { const elements = html.buildExpression( group.body, options.withPhantom(), false ); return makeFragment(elements); }, mathmlBuilder: (group, options) => { const inner = mml.buildExpression(group.body, options); return new MathNode("mphantom", inner); }, });值得注意的细节:
handler中通过ordargument(body)将参数归一化为节点数组(若参数是单元素ordgroup则直接取出,见 src/defineFunction.ts 中的normalizeArgument/ordargument工具函数);- HTML 侧用
options.withPhantom()渲染“隐形”内容,即保留尺寸但不绘制; - MathML 侧输出
<mphantom>节点; - 同一文件中还定义了
\vphantom(只保留高度),并在文件末尾用defineMacro("\\hphantom", "\\smash{\\phantom{#1}}")复用了宏机制。
3.3 经典示例二:多个相关函数共享一次注册
src/functions/delimsizing.ts 演示了names列表的用法——把 16 个定界符尺寸命令(\bigl、\Bigl、\biggl、\Biggl、\bigr、\bigm、\big等)放在同一次defineFunction调用中注册,通过context.funcName查表得到各自的尺寸与数学类别:
const delimiterSizes = { "\\bigl" : {mclass: "mopen", size: 1}, "\\Bigl" : {mclass: "mopen", size: 2}, "\\biggl": {mclass: "mopen", size: 3}, "\\Biggl": {mclass: "mopen", size: 4}, // ... \bigr/\Bigr/\bigm/\Bigm/\big/\Big/\bigg/\Bigg 等 }; defineFunction({ type: "delimsizing", names: [ "\\bigl", "\\Bigl", "\\biggl", "\\Biggl", "\\bigr", "\\Bigr", "\\biggr", "\\Biggr", "\\bigm", "\\Bigm", "\\biggm", "\\Biggm", "\\big", "\\Big", "\\bigg", "\\Bigg", ], numArgs: 1, argTypes: ["primitive"], handler: (context, args) => { const delim = checkDelimiter(args[0], context); return { type: "delimsizing", mode: context.parser.mode, size: delimiterSizes[context.funcName].size, mclass: delimiterSizes[context.funcName].mclass, delim: delim.text, }; }, // htmlBuilder / mathmlBuilder ... });argTypes: ["primitive"]表示参数按“原语”方式解析;checkDelimiter还会对照delimiters集合校验参数是否为合法定界符,否则抛出ParseError。这个模式非常适合批量实现“同一语义、不同尺寸/类别”的命令族。
四、定义宏:defineMacro与“食道”(gullet)
纯文本替换类命令不需要完整的函数实现,直接在宏表中注册即可。宏的统一入口是 src/macros.ts,通过defineMacro注册,并在“gullet”(即MacroExpander,对应 src/MacroExpander.ts)中完成展开。
defineMacro既支持纯字符串形式的展开(如defineMacro("\\@ifstar", "\\@ifnextchar *{\\@firstoftwo{#1}}")),也支持函数形式的动态展开——回调接收一个MacroContextInterface(见 src/defineMacro.ts),可调用popToken()、consumeArgs(n)、future()、consumeSpaces()、expandOnce()等接口操作 token 流,返回{tokens, numArgs}。
src/macros.ts 中现成实现了一批可直接借鉴的“宏工具”,例如:
\noexpand:让下一个 token 不再展开,语义上等价于\relax;\expandafter:先展开目标 token 之后的内容,再把原 token 放回;\@firstoftwo/\@secondoftwo:取两参数中的第一个/第二个;\@ifnextchar:预读下一个非空格字符并据此选择分支;\@ifstar:预读下一个符号是否为*,实现星号变体语法。
从源码结构看,这些宏与 TeX/LaTeX 内核同名原语保持了一致的语义,是编写复杂命令组合(如带星号变体的新命令)时的重要参考。
五、本地开发环境:启动交互式编辑器
原文档给出的本地开发流程为:
corepack enable # 启用 corepack(若尚未启用) pnpm install # 安装依赖 pnpm start # 启动 webpack-dev-serverpnpm start实际执行的是webpack serve --config webpack.dev.js(见 package.json 的scripts字段)。webpack.dev.js 中硬编码了端口7936,并将static/目录作为静态资源根,allowedHosts: 'all'允许从任意主机访问——这便于在局域网或容器中调试。
启动后访问http://localhost:7936/即可获得一个交互式 TeX 编辑器,用于实时验证改动。调试 Jest 测试时,也可以把测试用例直接粘贴进该编辑器反复运行,其中的 permalink(永久链接)功能对重复跑同一用例非常实用。
六、Jest 单元测试:解析器与树构建的正确性保障
JavaScript 解析器以及部分 HTML / MathML 树构建逻辑由 Jest 测试覆盖,测试代码集中在 test 目录(测试文件命名如katex-spec.ts、mathml-spec.ts、errors-spec.ts、dup-spec.ts等,见 package.json 中 jest 配置的testMatch)。
常用命令(均来自 package.json 的scripts):
| 命令 | 对应脚本 | 用途 |
|---|---|---|
pnpm test:jest | jest | 运行全部 Jest 测试 |
pnpm test:jest:watch | jest --watch | 监听模式运行 |
pnpm test:jest:update | jest --updateSnapshot | 更新快照(snapshot) |
pnpm test:jest:coverage | jest --coverage | 收集代码覆盖率,报告位于coverage/lcov-report/index.html |
原文档的几条硬性要求值得牢记:
- 每次改动后都要跑 Jest 测试,即使只是新增一个小符号;CI 在提交 Pull Request 时也会自动运行这些测试,作为兜底防线;
- 只要改动到
Parser(src/Parser.ts),就必须补充对应的 Jest 测试; - 部分测试通过**快照测试(snapshot testing)**验证输出树结构,快照更新用
pnpm test:jest:update。仓库中的快照样例见 test/snapshots/katex-spec.ts.snap 与 test/snapshots/mathml-spec.ts.snap; - 覆盖率收集范围在 jest 配置中限定为
src/**与contrib/**(排除了unicodeSymbols与 mhchem 目录)。
七、截图测试:像素级验证最终渲染效果
单测验证的是“结构正确”,但数学排版最终好不好看,需要依赖截图测试。KaTeX 用浏览器对一组预定义的表达式截图,并与基准图片逐字节比对。
- 截图工具链封装在 dockers/screenshotter(含 README.md 与
screenshotter.sh入口脚本); - 被测表达式的清单在 test/screenshotter/ss_data.yaml 中定义,基准图片存放于 test/screenshotter/images;
- 比对新旧图片时,差异是“不同即不同”的字节级比对:若图片发生变化,必须肉眼检查——要么确实没有可见变化(可接受),要么变化与你的新增一致(需要在 PR 中解释原因与预期),否则要查清变化来源并修复;
- 如果你新增的功能依赖最终视觉呈现,请务必同时添加一条截图测试;
- 可以用 dockers/texcmp 工具把 KaTeX 的截图输出与真实 LaTeX 的输出做对比,生成“视觉差异图(visual diff)”,附在引入新功能的 Pull Request 中通常很有说服力。
原文档还明确:截图测试不会在 CI 中自动运行,需要贡献者自觉执行——这是与 Jest 测试最大的不同点。凡改动超出“单个符号”规模,都应主动跑一遍截图测试。
八、跨浏览器与构建
8.1 多浏览器验证
KaTeX 支持所有主流浏览器(Chrome、Safari、Firefox、Opera、Edge 等)。由于单机难以覆盖全部浏览器,原文档建议:条件允许时,尽量在尽可能多的浏览器中实测自己的改动。
8.2 构建发布产物
KaTeX 使用 webpack 构建,配置文件为 webpack.config.js。执行:
pnpm buildpnpm build实际为rimraf dist/ && mkdirp dist && cp README.md dist && rollup -c --failAfterWarnings && webpack && node update-sri.js package dist/README.md(见 package.json),即先清空并重建dist/,再依次执行 Rollup(生成 ESM 与 UMD 产物)与 webpack 打包,最后更新 SRI(Subresource Integrity)哈希。
原文档中有一条“清理 yarn 残留”的历史命令(删除.pnp.cjs/.pnp.loader.mjs等文件)。需要说明的是:当前仓库已通过packageManager: "pnpm@11.4.0"声明切换到 pnpm(见 package.json),依赖管理统一走corepack + pnpm,因此该清理步骤通常已不再需要,仅在残留旧版 yarn 生成物时才有意义。
九、代码风格指南与静态检查
KaTeX 对代码风格有明确约定(见 CONTRIBUTING.md 的 Style guide 一节):
| 规则 | 约定 |
|---|---|
| 缩进 | 4 个空格 |
| 行长 | 不超过 80 个字符 |
| 逗号 | 放在行尾(commas last) |
| 变量声明 | 声明在使用它的最外层作用域 |
| 命名 | JavaScript 用 camelCase;Python 用 snake_case |
| 总原则 | 与周围代码风格保持一致 |
提交前必须通过两轮静态检查:
pnpm test:lint # ESLint(JavaScript/TypeScript)+ stylelint(样式表) pnpm test:ts # tsc --noEmit 类型检查test:lint展开为eslint .与stylelint src/styles/katex.scss static/main.css website/static/**/*.css(见 package.json)。这两项必须全部通过才能提交代码,否则会阻塞合并。
十、Pull Request 规范
原文档对 PR 提出了明确要求,这也是最终合入主分支的“入场券”:
- 标题与描述遵循 Angular Commit Message Conventions,保证提交信息结构统一、可被工具解析;
- 尽可能关联原始 issue,方便评审者追溯问题上下文;
- 新增命令必须同步更新 docs/support_table.md 与 docs/supported.md,确保支持范围文档与代码保持一致;
- 合入前提交应 squash(压缩),保持主分支历史整洁;
- 大型 PR 应尽量拆分为多个小 PR,或至少拆成多个逻辑内聚的提交,降低评审负担。
此外,原文档提醒:KaTeX 的贡献者还需要先签署贡献者许可协议(CLA),且项目采用 MIT 许可证(见 LICENSE)。
十一、一张贡献流程图:从符号到合并
将上文各环节串起来,一次典型的贡献旅程如下:
- 在交互式编辑器或支持表中确认目标命令确实缺失;
- 单个符号 → 在 src/symbols.ts 用
defineSymbol注册;函数 → 在 src/functions 新建文件并用defineFunction实现 handler 与两个 builder;纯替换 → 在 src/macros.ts 用defineMacro注册; - 打开控制台确认无
No character metrics警告,必要时借助 dockers/fonts 重新生成字体度量; - 跑
pnpm test:jest补充/验证单测(改动 Parser 必须加测试); - 改动较大时用 dockers/screenshotter 跑截图测试,并用 dockers/texcmp 与 LaTeX 输出对比;
- 依次通过
pnpm test:lint与pnpm test:ts; - 更新 docs/supported.md 与 docs/support_table.md;
- 遵循 Angular Commit 规范提交 PR,关联 issue,等待 CI 与评审。
结语
KaTeX 的贡献门槛设计得相当清晰:符号、函数、宏三条路径各自对应独立的注册机制(defineSymbol/defineFunction/defineMacro),配合 Jest 结构测试、截图视觉测试、lint 与类型检查四道质量闸门,即使只改动几十行代码,也能在合入前获得充分的正确性保障。希望本文能帮助你在为 KaTeX 补齐符号与函数的过程中,同时深入理解一个成熟数学排版引擎的“解析—构建—渲染”分层架构。
【免费下载链接】KaTeXFast math typesetting for the web.项目地址: https://gitcode.com/GitHub_Trending/ka/KaTeX
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考