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