成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

造輪子系列(三): 一個(gè)簡(jiǎn)單快速的html虛擬語(yǔ)法樹(shù)(AST)解析器

Genng / 3580人閱讀

摘要:前言虛擬語(yǔ)法樹(shù)是解釋器編譯器進(jìn)行語(yǔ)法分析的基礎(chǔ)也是眾多前端編譯工具的基礎(chǔ)工具比如等對(duì)于由于前端輪子眾多人力過(guò)于充足早已經(jīng)被人們玩膩了光是語(yǔ)法分析器就有等等若干種并且也有了的社區(qū)標(biāo)準(zhǔn)這篇文章主要介紹如何去寫(xiě)一個(gè)解析器但是并不是通過(guò)分析而是通過(guò)

前言

虛擬語(yǔ)法樹(shù)(Abstract Syntax Tree, AST)是解釋器/編譯器進(jìn)行語(yǔ)法分析的基礎(chǔ), 也是眾多前端編譯工具的基礎(chǔ)工具, 比如webpack, postcss, less等. 對(duì)于ECMAScript, 由于前端輪子眾多, 人力過(guò)于充足, 早已經(jīng)被人們玩膩了. 光是語(yǔ)法分析器就有uglify, acorn, bablyon, typescript, esprima等等若干種. 并且也有了AST的社區(qū)標(biāo)準(zhǔn): ESTree.

這篇文章主要介紹如何去寫(xiě)一個(gè)AST解析器, 但是并不是通過(guò)分析JavaScript, 而是通過(guò)分析html5的語(yǔ)法樹(shù)來(lái)介紹, 使用html5的原因有兩點(diǎn): 一個(gè)是其語(yǔ)法簡(jiǎn)單, 歸納起來(lái)只有兩種: TextTag, 其次是因?yàn)镴avaScript的語(yǔ)法分析器已經(jīng)有太多太多, 再造一個(gè)輪子毫無(wú)意義, 而對(duì)于html5, 雖然也有不少的AST分析器, 比如htmlparser2, parser5等等, 但是沒(méi)有像ESTree那么標(biāo)準(zhǔn), 同時(shí), 這些分析器都有一個(gè)問(wèn)題: 那就是定義的語(yǔ)法樹(shù)中無(wú)法對(duì)標(biāo)簽屬性進(jìn)行操作. 所以為了解決這個(gè)問(wèn)題, 才寫(xiě)了一個(gè)html的語(yǔ)法分析器, 同時(shí)定義了一個(gè)完善的AST結(jié)構(gòu), 然后再有的這篇文章.

AST定義

