File size: 3,420 Bytes
89ce340
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import type { Token, HTMLNode, TagToken, NormalElement, TagEndToken, AttributeToken, TextToken } from './types'
import { closingTags, closingTagAncestorBreakers, voidTags } from './tags'

interface StackItem {
  tagName: string | null
  children: HTMLNode[]
}

interface State {
  stack: StackItem[]
  cursor: number
  tokens: Token[]
}

export const parser = (tokens: Token[]) => {
  const root: StackItem = { tagName: null, children: [] }
  const state: State = { tokens, cursor: 0, stack: [root] }
  parse(state)
  return root.children
}

export const hasTerminalParent = (tagName: string, stack: StackItem[]) => {
  const tagParents = closingTagAncestorBreakers[tagName]
  if (tagParents) {
    let currentIndex = stack.length - 1
    while (currentIndex >= 0) {
      const parentTagName = stack[currentIndex].tagName
      if (parentTagName === tagName) break
      if (parentTagName && tagParents.includes(parentTagName)) return true
      currentIndex--
    }
  }
  return false
}

export const rewindStack = (stack: StackItem[], newLength: number) => {
  stack.splice(newLength)
}

export const parse = (state: State) => {
  const { stack, tokens } = state
  let { cursor } = state
  let nodes = stack[stack.length - 1].children
  const len = tokens.length
  
  while (cursor < len) {
    const token = tokens[cursor]
    if (token.type !== 'tag-start') {
      nodes.push(token as TextToken)
      cursor++
      continue
    }

    const tagToken = tokens[++cursor] as TagToken
    cursor++
    const tagName = tagToken.content.toLowerCase()
    if (token.close) {
      let index = stack.length
      let shouldRewind = false
      while (--index > -1) {
        if (stack[index].tagName === tagName) {
          shouldRewind = true
          break
        }
      }
      while (cursor < len) {
        if (tokens[cursor].type !== 'tag-end') break
        cursor++
      }
      if (shouldRewind) {
        rewindStack(stack, index)
        break
      } 
      else continue
    }

    const isClosingTag = closingTags.includes(tagName)
    let shouldRewindToAutoClose = isClosingTag
    if (shouldRewindToAutoClose) {
      shouldRewindToAutoClose = !hasTerminalParent(tagName, stack)
    }

    if (shouldRewindToAutoClose) {
      let currentIndex = stack.length - 1
      while (currentIndex > 0) {
        if (tagName === stack[currentIndex].tagName) {
          rewindStack(stack, currentIndex)
          const previousIndex = currentIndex - 1
          nodes = stack[previousIndex].children
          break
        }
        currentIndex = currentIndex - 1
      }
    }

    const attributes = []
    let tagEndToken: TagEndToken | undefined
    while (cursor < len) {
      const _token = tokens[cursor]
      if (_token.type === 'tag-end') {
        tagEndToken = _token
        break
      }
      attributes.push((_token as AttributeToken).content)
      cursor++
    }

    if (!tagEndToken) break

    cursor++
    const children: HTMLNode[] = []
    const elementNode: NormalElement = {
      type: 'element',
      tagName: tagToken.content,
      attributes,
      children,
    }
    nodes.push(elementNode)

    const hasChildren = !(tagEndToken.close || voidTags.includes(tagName))
    if (hasChildren) {
      stack.push({tagName, children})
      const innerState = { tokens, cursor, stack }
      parse(innerState)
      cursor = innerState.cursor
    }
  }
  state.cursor = cursor
}