Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
160 lines (129 sloc) 4.41 KB

1.2 一个兼容性更佳的HTML parser

前言

分支1.2的 HTML Parser 在解析 HTML 还没有处理兼容问题,如果你对这个不敢兴趣,可以跳过这一章节阅读后续的文章。

1. p标签

<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)
  }
}

2.类li标签

<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...
}

3.空白符

其实在真实的渲染环境中,下边的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新增代码