在实现一个清理 HTML 冗余标签(word 粘贴过来的)功能,最简单的,莫过于:
// MSWordHtmlCleaners.js https://gist.github.com/ronanguilloux/2915995
cleanPaste : function(html) {
// Remove additional MS Word content
html = html.replace(/<(\/)*(\\?xml:|meta|link|span|font|del|ins|st1:|[ovwxp]:)((.|\s)*?)>/gi, ''); // Unwanted
// tags
html = html.replace(/(class|style|type|start)=("(.*?)"|(\w*))/gi, ''); // Unwanted
// sttributes
html = html.replace(/<style(.*?)style>/gi, ''); // Style tags
html = html.replace(/<script(.*?)script>/gi, ''); // Script tags
html = html.replace(/<!--(.*?)-->/gi, ''); // HTML comments
return html;
},
不过,由于某些正则功能的缺失,网页的 JS 正则是不能完全匹配对应标签的。故所以这种通过正则过滤的办法是有潜在问题的。于是考虑另外一种方法,如老外写的这个 http://booden.net/ContentCleaner.aspx ,它是通过 DOM 方法删除标签的,——我觉得可行,就拿来改造。遇到的问题不少,记录如下。
首先,他依赖 jQuery,我希望是原生的,开始我以为修改下问题不大,但后来发现没那么容易,首当其冲的是 DOM 遍历。写一个遍历并递归 DOM 的代码不困难,麻烦在于,当你使用 for 遍历,集合的总数是固定的(length),而当你删除了一个元素,等于破坏了这个数组,数组下标也乱了。于是改为 while(firstChild) 循环的方法,有点链表那样的写法,总是会出现死循环的现象,可能是指针有误。
苦于没有办法快放弃之际,忽然想起老外那个代码有段函数 removeComments(),想想那正是原生的遍历方法,它是删除注释节点后而继续迭代的。于是拿来改造用,并得到成功!
后来想想,可能这就是迭代器与 for 的区别,因为集合里面的元素允许动态变化,用 for 遍历肯定有问题,这时就适宜用迭代器,诸如 hasNext()、getNextItem() 的 API,尽管会啰嗦点。
在删除 DOM 元素属性时,也遇到这种问题。例如下面代码,每次只能删除一个属性,而不是全部!
var attrs = el.attributes;
for (var i = 0; i < attrs.length; i++) {
console.log(attrs[i]);
el.removeAttribute(attrs[i].name);
}
解决方法是如 MDN 所说的,
for(var i = attrs.length - 1; i >= 0; i--) {
var name = attrs[i].name;
if (attributesAllowed[tag] == null || attributesAllowed[tag].indexOf("|" + name.toLowerCase() + "|") == -1){
node.removeAttribute(name);
}
}
最后,完整的清理 HTML 代码如下
// 清理冗余 HTML
cleanHTML(){
// 类似于 白名单
var tagsAllowed = "|h1|h2|h3|p|div|a|b|strong|br|ol|ul|li|pre|img|br|hr|font|";
var attributesAllowed = {};
attributesAllowed["div"] = "|id|class|";
attributesAllowed["a"] = "|id|class|href|name|";
attributesAllowed["img"] = "|src|";
this.everyNode(this.iframeBody, node => {
var isDelete = false;
if (node.nodeType === 1) {
var tag = node.tagName.toLowerCase();
if (tagsAllowed.indexOf("|" + tag + "|") === -1)
isDelete = true;
if (!isDelete) { // 删除属性
var attrs = node.attributes;
for(var i = attrs.length - 1; i >= 0; i--) {
var name = attrs[i].name;
if (attributesAllowed[tag] == null || attributesAllowed[tag].indexOf("|" + name.toLowerCase() + "|") == -1){
node.removeAttribute(name);
}
}
}
} else if (node.nodeType === 8) {// 删除注释
isDelete = true;
}
return isDelete;
});
},
everyNode (el, fn) {
var objChildNode = el.firstChild;
while (objChildNode) {
if (fn(objChildNode)) { // 返回 true 则删除
var next = objChildNode.nextSibling;
el.removeChild(objChildNode);
objChildNode = next;
} else {
if (objChildNode.nodeType === 1)
this.everyNode(objChildNode, fn);
objChildNode = objChildNode.nextSibling;
}
}
}
附:测试的标签
<!-- test -->
<h3>Some supported heading</h3>
<h6>non supported heading</h6>
<style type="text/css">
font {
font-family: arial;
font-size: 140%;
}
</style>
<pre id="script">dynamically added text.</pre>
<font color="red">RED</font>
<b onclick="alert('BOLD')">bold</b>
<!-- What to do with this? -->
<!--? test ?-->
<i>italic<script>$("#script").text('dynamically added text.');</script></i>
<span style="FONT-SIZE: 10pt; FONT-FAMILY: Arial"><span style="FONT-SIZE: 10pt; FONT-FAMILY: Arial">
<p class="MsoNormal"><b style="mso-bidi-font-weight: normal">
<span style="FONT-SIZE: 10pt; BACKGROUND: red; COLOR: #ff9900; mso-bidi-font-family: Arial; mso-highlight: red">Sample MS Word content</span></b>
<b style="mso-bidi-font-weight: normal"><span style="FONT-SIZE: 10pt; COLOR: #ff9900; mso-bidi-font-family: Arial">
<!--?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /--><o:p></o:p></span></b></p></span></span>