import { 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
}