import { Token, TokenType, TokenizerError } from './tokenizer'; /** * AST Node types for EFTL */ export interface AstNode { type: string; line: number; column: number; children?: AstNode[]; } export interface ParserError { message: string; line: number; column: number; length: number; } /** * EFTL Parser - validates structure and reports errors */ export class EftlParser { private tokens: Token[]; private pos: number = 0; private errors: ParserError[] = []; private blockStack: { type: TokenType; token: Token }[] = []; constructor(tokens: Token[]) { this.tokens = tokens; } parse(): { errors: ParserError[] } { this.pos = 0; this.errors = []; this.blockStack = []; while (!this.isAtEnd()) { this.parseTopLevel(); } // Check for unclosed blocks for (const block of this.blockStack) { this.errors.push({ message: `Unclosed block: ${block.token.value}`, line: block.token.line, column: block.token.column, length: block.token.length }); } return { errors: this.errors }; } private parseTopLevel(): void { const token = this.current(); switch (token.type) { case TokenType.EFTL_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.EFTL_CLOSE: this.popBlock(TokenType.EFTL_OPEN, token); this.advance(); break; case TokenType.VAR_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.VAR_CLOSE: this.popBlock(TokenType.VAR_OPEN, token); this.advance(); break; case TokenType.IF_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.IF_CLOSE: this.popBlock(TokenType.IF_OPEN, token); this.advance(); break; case TokenType.CONDITION_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.CONDITION_CLOSE: this.popBlock(TokenType.CONDITION_OPEN, token); this.advance(); break; case TokenType.THEN_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.THEN_CLOSE: this.popBlock(TokenType.THEN_OPEN, token); this.advance(); break; case TokenType.ELSE_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.ELSE_CLOSE: this.popBlock(TokenType.ELSE_OPEN, token); this.advance(); break; case TokenType.ELSE_IF_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.ELSE_IF_CLOSE: this.popBlock(TokenType.ELSE_IF_OPEN, token); this.advance(); break; case TokenType.WHILE_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.WHILE_CLOSE: this.popBlock(TokenType.WHILE_OPEN, token); this.advance(); break; case TokenType.DO_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.DO_CLOSE: this.popBlock(TokenType.DO_OPEN, token); this.advance(); break; case TokenType.TAG_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.TAG_CLOSE: this.popBlock(TokenType.TAG_OPEN, token); this.advance(); break; case TokenType.SPLIT_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.SPLIT_CLOSE: this.popBlock(TokenType.SPLIT_OPEN, token); this.advance(); break; case TokenType.TRIM_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.TRIM_CLOSE: this.popBlock(TokenType.TRIM_OPEN, token); this.advance(); break; case TokenType.CONTAINS_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.CONTAINS_CLOSE: this.popBlock(TokenType.CONTAINS_OPEN, token); this.advance(); break; case TokenType.FORMAT_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.FORMAT_CLOSE: this.popBlock(TokenType.FORMAT_OPEN, token); this.advance(); break; case TokenType.EXPR_OPEN: case TokenType.EXPR_OUTPUT_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.EXPR_CLOSE: // Can close either EXPR_OPEN or EXPR_OUTPUT_OPEN const exprBlock = this.blockStack.pop(); if (!exprBlock || (exprBlock.type !== TokenType.EXPR_OPEN && exprBlock.type !== TokenType.EXPR_OUTPUT_OPEN)) { this.errors.push({ message: `Unexpected closing: ${token.value}`, line: token.line, column: token.column, length: token.length }); if (exprBlock) { this.blockStack.push(exprBlock); } } this.advance(); break; case TokenType.COMMENT_OPEN: this.pushBlock(token); this.advance(); break; case TokenType.COMMENT_CLOSE: this.popBlock(TokenType.COMMENT_OPEN, token); this.advance(); break; // Self-closing tags - no block needed case TokenType.VALUE_OF: case TokenType.SIZE_OF: case TokenType.HEADER: if (!token.value.endsWith('/]')) { this.pushBlock(token); } this.advance(); break; case TokenType.TEXT: case TokenType.EOF: this.advance(); break; default: // Unknown token, skip this.advance(); break; } } private pushBlock(token: Token): void { if (token.value.endsWith('/]')) { return; // It's self-closing, don't push to stack } this.blockStack.push({ type: token.type, token }); } private popBlock(expectedOpenType: TokenType, closeToken: Token): void { if (this.blockStack.length === 0) { this.errors.push({ message: `Unexpected closing tag: ${closeToken.value} (no matching opening tag)`, line: closeToken.line, column: closeToken.column, length: closeToken.length }); return; } const top = this.blockStack[this.blockStack.length - 1]; if (top.type !== expectedOpenType) { // Find the expected closing tag name const expectedClose = this.getMatchingClose(top.type); this.errors.push({ message: `Mismatched closing tag: expected ${expectedClose}, got ${closeToken.value}`, line: closeToken.line, column: closeToken.column, length: closeToken.length }); return; } this.blockStack.pop(); } private getMatchingClose(openType: TokenType): string { const closeMap: { [key: string]: string } = { [TokenType.EFTL_OPEN]: '[/EFTL]', [TokenType.VAR_OPEN]: '[/VAR]', [TokenType.IF_OPEN]: '[/IF]', [TokenType.CONDITION_OPEN]: '[/CONDITION]', [TokenType.THEN_OPEN]: '[/THEN]', [TokenType.ELSE_OPEN]: '[/ELSE]', [TokenType.ELSE_IF_OPEN]: '[/ELSE IF]', [TokenType.WHILE_OPEN]: '[/WHILE]', [TokenType.DO_OPEN]: '[/DO]', [TokenType.TAG_OPEN]: '[/TAG]', [TokenType.SPLIT_OPEN]: '[/SPLIT]', [TokenType.TRIM_OPEN]: '[/TRIM]', [TokenType.CONTAINS_OPEN]: '[/CONTAINS]', [TokenType.FORMAT_OPEN]: '[/FORMAT]', [TokenType.VALUE_OF]: '[/VALUE_OF]', [TokenType.SIZE_OF]: '[/SIZE_OF]', [TokenType.HEADER]: '[/HEADER]', [TokenType.EXPR_OPEN]: '%]', [TokenType.EXPR_OUTPUT_OPEN]: '%]', [TokenType.COMMENT_OPEN]: '--]' }; return closeMap[openType] || 'unknown'; } private current(): Token { return this.tokens[this.pos] || { type: TokenType.EOF, value: '', line: 0, column: 0, length: 0 }; } private advance(): Token { if (!this.isAtEnd()) { this.pos++; } return this.tokens[this.pos - 1]; } private isAtEnd(): boolean { return this.current().type === TokenType.EOF; } }