184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
export interface ParsedAttribute {
|
|
name: string;
|
|
value: string;
|
|
type: AttributeType;
|
|
}
|
|
|
|
export enum AttributeType {
|
|
STRUCTURAL_DIRECTIVE = 'structural',
|
|
INPUT_BINDING = 'input',
|
|
OUTPUT_BINDING = 'output',
|
|
TWO_WAY_BINDING = 'two-way',
|
|
TEMPLATE_REFERENCE = 'reference',
|
|
REGULAR = 'regular',
|
|
}
|
|
|
|
export interface ParsedElement {
|
|
tagName: string;
|
|
attributes: ParsedAttribute[];
|
|
children: (ParsedElement | ParsedTextNode)[];
|
|
textContent?: string;
|
|
}
|
|
|
|
export interface ParsedTextNode {
|
|
type: 'text';
|
|
content: string;
|
|
}
|
|
|
|
export class TemplateParser {
|
|
parse(template: string): (ParsedElement | ParsedTextNode)[] {
|
|
const elements: (ParsedElement | ParsedTextNode)[] = [];
|
|
const stack: ParsedElement[] = [];
|
|
let currentPos = 0;
|
|
|
|
while (currentPos < template.length) {
|
|
const tagStart = template.indexOf('<', currentPos);
|
|
|
|
if (tagStart === -1) {
|
|
const textContent = template.substring(currentPos);
|
|
if (textContent.trim()) {
|
|
const textNode: ParsedTextNode = {
|
|
type: 'text',
|
|
content: textContent,
|
|
};
|
|
if (stack.length > 0) {
|
|
stack[stack.length - 1].children.push(textNode);
|
|
} else {
|
|
elements.push(textNode);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (tagStart > currentPos) {
|
|
const textContent = template.substring(currentPos, tagStart);
|
|
if (textContent.trim()) {
|
|
const textNode: ParsedTextNode = {
|
|
type: 'text',
|
|
content: textContent,
|
|
};
|
|
if (stack.length > 0) {
|
|
stack[stack.length - 1].children.push(textNode);
|
|
} else {
|
|
elements.push(textNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (template[tagStart + 1] === '/') {
|
|
const tagEnd = template.indexOf('>', tagStart);
|
|
if (tagEnd !== -1) {
|
|
const closingTag = template.substring(tagStart + 2, tagEnd).trim();
|
|
|
|
if (stack.length > 0 && stack[stack.length - 1].tagName === closingTag) {
|
|
const element = stack.pop()!;
|
|
if (stack.length === 0) {
|
|
elements.push(element);
|
|
} else {
|
|
stack[stack.length - 1].children.push(element);
|
|
}
|
|
}
|
|
|
|
currentPos = tagEnd + 1;
|
|
}
|
|
} else if (template[tagStart + 1] === '!') {
|
|
const commentEnd = template.indexOf('-->', tagStart);
|
|
currentPos = commentEnd !== -1 ? commentEnd + 3 : tagStart + 1;
|
|
} else {
|
|
const tagEnd = template.indexOf('>', tagStart);
|
|
if (tagEnd === -1) break;
|
|
|
|
const isSelfClosing = template[tagEnd - 1] === '/';
|
|
const tagContent = template.substring(
|
|
tagStart + 1,
|
|
isSelfClosing ? tagEnd - 1 : tagEnd
|
|
).trim();
|
|
|
|
const spaceIndex = tagContent.search(/\s/);
|
|
const tagName = spaceIndex === -1 ? tagContent : tagContent.substring(0, spaceIndex);
|
|
const attributesString = spaceIndex === -1 ? '' : tagContent.substring(spaceIndex + 1);
|
|
|
|
const element: ParsedElement = {
|
|
tagName,
|
|
attributes: this.parseAttributes(attributesString),
|
|
children: [],
|
|
};
|
|
|
|
if (isSelfClosing) {
|
|
if (stack.length === 0) {
|
|
elements.push(element);
|
|
} else {
|
|
stack[stack.length - 1].children.push(element);
|
|
}
|
|
} else {
|
|
stack.push(element);
|
|
}
|
|
|
|
currentPos = tagEnd + 1;
|
|
}
|
|
}
|
|
|
|
while (stack.length > 0) {
|
|
const element = stack.pop()!;
|
|
if (stack.length === 0) {
|
|
elements.push(element);
|
|
} else {
|
|
stack[stack.length - 1].children.push(element);
|
|
}
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
private parseAttributes(attributesString: string): ParsedAttribute[] {
|
|
const attributes: ParsedAttribute[] = [];
|
|
const regex = /([^\s=]+)(?:="([^"]*)")?/g;
|
|
let match;
|
|
|
|
while ((match = regex.exec(attributesString)) !== null) {
|
|
const name = match[1];
|
|
const value = match[2] || '';
|
|
const type = this.detectAttributeType(name);
|
|
|
|
attributes.push({ name, value, type });
|
|
}
|
|
|
|
return attributes;
|
|
}
|
|
|
|
private detectAttributeType(name: string): AttributeType {
|
|
if (name.startsWith('*')) {
|
|
return AttributeType.STRUCTURAL_DIRECTIVE;
|
|
}
|
|
if (name.startsWith('[(') && name.endsWith(')]')) {
|
|
return AttributeType.TWO_WAY_BINDING;
|
|
}
|
|
if (name.startsWith('[') && name.endsWith(']')) {
|
|
return AttributeType.INPUT_BINDING;
|
|
}
|
|
if (name.startsWith('(') && name.endsWith(')')) {
|
|
return AttributeType.OUTPUT_BINDING;
|
|
}
|
|
if (name.startsWith('#')) {
|
|
return AttributeType.TEMPLATE_REFERENCE;
|
|
}
|
|
return AttributeType.REGULAR;
|
|
}
|
|
|
|
traverseElements(elements: (ParsedElement | ParsedTextNode)[], callback: (element: ParsedElement) => void): void {
|
|
for (const element of elements) {
|
|
if (this.isTextNode(element)) {
|
|
continue;
|
|
}
|
|
callback(element);
|
|
if (element.children.length > 0) {
|
|
this.traverseElements(element.children, callback);
|
|
}
|
|
}
|
|
}
|
|
|
|
private isTextNode(node: ParsedElement | ParsedTextNode): node is ParsedTextNode {
|
|
return 'type' in node && node.type === 'text';
|
|
}
|
|
}
|