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