实现解析三种联合类型
- 目标,解析
<div>hi,{``{message}}</div>
- 测试
js
复制代码
// describe 与 it 配合使用,如果单独使用,就用 test
test("should", () => {
const ast = baseParse("<div>hi,{{message}}</div>")
expect(ast.children[0]).toStrictEqual({
type: NodeTypes.ELEMENT,
tag: "div",
children: [
{
type: NodeTypes.TEXT,
content: "hi,"
},
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "message"
}
}
]
})
})
- 功能实现
js
复制代码
function parseElement(context) {
let element: any = parseTag(context, TagType.Start)
element.children = parseChildren(context) // ✅ 元素标签里面嵌套内容
parseTag(context, TagType.End)
return element
}
function parseChildren(context) {
const nodes: any[] = []
// ✅ 混合模式会 出现标签嵌套字符串+插值语法,所以这里使用循环
while (!isEnd(context)) {
let node: any
const s = context.source
if (s.startsWith('{{')) {
node = parseInterpolation(context)
} else if (s[0] === '<') {
if (/[a-z]/i.test(context.source[1])) {
node = parseElement(context)
}
}
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
function isEnd(context) { // ✅ 这里判断循环终止条件
const s = context.source
if (s.startsWith("</div>")) {
return true
}
return !s
}
function parseText(context: any) {
// 1. 获取 content
let endIndex: number = context.source.length
// ✅ 当解析遇到插值语法时停止,并将目前解析的内容进行返回
const endToken = "{{"
if (context.source.indexOf(endToken) !== -1) {
endIndex = context.source.indexOf(endToken)
}
const content = parseTextData(context, endIndex)
// 2. 推进
advanceBy(context, content.length)
return {
type: NodeTypes.TEXT,
content
}
}
function parseTextData(context: any, length: number) {
return context.source.slice(0, length)
}
- 到目前一个简单的逻辑已经跑通,我们重构之前写死的逻辑,我们将测试中的 div 换成 p 标签进行实现
js
复制代码
test.only("should", () => {
const ast = baseParse("<p>hi,{{message}}</p>") // ✅
expect(ast.children[0]).toStrictEqual({
type: NodeTypes.ELEMENT,
tag: "p", // ✅
children: [
{
type: NodeTypes.TEXT,
content: "hi,"
},
{
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: "message"
}
}
]
})
})
- 实现过程
js
复制代码
// ✅ 我们在解析 element 时,先对标签进行记录
function parseElement(context) {
let element: any = parseTag(context, TagType.Start)
element.children = parseChildren(context, element.tag)
parseTag(context, TagType.End)
return element
}
// ✅ 层层传递,当遇到需要匹配标签的情况时,直接动态替换
function parseChildren(context, parentTag ) { // ✅
const nodes: any[] = []
// 混合模式会 出现标签嵌套字符串+插值语法,所以这里使用循环
while (!isEnd(context, parentTag)) { // ✅
let node: any
const s = context.source
if (s.startsWith('{{')) {
node = parseInterpolation(context)
} else if (s[0] === '<') {
if (/[a-z]/i.test(context.source[1])) {
node = parseElement(context)
}
}
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
function isEnd(context, parentTag) {
const s = context.source
if (parentTag && s.startsWith(`</${parentTag}>`)) { // ✅
return true
}
return !s
}
- 如果标签内嵌套标签怎么处理呢?比如这样
<div><p>hi</p>{``{message}}</div>
js
复制代码
function parseText(context: any) {
// 1. 获取 content
let endIndex: number = context.source.length
const endToken = ["<", "{{"] // ✅
for (let i = 0; i < endToken.length; i++) { // ✅
let index = context.source.indexOf(endToken[i])
if (index !== -1 && endIndex > index) {
endIndex = index
}
}
const content = parseTextData(context, endIndex)
// 2. 推进
advanceBy(context, content.length)
return {
type: NodeTypes.TEXT,
content
}
}
- 另一种情况
<div><span></div> 这里的 span 没有结束标签, 需要抛出错误,提示缺少 span 的结束标签
js
复制代码
// 单测
test.only("should throw error when lack end tag",() => {
expect(()=> {
baseParse("<div><span></div>")
}).toThrow("缺少结束标签:span")
})
- 功能实现
js
复制代码
function parseElement(context, ancestors:any[]=[]) { // ✅
let element: any = parseTag(context, TagType.Start)
ancestors.push(element) // ✅
element.children = parseChildren(context, ancestors)
ancestors.pop() // ✅
if(context.source.slice(2, 2 + element.tag.length)===element.tag) { // ✅
parseTag(context, TagType.End)
} else { // ✅
throw new Error(`缺少结束标签:${element.tag}`)
}
return element
}
function parseChildren(context, ancestors) { // ✅
const nodes: any[] = []
// 混合模式会 出现标签嵌套字符串+插值语法,所以这里使用循环
while (!isEnd(context, ancestors)) { // ✅
let node: any
const s = context.source
if (s.startsWith('{{')) {
node = parseInterpolation(context)
} else if (s[0] === '<') {
if (/[a-z]/i.test(context.source[1])) {
node = parseElement(context, ancestors) // ✅
}
}
if (!node) {
node = parseText(context)
}
nodes.push(node)
}
return nodes
}
function isEnd(context, ancestors) { // ✅
const s = context.source
if(s.startsWith("</")){
for(let i = 0; i < ancestors.length; i++) { // ✅
const tag = ancestors[i].tag
if(s.slice(2, 2 + tag.length)===tag) {
return true
}
}
}
return !s
}
- 优化
js
复制代码
function isEnd(context, ancestors) {
const s = context.source
if(s.startsWith("</")){
for(let i = 0; i < ancestors.length; i++) {
const tag = ancestors[i].tag
if(startWidthEndTagOpen(s, tag)) {// ✅
return true
}
}
}
return !s
}
function parseElement(context, ancestors:any[]=[]) {
let element: any = parseTag(context, TagType.Start)
ancestors.push(element) // 收集 Tag
element.children = parseChildren(context, ancestors)
ancestors.pop()
if(startWidthEndTagOpen(context.source, element.tag)) {// ✅
parseTag(context, TagType.End)
} else {
throw new Error(`缺少结束标签:${element.tag}`)
}
return element
}
function startWidthEndTagOpen(source, tag) {// ✅
return source.startsWith("</") && source.slice(2, 2+tag.length).toLocaleLowerCase() === tag.toLocaleLowerCase()
}
function isEnd(context, ancestors) {
const s = context.source
if(s.startsWith("</")){
for(let i = ancestors.length - 1; i >= 0; i--) {// ✅
const tag = ancestors[i].tag
if(startWidthEndTagOpen(s, tag)) {
return true
}
}
}
return !s
}