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