Typeof.net

用于 CSS 数学排版的一个行内元素砌块

在网页上排版数学公式一直是个麻烦事。依我的个性使用 Mathjax 处理是不可能的,相反我在这个过程里制作了一个简单的行内元素砌块,可以有效地用于公式排版。

作为公式排版的泰斗,T rm kern `-0.166` E rm justraise `-0.25` kern `-0.166` X rmT{E}X 中用于公式内各个部件组合的关键元素是所谓「盒子」(Box)。每一个盒子都有其高度(Height)和深度(Depth),而数学公式就是这些盒子的组合。可以看到,对于简单的文字(如 f(x)f(x) 而言),盒子的高度和深度都是由字体惟一确定的常量,而对于分式(如 a over b{a/b})之类由其他元素组合而来的盒子,盒子的高度和深度则由其内部的元素计算得到:

function FracBox(num, den){
	this.num = num;
	this.den = den;
	this.height = this.num.height + this.num.depth + FRAC_MIDDLE;
	this.depth = this.den.height + this.den.depth - FRAC_MIDDLE;
}

数学公式经常会遇到盒子的纵向堆叠,除上文中分式的例子外,积分符号(int __ 0 ^^ infty{{}0})、各种大型运算符(sum __ {i = 0}^^{n}{n{}i=0})以及矩阵,都是各种盒子的垂直叠加;而这些组合起来的盒子,还必须安置在距离外部基线指定距离处——正如分式的分数线必须和减号对齐一样。因此,在这里我们需要一种 HTML/CSS 砌块,它必须满足:

各位应该很容易想到,这种砌块用可以用两重元素实现:

<r>
	<ri>子盒子1</ri>
	<ri>子盒子2</ri>
</r>

我们把 <r> 设置成 inline-block<ri> 设置成 block,问题不就解决了么?大致是:

r {
	display: inline-block;
}
ri {
	display: block;
}

对于指定盒子位置的问题,我们可以把 <ri> 设置成 position:relative,同时为了避免盒子高度计算不精确造成偏移值不准,把它的高度定为 0,即做成:

r {
	display: inline-block;
}
ri {
	display: block;
	position: relative;
	height: 0;
}

在此基础上,在生成 HTML 时,由于我们知道每个子盒子的高度,子盒子的顶边缘到其内部基线的距离刚好是高度,因此这些盒子的 top 属性可以很容易地计算出来。

// 函数 arrx:输入盒子列表 `boxes` 和每个盒子相对外部基线的偏移 `rises`,输出一个 HTML 砌块。
// height 和 depth 为砌块整体的高度和深度
function arrx(boxes, rises, height, depth){
	var buf = '<r>';
	for(var j = 0; j < boxes.length; j++) if(boxes[j]) {
		buf += '<ri style="top:' + em(height - boxes[j].height - rises[j]) + '">' + boxes[j].write() + '</ri>'
	}
	buf += '</r>'
	return buf;
}

对于子盒子的处理基本上没有难处,但麻烦的是对砌块本身(<r>)的处理。根据规范,inline-block 元素的参考基线是其内部最后一行的基线。这句话说的倒是好听,问题是,每个子盒子里面仍然会有其他的复杂盒子组合,因此计算子盒子最后一行的位置会十分困难,这就导致砌块的自由组合变得困难。

然而方法并不是没有:既然 inline-block 盒子的基线以最后一行计,那么我们为什么不能在 <ri> 之后增加一个额外的、不可见的行来「统一」所有砌块的参考基线位置呢?反正 <ri> 元素的高度都是 0,这些额外元素的参考基线应该是在 <r> 砌块上边缘下方固定的位置:

<r>
	<eb>{</eb>
	<ri>子盒子1</ri>
	<ri>子盒子2</ri>
	<eb>}</eb>
</r>
eb {
	display: block;
	height: 0;
	width: 0;
	opacity: 0;
}

(嗯,我在前后各放了一个,免得一些浏览器把第一行当基线。)

这么一来发现,所有的 <r> 位置都统一到了花括弧的基线上,因此给 <r> 实现「指定高度和深度」就很简单了:给它赋予一个合适的 vertical-alignheight 就行。最终的结果是:

function arrx(boxes, rises, height, depth, cl){
	var buf = '<r style="height:' + em(height + depth) + ';vertical-align:' + em(height - CHAR_ASC) + '"' + (cl ? ' class="' + cl + '"' : '') + '><eb>{</eb>';
	for(var j = 0; j < boxes.length; j++) if(boxes[j]) {
		buf += '<ri style="top:' + em(height - boxes[j].height - rises[j]) + '">' + boxes[j].write() + '</ri>'
	}
	buf += '<eb>}</eb></r>'
	return buf;
}

完整的示例可参阅 http://jsbin.com/nugeqimose/,我在这里实现了简单的分数线和垂直堆叠。

目前,这套砌块已经用于 Typeof.net 中所有公式的排版。效果嘛,是这样的:

int _ 0 ^ infty e ^ {-3 pi r ^2} {sinh rm ~ pi x} over {sinh rm ~ 3 pi x} d rm x = 1 over {e ^ {2 pi / 3} sqrt: 3} sum __ {n = 0} ^^ {infty} e ^ {-2 n (n+1)pi} prod __ {j = 0} ^^ {n} (1 + e ^ {-(2 j+1) pi}) ^ {-2}{}{0}e{3πr{2}}{sinhπx/sinh3πx}dx={1/e{2π/3}{3}}{{}n=0}e{2n(n+1)π}{n{}j=0}(1+e{(2j+1)π}){2}