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 樹。

最後附上一個代碼流程圖連接

相關文章
相關標籤/搜索