EscCode Demo
撰写时间:2023-04-30
修订时间:2025-09-25
HTML的转义问题
在HTML中若要显示代码,一般使用code标签。如显示JavaScript代码:console.log('Hello');等。但如果要显示HTML代码,则需特别注意两个特殊字符:<及>。只要遇到成对的这两个字符,浏览器在解析阶段就会认为里面包含的就是特定的HTML元素。因此,若要显示这两个字符,我们必须经过转义
(Escaping
),即在HTML代码中分别使用<及>来代替它们,以此来告诉浏览器,请不要将它们解析为标准的HTML元素。
例如,假设我们要在代码中显示<div>aaa<div>。如果我们在HTML中如实编写,则会出现下面的效果:
aaa
结果只有aaa
,前后的<div>及</div>不见了,这是因为浏览器将其解析为这是一个div元素,因此按div元素的样式来渲染该元素。在本页面中,只有一个CSS样式,它为每个div元素都加了边框:
因此,在本页面中,凡是出现了边框的,都可以证明是被浏览器解析为div元素。对于div元素,上面的显示效果没有任何错误。但这不是我们所想要的效果,我们的目标是:要原汁原味地显示<div>aaa<div>。因此,我们必须经过转义。应这样编写代码:
&lt;div&gt;aaa&lt;/div&gt;
为什么出现了&?因为<是字符<的转义。浏览器又要试图将其解析为<。为避免出现这种情况,我们需要对&这个第三个特殊字符进行转义:在代码中使用&来取代它。
唉,又丑又麻烦。实际上,当写上面这段代码时,为了要达到显示这段代码的真实效果,我被迫又对&进行了第三次的转义。真的是太累了。
因此,我决定编写一个自定义的HTML标签esc-code来简化此问题。
不转义
在HTML网页中编写下面代码:
aaa
bbb
ccc
这里,esc-code标签没有escaped属性,因此不需要转义,让浏览器自由解析。效果:
aaa
bbb
ccc
浏览器按div标签来解析,因此出现了边框。
转义
在HTML网页中编写下面代码:
aaa
bbb
ccc
这里,esc-code标签加了一个escaped属性,因此需要转义。经转义后,浏览器自然认为它们不再是div标签。效果:
aaa
bbb
ccc
HTML代码得以原汁原味地输出。需注意的是,上面的断行信息丢失了,5行变成了1行。这是因为原始的code只不过是一个行内(inline)标签,若要保留断行信息,应在其外面加一个pre标签,如<pre><code></code></pre>,由pre标签来应对是否需要保留空白字符及如何断字的问题。
与pre标签组合使用
不转义
代码:
aaa
bbb
ccc
效果:
aaa
bbb
ccc
因为不转义,因此解析输出为div标签,都加上了边框。
转义
代码:
aaa
bbb
ccc
效果:
aaa
bbb
ccc
有escaped属性,因此需要转义。经转义后,得以输出原始代码。
需注意的是,为保持HTML网页代码的一致性,上面的代码如同其他代码一样,向右缩进对齐,因此,在渲染效果中也向右缩进了。这显然也不是我们想要的效果。但这不能归责于esc-code,这是pre标签的责任。因此,我们仍有必要再创建一个自定义的trim-pre标签来取代标准的pre标签。详见另一篇TrimPre Demo。
转义更多细节
3个属性值
let pElement = pc.appendHTMLStr(`Hello
`)[0];
pc.log(pElement.textContent);
pc.log(pElement.innerText);
pc.log('%s', pElement.innerHTML);
textContent与innerText属性值将只提取标签内容的纯文本信息,p的内部标签span作为标签而存在的文本内容已经丢失。
只有innerHTML才能将内部标签span保留下来,然后再作为一个HTML标签进行解析,因此颜色为灰色。
可见,如果需转换带有内部标签的标签,须使用其innerHTML属性值。
let pElement = pc.appendHTMLStr(`Hello
`)[0];
let leftReg = new RegExp('<', 'g');
let rightReg = new RegExp('>', 'g');
let str = pElement.innerHTML.replace(leftReg, '<').replace(rightReg, '>');
let output = pc.appendHTMLStr(``)[0];
output.textContent = str;
innerHTML的自动补全
下面代码:
#include <stdio.h>
int main() {
printf("Hello, world");
return 0;
}
将输出:
#include
int main() {
printf("Hello, world");
return 0;
}
<stdio.h>被解析为一个HTML标签而被吃掉了。
下面是其DOM树内容:
let pElement = document.querySelector('#sample-code');
pc.log(`child nodes length: %d`, pElement.childNodes.length);
for (let child of pElement.childNodes) {
pc.group(`nodeType: %d`, child.nodeType);
pc.log(`tagName: "%s"`, child.tagName);
pc.log(`textContent: "%s"`, child.textContent);
pc.groupEnd();
}
查看 DOM快速参考, 值为3的节点类型为TEXT_NODE,值为1的节点类型为ELEMENT_NODE。
取出其innerHTML。在取出其innerHTML时,发现被误识为HTML标签的<stdio.h>还被自动加上了结尾标签。
let pElement = document.querySelector('#sample-code');
let leftReg = new RegExp('<', 'g');
let rightReg = new RegExp('>', 'g');
let str = pElement.innerHTML.replace(leftReg, '<').replace(rightReg, '>');
pc.log(str);
解决方法
querySelector的参数中,如果代表标签名的字符串中带有.
号,如这里的stdio.h,则无法取出。
let pElement = document.querySelector('#sample-code');
{
let unexpectedNode = pElement.childNodes[1];
pc.log(unexpectedNode.tagName);
}
{
let unexpectedNode = pElement.querySelector(`stdio.h`);
pc.log(unexpectedNode.tagName);
}
我们可以使用TreeWalker来进行带有层级关系的遍历。
let pElement = document.querySelector('#sample-code').cloneNode(true);
let walker = document.createTreeWalker(pElement, NodeFilter.SHOW_ELEMENT);
while(walker.nextNode()) {
pc.log(walker.currentNode.tagName);
if (walker.currentNode.tagName === 'STDIO.H') {
walker.currentNode.replaceWith('<' + walker.currentNode.tagName + '>' + walker.currentNode.innerHTML);
}
}
pc.log(pElement.innerHTML);
对于这种误识的标签,取出其标签名,以<...>
包裹后,再加上其内部的文本内容,作为一个新的文本节点,替换原来的节点。
Esc-Code应对误识的标签
编写以下代码:
#include
#include
int main() {
printf("Hello, world");
return 0;
}
效果:
#include
#include
int main() {
printf("Hello, world");
return 0;
}
将拟不作为标签处理的名称放在non-tags属性内,中间使用逗号,
隔开。当有多个以上的标签需排除时,从上到下依序列出该标签名。但在内部,却优先处理最内层,最后才处理最外层。并且,因需要多次重复替换,故在替换时调用了各标签的textContent属性值,而不是innerHTML属性值。
转义Script
当esc-code内部有script标签时,浏览器将优先解析并运行该代码。
结果就是,代码先被执行,然后代码内容经转义后再显示出来。
此时,若不想代码被自动执行,须事先自行转换。
<script>console.log('Hello');</script>
这样就不会被识别为script标签,其代码就不会被执行。
转义html及body
当innerHTML遇到html及body标签时,会自动吃掉它们。其他标签则不会。因此,这两个标签也须经事先转义。
let src = `
<html>
<body>
Page Title
Main paragraph.
Section Title
Section paragraph.
</body>
</html>
`;
上面源码中这两个标签已经先转义,否则它们无法显示出来。
小结
当使用innerHTML时,可以说,到处都是陷阱。实际上,随着研究的深入,将发现这是一个不小的话题。限于时间及篇幅的原因,本文尚未涵盖所有的相关要点,后续慢慢整理、完善。
Resources
