Web编程技术营地
研究、演示、创新

EscCode Demo

撰写时间:2023-04-30

修订时间:2025-09-25

HTML的转义问题

HTML中若要显示代码,一般使用code标签。如显示JavaScript代码:console.log('Hello');等。但如果要显示HTML代码,则需特别注意两个特殊字符:<>。只要遇到成对的这两个字符,浏览器在解析阶段就会认为里面包含的就是特定的HTML元素。因此,若要显示这两个字符,我们必须经过转义Escaping),即在HTML代码中分别使用&lt;&gt;来代替它们,以此来告诉浏览器,请不要将它们解析为标准的HTML元素。

例如,假设我们要在代码中显示<div>aaa<div>。如果我们在HTML中如实编写,则会出现下面的效果:

aaa

结果只有aaa,前后的<div></div>不见了,这是因为浏览器将其解析为这是一个div元素,因此按div元素的样式来渲染该元素。在本页面中,只有一个CSS样式,它为每个div元素都加了边框:

因此,在本页面中,凡是出现了边框的,都可以证明是被浏览器解析为div元素。对于div元素,上面的显示效果没有任何错误。但这不是我们所想要的效果,我们的目标是:要原汁原味地显示<div>aaa<div>。因此,我们必须经过转义。应这样编写代码:

&amp;lt;div&amp;gt;aaa&amp;lt;/div&amp;gt;

为什么出现了&amp;?因为&lt;是字符<的转义。浏览器又要试图将其解析为<。为避免出现这种情况,我们需要对&这个第三个特殊字符进行转义:在代码中使用&amp;来取代它。

唉,又丑又麻烦。实际上,当写上面这段代码时,为了要达到显示这段代码的真实效果,我被迫又对&amp;进行了第三次的转义。真的是太累了。

因此,我决定编写一个自定义的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);

textContentinnerText属性值将只提取标签内容的纯文本信息,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, '&lt;').replace(rightReg, '&gt;'); 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, '&lt;').replace(rightReg, '&gt;'); 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标签时,浏览器将优先解析并运行该代码。


                        
                    

结果就是,代码先被执行,然后代码内容经转义后再显示出来。

此时,若不想代码被自动执行,须事先自行转换。

&lt;script&gt;console.log('Hello');&lt;/script&gt;

这样就不会被识别为script标签,其代码就不会被执行。

转义html及body

innerHTML遇到htmlbody标签时,会自动吃掉它们。其他标签则不会。因此,这两个标签也须经事先转义。

let src = ` <html> <body>

Page Title

Main paragraph.

Section Title

Section paragraph.

</body> </html> `;

上面源码中这两个标签已经先转义,否则它们无法显示出来。

小结

当使用innerHTML时,可以说,到处都是陷阱。实际上,随着研究的深入,将发现这是一个不小的话题。限于时间及篇幅的原因,本文尚未涵盖所有的相关要点,后续慢慢整理、完善。

Resources

  1. IANA: Media Types
  2. text/plain
  3. highlight security
  4. Cross Site Scripting (XSS)