原价$5,现价$3 变成了公式
主线位置:行内主线。第一遍扫到
$时只看它前后各一个字节,记成可开、可闭的MaybeMath;第二遍按花括号层级配对,配上的一段变成Event::InlineMath(数学与智能标点)。
flowchart TB d1["价$5 的 $"] --> o["后面是 5 能开"] d2["价$3 的 $"] --> c["前面是 价 能闭"] o --> pair["配成一对"] c --> pair pair --> m["InlineMath 5,现价"] d3["空格加 $"] --> nc["前面是空白 不能闭"]
实验在 exp-pulldown-cmark/pita-lab,bin p10_dollar(release),输出存在 out-p10.txt;main 的对照在 exp-pulldown-cmark/pita-mainprobe(origin/main,29d6cfa),输出 out-p10-main.txt。M2 Max、macOS 27、rustc 1.99.0,pulldown-cmark 0.13.5,选项 ENABLE_MATH。
现象
为了在少数页面里写公式,全站打开了 ENABLE_MATH。几种写价格的方式(每段先列事件,略去段落的 Start / End,再列 push_html 的输出):
--- $5 / $10
0..1 Text(Borrowed("$"))
1..5 Text(Borrowed("5 / "))
5..6 Text(Borrowed("$"))
6..8 Text(Borrowed("10"))
<p>$5 / $10</p>
--- $5/$10
0..4 InlineMath(Borrowed("5/"))
4..6 Text(Borrowed("10"))
<p><span class="math math-inline">5/</span>10</p>
--- 原价$5,现价$3
0..6 Text(Borrowed("原价"))
6..18 InlineMath(Borrowed("5,现价"))
18..19 Text(Borrowed("3"))
<p>原价<span class="math math-inline">5,现价</span>3</p>
--- 原价 $5,现价 $3
0..7 Text(Borrowed("原价 "))
7..8 Text(Borrowed("$"))
8..19 Text(Borrowed("5,现价 "))
19..20 Text(Borrowed("$"))
20..21 Text(Borrowed("3"))
<p>原价 $5,现价 $3</p>
--- 原价\$5,现价\$3
0..6 Text(Borrowed("原价"))
7..18 Text(Borrowed("$5,现价"))
19..21 Text(Borrowed("$3"))
<p>原价$5,现价$3</p>
--- 公式 $x^2$ 与 $y$
0..7 Text(Borrowed("公式 "))
7..12 InlineMath(Borrowed("x^2"))
12..17 Text(Borrowed(" 与 "))
17..20 InlineMath(Borrowed("y"))
<p>公式 <span class="math math-inline">x^2</span> 与 <span class="math math-inline">y</span></p>
原价$5,现价$3 里,5,现价 成了一段公式,页面上的 KaTeX 之类会把它排成斜体数学;$5/$10 也被吃掉一半。带空格的 $5 / $10 和 原价 $5,现价 $3 没事。
原因
第一遍 parse_line 的 $ 分支(第 958 行):
b'$' => {
let byte_suffix = &bytes[ix..];
let can_open = !byte_suffix[1..]
.first()
.copied()
.map_or(true, is_ascii_whitespace);
let can_close = ix > start
&& !bytes[..ix]
.last()
.copied()
.map_or(true, is_ascii_whitespace);
后面不是 ASCII 空白就能开,前面不是 ASCII 空白就能闭,不看是不是数字、是不是汉字。英文习惯在 $ 前面留空格,第二个 $ 前面是空白,闭不上,所以 $5 / $10 平安无事;中文不留空格,现价$3 的 $ 前面是 价,能闭,和 原价$5 的 $ 配成一对。$5/$10 的第二个 $ 前面是 /,同理。规则写在 specs/math.txt,它说语法参照 commonmark-hs 的 math 扩展;配对和花括号上下文见数学与智能标点。
main 上已经改了。提交 3d67fea「math: do not allow closer followed by digit」在 can_close 上加了一个条件 !bytes.get(ix + 1).copied().map_or(false, is_ascii_numeric),提交说明第一行说这是 commonmark-hs(specs/math.txt 参照的那个实现)里同一项改动的移植,接着写:
With this change, closing `$` cannot be followed by a decimal digit.
For example, `$1$` is fine, but `$1$2` is not.
它随 PR #1098 合入 main,没有进任何 tag。用 main 跑:
$5/$10 <p>$5/$10</p>
原价$5,现价$3 <p>原价$5,现价$3</p>
运费$8元,满$99包邮 <p>运费$8元,满$99包邮</p>
公式 $x^2$ 与 $y$ <p>公式 <span class="math math-inline">x^2</span> 与 <span class="math math-inline">y</span></p>
「闭合的 $ 后面不能紧跟数字」恰好覆盖了「$ 写在数字前面」的价格写法。0.13.5 里没有这条。
修法
内容里转义:写成
原价\$5,现价\$3,输出就是原文。适合自己能改的内容。只在需要的地方开
ENABLE_MATH:公式只出现在少数文档里,就按文档(比如 frontmatter 里的一个开关)决定开不开,不要全站打开。渲染时把像正文的公式还原:用户写的内容没法要求转义,就在事件流上判断。公式里出现汉字或全角标点,几乎一定是两个价格之间的正文,把它还原成带
$的文字:fn looks_like_prose(m: &str) -> bool { m.chars().any(|c| ('\u{3000}'..='\u{9fff}').contains(&c) || ('\u{ff00}'..='\u{ffef}').contains(&c)) } // Put inline math that looks like prose back as the text it came from. fn unmath<'a>(events: impl Iterator<Item = Event<'a>>) -> impl Iterator<Item = Event<'a>> { events.map(|ev| match ev { Event::InlineMath(m) if looks_like_prose(&m) => Event::Text(format!("${m}$").into()), ev => ev, }) }<p>原价$5,现价$3</p> <p>运费$8元,满$99包邮</p> <p>公式 <span class="math math-inline">x^2</span> 与 <span class="math math-inline">y</span></p>InlineMath只来自单个$,内容是原样交出的,所以$加内容加$就是源文。代价是公式里本来就写汉字的($\text{速度}$)也会被还原;这种公式要求作者用$$或者转义。$5/$10这样内容里没有汉字的,这个判断抓不到,要靠内容转义。
怎么检查
对内容扫一遍 InlineMath,把含汉字或全角标点的列出来:
for (ev, r) in Parser::new_ext(doc, Options::ENABLE_MATH).into_offset_iter() {
if let Event::InlineMath(m) = ev {
if looks_like_prose(&m) {
println!("{r:?} InlineMath({m:?})");
}
}
}
对 原价$5,现价$3。、公式 $x^2$ 照常。、运费$8元,满$99包邮 三段:
6..18 InlineMath(Borrowed("5,现价"))
54..66 InlineMath(Borrowed("8元,满"))
正常的公式不会被报出来。另外在内容里搜「$ 紧跟数字」的写法(正则 \$[0-9]),数一下有多少,决定是转义还是关掉 ENABLE_MATH。
小结
ENABLE_MATH下,$后面不是空白就能开、前面不是空白就能闭;中文不加空格,原价$5,现价$3中间的文字成了InlineMath,$5/$10也会;$5 / $10不会。- main 上的 #1098 规定闭合的
$后面不能紧跟数字,修好了这类价格,还没有发布。 - 修法:内容里写
\$;只在需要公式的文档开ENABLE_MATH;渲染用户内容时把含汉字的InlineMath还原成文字。