例子
实现3种类型结合的情况
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",
},
},
],
});
});
实现
目前问题
就目前的代码,是无法实现联合类型的判断,因为我们代码只运行了一次。
例子 <div><p>hi,</p>{{message}}</div>
,会直接判断为element类型,然后整个返回了。
但是我们想实现深层的推进,返回一个children的形式,思路大概如下:
- 写一个循环,多次调用parseChildren,多次去推进判断类型
- 要有结束循环的条件,结束循环的条件有2种情况
- 匹配上对应标签。例如 是 开头的标签,如果剩下的content是,那说明内容已经推进到尾部了,结束循环
- 若内容为空,结束循环
代码实现
创建children
每个tag包裹的东西,都应该放在对应的children里
// parse.ts
// 解析element
function parseElement(context: { source: string }): any {
// 这里调用两次 parseTag 处理前后标签
const element: any = parseTag(context, TagType.START);
// 增加 parseChildren,储存包裹的内容
+ element.children = parseChildren(context);
// 处理闭合标签
parseTag(context, TagType.END);
return element;
}
循环推进
通过判断标签、内容进行深度
- 生成对应tag传过去,当做匹配标签的条件
- 判断content是否有内容
function parseChildren(context: { source: string }, parentTag): any {
const nodes: any = [];
while (isEnd(context, parentTag)) {
let node;
const s = context.source;
/** 判断字符串类型
* 1. 为插值
* 2. 为element
*/
if (s.startsWith("{{")) {
// {{ 开头,即认为是插值
node = parseInterpolation(context);
} else if (s.startsWith("<") && /[a-z]/i.test(s[1])) {
// <开头,并且第二位是a-z,即认为是element类型
node = parseElement(context);
} else {
node = parseText(context);
}
nodes.push(node);
}
return nodes;
}
// 匹配是否结束标签
function isEnd(context: { source: string }, parentTag: any) {
const s = context.source;
// 判断tag是结束标签
if (parentTag && s.startsWith(`</${parentTag}>`)) {
return false;
}
// 返回内容本身
return s;
}
初始化tag
export function baseParse(content: string) {
const context = createContext(content);
+ return createRoot(parseChildren(context, ""));
}
生成tag
function parseElement(context: { source: string }): any {
// 这里调用两次 parseTag 处理前后标签
const element: any = parseTag(context, TagType.START);
// 增加 parseChildren,储存包裹的内容
+ element.children = parseChildren(context, element.tag);
// 处理闭合标签
parseTag(context, TagType.END);
return element;
}
处理text
function parseText(context: { source: string }): any {
const s = context.source
const endToken = '{{'
let endIndex = s.length
// 如果 context.source 包含了 {{,那么我们就以 {{ 作为结束点
const index = s.indexOf(endToken)
if (index !== -1) {
endIndex = index
}
// 获取当前字符串内容
const content = parseTextData(context, endIndex)
advanceBy(context, content.length)
return {
type: NodeType.TEXT,
content,
}
}
拓展
实现element包裹element
主要的逻辑点在切割text类型的时候,判断当前字符串是否包含 element 标签
- 写一个固定的ARRAY存储要匹配的标签
- 取当前最小值,即最靠近 text 的位置
function parseText(context: { source: string }): any {
const s = context.source;
let len = s.length;
/** 处理element包裹情况
* 1. 新建一个TAG_ARRAY,用来判断text后可能存在的符号
* 2. 取最贴近text的符号,因为 < 跟 {{ 可能同时都存在,取最小的,即离text内容最近的
*/
const TAG_ARRAY = ["<", "{{"];
for (let i = 0; i < TAG_ARRAY.length; i++) {
const tag = TAG_ARRAY[i];
const index = s.indexOf(tag);
/** 获取text的位置
* 1. 如果符号存在,并且小于len,取离text最近的内容
* 例如 hi,</p>{{message}},会先找到 < 的位置,覆盖len,又找到 {{,但是 {{ 比 len大,说明 {{ 符号在后面,所以不赋值
* 2. 如果不存在,直接切到最后面即可
*/
if (index !== -1 && index < len) {
len = index;
}
}
// 获取当前字符串内容
const content = parseTextData(context, len);
// 推进
advanceBy(context, len);
return {
type: NodeType.TEXT,
content,
};
}
兼容无闭合标签
目前代码遇到无闭合标签,会陷入死循环,会一直是true,切不完。。
思想的思路:把所有标签进行收集,在切割闭合标签前判断前后是否相等,不相等则说明,当前处理的标签没有写闭合标签。
收集标签
写一个栈储存所有tag
export function baseParse(content: string) {
const context = createContext(content);
+ return createRoot(parseChildren(context, []));
}
+ function parseChildren(context: { source: string }, ancestors): any {
const nodes: any = [];
while (isEnd(context, ancestors)) {
let node;
const s = context.source;
/** 判断字符串类型
* 1. 为插值
* 2. 为element
*/
if (s.startsWith("{{")) {
// {{ 开头,即认为是插值
node = parseInterpolation(context);
} else if (s.startsWith("<") && /[a-z]/i.test(s[1])) {
// <开头,并且第二位是a-z,即认为是element类型
+ node = parseElement(context, ancestors);
} else {
node = parseText(context);
}
nodes.push(node);
}
return nodes;
}
判断是否闭合标签
如果是闭合标签,则跳出当前tag的循环
function isEnd(context: { source: string }, ancestors: any) {
const s = context.source;
/** 是否结束标签
* 1. 判断是否</开头,是则进入循环
* 2. 从栈顶开始循环,栈是先入后出的,所以要从最底部开始循环
* 3. 判断当前的标签的tag是否跟栈的tag相等,相等则说明当前tag内容已经推导结束,需要结束当前children循环,进入下一个循环
*/
+ if (s.startsWith("</")) {
+ for (let i = ancestors.length - 1; i >= 0; i--) {
+ const tag = ancestors[i].tag;
+ if (startsWithEndTagOpen(s, tag)) {
+ return false;
+ }
+ }
+ }
// 返回内容本身
return s;
}
+ function startsWithEndTagOpen(source, tag) {
+ const endTokenLength = "</".length;
+ return source.slice(endTokenLength, tag.length + endTokenLength) === tag;
+ }
消费标签
function parseElement(context: { source: string }, ancestors): any {
// 这里调用两次 parseTag 处理前后标签
const element: any = parseTag(context, TagType.START);
// 收集标签
+ ancestors.push(element);
// 增加 parseChildren,储存包裹的内容
element.children = parseChildren(context, ancestors);
// 循环结束,把当前tag删除
+ ancestors.pop();
/** 切除闭合标签
* 1. 当前tag等于首部tag,说明是闭合标签,则进行切除
* 2. 不相等,则说明没有写闭合标签,报警告
*/
+ if (startsWithEndTagOpen(context.source, element.tag)) {
+ // 处理闭合标签
+ parseTag(context, TagType.END);
+ } else {
+ throw new Error(`不存在结束标签:${element.tag}`);
+ }
return element;
}