為了跟蹤每個(gè)節(jié)點(diǎn)的位置屬性, 首先定義一個(gè)基礎(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的語(yǔ)法類型最終可以歸結(jié)為兩種: 一種是Text, 另一種是Tag, 這里用一個(gè)枚舉類型來(lái)標(biāo)志它們.

export enum SyntaxKind {
  Text = "Text", // 文本類型
  Tag  = "Tag",  // 標(biāo)簽類型
}

對(duì)于文本, 其屬性只有一個(gè)原始的字符串value, 因此結(jié)構(gòu)如下:

export interface IText extends IBaseNode {
  type: SyntaxKind.Text; // 類型
  value: string;         // 原始字符串
}

而對(duì)于Tag, 則應(yīng)該包括標(biāo)簽開(kāi)始部分open, 屬性列表attributes, 標(biāo)簽名稱name, 子標(biāo)簽/文本body, 以及標(biāo)簽閉合部分close:

export interface ITag extends IBaseNode {
  type: SyntaxKind.Tag;  // 類型
  open: IText;           // 標(biāo)簽開(kāi)始部分, 比如 
name: string; // 標(biāo)簽名稱, 全部轉(zhuǎn)換為小寫(xiě) attributes: IAttribute[]; // 屬性列表 body: Array // 子節(jié)點(diǎn)列表, 如果是一個(gè)非自閉合的標(biāo)簽, 并且起始標(biāo)簽已結(jié)束, 則為一個(gè)數(shù)組 | void // 如果是一個(gè)自閉合的標(biāo)簽, 則為void 0 | null; // 如果起始標(biāo)簽未結(jié)束, 則為null close: IText // 關(guān)閉標(biāo)簽部分, 存在則為一個(gè)文本節(jié)點(diǎn) | void // 自閉合的標(biāo)簽沒(méi)有關(guān)閉部分 | null; // 非自閉合標(biāo)簽, 但是沒(méi)有關(guān)閉標(biāo)簽部分 }

標(biāo)簽的屬性是一個(gè)鍵值對(duì), 包含名稱name及值value部分, 定義結(jié)構(gòu)如下:

export interface IAttribute extends IBaseNode {
  name: IText;  // 名稱
  value: IAttributeValue | void; // 值
}

其中名稱是普通的文本節(jié)點(diǎn), 但是值比較特殊, 表現(xiàn)在其可能被單/雙引號(hào)包起來(lái), 而引號(hào)是無(wú)意義的, 因此定義一個(gè)標(biāo)簽值結(jié)構(gòu):

export interface IAttributeValue extends IBaseNode {
  value: string; // 值, 不包含引號(hào)部分
  quote: """ | """ | void; // 引號(hào)類型, 可能是", ", 或者沒(méi)有
}
Token解析

AST解析首先需要解析原始文本得到符號(hào)列表, 然后再通過(guò)上下文語(yǔ)境分析得到最終的語(yǔ)法樹(shù).

相對(duì)于JSON, html雖然看起來(lái)簡(jiǎn)單, 但是上下文是必需的, 所以雖然JSON可以直接通過(guò)token分析得到最終的結(jié)果, 但是html卻不能, token分析是第一步, 這是必需的. (JSON解析可以參考我的另一篇文章: 徒手寫(xiě)一個(gè)JSON解析器(Golang)).

token解析時(shí), 需要根據(jù)當(dāng)前的狀態(tài)來(lái)分析token的含義, 然后得出一個(gè)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",   // 開(kāi)始標(biāo)簽結(jié)束符, 可能是 "/", 或者 "", "--"
  CloseTag    = "CloseTag",     // 關(guān)閉標(biāo)簽
  Whitespace  = "Whitespace",   // 開(kāi)始標(biāo)簽類屬性值之間的空白
  AttrValueEq = "AttrValueEq",  // 屬性中的=
  AttrValueNq = "AttrValueNq",  // 屬性中沒(méi)有引號(hào)的值
  AttrValueSq = "AttrValueSq",  // 被單引號(hào)包起來(lái)的屬性值
  AttrValueDq = "AttrValueDq",  // 被雙引號(hào)包起來(lái)的屬性值
}

Token分析時(shí)并沒(méi)有考慮屬性的鍵/值關(guān)系, 均統(tǒng)一視為屬性中的一個(gè)片段, 同時(shí), 視=為一個(gè)
特殊的獨(dú)立段片段, 然后交給上層的parser去分析鍵值關(guān)系. 這么做的原因是為了在token分析
時(shí)避免上下文處理, 并簡(jiǎn)化狀態(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",
}

整個(gè)解析采用函數(shù)式編程, 沒(méi)有使用OO, 為了簡(jiǎn)化在函數(shù)間傳遞狀態(tài)參數(shù), 由于是一個(gè)同步操作,
這里利用了JavaScript的事件模型, 采用全局變量來(lái)保存狀態(tài). Token分析時(shí)所需要的全局變量列表如下:

let state: State          // 當(dāng)前的狀態(tài)
let buffer: string        // 輸入的字符串
let bufSize: number       // 輸入字符串長(zhǎng)度
let sectionStart: number  // 正在解析的Token的起始位置
let index: number         // 當(dāng)前解析的字符的位置
let tokens: IToken[]      // 已解析的token列表
let char: number          // 當(dāng)前解析的位置的字符的UnicodePoint

在開(kāi)始解析前, 需要初始化全局變量:

function init(input: string) {
  state        = State.Literal
  buffer       = input
  bufSize      = input.length
  sectionStart = 0
  index        = 0
  tokens       = []
}

然后開(kāi)始解析, 解析時(shí)需要遍歷輸入字符串中的所有字符, 并根據(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)的處理
    // 文章忽略了對(duì)各個(gè)狀態(tài)的處理, 詳細(xì)了解可以查看源代碼
    }
    index++
  }
  const _nodes = nodes
  // 清空狀態(tài)
  init("")
  return _nodes
}
語(yǔ)法樹(shù)解析

