add first working version

This commit is contained in:
2026-08-17 18:22:16 +02:00
parent 160ed5e409
commit f783617393
12 changed files with 1927 additions and 2 deletions
+319
View File
@@ -0,0 +1,319 @@
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:
this.advance();
break;
case TokenType.TEXT:
case TokenType.EOF:
this.advance();
break;
default:
// Unknown token, skip
this.advance();
break;
}
}
private pushBlock(token: Token): void {
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.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;
}
}