137 lines
3.4 KiB
TypeScript
137 lines
3.4 KiB
TypeScript
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;
|
|
};
|