在本小节中,我们将会实现 插值
、element
、text
联合解析
test('happy path', () => {
const ast = baseParse('<div>hi,{{message}}</div>')
expect(ast.children[0]).toStrictEqual({
type: NodeType.ELEMENT,
tag: 'div',
children: [
{
type: NodeType.TEXT,
content: 'hi,',
},
{
type: NodeType.INTERPOLATION,
content: {
type: NodeType.SIMPLE_EXPRESSION,
content: 'message',
},
},
],
})
})
首先,我们在 parseElement
的时候 parse 前 Tag 过后直接就 parse 了后 Tag,现在肯定是不可以,我们还需要 parseChildren
function parseElement(context: { source: string }): any {
const element: any = parseTag(context, TagType.START)
// 增加 parseChildren
element.children = parseChildren(context)
parseTag(context, TagType.END)
return element
}
此时 element.children
长这样,我们发现在 parseText
的时候没有考虑到插值
[ { type: 3, content: 'hi,{{message}}</div>' } ]
function parseText(context: { source: string }): any {
// 也就是在这里,我们直接截取到了字符串的结尾
const content = parseTextData(context, context.source.length)
advanceBy(context, content.length)
return {
type: NodeType.TEXT,
content,
}
}
此时我们需要修改一下
function parseText(context: { source: string }): any {
// 如果 context.source 包含了 {{,那么我们就以 {{ 作为结束点
const s = context.source
const endToken = '{{'
let endIndex = s.length
const index = s.indexOf(endToken)
if (index !== -1) {
endIndex = index
}
// 此时我们的 content 就变成了 hi,
const content = parseTextData(context, endIndex)
advanceBy(context, content.length)
return {
type: NodeType.TEXT,
content,
}
}
但是现在我们 parseText
之后就没有下文了,所以我们在 parseChildren 的时候其实可以写一个循环,循环处理
function parseElement(context: { source: string }): any {
const element: any = parseTag(context, TagType.START)
// parentTag 是从这里来的
element.children = parseChildren(context, element.tag)
parseTag(context, TagType.END)
return element
}
function parseChildren(context: { source: string }, parentTag: string): any {
const nodes: any = []
// 这里进行循环处理,parentTag 是从哪里来的?parseElement
while (!isEnd(context, parentTag)) {
let node
const s = context.source
if (s.startsWith('{{')) {
node = parseInterpolation(context)
} else if (s.startsWith('<') && /[a-z]/i.test(s[1])) {
node = parseElement(context)
}
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
// 循环的结束条件有两个:1. 遇到结束标签 2. context.source 没有值了
function isEnd(context: { source: string }, parentTag: string) {
const s = context.source
// 2. 遇到结束标签
if (parentTag && s.startsWith(`</${parentTag}>`)) {
return true
}
// 1. source 有值
return !s
}
现在我们的 happy path
测试就可以跑通了
test('nested element', () => {
const ast = baseParse('<div><p>hi,</p>{{message}}</div>')
expect(ast.children[0]).toStrictEqual({
type: NodeType.ELEMENT,
tag: 'div',
children: [
{
type: NodeType.ELEMENT,
tag: 'p',
children: [
{
type: NodeType.TEXT,
content: 'hi,',
},
],
},
{
type: NodeType.INTERPOLATION,
content: {
type: NodeType.SIMPLE_EXPRESSION,
content: 'message',
},
},
],
})
})
这个测试样例会在 element
中嵌套 element
那么问题出现在哪里?这是因为我们在处理 text
的时候,结束条件我们只写了 {{
,结果遇到了 </
还是不会停止,所以我们需要加一下处理
function parseText(context: { source: string }): any {
const s = context.source
// 加上 < 的处理
const endTokens = ['<', '{{']
let endIndex = s.length
for (let i = 0; i < endTokens.length; i++) {
const index = s.indexOf(endTokens[i])
// endIndex > index 说明我们取最前面的
if (index !== -1 && endIndex > index) {
endIndex = index
}
}
const content = parseTextData(context, endIndex)
advanceBy(context, content.length)
return {
type: NodeType.TEXT,
content,
}
}
这里我们处理嵌套的就完毕了
test('should throw error when lack end tag', () => {
expect(() => {
baseParse('<div><span></div>')
}).toThrow()
})
这里我们将会处理,如果没有结束标签,那么就会抛出异常
此时我们进行测试,发现是会死循环的,这是因为我们在 parseChildren
中的循环,这里的情况显然是无法触发跳出循环的,因为 context.source
没有消费完,而且没有结束标签,所以就死循环了
我们可以使用一种栈来储存 tag,在解析 tag 的时候将 tag 储存,并在解析完毕后将这个 tag 弹出。
而实现报错的方式,我们可以将当前的内容与前一个 tag 进行对比,如果对比不上就报错,例如图中例子的
<div><span></div>
,最终我们将 context.source
推进到了 </div>
,此时和上一个 <span>
得出的 tag span
进行对比,对比不上,那么报错。
export function baseParse(content: string) {
const context = createContext(content)
// 第二个参数,ancestors
return createRoot(parseChildren(context, []))
}
function parseChildren(context: { source: string }, ancestors): any {
const nodes: any = []
// 在 isEnd 的时候进行消费
while (!isEnd(context, ancestors)) {
let node
const s = context.source
if (s.startsWith('{{')) {
node = parseInterpolation(context)
} else if (s.startsWith('<') && /[a-z]/i.test(s[1])) {
// 在 parseElement 的时候进行收集
node = parseElement(context, ancestors)
}
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
function parseElement(context: { source: string }, ancestors): any {
const element: any = parseTag(context, TagType.START)
// 收集
ancestors.push(element)
// 这里的第二个参数记得改一下
element.children = parseChildren(context, ancestors)
// 弹出
ancestors.pop()
parseTag(context, TagType.END)
return element
}
function isEnd(context: { source: string }, ancestors) {
const s = context.source
// 2. 遇到结束标签
if (s.startsWith('</')) {
// 这里从栈顶开始循环
for (let i = ancestors.length - 1; i >= 0; i--) {
// 如果说栈里存在这个标签,那么就跳出循环
const tag = ancestors[i].tag
if (startsWithEndTagOpen(context.source, tag)) {
return true
}
}
}
// 1. source 有值
return !s
}
function startsWithEndTagOpen(source, tag) {
const endTokenLength = '</'.length
return source.slice(endTokenLength, tag.length + endTokenLength) === tag
}
那么我们是在哪里报错的呢
function parseElement(context: { source: string }, ancestors): any {
const element: any = parseTag(context, TagType.START)
ancestors.push(element)
element.children = parseChildren(context, ancestors)
ancestors.pop()
// 在这里进行报错,如果我们查到前后结束的标签不一致,就报错
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.END)
} else {
throw new Error(`不存在结束标签:${element.tag}`)
}
return element
}