add first working version
This commit is contained in:
+319
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user