Skip to content

Template の Parser

前回までで parse の成果物である,AST について詳しくみてきました.
ここからは実際にその AST を生成する parser について見ていきます.

parser は parsetokenize という 2 つステップに分かれています.
順番としては,まず tokenize です.

Tokenize

tokenize は字句解析のステップです.
字句解析というのは,簡単にいうと単なる文字列であるコードをトークン (字句) という単位に解析することです.
トークンとは,あるまとまりのある文字列です.具体的にどういうものがあるかについては実際にソースコードを見てみましょう.

87export enum State {
88  Text = 1,
89
90  // interpolation
91  InterpolationOpen,
92  Interpolation,
93  InterpolationClose,
94
95  // Tags
96  BeforeTagName, // After <
97  InTagName,
98  InSelfClosingTag,
99  BeforeClosingTagName,
100  InClosingTagName,
101  AfterClosingTagName,
102
103  // Attrs
104  BeforeAttrName,
105  InAttrName,
106  InDirName,
107  InDirArg,
108  InDirDynamicArg,
109  InDirModifier,
110  AfterAttrName,
111  BeforeAttrValue,
112  InAttrValueDq, // "
113  InAttrValueSq, // '
114  InAttrValueNq,
115
116  // Declarations
117  BeforeDeclaration, // !
118  InDeclaration,
119
120  // Processing instructions
121  InProcessingInstruction, // ?
122
123  // Comments & CDATA
124  BeforeComment,
125  CDATASequence,
126  InSpecialComment,
127  InCommentLike,
128
129  // Special tags
130  BeforeSpecialS, // Decide if we deal with `<script` or `<style`
131  BeforeSpecialT, // Decide if we deal with `<title` or `<textarea`
132  SpecialStartSequence,
133  InRCDATA,
134
135  InEntity,
136
137  InSFCRootTagName,
138}

Tips

実は,このトーカナイザは htmlparser2 というライブラリのフォーク実装です.

https://github.com/fb55/htmlparser2/tree/master

このパーサーは最も早い HTML パーサーとして知られていて,Vue.js は v3.4 以降このパーサーを利用することによってパフォーマンスを大きく向上 させました.

ソースコード上でもその旨が記載されています

