成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

造輪子系列(三): 一個簡單快速的html虛擬語法樹(AST)解析器

SQC / 2957人閱讀

摘要:前言虛擬語法樹是解釋器編譯器進行語法分析的基礎也是眾多前端編譯工具的基礎工具比如等對于由于前端輪子眾多人力過于充足早已經被人們玩膩了光是語法分析器就有等等若干種并且也有了的社區(qū)標準這篇文章主要介紹如何去寫一個解析器但是并不是通過分析而是通過

前言

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

這篇文章主要介紹如何去寫一個AST解析器, 但是并不是通過分析JavaScript, 而是通過分析html5的語法樹來介紹, 使用html5的原因有兩點: 一個是其語法簡單, 歸納起來只有兩種: TextTag, 其次是因為JavaScript的語法分析器已經有太多太多, 再造一個輪子毫無意義, 而對于html5, 雖然也有不少的AST分析器, 比如htmlparser2, parser5等等, 但是沒有像ESTree那么標準, 同時, 這些分析器都有一個問題: 那就是定義的語法樹中無法對標簽屬性進行操作. 所以為了解決這個問題, 才寫了一個html的語法分析器, 同時定義了一個完善的AST結構, 然后再有的這篇文章.

AST定義

為了跟蹤每個節(jié)點的位置屬性, 首先定義一個基礎節(jié)點, 所有的結點都繼承于此結點:

export interface IBaseNode {
  start: number;  // 節(jié)點起始位置
  end: number;    // 節(jié)點結束位置
}

如前所述, html5的語法類型最終可以歸結為兩種: 一種是Text, 另一種是Tag, 這里用一個枚舉類型來標志它們.

export enum SyntaxKind {
  Text = "Text", // 文本類型
  Tag  = "Tag",  // 標簽類型
}

對于文本, 其屬性只有一個原始的字符串value, 因此結構如下:

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

而對于Tag, 則應該包括標簽開始部分open, 屬性列表attributes, 標簽名稱name, 子標簽/文本body, 以及標簽閉合部分close:

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

標簽的屬性是一個鍵值對, 包含名稱name及值value部分, 定義結構如下:

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

其中名稱是普通的文本節(jié)點, 但是值比較特殊, 表現(xiàn)在其可能被單/雙引號包起來, 而引號是無意義的, 因此定義一個標簽值結構:

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

AST解析首先需要解析原始文本得到符號列表, 然后再通過上下文語境分析得到最終的語法樹.

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

token解析時, 需要根據當前的狀態(tài)來分析token的含義, 然后得出一個token列表.

首先定義token的結構:

export interface IToken {
  start: number;    // 起始位置
  end: number;      // 結束位置
  value: string;    // token
  type: TokenKind;  // 類型
}

Token類型一共有以下幾種:

export enum TokenKind {
  Literal     = "Literal",      // 文本
  OpenTag     = "OpenTag",      // 標簽名稱
  OpenTagEnd  = "OpenTagEnd",   // 開始標簽結束符, 可能是 "/", 或者 "", "--"
  CloseTag    = "CloseTag",     // 關閉標簽
  Whitespace  = "Whitespace",   // 開始標簽類屬性值之間的空白
  AttrValueEq = "AttrValueEq",  // 屬性中的=
  AttrValueNq = "AttrValueNq",  // 屬性中沒有引號的值
  AttrValueSq = "AttrValueSq",  // 被單引號包起來的屬性值
  AttrValueDq = "AttrValueDq",  // 被雙引號包起來的屬性值
}

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

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

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

在開始解析前, 需要初始化全局變量:

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

然后開始解析, 解析時需要遍歷輸入字符串中的所有字符, 并根據當前狀態(tài)進行相應的處理
(改變狀態(tài), 輸出token等), 解析完成后, 清空全局變量, 返回結束.

export function tokenize(input: string): IToken[] {
  init(input)
  while (index < bufSize) {
    char = buffer.charCodeAt(index)
    switch (state) {
    // ...根據不同的狀態(tài)進行相應的處理
    // 文章忽略了對各個狀態(tài)的處理, 詳細了解可以查看源代碼
    }
    index++
  }
  const _nodes = nodes
  // 清空狀態(tài)
  init("")
  return _nodes
}
語法樹解析

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

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查看源代碼.

結語

項目已開源, 名稱是html5parser, 可以通過npm/yarn安裝:

npm install html5parser -S
# OR
yarn add html5parser 

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

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

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

轉載請注明本文地址:http://systransis.cn/yun/51153.html

相關文章

  • 輪子系列(): 一個簡單快速html虛擬語法(AST)解析

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

    Genng 評論0 收藏0
  • 輪子系列(一): 一個速度九分快JSON解析

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

    lewif 評論0 收藏0
  • Vue源碼解析:模版字符串轉AST語法

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

    chengjianhua 評論0 收藏0

發(fā)表評論

0條評論

SQC

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<