Spaces:
No application file
No application file
| 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; | |
| }; | |