1
+ < body > </ body >
2
+
3
+ < script >
4
+ /**
5
+ * 模板解析
6
+ * @param {String } str 模板字符串
7
+ */
8
+ function parse ( str ) {
9
+ const context = {
10
+ // 存储模板字符串
11
+ source : str ,
12
+ // 前进 num 个字符
13
+ advanceBy ( num ) {
14
+ context . source = context . source . slice ( num )
15
+ } ,
16
+ advanceSpaces ( ) {
17
+ // 匹配空格
18
+ const match = / ^ [ \t \r \n \f ] + / . exec ( context . source )
19
+ if ( match ) {
20
+ context . advanceBy ( match [ 0 ] . length )
21
+ }
22
+ }
23
+ }
24
+ const nodes = parseChildren ( context , [ ] )
25
+ // 根节点
26
+ const root = {
27
+ type : 'Root' ,
28
+ children : nodes
29
+ }
30
+ return root
31
+ }
32
+
33
+ /**
34
+ * 解析子节点
35
+ * @param {Object } context 上下文
36
+ * @param {Array } ancestors 祖先节点
37
+ */
38
+ function parseChildren ( context , ancestors = [ ] ) {
39
+ const nodes = [ ]
40
+
41
+ while ( ! isEnd ( context , ancestors ) ) {
42
+ let node
43
+ const s = context . source
44
+
45
+ if ( s . startsWith ( '{{' ) ) {
46
+ // 解析插值表达式
47
+ node = parseInterpolation ( context )
48
+ } else if ( s [ 0 ] === '<' ) {
49
+ if ( s [ 1 ] === '/' ) {
50
+ // 结束标签
51
+ } else if ( / [ a - z ] / i. test ( s [ 1 ] ) ) {
52
+ // 解析开始标签
53
+ node = parseElement ( context , ancestors )
54
+ }
55
+ }
56
+ if ( ! node ) {
57
+ node = parseText ( context )
58
+ }
59
+ nodes . push ( node )
60
+ }
61
+
62
+ return nodes
63
+ }
64
+
65
+ /**
66
+ * 解析插值表达式
67
+ * @param {* } context 上下文
68
+ * @returns
69
+ */
70
+ function parseInterpolation ( context ) {
71
+ const { advanceBy } = context
72
+ // 移除 {{
73
+ advanceBy ( 2 )
74
+ const closeIndex = context . source . indexOf ( '}}' )
75
+ const rawContent = context . source . slice ( 0 , closeIndex )
76
+ // 去掉前后空格
77
+ const content = rawContent . trim ( )
78
+ advanceBy ( rawContent . length )
79
+ // 移除 }}
80
+ advanceBy ( 2 )
81
+
82
+ return {
83
+ type : 'Interpolation' ,
84
+ content : {
85
+ type : 'Expression' ,
86
+ content
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 解析元素
93
+ * @param {* } context
94
+ * @param {* } ancestors
95
+ * @returns
96
+ */
97
+ function parseElement ( context , ancestors ) {
98
+ // 解析开始标签
99
+ // <div></div>
100
+ const element = parseTag ( context )
101
+
102
+ ancestors . push ( element )
103
+ element . children = parseChildren ( context , ancestors )
104
+ ancestors . pop ( )
105
+
106
+ if ( context . source . startsWith ( `</${ element . tag } ` ) ) {
107
+ // 解析结束标签
108
+ parseTag ( context , 'end' )
109
+ } else {
110
+ console . error ( `缺失结束标签:${ element . tag } ` )
111
+ }
112
+
113
+ return element
114
+ }
115
+
116
+ /**
117
+ * 解析标签
118
+ * @param {* } context
119
+ * @param {* } type
120
+ * @returns
121
+ */
122
+ function parseTag ( context , type = 'start' ) {
123
+ const { source, advanceBy, advanceSpaces } = context
124
+ // <div></div>
125
+ // type=start: ['<div', 'div', index: 0, input: '<div>', groups: undefined]
126
+ const match =
127
+ type === 'start'
128
+ ? / ^ < ( [ a - z ] [ ^ \t \r \n \f / > ] * ) / i. exec ( source ) // 匹配开始标签
129
+ : / ^ < \/ ( [ a - z ] [ ^ \t \r \n \f / > ] * ) / i. exec ( source ) // 匹配结束标签
130
+ const tag = match [ 1 ]
131
+
132
+ // 移除 <div
133
+ advanceBy ( match [ 0 ] . length )
134
+ // 移除多余空格
135
+ advanceSpaces ( )
136
+
137
+ const props = parseAttributes ( context )
138
+
139
+ // 暂时不处理自闭合标签
140
+ // 移除 >
141
+ advanceBy ( 1 )
142
+
143
+ return {
144
+ type : 'Element' ,
145
+ tag,
146
+ props,
147
+ children : [ ]
148
+ }
149
+ }
150
+
151
+ /**
152
+ * 解析标签属性
153
+ * @param {* } context
154
+ * @returns {Array }
155
+ * @example
156
+ *
157
+ * id="foo" class="bar"
158
+ * =>
159
+ * [{ type: 'Attribute', name: 'id', value: 'foo' }, { type: 'Attribute', name: 'class', value: 'bar' }]
160
+ */
161
+ function parseAttributes ( context ) {
162
+ const { advanceBy, advanceSpaces } = context
163
+ const props = [ ]
164
+
165
+ // example: id="foo" class="bar"></div>
166
+ while ( ! context . source . startsWith ( '>' ) && ! context . source . startsWith ( '/>' ) ) {
167
+ const match = / ( [ \w : - @ ] + ) = / . exec ( context . source )
168
+ // 属性名称
169
+ let name = match [ 1 ]
170
+ // 移除 id
171
+ advanceBy ( name . length )
172
+ // 移除 =
173
+ advanceBy ( 1 )
174
+
175
+ let isStatic = true
176
+ if ( name . startsWith ( '@' ) ) { // @click -> onClick
177
+ isStatic = false
178
+ const eventName = name . slice ( 1 )
179
+ name = 'on' + eventName [ 0 ] . toUpperCase ( ) + eventName . slice ( 1 )
180
+ }
181
+
182
+ let value = ''
183
+
184
+ const quote = context . source [ 0 ]
185
+ const isQuoted = quote === '"' || quote === "'"
186
+ if ( isQuoted ) {
187
+ advanceBy ( 1 )
188
+ const endIndex = context . source . indexOf ( quote )
189
+ value = context . source . slice ( 0 , endIndex )
190
+ advanceBy ( value . length )
191
+ advanceBy ( 1 )
192
+ } else {
193
+ // 处理非引号包裹的属性值
194
+ }
195
+ // 移除空格
196
+ advanceSpaces ( )
197
+
198
+ props . push ( {
199
+ type : 'Attribute' ,
200
+ name,
201
+ value,
202
+ isStatic,
203
+ } )
204
+ }
205
+
206
+ return props
207
+ }
208
+
209
+ /**
210
+ * 解析文本
211
+ * @param {* } context
212
+ * @returns
213
+ * @examples
214
+ *
215
+ * case 1: template</div>
216
+ * case 2: template {{ msg }}</div>
217
+ * ...
218
+ */
219
+ function parseText ( context ) {
220
+ let endIndex = context . source . length
221
+ const ltIndex = context . source . indexOf ( '<' )
222
+ const delimiterIndex = context . source . indexOf ( '{{' )
223
+
224
+ if ( ltIndex > - 1 && ltIndex < endIndex ) {
225
+ endIndex = ltIndex
226
+ }
227
+ if ( delimiterIndex > - 1 && delimiterIndex < endIndex ) {
228
+ endIndex = delimiterIndex
229
+ }
230
+
231
+ const content = context . source . slice ( 0 , endIndex )
232
+
233
+ context . advanceBy ( content . length )
234
+
235
+ return {
236
+ type : 'Text' ,
237
+ content
238
+ }
239
+ }
240
+
241
+ /**
242
+ * 是否解析结束
243
+ * @param {* } context
244
+ * @param {* } ancestors
245
+ */
246
+ function isEnd ( context , ancestors ) {
247
+ if ( ! context . source ) return true
248
+ // 与节点栈内全部的节点比较
249
+ for ( let i = ancestors . length - 1 ; i >= 0 ; -- i ) {
250
+ if ( context . source . startsWith ( `</${ ancestors [ i ] . tag } ` ) ) {
251
+ return true
252
+ }
253
+ }
254
+ }
255
+
256
+ console . log ( '开始解析:' )
257
+ const ast = parse (
258
+ `<div id="foo" class="bar"><p>{{ msg }}</p><p>Template</p></div>`
259
+ )
260
+
261
+ console . log ( ast )
262
+ console . log ( JSON . stringify ( ast , null , 2 ) )
263
+
264
+ </ script >
0 commit comments