KaTeX 贡献实战指南:为开源数学排版引擎添加符号、函数与宏
2026/9/13 15:30:45 网站建设 项目流程

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.jssrc/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 分组类型,如textordmathordrelbinopenclosepunctinner
replace视情况该符号被替换成的字符,例如\phireplace值为\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]; } }

最后一个参数acceptUnicodeChartrue时,会同时把replace对应的 Unicode 字符注册为同一符号,方便用户直接输入 Unicode 字符而非 LaTeX 命令。典型的注册语句如:

defineSymbol(math, main, rel, "\u2261", "\\equiv", true); defineSymbol(math, main, punct, "\u002e", "\\ldotp");

2.2 三步确定新符号的注册参数

  1. 确定 Unicode 字符:把目标命令放到 MathJax 等渲染器中跑一次,观察其输出的 Unicode 码点,以此作为replace值。
  2. 确定分组(group):在 src/symbols.ts 的符号表中寻找同类符号。例如要添加\neq,就去找=所属的分组;若找不到相似参考,可以把新符号与不同类型的符号混排,观察间距是否符合 TeX 的间距规则来推断其分组(关系符、二元运算符、标点等在 TeX 中有不同的自动间距)。
  3. 渲染验证:符号可渲染后,打开浏览器 JavaScript 控制台,确认没有No character metrics for '_'之类的警告。该警告表示当前字体度量数据中缺少该字符,需要重新生成字体与度量文件——相关工具位于 dockers/fonts(包含buildFonts.shbuildMetrics.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—(必填)函数必选参数个数
numOptionalArgs0可选参数个数;找不到可选参数时以null传给 handler
argTypes对应每个参数的类型数组,长度为numOptionalArgs + numArgs,可选参数类型在前
allowedInArgumentfalse是否展开为单个 token 或花括号包裹的一组 token;若可被包裹,就能作为\sqrt(无可选参数形式)或上/下标的参数
allowedInTextfalse是否允许在文本模式中使用
allowedInMathtrue是否允许在数学模式中使用
infix未设置是否为中缀运算符,必须显式置true
primitive未设置是否为 TeX 原语
handler—(通常必填)解析回调,接收(context, args, optArgs),返回一个ParseNode

构建器则通过可选的htmlBuildermathmlBuilder提供,分别返回表示 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-server

pnpm 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.tsmathml-spec.tserrors-spec.tsdup-spec.ts等,见 package.json 中 jest 配置的testMatch)。

常用命令(均来自 package.json 的scripts):

命令对应脚本用途
pnpm test:jestjest运行全部 Jest 测试
pnpm test:jest:watchjest --watch监听模式运行
pnpm test:jest:updatejest --updateSnapshot更新快照(snapshot)
pnpm test:jest:coveragejest --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 build

pnpm 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 提出了明确要求,这也是最终合入主分支的“入场券”:

  1. 标题与描述遵循 Angular Commit Message Conventions,保证提交信息结构统一、可被工具解析;
  2. 尽可能关联原始 issue,方便评审者追溯问题上下文;
  3. 新增命令必须同步更新 docs/support_table.md 与 docs/supported.md,确保支持范围文档与代码保持一致;
  4. 合入前提交应 squash(压缩),保持主分支历史整洁;
  5. 大型 PR 应尽量拆分为多个小 PR,或至少拆成多个逻辑内聚的提交,降低评审负担。

此外,原文档提醒:KaTeX 的贡献者还需要先签署贡献者许可协议(CLA),且项目采用 MIT 许可证(见 LICENSE)。

十一、一张贡献流程图:从符号到合并

将上文各环节串起来,一次典型的贡献旅程如下:

  1. 在交互式编辑器或支持表中确认目标命令确实缺失;
  2. 单个符号 → 在 src/symbols.ts 用defineSymbol注册;函数 → 在 src/functions 新建文件并用defineFunction实现 handler 与两个 builder;纯替换 → 在 src/macros.ts 用defineMacro注册;
  3. 打开控制台确认无No character metrics警告,必要时借助 dockers/fonts 重新生成字体度量;
  4. pnpm test:jest补充/验证单测(改动 Parser 必须加测试);
  5. 改动较大时用 dockers/screenshotter 跑截图测试,并用 dockers/texcmp 与 LaTeX 输出对比;
  6. 依次通过pnpm test:lintpnpm test:ts
  7. 更新 docs/supported.md 与 docs/support_table.md;
  8. 遵循 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询