Formula
math排版数学公式,分式、矩阵、求和积分都按真正的二维版式呈现
用法
为什么需要它
同一串题库数据:直接当文本渲染会露出原始记号,交给 Formula 才是数学排版。这就是该不该用它的判据。
将 \frac{3}{8} 化成小数为 ____ ,并比较 \sqrt{2} 与 \frac{3}{2} 的大小。
将 化成小数为 ,并比较 与 的大小。
const src = "将 \\frac{3}{8} 化成小数为 ____ ,并比较 \\sqrt{2} 与 \\frac{3}{2} 的大小。"
{/* 直接当文本:露馅 */}
<p>{src}</p>
{/* 真数学排版 */}
<Formula>{src}</Formula>裸记号与填空槽
上游还没把公式包成 $…$ 时(PDF/Word/OCR 抽出来的题面就是这样),整串退到裸记号切分,题面照样排得出来;____ 渲染成可书写的空位,而不是四个下划线。
将 化成小数为 ,此时 。
化成小数为 (blankWidth=4)
段内也认: 与
{/* 没有一个 $,公式边界由切分器认 */}
<Formula>{"将 \\frac{3}{8} 化成小数为 ____"}</Formula>
{/* 填空槽在 $ 外面也认 */}
<Formula>{"$\\frac{3}{8}$ 化成小数为 ____"}</Formula>常用记号
题面高频记号写成什么、排出来什么,上下对照。这不是能力清单 —— 背后是 KaTeX,完整 LaTeX 都支持。
\frac{16}{9}\sqrt{a^{2}+b^{2}} 与 \sqrt[3]{8}与
y=ax^{2} 与 90^\circ与
a_{1}+a_n=S_\beta可记作____万元可记作万元
\overline{AB} 与 \widehat{ABC}与
\overrightarrow{AB} 与 \vec{a}与
\overset{\frown}{AB}\mathbb{Q}\subset\mathbb{R}\text{甲组}与\mathbf{乙组}与
\left(\frac{a+b}{c}\right)^{n}\{x\mid x>0\}\angle ABC\cong\triangle DEFconst NOTATIONS = [
{ label: "分数", src: "\\frac{16}{9}" },
{ label: "根号 / 根指数", src: "\\sqrt{a^{2}+b^{2}} 与 \\sqrt[3]{8}" },
// …
]
<div className="grid gap-3 sm:grid-cols-2">
{NOTATIONS.map(({ label, src }) => (
<div key={src} className="rounded-lg border border-border p-3">
<div className="text-xs text-muted-foreground">{label}</div>
<code className="mt-1 block font-mono text-xs break-all text-muted-foreground">{src}</code>
<p className="mt-2 border-t border-border pt-2 text-base leading-8">
<Formula>{src}</Formula>
</p>
</div>
))}
</div>分段函数
高中函数题的主力题型。拍平成一行、行分隔符变成分号的话题干就读不懂了 —— 这里是真正的二维排版。
<Formula>{"$$f(x)=\\begin{cases} -x^{2}-2ax-a, & x<0 \\\\ e^{x}+\\ln(x+1), & x \\geq 0 \\end{cases}$$"}</Formula>中文与公式混排
默认只排版分隔符里的内容,分隔符外原样输出 —— 边界由上游数据显式携带,渲染层不猜。
已知函数 在区间上单调递增,求 的值。
<Formula>{"已知函数 $f(x)=x^{2}$ 在区间上单调递增,求 $f(1)$ 的值。"}</Formula>大型定界符
括号高度跟着内容长,而不是一个定高的字符括号。
<Formula mode="math" display>{"\\left( \\frac{a+b}{c} \\right)^{n}"}</Formula>求和 / 积分 / 极限
块级排版下,上下限落在符号的正上下方,而不是挤成角标。
<Formula mode="math" display>{"\\int_{0}^{1} x^{2}\\,dx = \\frac{1}{3}"}</Formula>矩阵
pmatrix / bmatrix / vmatrix 都认,定界符各自不同。
<Formula mode="math" display>{"\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}"}</Formula>坏数据看得见
认不出的控制序列就地标红、原样露出,其余部分照常排版 —— 安静地渲染成看起来对的东西才最危险。
<Formula mode="math">{"\\begin{cases}x=my\\y^2=6x\\end{cases}"}</Formula>何时用
判据:这段文本里有没有 `\frac{}{}` / `x^{2}` / `____` 这类记号。 有就用它,没有就用普通 <p>。典型场景是题干、选项、解析、公式说明;数据多半来自 PDF/Word 抽取的题库,那种串直接当纯文本渲染,屏幕上就是字面的 \frac{3}{8} 而不是上下叠放的分数。
不要用它渲染整段富文本(用 Markdown)或代码(用 Code)。
0.25.0 起这是本库唯一的数学渲染路径
此前还有个零依赖的 MathText,用 CSS 拼行内版式(inline-flex 叠分数、border-t 当根号横线)。它在 0.25.0 退役并从主 barrel 移除了。原因不是「能力不够」,是排出来的东西不对:
√是个定高字符,而横线是旁边兄弟盒的border-t。被开方数一旦含上标(\sqrt{a^{2}+b^{2}}),内容盒变高变宽,横线就接不上根号顶点,末尾那个指数还顶到线外;- 弧与帽子(
\overset{\frown}{AB}、\widehat{ABC})不跟随内容宽度,中文教材里跨 AB 两个字母的弧被排成 A 头上一顶帽子; - 这类缺陷是 CSS 拼贴的固有极限 —— 分数线粗细、上下标基线、定界符高度,修完一处还有下一处。
而它当初的卖点「零依赖换不撑乱中文行高」,实测在 KaTeX 下同样成立:行内公式不会撑开行距。那个差异一直是假的,代价却是全线错误的排版。
从 MathText 迁移:
| 原来 | 现在 |
|---|---|
import { MathText } from "@hulianui/ui" | import { Formula } from "@hulianui/ui/math" |
import { QuestionCard } from "@hulianui/ui" | import { QuestionCard } from "@hulianui/ui/math" |
<MathText>{stem}</MathText> | <Formula>{stem}</Formula> |
mathToPlain(src) | mathToPlain(src)(同名同义,改从 @hulianui/ui/math 引) |
parseMath / parseMathDocument | 不再导出 —— 它们是给 MathText 自定义渲染用的,排版已由 KaTeX 接管 |
delimiters={true} | 不需要了:mixed 模式默认就认 $,没有 $ 时自动退到裸记号切分 |
scriptScale | 不再有 —— 上下标尺寸由 TeX 的排版规则决定,不该由调用方拨 |
blankWidth(填空槽宽度)原样保留。视觉上会有两处变化,都是改对了而不是回归:变量按 TeX 规矩显示为斜体;公式比周围正文约大 1.21 倍(见「禁忌 / 坑」)。
用法
// 混排(默认):只有分隔符里的内容被排版
<Formula>{"已知 $f(x)=x^{2}$,求 $f(1)$。"}</Formula>
// 分段函数
<Formula>{"$$f(x)=\\begin{cases} -x^{2}, & x<0 \\\\ e^{x}, & x \\geq 0 \\end{cases}$$"}</Formula>
// 写死一条公式,省掉包 $ 的仪式
<Formula mode="math" display>{"\\int_{0}^{1} x^{2}\\,dx = \\frac{1}{3}"}</Formula>分隔符
mode="mixed"(默认)下认四种,分隔符本身不进渲染结果:
| 写法 | 排版 |
|---|---|
$…$ | 行内 |
\(…\) | 行内 |
$$…$$ | 块级(独立成行、居中) |
\[…\] | 块级 |
三条边界规则:
- `\$` 是字面美元符号,不参与配对,渲染成
$。 - 找不到闭分隔符时,开分隔符按字面文本处理。
定价 $100 元整句原样输出,不会把后半段吞成公式。 - 行内分隔符不跨空行。这是 TeX 自己的规则(
$内出现空行是Missing $ inserted),顺带把售价 $100\n\n成本 $80这类跨段误配对挡在外面。块级$$/\[不受此限。
为什么渲染层必须认 $
中文与公式混排时,「哪一段是式子」是上游已经知道的信息。渲染层不认,上游就只能在入库时把 $ 剥掉来迁就它 —— 而剥 $ 是有损的:
$\{a_n\}$剥完变成{a_n},{}是集合还是 LaTeX 分组再也分不出来;- 喂给 LLM 时公式与中文粘成一片,模型只能猜哪一段是式子;
- 要做 Word 导出(LaTeX→MathML→OMML)时,切不出公式段就无从转换。
边界是必须显式携带的信息,不该由渲染层猜、更不该逼上游把它删掉。上游有 `$` 就别剥。
没有 $ 的存量数据
整串一个成对分隔符都没有时,自动退到裸记号切分:扫出 \frac{3}{8}、x^{2}、\angle ABC 这类片段交给 KaTeX,其余按文本原样输出。PDF/Word/OCR 直出的题面就是这样,上游还没来得及包 $ 时不该整题露出字面记号。
切分只认一条判据:没有 `\` / `^` / `_` 这类触发字符就不是公式。所以 P(2,3)、选项标号 A.、(a+b) 一律留作文本 —— 宁可少排也不误排,把中文正文喂给 KaTeX 会得到一串红色报错,比不排版糟糕得多。
它是兜底,不是推荐做法。 只要有一处成对分隔符,整串就走精确路径、不再猜边界;同一串里半带半不带 $,没包的那半会原样露出 —— 这是有意的,数据不一致要看得见。规范做法始终是让上游把公式包成 $…$。
需要自己处理切分结果时用 splitBareMath(src),判断要不要走 KaTeX 这条贵路径用 hasBareMath(src)。
填空槽
____(2 个及以上连续下划线)渲染成可书写的空位,宽度由 blankWidth 控制(默认 2.5em)。单个 _ 仍是下标。
分隔符内外都认,两处各举一例:
| 写法 | 填空槽位置 |
|---|---|
$\frac{3}{8}$ 化成小数为 ____ | 段外 |
$\overrightarrow{AC}=___$ | 段内 |
段内那种不是可有可无的形态:待填的正是那个向量表达式的值,把 $ 断在下划线前只会让边界更难写。
两处的实现不同,无障碍行为因此有差异:
- 段外是真 DOM 空位,带
aria-label,读屏读到的是「填空」而不是一串下划线。 - 段内由 KaTeX 排(
\rule),公式结构完整 —— 填空落在分数分子、根号里都成立(\frac{___}{2}、\sqrt{___})—— 但 KaTeX 输出里挂不上 `aria-label`,读屏读的是 MathML。要让读屏念出「填空」,把填空槽移到$外面。
段内的线随字号缩放,段外那条是 1px;正文字号下两者观感一致,放大到 1.5em 以上时段内会略粗。
Props
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
children | string | - | LaTeX 源,或含 LaTeX 段落的正文 |
mode | "mixed" | "math" | "mixed" | mixed 认分隔符、只排版分隔符内;math 整串都是 LaTeX |
display | boolean | false | 块级排版。仅 `mode="math"` 生效 —— mixed 下由各段自己的分隔符决定 |
blankWidth | number | 2.5 | 填空槽(____)最小宽度,单位 em |
macros | Record<string, string> | - | 自定义宏,透传给 KaTeX |
className | string | - | - |
配套纯函数
splitMathSegments(src)→MathSegment[],把正文切成{ type: "text" \| "math", content, display }。Word/OMML 导出链路要的就是它 —— 拿原始 LaTeX 切段,再逐段转换。splitBareMath(src)→BareSegment[],把没有 `$` 的正文切成{ type: "text" | "math" | "blank", content }。hasBareMath(src)→boolean,整串有没有可切出来的公式或填空槽。formulaToPlain(src)→string,转成可检索的朴素文本($\frac{3}{8}$→3/8),分隔符不进结果。mathToPlain(src, { delimiters })→string,同一套降级的底层实现,delimiters决定认不认$。
检索、导出、纯文本比对一律用 `formulaToPlain`,别把带记号的原串直接甩给搜索框 —— 用户搜「3/8」应该能命中。
题目域(与 QuestionCard 同住此路径)
import {
gradeObjective, validateQuestion, defaultShape, normalizeOptions, blankCount,
splitStemFigures, toWireAnswer, fromWire, answerText,
type Question, type QuestionType, type QuestionAnswerValue,
} from "@hulianui/ui/math"validateQuestion/defaultShape/normalizeOptions/blankCount:题型驱动的形状规则,与消费方后端_check_type_shape同构。splitStemFigures:题干先切图再排公式,判据在stem-figures.contract.json。gradeObjective:客观题判分,默认档与服务端逐字同口径;归一 / 容差 / 等价比较器均 opt-in。服务端才是判分 SSOT。answerText:答案 JSON → 人读文本,按形状分派。MathTextarea:录题用的公式输入框(模板 / 自检 / 预览),见 MathTextarea。QuestionEditor:一道题的结构化编辑(七型 / 题图 / 选项 / 填空 / 分步给分 / 预览),见 QuestionEditor。QuestionAnswer:学生作答卡(按题型给控件 / 选项缺失明说 / 主观题只读 / canSubmit / 结果区),见 QuestionAnswer。
坏数据怎么显示
KaTeX 配了 throwOnError: false,出错分两档,都不静默吞、都不抛异常拆掉整棵树:
- 认不出的控制序列 → 就地标红、原样露出(
\y显示成红色的\y),其余部分照常排版; - 整体解析失败(如花括号没闭合)→ 整条原文红色显示,带
katex-error类。
立场是损坏的公式必须看得见。安静地把损坏的 \begin{cases}x=my\y^2=6x\end{cases} 渲染成看起来正常的一行,比直接报错危险得多 —— 那种「最像真的」的产物没人会发现它错了。
禁忌 / 坑
- 别把 Formula 加进 `@hulianui/ui` 主 barrel。subpath 的全部意义就是让 KaTeX 只落在用得上它的页面。一旦进主 barrel,所有消费者都开始付这 86KB —— 体积门禁里
math入口的基线就是拿来盯它的。 - 一屏几十个实例时,KaTeX 排版是页面上最贵的一步。组件已经是 memo 的,但父级传新对象(尤其
macros)照样会让它整屏重排。真到瓶颈时的出路是服务端预渲染(本件 RSC 安全,排版可以整段发生在服务端)或列表虚拟滚动,不是换一个「更轻的排版」—— 那条路走过了,省下的成本换来的是错的版式。 - `macros` 要提到模块级常量。组件是 memo 的,行内字面量每次渲染都是新对象,memo 每次失效 —— 而失效的代价正是最贵的那一步。(组件内部会浅拷贝
macros再交给 KaTeX:KaTeX 把它当可变宏表用,\def会写回去,不拷贝的话一道题里的\def就漏到后面所有公式上。) - `textContent` 里带着原始 LaTeX。KaTeX 会把源码原样塞进 MathML 的
<annotation>(读屏与复制用),所以container.textContent会同时包含排版结果与\begin{cases}…原文。写测试或做文本提取时对.katex-html取,别对整个容器取。 - 公式比周围正文大约 1.21 倍。这是 KaTeX(也是 TeX)的标准视觉尺寸,不是 bug。要压平就自己覆盖
.katex { font-size: 1em },代价是符号相对中文会偏小。 - 块级公式别塞进 `<p>` 的中间。
$$…$$会渲染出display:block的盒子,夹在一行中文里会把这行劈成三段。块级公式应当独占一个段落。 - `mode="math"` 时 `display` 才有效。
mixed下传display不报错也不生效 —— 混排里每段的行内/块级由它自己的分隔符决定,看版式发现不了你传错了(行内公式照常渲染,只是你以为的块级没出现)。要块级就写$$…$$。 - `formulaToPlain` 的输出别拿去做 OMML 导出的输入。它底层走的是零依赖的轻量解析器,
\begin{cases}会被拍平 —— 而导出链路要的恰恰是被拍掉的行结构。导出请用splitMathSegments切段 + 原始 LaTeX。 - 段内填空槽读屏读不出「填空」。段外是真 DOM 空位(带
aria-label),段内是 KaTeX 排的\rule—— KaTeX 输出里挂不上 aria,读屏读的是 MathML。无障碍要求严格的题面,把填空槽写在$外面。 - `strict` 关着。KaTeX 默认会对数学模式里的裸中日韩字符 console.warn,一屏几十道题就是几百条警告,因此本组件设了
strict: "ignore"。渲染结果不受影响,但也意味着 KaTeX 不会再提醒你「这段中文应该包\text{}」。 - 组件返回
<span>;KaTeX 输出 HTML + MathML 双份,HTML 那半带aria-hidden,读屏读的是 MathML,无需额外配 aria。
相关
- MathField —— 可视化公式键盘,独立子路径
@hulianui/ui/math-field(可选 peer mathlive),另给createCasComparator第 3 档判分 - MathTextarea —— 公式输入框,预览内部就是本组件;同住
@hulianui/ui/math - QuestionEditor —— 出题编辑器,题干 / 选项 / 预览内部就是本组件;同住
@hulianui/ui/math - QuestionAnswer —— 学生作答卡,题干 / 选项 / 结果区内部就是本组件;同住
@hulianui/ui/math - QuestionCard —— 题目卡片,题干/选项内部就是本组件;同住
@hulianui/ui/math - Prose —— 长文排版容器
- Markdown —— 整段富文本
Playground
<Formula>{"$$f(x)=\\begin{cases} x^{2}, & x<0 \\\\ e^{x}, & x \\geq 0 \\end{cases}$$"}</Formula>