中文网页实现标点挤压

本文具体的实现代码,可查看CJ Typographer

在中文网页中,常常会遇到标点符号连续出现的情况。如果不对标点符号之间的间隔做调整,可能会留下过多的空隙,显得不美观。这时可以对标点进行挤压,保持版面美观。

标点挤压前后对比

关于标点的分类与挤压规则,可参阅标点符号的挤压

实现思路

要在网页上实现标点挤压,可利用OpenType字体的halt(Alternate Half Widths)功能,将全角字符以半角的形式显示。大部分日文字体以及部分中文字体,如思源黑体,便支持该功能。在网页上,只需要设置CSS属性 font-feature-settings: "halt" 便可打开该特性。所以,需要在HTML页面上搜索出需要应用该功能的字符,用 <span> 元素将这些字符单独包裹起来,然后设置CSS属性。

在开始搜索之前,可以了解一下HTML的结构。HTML是树形结构,根结点为 html 元素。可以包含文字的结点基本只有 TEXT_NODE,必然为叶子结点,而 ELEMENT_NODE 可以包含子结点。若不考虑结点的文字前后相连的情况,使用任何一种树遍历的方式,在子结点中使用正则表达式进行搜索替换即可。但由于考虑到行内元素带来的相邻结点文字相连的情况,所以需要设计一个合适的遍历算法。

结点遍历

由于前一个结点的最后一个字符要传递给后一个结点,后一个结点的第一个字符要传回给前一个结点,所以使用先右兄弟结点,再第一个子结点的顺序,对树进行遍历,对找到的叶子结点进行排版优化。

除此之外,在向前或向后传递字符时,还需考虑更多的情况:

  • 若当前元素为非行内元素,应向前和向后皆传递空字符。在代码中由 isInline 函数进行判断。
  • 若当前元素可以被忽略时,如行内元素没有文本内容,或被隐藏,此时可忽略该元素并将前后两个元素相接。在代码中由 isIgnorable 函数进行判断。
  • 若当前元素应该被直接跳过时,如 <code> 元素,不应再处理其子结点,且应向前和向后皆传递空字符。在代码中由 shouldSkip 函数进行判断。
function traverseAndStyle(node, 
           precedingChar = "", 
           followingChar = "") {
  let inline = isInline(node);
  let ignorable = isIgnorable(node);
  let shouldSkip = shouldSkip(node);

  /* 处理右兄弟结点 */
  if (node.nextSibling) {
    let lastChar = node.textContent.length > 0 ? node.textContent[node.textContent.length-1] : "";
    if (!inline) {
      lastChar = "";
    } else if (ignorable) {
      lastChar = precedingChar;
    }
    followingChar = this.traverseAndStyle(node.nextSibling, lastChar, followingChar);
  }

  let firstChar = node.textContent.length > 0 ? node.textContent[0] : "";
  /* 处理第一个子结点 */
  if (node.hasChildNodes()) {
    firstChar = traverseAndStyle(node.childNodes[0], precedingChar, followingChar);
  } else {
    applyStyling(node, precedingChar, followingChar);
  }

  if (ignorable && inline)
    firstChar = followingChar;

  return firstChar;
}

最后,只要使用正则表达式对叶子结点里的内容逐一进行匹配与替换即可。

除了标点压缩,在同一框架下,还能实现中西文混排时,汉字与拉丁字母、阿拉伯数字之间自动增加间隔。本文提到的两个需求皆已在CJ Typographer中实现,本网站也已用上该脚本进行排版优化。推荐对网页排版有要求的人使用!