parse

baseparser

解析一段 html 文本时,能够先将其解析为简单的对象,而后在对属性和文本进行进一步的加工来丰富每一个解析节点对象。最终造成一颗 AST。javascript

第一步 --- 想要什么

对于一点 html 文本,解析以前,应该知道咱们想要什么,好比 <div id="app">name</div>,在看到这段文本时,咱们的解析目标应该html

  • 解析出标签的名字,即 tagName = div
  • 解析出属性,即 attrList = [id = "app"]
  • 解析出中间文本,也就是子元素,即 children = ['name']

上面的指望聚集一下,就获得了指望获得的节点对象vue

const astNode = {
  tagName: "div",
  attrList: ['id = "app"'],
  children: ["name"],
};
复制代码

这只是最原始的 AST 节点,可是这也是咱们必须解析出来的结构。对于一段 html 字符串而言,解析出这三部分,咱们须要不断地对输入的字符串进行操做,解析一段,就截掉解析的这一段,一遍往下进行。java

第二步 --- 配套资料收集

对于字符串操做而言,遍历字符串是每一个人都能想到的,并且这种方法可行。可是这里选择使用正则去总体匹配一段字符串,这样子会更快地解析出一段 html 包含的 ast 结构。markdown

既然解析的目标是 html 字符串,那不妨先列举出要用到的正则。app

// 开始标签
const startTag =
  /^<((?:[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)/;

// 开始标签结束
const startTagClose = /^\s*(\/?)>/;

// 结束标签
const endTag = /^<\/([a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*[^>]*)>/
复制代码

上面的正则是无耻的从 vue 源码中直接扣出来的,若是你想了解这些正则的匹配模式,能够在这里 输入正则查看。函数

到这里,思想和资料已经准备完毕,下面就开始动手写出能解析出第一步结构的解析函数oop

第三步 --- 动手

注意,这里的代码不是一步到位的,须要一步步的去完善,最终达到咱们的效果。接下来,将从解析一段 html 字符串开始。ui

给出须要解析的第一段 html 字符串:<div></div>spa

function parse(input) {
    let root = null // 用来保存解析到的 ast 节点
    let tagName = '' // 当前正在解析的标签名称
    // 无论怎么样,都要遍历字符串
    while(input) {
        let textEnd = input.indexOf('<')
        if(textEnd === 0){
            // < 打头的,多是开始标签,也多是结束标签,也可能只是个 <
            // 首先尝试匹配开始标签
            const match = input.match(startTag)
            if(match){
                // 说明是开始标签
                input = input.slice(match[0].length)
                // 检查标签是否正常闭合
                const closeStart = input.match(startTagClose)
                if(closeStart){
                    input = input.slice(closeStart[0].length)
                    // 表示标签正常闭合
                    root = {
                        tagName: match[1]
                    }
                    if(closeStart[1] === '/'){
                        // 表示是自闭合标签
                        input = input.slice(closeStart[0].length)
                        continue;
                    }
                    tagName = root.tagName
                }
            }
            const matchEnd = input.match(endTag)
            if(matchEnd){
                // 说明匹配到告终束标签
                if(matchEnd[1] !== tagName){
                    // 结束和开始标签不配对,说明不是合法标签,不进行保存
                    root = null
                    break
                }
                input = input.slice(matchEnd[0].length)
            }
        }
    }
    return root
}

console.log('parse', parse('<div></div>'));

复制代码

上述代码是一个流程代码,创建在若干假设的基础上:

  • 当字符串的开头是 < 时,就认为是 开始标签结束标签文本其中的一个。

    • 这里先不考虑是文本 的状况,因此只能是前两种
  • 存在两种闭合标签

    • 自闭合标签 <b />
    • 双标签闭合 <div></div>

明确了这两种前提,整个流程就清晰起来了。检测到字符串是以< 开头,则一次作开始标签匹配结束标签匹配

开始标签的处理

// 匹配开始标签
const match = input.match(startTag)
if(match){
    // 说明是开始标签
    input = input.slice(match[0].length)
    // 检查标签是否正常闭合
    const closeStart = input.match(startTagClose)
    if(closeStart){
        // 标签正常闭合
        input = input.slice(closeStart[0].length)
        root = {
            tagName: match[1]
        }
        if(closeStart[1] === '/'){
            // 表示是自闭合标签
            input = input.slice(closeStart[0].length)
            continue;
        }
        tagName = root.tagName
    }
}
复制代码

对开始标签的处理并不难,难点在于你知道 match 的内容,下面举例说明:

const a = '<div>'

const match = a.match(startTag)

/** * match 的主要内容以下: * * [ * '<div', // 匹配到的部分 * 'div' // 匹配到的标签名 * ] * */

复制代码

要确保开始标签完整闭合,这才是一个完整的开始标签,因此就有了:

const a = '>'

const closeStart = input.match(startTagClose)

/** * match 的主要内容以下: * * [ * '>', // 匹配到的部分 * undefined // 若是是自闭合标签,这里是 / * ] * */

复制代码

至此,一个开始标签完整的匹配完毕,下面整理一下思路:

对于 <div> 这个字符串

  1. 匹配 <div 部分,得到标签名 div
  2. 匹配 1 中剩余的部分 > 来肯定标签是不是个完整的标签 2.1 若是匹配到了 / 说明是自闭合标签,整个标签匹配结束 2.2 没有匹配到 / 说明不是自闭合标签,开始标签结束

结束标签的处理

const matchEnd = input.match(endTag)
if(matchEnd){
    // 说明匹配到告终束标签
    if(matchEnd[1] !== tagName){
        // 结束和开始标签不配对,说明不是合法标签,不进行保存
        root = null
        break
    }
    input = input.slice(matchEnd[0].length)
}
复制代码

结束标签的处理相对来讲简单不少,只须要确认下开始和结束的标签名是否对应的上就行。

总结

这篇文章简要的分析了解析 html 字符串须要的准备以及简要的实现。这里能够解析没有属性和子元素的 html 字符串。存在不少不足之处,后面将会有一些列文章来完善这个解析过程,最终得到一个完整的相似 vue complier 的 ast 树。

最后附上一个代码流程图连接

相关文章
相关标签/搜索