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