5.5. 案例:Dumb Quotes

转换智能引号为原始引号

DumbQuotes 是应许多网络博客的需要而生的:大多数出版软件可以自动转换直接的 ASCII 引号为 “智能引号(smart quotes)”,但是当作者在文章中复制粘贴文本时,同样是这些软件处理“智能引号”却很愚笨。常见的例子是博客想引用其他网站的一段文字。在浏览器中选中几句,粘贴到自己网站的表单中提交,这段文字却看起来完全不同,因为他们的出版软件没有处理好字符编码。

当然,我不能修复世界上每个出版系统,但是我可以为自己写个用户脚本来解决,自动转换智能引号和其他有问题的高位字符为等同的7位 ASCII 字符。

例 5.5.  dumbquotes.user.js

// ==UserScript==
// @name          DumbQuotes
// @namespace     http://diveintogreasemonkey.org/download/
// @description   straighten curly quotes and apostrophes, simplify fancy dashes, etc.
// @include       *
// ==/UserScript==

var replacements, regex, key, textnodes, node, s;

replacements = {
	"\xa0": " ",
	"\xa9": "(c)",
	"\xae": "(r)",
	"\xb7": "*",
	"\u2018": "'",
	"\u2019": "'",
	"\u201c": '"',
	"\u201d": '"',
	"\u2026": "...",
	"\u2002": " ",
	"\u2003": " ",
	"\u2009": " ",
	"\u2013": "-",
	"\u2014": "--",
	"\u2122": "(tm)"};
regex = {};
for (key in replacements) {
	regex[key] = new RegExp(key, 'g');
}

textnodes = document.evaluate(
	"//text()",
	document,
	null,
	XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
	null);
for (var i = 0; i < textnodes.snapshotLength; i++) {
	node = textnodes.snapshotItem(i);
	s = node.data;
	for (key in replacements) {
		s = s.replace(regex[key], replacements[key]);
	}
	node.data = s;
}

这段代码分为四个步骤:

  1. 先定义字符替换规则列表,映射某些8位字符到对应的7位字符。
  2. 获取当前页面中的所有文字结点。
  3. 遍历文字结点清单。
  4. 在每个文字结点中,替换每个8位字符为等价的7位字符。

第一步其实是两步。Javascript 的字符串替换基于正则表达式。所以要替换8位字符为等价的7位字符,需要创建一个正则表达式集合。

replacements = {
	"\xa0": " ",
	"\xa9": "(c)",
	"\xae": "(r)",
	"\xb7": "*",
	"\u2018": "'",
	"\u2019": "'",
	"\u201c": '"',
	"\u201d": '"',
	"\u2026": "...",
	"\u2002": " ",
	"\u2003": " ",
	"\u2009": " ",
	"\u2013": "-",
	"\u2014": "--",
	"\u2122": "(tm)"};
regex = {};
for (key in replacements) {
	regex[key] = new RegExp(key, 'g');
}

我使用花括号语法迅速建立了一个关联数组。等价于(但是键入更少)单独把每个关键字和值对应起来:

replacements["\xa0"] = " ";
replacements["\xa9"] = "(c)";
replacements["\xae"] = "(r)";
// 等等

每个8位的字符都是用十六进制值表示的,是用了逃逸语法(escaping syntax)"\xa0""\u2018"。我们完成字符到字符串的关联数组后,我会遍历整个数组,然后建立正则表达式对象列表。每个正则表达式对象会在全局范围搜索8位字符。(第二个参数 'g' 意义为全局搜索;否则每个正则表达式只会搜索和替换第一次出现的特定8位字符,这样我可能会漏掉很多。)

下一步是获取当前文档中的全部文本节点。您可以非常想说,“嗨,我只要用 document.body.innerHTML 就得到全部页面的字符串,然后搜索替换就成了。

var tmp = document.body.innerHTML;
// 在 tmp 上完成一批搜索和替换
document.body.innerHTML = tmp;

但这是个坏习惯,因为 innerHTML 会返回页面中的全部源代码:所有的标签、所有的脚本、所有的属性等等。在这种情况下,有可能不会造成问题(HTML 标签并不包含8位字符),但它在其他情况下不堪设想,很难调试。你要问你自己到底要搜索和替换什么。如果答案是“原始的页面源代码”,那么就去使用 innerHTML。然而在这种情况下,答案是“全部的页面文字”,所以正确的方法是使用 XPath 查询获取所有文本节点。

textnodes = document.evaluate(
	"//text()",
	document,
	null,
	XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
	null);

这里使用了 XPath 函数,text(),用来匹配任意文本节点。您可能对查询元素节点很熟悉了:所有 <a> 元素集,或者所有具有 alt 属性的 <img> 元素集。但是 DOM 也包含有节点中存在的文本内容。(也有其他类型的节点,例如注释和处理指令。)现在它也是我们感兴趣的文本节点。

第3步是遍历所有文本节点。这完全跟遍历像 //a[@href] XPath 查询返回的节点集一样;唯一的不同就是遍历中的是文本节点,而不是元素节点。

for (var i = 0; i < textnodes.snapshotLength; i++) {
node = textnodes.snapshotItem(i);
s = node.data;
// 做替换操作
node.data = s;

node 是循环中的当前文本节点,而 snode 中存在文本的字符串变量。我打算使用 s 完成替换操作,然后把处理结果复制回原始节点中。

所以现在我有了单个节点的文本,我需要完成替换操作。因为我已经建好了正则表达式列表和替换字符串列表,所以这相对简单。

    for (key in replacements) {
s = s.replace(regex[key], replacements[key]);
}
← 案例:Offsite Blank
案例:Frownies →