35-编译模块-联合类型实现

137 阅读1分钟

例子

实现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的形式,思路大概如下:

  1. 写一个循环,多次调用parseChildren,多次去推进判断类型
  2. 要有结束循环的条件,结束循环的条件有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;
}