Template の Parser
前回までで parse の成果物である,AST について詳しくみてきました.
ここからは実際にその AST を生成する parser について見ていきます.
parser は parse
と tokenize
という 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 }
例えば,state
が Text
の場合で,現在の文字が <
だった場合には ontext
コールバックを実行しつつ,state
を State.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 を構築していきます.
実装量自体は多いですが,基本的にはこれらを地道にやっていくだけです.