Skip to content

Latest commit

 

History

History
306 lines (260 loc) · 7.86 KB

40. 三种类型联合解析.md

File metadata and controls

306 lines (260 loc) · 7.86 KB

三种类型联合解析

在本小节中,我们将会实现 插值elementtext 联合解析

1. happy path

1.1 测试样例

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',
        },
      },
    ],
  })
})

1.2 实现

首先,我们在 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
}

1.2.1 parseText

此时 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,
  }
}

1.2.2 parseInterpolation

但是现在我们 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 测试就可以跑通了

2. 边缘情况一

2.1 测试样例

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

2.2 实现

那么问题出现在哪里?这是因为我们在处理 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,
  }
}

这里我们处理嵌套的就完毕了

3. 边缘情况二

3.1 测试样例

test('should throw error when lack end tag', () => {
  expect(() => {
    baseParse('<div><span></div>')
  }).toThrow()
})

这里我们将会处理,如果没有结束标签,那么就会抛出异常

此时我们进行测试,发现是会死循环的,这是因为我们在 parseChildren 中的循环,这里的情况显然是无法触发跳出循环的,因为 context.source 没有消费完,而且没有结束标签,所以就死循环了

3.2 实现

我们可以使用一种栈来储存 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
}