Skip to content

Commit eccd237

Browse files
committed
docs: parser
1 parent 09bee07 commit eccd237

File tree

2 files changed

+390
-7
lines changed

2 files changed

+390
-7
lines changed

demo/20-parse-template.html

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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

Comments
 (0)