MZhou's blog

CSS3着重符及其fallback

在东亚国家,人们会在文章中重要文字旁加上小符号以突出其重要性。如下:

标准

在中文里面,我们一般会在文字下方加上圆形符号。在日语中会在文字上方加上小顿号。在CSS3中如下属性可以控制着重符号:

  1. text-emphasis
  2. text-emphasis-style
  3. text-emphasis-color
  4. text-emphasis-position

text-emphasistext-emphasis-styletext-emphasis-color的快捷方式,注意它并不包含text-emphasis-position

text-emphasis-style属性用于控制着重符号的样式。基本的符号形状有dot | circle | double-circle | triangle | sesame这几种,它们又分别有“空心(open)”、“填充(filled)”两种展现形式。当你只填了filledopen是,符号形状的默认值和文字是竖排还是横排(writing-modes)有关,比如横排时默认是filled circle,竖排时是filled sesame。如果你之前填了符号形状,则默认的展现形式是filled。当然你还可以自定义符号,但是只能显示一个字符。

另一个重要的属性是text-emphasis-color,它控制了着重符号的颜色,默认使用当前文字的颜色。

最后一个属性是text-emphasis-position,它有这几种可选值[ over | under ] && [ right | left ]。它的默认值计算方式更为复杂些,与横竖排版和所处语言环境都有关系。横排情况下,中文环境默认值为under,日文环境默认值为over。竖排情况下,中文和日文环境下默认值都为right

在CSS中,一般着重符号的字体大小是其对应文字的一半。且当行高有足够空间来绘制着重符时,它不会影响到对应文字的行高。

在2013年8月1日,这个标准成为“候选推荐标准”,这对喜爱文字排版的人来说是个好消息。遗憾地是目前只有webkit内核的浏览器是支持它的,并且需要使用webkit前缀。所以在使用时需要做fallback。

FALLBACK

在做fallback时,有这么几点是需要考虑的:

  1. 如何应对letter-spacing样式和文字宽度不一致的情况
  2. 如何处理浏览器的最小字体配置
  3. 如何空间是否足够绘制着重符(计算行高)
  4. 如何减少对现有html的影响
  5. 如何获得所处语言环境

对于第一点的解决方案是:对每个字符用span包裹,方法类似于letter.js。然后在每个span内部插入另一个包含着重符的span,它的宽度为百分百,且绝对定位。如下面样例所示:

对于有letter-spacing的情况,可以设置span的letter-spacing为0,然后使用margin-right来替代它。

如果你是用chrome,你可能已经注意到了“最小字体”导致的问题:着重符号太大了。在chrome下着重符号是12px,而不是8px(16/2)。为了解决问题2,我们需要想想其他方法。我首先考虑到的是zoom属性,它支持chrome(所有版本)、safari和IE。可惜的是在chrome下zoom:0.5也不能使字体变小。然后考虑到的是transform:scale(0.5),幸运地是它能使文字比最小字体还要小。不过支持的浏览器不够多,参考这里。所以必须要考虑在不支持transform的时候使用fallback。我的处理方法是使用绝对大小(px)。虽然不能使着重符号字体变小,但是至少可以保证着重符位置正确。

在绘制着重符时,如果行高内有足够的高度,则着重符不会扩大行高。如果高度不够,则扩大行高。第二种情况需要设置display:inline-block; 及padding-bottom,来模拟行高高度的扩大。为了做高度是否充足的判断,我们就需要计算字体大小和行高。当你设置字体大小为1em时,对于IE这样的浏览器,获得地长度其实并不是以px为单位。这时需要一些hack:

// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
var PIXEL = /^\d+(px)?$/i;
function getPixelValue(element, value) {
    if (PIXEL.test(value)) {
        return parseInt(value);
    }
    var style = element.style.left;
    var runtimeStyle = element.runtimeStyle.left;
    element.runtimeStyle.left = element.currentStyle.left;
    element.style.left = value || 0;
    value = element.style.pixelLeft;
    element.style.left = style;
    element.runtimeStyle.left = runtimeStyle;
    return value;
};

幸运地是,jQuery已经处理了这种情况。这样我们就可以得到正确的字体大小和行高(需要特殊处理行高为缩放因子和normal的情况)。不过受到了“文字可能比设置地字体大小更大”、“同行有更高行高的元素(例如图片)”等等特殊情况的限制,导致了计算结果并不一定正确。幸运地是对最终结果的影响并不是很大。

第四个问题指的是innerText/$('em').text()的返回值在做了fallback之后就不再正确了,同时受到影响的还有innerHTML。对于后者我没有想到好的方案。对于前者,我们可以把着重符放在span标签的before伪元素上。这样得到的innerText值还是正确的。不过也引入了另一个问题:如何用js修改before伪元素的样式。我采用的方法是插入css rule,下面有简单的代码。在实际情况下,因为不能删掉css rule,所以需要做好css rule的缓存复用。

var styleSheet;
function addCSSRule(selector, rules) {
    if (!styleSheet) {
        var style = document.createElement('style');
        style.type = 'text/css';
        $('head').eq(0).prepend(style);
        styleSheet = document.styleSheets[0];
    }

    if (styleSheet.insertRule) {
        styleSheet.insertRule(
            selector + '{' + rules + '}',
            styleSheet.cssRules.length
        );
    } else {
        // IE
        styleSheet.addRule(selector, rules, -1);
    }
}

最后一个问题的解决方案则比较简单粗暴。获取navigator.language || navigator.browserLanguage的值判断其所处的语言环境。

jQuery.emphasis.js

解决了这些问题之后,终于得到了一个可用的fallback。再根据标准来修改优化代码,就得到了jQuery.emphasis.js。这里有些它的demo。不过它没有解决所有的问题,目前已知的缺陷如下:

  1. 不支持竖排(即不支持positionright/left
  2. 不支持特殊情况下的inline-block元素(比如默认元素有padding-bottom
  3. 如果浏览器不支持transform,则会有最小字体导致的着重符过大问题

如果你还发现了其他问题,欢迎feedback

MZhou's blog - Taste of life.

zmmbreeze / @zhoumm