【手寫簡易瀏覽器】html parser 篇

上篇文章介紹了手寫簡易瀏覽器整體的思路,這篇開始寫 html parser。

思路分析

實現 html parser 主要分爲詞法分析和語法分析兩步。

詞法分析

詞法分析需要把每一種類型的 token 識別出來,具體的類型有:

這是最外層的 token,開始標籤內部還要分出屬性,如 這種。

也就是有這幾種情況:

第一層判斷是否包含 <,如果不包含則是 text,如果包含則再判斷是哪一種,如果是開始標籤,還要對其內容再取屬性,直到遇到> 就重新判斷。

語法分析

語法分析就是對上面分出的 token 進行組裝,生成 ast。

html 的 ast 的組裝主要是考慮父子關係,記錄當前的 parent,然後 text、children 都設置到當前 parent 上。

我們來用代碼實現一下:

代碼實現

詞法分析

首先,我們要把 startTag、endTag、comment、docType 還有 attribute 的正則表達式寫出來:

正則

const endTagReg = /^<\/([a-zA-Z0-9\-]+)>/;
const commentReg = /^<!\-\-[^(-->)]*\-\->/;
const docTypeReg = /^<!doctype [^>]+>/;
const attributeReg = /^(?:[ ]+([a-zA-Z0-9\-]+=[^>]+))/;
const startTagReg = /^<([a-zA-Z0-9\-]+)(?:([ ]+[a-zA-Z0-9\-]+=[^> ]+))*>/;

分詞

function parse(html, options) {
    function advance(num) {
        html = html.slice(num);
    }

    while(html){
        if(html.startsWith('<')) {
            //...
        } else {
            let textEndIndex = html.indexOf('<');
            options.onText({
                type: 'text',
                value: html.slice(0, textEndIndex)
            });
            textEndIndex = textEndIndex === -1 ? html.length: textEndIndex;
            advance(textEndIndex);
        }
    }
}

第二層處理 <!-- 和 <!doctype 和結束標籤、開始標籤:

const commentMatch = html.match(commentReg);
if (commentMatch) {
    options.onComment({
        type: 'comment',
        value: commentMatch[0]
    })
    advance(commentMatch[0].length);
    continue;
}

const docTypeMatch = html.match(docTypeReg);
if (docTypeMatch) {
    options.onDoctype({
        type: 'docType',
        value: docTypeMatch[0]
    });
    advance(docTypeMatch[0].length);
    continue;
}

const endTagMatch = html.match(endTagReg);
if (endTagMatch) {
    options.onEndTag({
        type: 'tagEnd',
        value: endTagMatch[1]
    });
    advance(endTagMatch[0].length);
    continue;
}

const startTagMatch = html.match(startTagReg);
if(startTagMatch) {    
    options.onStartTag({
        type: 'tagStart',
        value: startTagMatch[1]
    });

    advance(startTagMatch[1].length + 1);
    let attributeMath;
    while(attributeMath = html.match(attributeReg)) {
        options.onAttribute({
            type: 'attribute',
            value: attributeMath[1]
        });
        advance(attributeMath[0].length);
    }
    advance(1);
    continue;
}

經過詞法分析,我們能拿到所有的 token:

語法分析

token 拆分之後,我們需要再把這些 token 組裝在一起,只處理 startTag、endTag 和 text 節點。通過 currentParent 記錄當前 tag。

function htmlParser(str) {
    const ast = {
        children: []
    };
    let curParent = ast;
    let prevParent = null;
    const domTree = parse(str,{
        onComment(node) {
        },
        onStartTag(token) {
            const tag = {
                tagName: token.value,
                attributes: [],
                text: '',
                children: []
            };
            curParent.children.push(tag);
            prevParent = curParent;
            curParent = tag;
        },
        onAttribute(token) {
            const [ name, value ] = token.value.split('=');
            curParent.attributes.push({
                name,
                value: value.replace(/^['"]/, '').replace(/['"]$/, '')
            });
        },
        onEndTag(token) {
            curParent = prevParent;
        },
        onDoctype(token) {
        },
        onText(token) {
            curParent.text = token.value;
        }
    });
    return ast.children[0];
}

我們試一下效果:

const htmlParser = require('./htmlParser');

const domTree = htmlParser(`
<!doctype html>
<body>
    <div>
        <!--button-->
        <button>按鈕</button>
        <div id="container">
            <div class="box1">
                <p>box1 box1 box1</p>
            </div>
            <div class="box2">
                <p>box2 box2 box2</p>
            </div>
        </div>
    </div>
</body>
`);

console.log(JSON.stringify(domTree, null, 4));

成功生成了正確的 AST。

總結

這篇是簡易瀏覽器中 html parser 的實現,少了自閉合標籤的處理,就是差一個 if else,後面會補上。

我們分析了思路並進行了實現:通過正則來進行 token 的拆分,把拆出的 token 通過回調函數暴露出去,之後進行 AST 的組裝,需要記錄當前的 parent,來生成父子關係正確的 AST。

html parser 其實也是淘系前端的多年不變的面試題之一,而且 vue template compiler 還有 jsx 的 parser 也會用到類似的思路。還是有必要掌握的。希望本文能幫大家理清思路。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ku6yNZqIKH9wBSGbUdhW0A