摘要:前言虛擬語法樹是解釋器編譯器進(jìn)行語法分析的基礎(chǔ)也是眾多前端編譯工具的基礎(chǔ)工具比如等對于由于前端輪子眾多人力過于充足早已經(jīng)被人們玩膩了光是語法分析器就有等等若干種并且也有了的社區(qū)標(biāo)準(zhǔn)這篇文章主要介紹如何去寫一個解析器但是并不是通過分析而是通過
前言
虛擬語法樹(Abstract Syntax Tree, AST)是解釋器/編譯器進(jìn)行語法分析的基礎(chǔ), 也是眾多前端編譯工具的基礎(chǔ)工具, 比如webpack, postcss, less等. 對于ECMAScript, 由于前端輪子眾多, 人力過于充足, 早已經(jīng)被人們玩膩了. 光是語法分析器就有uglify, acorn, bablyon, typescript, esprima等等若干種. 并且也有了AST的社區(qū)標(biāo)準(zhǔn): ESTree.
這篇文章主要介紹如何去寫一個AST解析器, 但是并不是通過分析JavaScript, 而是通過分析html5的語法樹來介紹, 使用html5的原因有兩點(diǎn): 一個是其語法簡單, 歸納起來只有兩種: Text和Tag, 其次是因?yàn)镴avaScript的語法分析器已經(jīng)有太多太多, 再造一個輪子毫無意義, 而對于html5, 雖然也有不少的AST分析器, 比如htmlparser2, parser5等等, 但是沒有像ESTree那么標(biāo)準(zhǔn), 同時, 這些分析器都有一個問題: 那就是定義的語法樹中無法對標(biāo)簽屬性進(jìn)行操作. 所以為了解決這個問題, 才寫了一個html的語法分析器, 同時定義了一個完善的AST結(jié)構(gòu), 然后再有的這篇文章.
AST定義為了跟蹤每個節(jié)點(diǎn)的位置屬性, 首先定義一個基礎(chǔ)節(jié)點(diǎn), 所有的結(jié)點(diǎn)都繼承于此結(jié)點(diǎn):
export interface IBaseNode { start: number; // 節(jié)點(diǎn)起始位置 end: number; // 節(jié)點(diǎn)結(jié)束位置 }
如前所述, html5的語法類型最終可以歸結(jié)為兩種: 一種是Text, 另一種是Tag, 這里用一個枚舉類型來標(biāo)志它們.
export enum SyntaxKind { Text = "Text", // 文本類型 Tag = "Tag", // 標(biāo)簽類型 }
對于文本, 其屬性只有一個原始的字符串value, 因此結(jié)構(gòu)如下:
export interface IText extends IBaseNode { type: SyntaxKind.Text; // 類型 value: string; // 原始字符串 }
而對于Tag, 則應(yīng)該包括標(biāo)簽開始部分open, 屬性列表attributes, 標(biāo)簽名稱name, 子標(biāo)簽/文本body, 以及標(biāo)簽閉合部分close:
export interface ITag extends IBaseNode { type: SyntaxKind.Tag; // 類型 open: IText; // 標(biāo)簽開始部分, 比如name: string; // 標(biāo)簽名稱, 全部轉(zhuǎn)換為小寫 attributes: IAttribute[]; // 屬性列表 body: Array// 子節(jié)點(diǎn)列表, 如果是一個非自閉合的標(biāo)簽, 并且起始標(biāo)簽已結(jié)束, 則為一個數(shù)組 | void // 如果是一個自閉合的標(biāo)簽, 則為void 0 | null; // 如果起始標(biāo)簽未結(jié)束, 則為null close: IText // 關(guān)閉標(biāo)簽部分, 存在則為一個文本節(jié)點(diǎn) | void // 自閉合的標(biāo)簽沒有關(guān)閉部分 | null; // 非自閉合標(biāo)簽, 但是沒有關(guān)閉標(biāo)簽部分 } 標(biāo)簽的屬性是一個鍵值對, 包含名稱name及值value部分, 定義結(jié)構(gòu)如下:
export interface IAttribute extends IBaseNode { name: IText; // 名稱 value: IAttributeValue | void; // 值 }其中名稱是普通的文本節(jié)點(diǎn), 但是值比較特殊, 表現(xiàn)在其可能被單/雙引號包起來, 而引號是無意義的, 因此定義一個標(biāo)簽值結(jié)構(gòu):
export interface IAttributeValue extends IBaseNode { value: string; // 值, 不包含引號部分 quote: """ | """ | void; // 引號類型, 可能是", ", 或者沒有 }Token解析AST解析首先需要解析原始文本得到符號列表, 然后再通過上下文語境分析得到最終的語法樹.
相對于JSON, html雖然看起來簡單, 但是上下文是必需的, 所以雖然JSON可以直接通過token分析得到最終的結(jié)果, 但是html卻不能, token分析是第一步, 這是必需的. (JSON解析可以參考我的另一篇文章: 徒手寫一個JSON解析器(Golang)).
token解析時, 需要根據(jù)當(dāng)前的狀態(tài)來分析token的含義, 然后得出一個token列表.
首先定義token的結(jié)構(gòu):
export interface IToken { start: number; // 起始位置 end: number; // 結(jié)束位置 value: string; // token type: TokenKind; // 類型 }Token類型一共有以下幾種:
export enum TokenKind { Literal = "Literal", // 文本 OpenTag = "OpenTag", // 標(biāo)簽名稱 OpenTagEnd = "OpenTagEnd", // 開始標(biāo)簽結(jié)束符, 可能是 "/", 或者 "", "--" CloseTag = "CloseTag", // 關(guān)閉標(biāo)簽 Whitespace = "Whitespace", // 開始標(biāo)簽類屬性值之間的空白 AttrValueEq = "AttrValueEq", // 屬性中的= AttrValueNq = "AttrValueNq", // 屬性中沒有引號的值 AttrValueSq = "AttrValueSq", // 被單引號包起來的屬性值 AttrValueDq = "AttrValueDq", // 被雙引號包起來的屬性值 }Token分析時并沒有考慮屬性的鍵/值關(guān)系, 均統(tǒng)一視為屬性中的一個片段, 同時, 視=為一個
特殊的獨(dú)立段片段, 然后交給上層的parser去分析鍵值關(guān)系. 這么做的原因是為了在token分析
時避免上下文處理, 并簡化狀態(tài)機(jī)狀態(tài)表. 狀態(tài)列表如下:enum State { Literal = "Literal", BeforeOpenTag = "BeforeOpenTag", OpeningTag = "OpeningTag", AfterOpenTag = "AfterOpenTag", InValueNq = "InValueNq", InValueSq = "InValueSq", InValueDq = "InValueDq", ClosingOpenTag = "ClosingOpenTag", OpeningSpecial = "OpeningSpecial", OpeningDoctype = "OpeningDoctype", OpeningNormalComment = "OpeningNormalComment", InNormalComment = "InNormalComment", InShortComment = "InShortComment", ClosingNormalComment = "ClosingNormalComment", ClosingTag = "ClosingTag", }整個解析采用函數(shù)式編程, 沒有使用OO, 為了簡化在函數(shù)間傳遞狀態(tài)參數(shù), 由于是一個同步操作,
這里利用了JavaScript的事件模型, 采用全局變量來保存狀態(tài). Token分析時所需要的全局變量列表如下:let state: State // 當(dāng)前的狀態(tài) let buffer: string // 輸入的字符串 let bufSize: number // 輸入字符串長度 let sectionStart: number // 正在解析的Token的起始位置 let index: number // 當(dāng)前解析的字符的位置 let tokens: IToken[] // 已解析的token列表 let char: number // 當(dāng)前解析的位置的字符的UnicodePoint在開始解析前, 需要初始化全局變量:
function init(input: string) { state = State.Literal buffer = input bufSize = input.length sectionStart = 0 index = 0 tokens = [] }然后開始解析, 解析時需要遍歷輸入字符串中的所有字符, 并根據(jù)當(dāng)前狀態(tài)進(jìn)行相應(yīng)的處理
(改變狀態(tài), 輸出token等), 解析完成后, 清空全局變量, 返回結(jié)束.export function tokenize(input: string): IToken[] { init(input) while (index < bufSize) { char = buffer.charCodeAt(index) switch (state) { // ...根據(jù)不同的狀態(tài)進(jìn)行相應(yīng)的處理 // 文章忽略了對各個狀態(tài)的處理, 詳細(xì)了解可以查看源代碼 } index++ } const _nodes = nodes // 清空狀態(tài) init("") return _nodes }語法樹解析在獲取到token列表之后, 需要根據(jù)上下文解析得到最終的節(jié)點(diǎn)樹, 方式與tokenize相似,
均采用全局變量保存?zhèn)鬟f狀態(tài), 遍歷所有的token, 不同之處在于這里沒有一個全局的狀態(tài)機(jī).
因?yàn)闋顟B(tài)完全可以通過正在解析的節(jié)點(diǎn)的類型來判斷.export function parse(input: string): INode[] { init(input) while (index < count) { token = tokens[index] switch (token.type) { case TokenKind.Literal: if (!node) { node = createLiteral() pushNode(node) } else { appendLiteral(node) } break case TokenKind.OpenTag: node = void 0 parseOpenTag() break case TokenKind.CloseTag: node = void 0 parseCloseTag() break default: unexpected() break } index++ } const _nodes = nodes init() return _nodes }不太多解釋, 可以到GitHub查看源代碼.
結(jié)語項(xiàng)目已開源, 名稱是html5parser, 可以通過npm/yarn安裝:
npm install html5parser -S # OR yarn add html5parser或者到GitHub查看源代碼: acrazing/html5parser.
目前對正常的HTML解析已完全通過測試, 已知的BUG包括對注釋的解析, 以及未正常結(jié)束的
輸入的解析處理(均在語法分析層面, token分析已通過測試).文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/51153.html
摘要:前言虛擬語法樹是解釋器編譯器進(jìn)行語法分析的基礎(chǔ)也是眾多前端編譯工具的基礎(chǔ)工具比如等對于由于前端輪子眾多人力過于充足早已經(jīng)被人們玩膩了光是語法分析器就有等等若干種并且也有了的社區(qū)標(biāo)準(zhǔn)這篇文章主要介紹如何去寫一個解析器但是并不是通過分析而是通過 前言 虛擬語法樹(Abstract Syntax Tree, AST)是解釋器/編譯器進(jìn)行語法分析的基礎(chǔ), 也是眾多前端編譯工具的基礎(chǔ)工具, 比如...
摘要:具體來講,就是在遇到和的子節(jié)點(diǎn)的時候要壓入棧,遇到一個的結(jié)束符的時候要彈出棧。同時還要保存棧結(jié)點(diǎn)對應(yīng)的以及其狀態(tài)信息。所以我定義了一個棧結(jié)點(diǎn)結(jié)構(gòu)其中表示當(dāng)前棧節(jié)點(diǎn)的狀態(tài),表示其所代表的值表示其父節(jié)點(diǎn),根節(jié)點(diǎn)的父節(jié)點(diǎn)為。 前一陣子看到了一個Golang的JSON庫go-simplejson,用來封裝與解析匿名的JSON,說白了就是用map或者slice等來解析JSON,覺得挺好玩,后來有...
摘要:通過對源碼閱讀,想寫一寫自己的理解,能力有限故從尤大佬第一次提交開始讀,準(zhǔn)備陸續(xù)寫模版字符串轉(zhuǎn)語法樹語法樹轉(zhuǎn)函數(shù)雙向綁定原理虛擬比較原理其中包含自己的理解和源碼的分析,盡量通俗易懂由于是的最早提交,所以和最新版本有很多差異,后續(xù)將陸續(xù)補(bǔ)充, 通過對 Vue2.0 源碼閱讀,想寫一寫自己的理解,能力有限故從尤大佬2016.4.11第一次提交開始讀,準(zhǔn)備陸續(xù)寫: 模版字符串轉(zhuǎn)AST語法...
閱讀 2061·2021-11-23 09:51
閱讀 2203·2021-09-29 09:34
閱讀 3694·2021-09-22 15:50
閱讀 3556·2021-09-22 15:23
閱讀 2559·2019-08-30 15:55
閱讀 699·2019-08-30 15:53
閱讀 3066·2019-08-29 17:09
閱讀 2624·2019-08-29 13:57