Scape/src/Parser/Parser.php

425 lines
14 KiB
PHP
Raw Normal View History

2026-01-06 23:29:10 +00:00
<?php
declare(strict_types=1);
namespace Eyrie\Parser;
use Eyrie\Parser\Node\Node;
use Eyrie\Parser\Node\RootNode;
use Eyrie\Parser\Node\TextNode;
use Eyrie\Parser\Node\ExpressionNode;
use Eyrie\Parser\Node\IfNode;
use Eyrie\Parser\Node\ForeachNode;
use Eyrie\Parser\Node\LoopNode;
use Eyrie\Parser\Node\ExtendsNode;
use Eyrie\Parser\Node\BlockNode;
use Eyrie\Parser\Node\SuperNode;
use Eyrie\Parser\Node\IncludeNode;
use Eyrie\Parser\Node\ComponentNode;
class Parser
{
private array $tokens;
private int $cursor = 0;
public function __construct(array $tokens)
{
$this->tokens = $tokens;
}
public function parse(): RootNode
{
$root = new RootNode();
while (!$this->isEOF()) {
$node = $this->parseNode();
if ($node) {
$root->children[] = $node;
}
}
return $root;
}
private function parseNode(): ?Node
{
$token = $this->peek();
if ($token->type === TokenType::TEXT) {
$this->consume();
return new TextNode($token->value, $token->line);
}
if ($token->type === TokenType::OUTPUT_START) {
return $this->parseOutput();
}
if ($token->type === TokenType::CONTROL_START) {
return $this->parseControl();
}
if ($token->type === TokenType::BLOCK_START) {
return $this->parseBlockTag();
}
if ($token->type === TokenType::COMPONENT_START) {
return $this->parseComponent();
}
// Placeholder for other tags
$this->consume();
return null;
}
private function parseOutput(): ExpressionNode
{
$startToken = $this->consume(TokenType::OUTPUT_START);
$expression = '';
$filters = [];
while (!$this->isEOF() && $this->peek()->type !== TokenType::OUTPUT_END) {
if ($this->peek()->value === '|') {
$this->consume(); // |
$filterName = $this->consume(TokenType::IDENTIFIER)->value;
$args = [];
if ($this->peek()->value === '(') {
$this->consume(); // (
while ($this->peek()->value !== ')') {
$args[] = $this->consume()->value;
if ($this->peek()->value === ',') $this->consume();
}
$this->consume(); // )
}
$filters[] = ['name' => $filterName, 'args' => $args];
} else {
$expression .= $this->consume()->value;
}
}
$this->consume(TokenType::OUTPUT_END);
return new ExpressionNode(trim($expression), $startToken->line, $filters);
}
private function parseControl(): ?Node
{
$this->consume(TokenType::CONTROL_START);
$token = $this->peek();
if ($token->value === 'if') {
return $this->parseIf();
}
if ($token->value === 'foreach') {
return $this->parseForeach();
}
if ($token->value === 'loop') {
return $this->parseLoop();
}
// Handle other control structures
while (!$this->isEOF() && $this->peek()->type !== TokenType::CONTROL_END) {
$this->consume();
}
$this->consume(TokenType::CONTROL_END);
return null;
}
private function parseIf(): IfNode
{
$startToken = $this->consume(); // consume 'if'
$condition = '';
while ($this->peek()->type !== TokenType::CONTROL_END) {
$condition .= $this->consume()->value . ' ';
}
$this->consume(TokenType::CONTROL_END);
$body = [];
$elseifs = [];
$else = null;
while (!$this->isEOF()) {
$token = $this->peek();
if ($token->type === TokenType::CONTROL_START) {
$next = $this->tokens[$this->cursor + 1] ?? null;
if ($next && $next->value === 'endif') {
$this->consume(TokenType::CONTROL_START);
$this->consume(); // endif
$this->consume(TokenType::CONTROL_END);
break;
}
if ($next && $next->value === 'elseif') {
$this->consume(TokenType::CONTROL_START);
$this->consume(); // elseif
$elseifCond = '';
while ($this->peek()->type !== TokenType::CONTROL_END) {
$elseifCond .= $this->consume()->value . ' ';
}
$this->consume(TokenType::CONTROL_END);
$elseifBody = [];
while (!$this->isEOF()) {
$t = $this->peek();
if ($t->type === TokenType::CONTROL_START) {
$n = $this->tokens[$this->cursor + 1] ?? null;
if ($n && in_array($n->value, ['elseif', 'else', 'endif'])) break;
}
$elseifBody[] = $this->parseNode();
}
$elseifs[] = ['condition' => trim($elseifCond), 'body' => array_filter($elseifBody)];
continue;
}
if ($next && $next->value === 'else') {
$this->consume(TokenType::CONTROL_START);
$this->consume(); // else
$this->consume(TokenType::CONTROL_END);
$elseBody = [];
while (!$this->isEOF()) {
$t = $this->peek();
if ($t->type === TokenType::CONTROL_START) {
$n = $this->tokens[$this->cursor + 1] ?? null;
if ($n && $n->value === 'endif') break;
}
$elseBody[] = $this->parseNode();
}
$else = array_filter($elseBody);
continue;
}
}
$body[] = $this->parseNode();
}
return new IfNode(trim($condition), array_filter($body), $elseifs, $else, $startToken->line);
}
private function parseForeach(): ForeachNode
{
$startToken = $this->consume(); // consume 'foreach'
$item = $this->consume(TokenType::IDENTIFIER)->value;
$this->consume(TokenType::IDENTIFIER); // consume 'in'
$items = '';
while ($this->peek()->type !== TokenType::CONTROL_END) {
$items .= $this->consume()->value . ' ';
}
$this->consume(TokenType::CONTROL_END);
$body = [];
while (!$this->isEOF()) {
$token = $this->peek();
if ($token->type === TokenType::CONTROL_START) {
$next = $this->tokens[$this->cursor + 1] ?? null;
if ($next && $next->value === 'endforeach') {
$this->consume(TokenType::CONTROL_START);
$this->consume(); // endforeach
$this->consume(TokenType::CONTROL_END);
break;
}
}
$body[] = $this->parseNode();
}
return new ForeachNode(trim($items), $item, array_filter($body), $startToken->line);
}
private function parseLoop(): LoopNode
{
$startToken = $this->consume(); // consume 'loop'
$this->consume(TokenType::IDENTIFIER); // consume 'from'
$from = '';
while ($this->peek()->value !== 'to') {
$from .= $this->consume()->value . ' ';
}
$this->consume(TokenType::IDENTIFIER); // consume 'to'
$to = '';
while ($this->peek()->type !== TokenType::CONTROL_END) {
$to .= $this->consume()->value . ' ';
}
$this->consume(TokenType::CONTROL_END);
$body = [];
while (!$this->isEOF()) {
$token = $this->peek();
if ($token->type === TokenType::CONTROL_START) {
$next = $this->tokens[$this->cursor + 1] ?? null;
if ($next && $next->value === 'endloop') {
$this->consume(TokenType::CONTROL_START);
$this->consume(); // endloop
$this->consume(TokenType::CONTROL_END);
break;
}
}
$body[] = $this->parseNode();
}
return new LoopNode(trim($from), trim($to), array_filter($body), $startToken->line);
}
private function parseBlockTag(): ?Node
{
$this->consume(TokenType::BLOCK_START);
$token = $this->peek();
if ($token->value === 'extends') {
return $this->parseExtends();
}
if ($token->value === 'block') {
return $this->parseBlock();
}
if ($token->value === 'super') {
return $this->parseSuper();
}
if ($token->value === 'include') {
return $this->parseInclude();
}
// Handle other block tags (e.g. include, super)
while (!$this->isEOF() && $this->peek()->type !== TokenType::BLOCK_END) {
$this->consume();
}
$this->consume(TokenType::BLOCK_END);
return null;
}
private function parseExtends(): ExtendsNode
{
$startToken = $this->consume(); // extends
$layout = $this->consume(TokenType::STRING)->value;
$layout = substr($layout, 1, -1); // Strip quotes
$this->consume(TokenType::BLOCK_END);
return new ExtendsNode($layout, $startToken->line);
}
private function parseBlock(): BlockNode
{
$startToken = $this->consume(); // block
$name = $this->consume(TokenType::IDENTIFIER)->value;
$context = [];
if ($this->peek()->value === 'with') {
$this->consume(); // with
if ($this->peek()->value === '{') {
$this->consume(); // {
while (!$this->isEOF() && $this->peek()->value !== '}') {
$propName = $this->consume(TokenType::IDENTIFIER)->value;
$this->consume(TokenType::OPERATOR); // :
// Consume the value correctly
$valueToken = $this->consume();
$expr = $valueToken->value;
$context[$propName] = ['expr' => $expr];
if ($this->peek()->value === ',') {
$this->consume();
}
}
if ($this->peek()->value === '}') {
$this->consume(); // }
}
}
}
$this->consume(TokenType::BLOCK_END);
$body = [];
while (!$this->isEOF()) {
$token = $this->peek();
if ($token->type === TokenType::BLOCK_START) {
$next = $this->tokens[$this->cursor + 1] ?? null;
if ($next && $next->value === 'endblock') {
$this->consume(TokenType::BLOCK_START);
$this->consume(); // endblock
$this->consume(TokenType::BLOCK_END);
break;
}
}
$body[] = $this->parseNode();
}
return new BlockNode($name, array_filter($body), $context, $startToken->line);
}
private function parseSuper(): SuperNode
{
$startToken = $this->consume(); // super
$this->consume(TokenType::BLOCK_END);
return new SuperNode($startToken->line);
}
private function parseInclude(): IncludeNode
{
$startToken = $this->consume(); // include
$template = $this->consume(TokenType::STRING)->value;
$template = substr($template, 1, -1); // Strip quotes
$this->consume(TokenType::BLOCK_END);
return new IncludeNode($template, [], $startToken->line);
}
private function parseComponent(): ComponentNode
{
$startToken = $this->consume(TokenType::COMPONENT_START);
$name = $this->consume(TokenType::IDENTIFIER)->value;
$props = [];
while ($this->peek()->type !== TokenType::COMPONENT_END) {
$propName = $this->consume(TokenType::IDENTIFIER)->value;
$this->consume(TokenType::OPERATOR); // =
$token = $this->peek();
if ($token->type === TokenType::STRING) {
$val = $this->consume()->value;
$props[$propName] = substr($val, 1, -1); // Strip quotes
} elseif ($token->value === '{') {
$this->consume(); // {
$expr = '';
while ($this->peek()->value !== '}') {
$exprToken = $this->consume();
$expr .= $exprToken->value;
}
$this->consume(); // }
$props[$propName] = ['expr' => trim($expr)];
} else {
$props[$propName] = $this->consume()->value;
}
}
$this->consume(TokenType::COMPONENT_END);
return new ComponentNode($name, $props, $startToken->line);
}
private function peek(): Token
{
return $this->tokens[$this->cursor];
}
private function consume(?TokenType $type = null): Token
{
$token = $this->peek();
if ($type !== null && $token->type !== $type) {
throw new \RuntimeException(sprintf('Expected token %s, got %s at line %d', $type->value, $token->type->value, $token->line));
}
$this->cursor++;
return $token;
}
private function isEOF(): bool
{
return $this->peek()->type === TokenType::EOF;
}
}