分支1.2的 HTML Parser 在解析 HTML 还没有处理兼容问题,如果你对这个不敢兴趣,可以跳过这一章节阅读后续的文章。
<p>
<span>嵌套在p里边的span</span>
<div>嵌套在p里边的div</div>
</p>
会被浏览器解析成:
<p>
<span>嵌套在p里边的span</span>
</p>
<div>嵌套在p里边的div</div>
<p>
</p>
p标签下只允许嵌套以下标签(https://html.spec.whatwg.org/multipage/dom.html#phrasing-content).
所以在处理StartToken的时候,如果当前处于p标签下,那么要检查当前标签是不是在这些标签里边,如果是的话要提前闭合p标签,同时之前的
闭合标签会变成空的p标签 (br标签同理)。// compiler/parser/html-parser.js
function handleStartTag (match) {
// blabla...
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag) // 提前闭合p标签
}
// blabla...
}
function parseEndTag (tagName, start, end) {
// blabla...
if (pos >= 0) {
// 如果找到匹配的起始标签就正常处理
// blabla...
} else if (lowerCasedTagName === 'br') {
// 单独出现 </br> 标签 直接处理成单标签 <br>
options.start(tagName, [], true/*unary*/)
} else if (lowerCasedTagName === 'p') {
// 单独出现 </p> 标签 直接处理成 <p></p>
options.start(tagName, [], false/*unary*/)
options.end(tagName, start, end)
}
}
<ul>
<li>Item 1
<li>Item 2</li>
</ul>
会被浏览器解析成:
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
第一个 li 标签遇到下一个li起始标签时,就会发现自己没闭合,此时解析器会自动闭合第一个 li 标签。
类似的标签还有: colgroup, dd, dt, li, options, p, td, tfoot, th, thead, tr, source
因此我们需要在 StartToken 的时候进行处理:
// compiler/parser/html-parser.js
function handleStartTag (match) {
// blabla...
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag) // 提前闭合p标签
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
// 像 li 这种可以可以忽略闭合标签
parseEndTag(tagName)
}
// blabla...
}
其实在真实的渲染环境中,下边的HTML每个 li 后边都会有一段空白符(换行加下一行的tab缩进, 你可以通过 liDom.nextSibling 看到这段文本)
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
首先连续的空白符其实在渲染的时候只会被渲染成一个空格(不在pre标签里的情况),所以我们在处理 CharsToken 的时候:
// compiler/parser/index.js
function parse (template) {
// blabla...
parseHTML(template, {
start (tag, attrs, unary) { /* blabla... */ },
end () { /* blabla... */ },
chars (text) {
// blabla...
// 如果文本节点为多个空格 也即是text.trim() == false
// 同时所在的父亲节点含有其他孩子节点,那么要生成一个单空格的文本节点
text = inPre || text.trim() ? // 同时需要考虑在 pre标签里边的空白符不能做这样的转化
text :
(children.length ? ' ' : '')
// blabla...
}
}
return root
}
其次,第二个 li 标签后边的空白符删除后并不会有副作用,所以我们在处理 EndToken 的时候,把这个空白符干掉。
// compiler/parser/index.js
function parse (template) {
// blabla...
parseHTML(template, {
start (tag, attrs, unary) { /* blabla... */ },
end () {
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
// 把孩子节点中最后一个空白节点删掉
element.children.pop()
}
stack.length -= 1
currentParent = stack[stack.length - 1]
endPre(element)
},
chars (text) { /* blabla... */ }
return root
}
src源码新增了61行,总共700行,查看分支1.2.1代码,查看1.2.1新增代码