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; } }