在獲取到token列表之后, 需要根據(jù)上下文解析得到最終的節(jié)點(diǎn)樹(shù), 方式與tokenize相似,
均采用全局變量保存?zhèn)鬟f狀態(tài), 遍歷所有的token, 不同之處在于這里沒(méi)有一個(gè)全局的狀態(tài)機(jī).
因?yàn)闋顟B(tài)完全可以通過(guò)正在解析的節(jié)點(diǎn)的類型來(lái)判斷.

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é)語(yǔ)

項(xiàng)目已開(kāi)源, 名稱是html5parser, 可以通過(guò)npm/yarn安裝:

npm install html5parser -S
# OR
yarn add html5parser 

或者到GitHub查看源代碼: acrazing/html5parser.

目前對(duì)正常的HTML解析已完全通過(guò)測(cè)試, 已知的BUG包括對(duì)注釋的解析, 以及未正常結(jié)束的
輸入的解析處理(均在語(yǔ)法分析層面, token分析已通過(guò)測(cè)試).

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/87397.html

相關(guān)文章

  • 輪子系列(): 一個(gè)簡(jiǎn)單快速html虛擬語(yǔ)法樹(shù)(AST)解析

    摘要:前言虛擬語(yǔ)法樹(shù)是解釋器編譯器進(jìn)行語(yǔ)法分析的基礎(chǔ)也是眾多前端編譯工具的基礎(chǔ)工具比如等對(duì)于由于前端輪子眾多人力過(guò)于充足早已經(jīng)被人們玩膩了光是語(yǔ)法分析器就有等等若干種并且也有了的社區(qū)標(biāo)準(zhǔn)這篇文章主要介紹如何去寫(xiě)一個(gè)解析器但是并不是通過(guò)分析而是通過(guò) 前言 虛擬語(yǔ)法樹(shù)(Abstract Syntax Tree, AST)是解釋器/編譯器進(jìn)行語(yǔ)法分析的基礎(chǔ), 也是眾多前端編譯工具的基礎(chǔ)工具, 比如...

    SQC 評(píng)論0 收藏0
  • 輪子系列(一): 一個(gè)速度九分快JSON解析

    摘要:具體來(lái)講,就是在遇到和的子節(jié)點(diǎn)的時(shí)候要壓入棧,遇到一個(gè)的結(jié)束符的時(shí)候要彈出棧。同時(shí)還要保存棧結(jié)點(diǎn)對(duì)應(yīng)的以及其狀態(tài)信息。所以我定義了一個(gè)棧結(jié)點(diǎn)結(jié)構(gòu)其中表示當(dāng)前棧節(jié)點(diǎn)的狀態(tài),表示其所代表的值表示其父節(jié)點(diǎn),根節(jié)點(diǎn)的父節(jié)點(diǎn)為。 前一陣子看到了一個(gè)Golang的JSON庫(kù)go-simplejson,用來(lái)封裝與解析匿名的JSON,說(shuō)白了就是用map或者slice等來(lái)解析JSON,覺(jué)得挺好玩,后來(lái)有...

    lewif 評(píng)論0 收藏0
  • Vue源碼解析:模版字符串轉(zhuǎn)AST語(yǔ)法樹(shù)

    摘要:通過(guò)對(duì)源碼閱讀,想寫(xiě)一寫(xiě)自己的理解,能力有限故從尤大佬第一次提交開(kāi)始讀,準(zhǔn)備陸續(xù)寫(xiě)模版字符串轉(zhuǎn)語(yǔ)法樹(shù)語(yǔ)法樹(shù)轉(zhuǎn)函數(shù)雙向綁定原理虛擬比較原理其中包含自己的理解和源碼的分析,盡量通俗易懂由于是的最早提交,所以和最新版本有很多差異,后續(xù)將陸續(xù)補(bǔ)充, 通過(guò)對(duì) Vue2.0 源碼閱讀,想寫(xiě)一寫(xiě)自己的理解,能力有限故從尤大佬2016.4.11第一次提交開(kāi)始讀,準(zhǔn)備陸續(xù)寫(xiě): 模版字符串轉(zhuǎn)AST語(yǔ)法...

    chengjianhua 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<