1/**
2 * This Tokenizer is adapted from htmlparser2 under the MIT License listed at
3 * https://github.com/fb55/htmlparser2/blob/master/LICENSE

トークンはソースコード上では State として表現されています.
tokenizer は state という内部状態を一つ持ち,これは State という enum で定義された状態のどれか一つに定ります.

具体的に覗いてみると,まずはデフォルトの状態である Text, そして,Interpolation の開始を表す {{, Interpolation の終了を表す }}, その間の状態,タグの開始を表す <, タグの終了を表す > などが定義されています.

以下の,

47export enum CharCodes {
48  Tab = 0x9, // "\t"
49  NewLine = 0xa, // "\n"
50  FormFeed = 0xc, // "\f"
51  CarriageReturn = 0xd, // "\r"
52  Space = 0x20, // " "
53  ExclamationMark = 0x21, // "!"
54  Number = 0x23, // "#"
55  Amp = 0x26, // "&"
56  SingleQuote = 0x27, // "'"
57  DoubleQuote = 0x22, // '"'
58  GraveAccent = 96, // "`"
59  Dash = 0x2d, // "-"
60  Slash = 0x2f, // "/"
61  Zero = 0x30, // "0"
62  Nine = 0x39, // "9"
63  Semi = 0x3b, // ";"
64  Lt = 0x3c, // "<"
65  Eq = 0x3d, // "="
66  Gt = 0x3e, // ">"
67  Questionmark = 0x3f, // "?"
68  UpperA = 0x41, // "A"
69  LowerA = 0x61, // "a"
70  UpperF = 0x46, // "F"
71  LowerF = 0x66, // "f"
72  UpperZ = 0x5a, // "Z"
73  LowerZ = 0x7a, // "z"
74  LowerX = 0x78, // "x"
75  LowerV = 0x76, // "v"
76  Dot = 0x2e, // "."
77  Colon = 0x3a, // ":"
78  At = 0x40, // "@"
79  LeftSquare = 91, // "["
80  RightSquare = 93, // "]"
81}
82
83const defaultDelimitersOpen = new Uint8Array([123, 123]) // "{{"
84const defaultDelimitersClose = new Uint8Array([125, 125]) // "}}"

あたりを見てもわかる通り,解析する文字列は Uint8Array や数値としてエンコードし,パフォーマンスの向上を図っています.(あまり詳しくないですが,おそらく数値の方が比較演算が早かったりするのでしょう.)

htmlparser2 のフォーク実装なので,Vue.js のソースコードリーディングかと言われると微妙なところはありますが,実際に少し Tokenizer の実装を読んでみましょう.
以下からが Tokenizer の実装です.

236export default class Tokenizer {

コンストラクタを見てわかる通り,各 Token に対するコールバックを定義することで,「tokenize -> parse」を実現しています.
(後述の parser.ts の方で実際に template のパース処理をこのコールバックを定義することで実現しています)

265  constructor(
266    private readonly stack: ElementNode[],
267    private readonly cbs: Callbacks,
268  ) {
180export interface Callbacks {
181  ontext(start: number, endIndex: number): void
182  ontextentity(char: string, start: number, endIndex: number): void
183
184  oninterpolation(start: number, endIndex: number): void
185
186  onopentagname(start: number, endIndex: number): void
187  onopentagend(endIndex: number): void
188  onselfclosingtag(endIndex: number): void
189  onclosetag(start: number, endIndex: number): void
190
191  onattribdata(start: number, endIndex: number): void
192  onattribentity(char: string, start: number, end: number): void
193  onattribend(quote: QuoteType, endIndex: number): void
194  onattribname(start: number, endIndex: number): void
195  onattribnameend(endIndex: number): void
196
197  ondirname(start: number, endIndex: number): void
198  ondirarg(start: number, endIndex: number): void
199  ondirmodifier(start: number, endIndex: number): void
200
201  oncomment(start: number, endIndex: number): void
202  oncdata(start: number, endIndex: number): void
203
204  onprocessinginstruction(start: number, endIndex: number): void
205  // ondeclaration(start: number, endIndex: number): void
206  onend(): void
207  onerr(code: ErrorCodes, index: number): void
208}

そして,parse というメソッドが最初の関数です.

923  /**
924   * Iterates through the buffer, calling the function corresponding to the current state.
925   *
926   * States that are more likely to be hit are higher up, as a performance improvement.
927   */
928  public parse(input: string): void {

buffer にソースを読み込み (格納し),1 文字づつ読み進めます.

929    this.buffer = input
930    while (this.index < this.buffer.length) {

特定の state の際にコールバックを実行します.
初期値は State.Text なのでまずそこに入ります.

935      switch (this.state) {
936        case State.Text: {
937          this.stateText(c)
938          break
939        }
940        case State.InterpolationOpen: {
941          this.stateInterpolationOpen(c)
942          break
943        }

例えば,stateText の場合で,現在の文字が < だった場合には ontext コールバックを実行しつつ,stateState.BeforeTagName に更新します.

318  private stateText(c: number): void {
319    if (c === CharCodes.Lt) {
320      if (this.index > this.sectionStart) {
321        this.cbs.ontext(this.sectionStart, this.index)
322      }
323      this.state = State.BeforeTagName
324      this.sectionStart = this.index

このように,特定の状態下で文字を読み,その文字の種類によって状態を遷移させ次々と読み進めます.
基本的にはこれの繰り返しです.

他の状態の他の文字の場合はあまりに実装が多いので割愛します. (多いですが,やってることは一緒です.)

Parse

さて,トーカナイザの実装があらかた理解できたので次は parse です.
こちらは parser.ts に実装があります.

packages/compiler-core/src/parser.ts

ここで先ほどの Tokenizer が利用されています

97const tokenizer = new Tokenizer(stack, {

各トークンに対してコールバックを登録し,template の AST を組み立てて行っています.

とりあえず一つの例を見てみましょう.
oninterpolation というコールバックに注目してください.

こちらは名前の通り,Interpolation Node に関連する処理です.

108  oninterpolation(start, end) {

delimiter (デフォルトでは {{}}) の長さと,渡ってくる index を元に,Interpolation の inner (コンテンツ) の index を計算しています.

112    let innerStart = start + tokenizer.delimiterOpen.length
113    let innerEnd = end - tokenizer.delimiterClose.length

その index を元に inner のコンテンツを取得します

120    let exp = getSlice(innerStart, innerEnd)

そして最後に Node を生成します.

129    addNode({
130      type: NodeTypes.INTERPOLATION,
131      content: createExp(exp, false, getLoc(innerStart, innerEnd)),
132      loc: getLoc(start, end),
133    })

addNode はすでに stack がある場合はそこに,ない場合は root の children に Node を push する関数です.

916function addNode(node: TemplateChildNode) {
917  ;(stack[0] || currentRoot).children.push(node)
918}

stack というのは,element がネストしていくたびにその element を積んでいくスタックです.

せっかくなのでその処理も見てみましょう.

opentag (開始タグ) が終わったタイミング,例えば <p> だった場合は > のタイミングで stack に現在の tag を unshift しています

567function endOpenTag(end: number) {
568  if (tokenizer.inSFCRoot) {
569    // in SFC mode, generate locations for root-level tags' inner content.
570    currentOpenTag!.innerLoc = getLoc(end + 1, end + 1)
571  }
572  addNode(currentOpenTag!)
573  const { tag, ns } = currentOpenTag!
574  if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
575    inPre++
576  }
577  if (currentOptions.isVoidTag(tag)) {
578    onCloseTag(currentOpenTag!, end)
579  } else {
580    stack.unshift(currentOpenTag!)
581    if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
582      tokenizer.inXML = true
583    }
584  }
585  currentOpenTag = null
586}
580    stack.unshift(currentOpenTag!)

そして,onclosetag で stack を shift します.

154  onclosetag(start, end) {
155    const name = getSlice(start, end)
156    if (!currentOptions.isVoidTag(name)) {
157      let found = false
158      for (let i = 0; i < stack.length; i++) {
159        const e = stack[i]
160        if (e.tag.toLowerCase() === name.toLowerCase()) {
161          found = true
162          if (i > 0) {
163            emitError(ErrorCodes.X_MISSING_END_TAG, stack[0].loc.start.offset)
164          }
165          for (let j = 0; j <= i; j++) {
166            const el = stack.shift()!

このように,Tokenizer のコールバックを駆使して AST を構築していきます.
実装量自体は多いですが,基本的にはこれらを地道にやっていくだけです.