Skip to content

Latest commit

 

History

History
159 lines (129 loc) · 4.41 KB

1.2.1.md

File metadata and controls

159 lines (129 loc) · 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新增代码