当然,我不能修复世界上每个出版系统,但是我可以为自己写个用户脚本来解决,自动转换智能引号和其他有问题的高位字符为等同的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;
}这段代码分为四个步骤:
- 先定义字符替换规则列表,映射某些8位字符到对应的7位字符。
- 获取当前页面中的所有文字结点。
- 遍历文字结点清单。
- 在每个文字结点中,替换每个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 是循环中的当前文本节点,而 s 是 node 中存在文本的字符串变量。我打算使用 s 完成替换操作,然后把处理结果复制回原始节点中。
所以现在我有了单个节点的文本,我需要完成替换操作。因为我已经建好了正则表达式列表和替换字符串列表,所以这相对简单。
for (key in replacements) {
s = s.replace(regex[key], replacements[key]);
}