mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
129 lines
3.3 KiB
TypeScript
129 lines
3.3 KiB
TypeScript
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
|
|
} |