diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3bd0684..dee77c4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -624,12 +624,6 @@ parameters: count: 1 path: src/Compiler.php - - - message: '#^Parameter \#1 \$templatePath of method Phalcon\\Volt\\Parser\\Parser\:\:parseView\(\) expects string, string\|null given\.$#' - identifier: argument.type - count: 1 - path: src/Compiler.php - - message: '#^Parameter \#1 \$test of method Phalcon\\Volt\\Compiler\:\:resolveTest\(\) expects array, mixed given\.$#' identifier: argument.type diff --git a/src/Compiler.php b/src/Compiler.php index a5d7730..2622d7c 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -1385,8 +1385,8 @@ public function compileSource(string $viewCode, bool $extendsMode = false): stri $this->autoescape = (bool)$this->options['autoescape']; } - $parser = new Parser($viewCode); - $intermediate = $parser->parseView($this->currentPath); + $parser = new Parser(); + $intermediate = $parser->parse($viewCode, $this->currentPath ?? ''); $compilation = $this->statementList($intermediate, $extendsMode); /** @@ -2215,7 +2215,7 @@ public function getUniquePrefix(): string */ public function parse(string $viewCode): array { - return (new Parser($viewCode))->parseView("eval code"); + return (new Parser())->parse($viewCode, 'eval code'); } /** diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index b2286d1..4df2ccb 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -33,584 +33,143 @@ class Parser private string $debugFile = 'volt.txt'; - private ?Token $token = null; - - public function __construct(private string $code) - { - } - /** + * @param string $code * @param string $templatePath * * @return array * @throws Exception */ - public function parseView(string $templatePath): array + public function parse(string $code, string $templatePath = 'eval code'): array { - if (strlen($this->code) === 0) { + if (strlen($code) === 0) { return []; } - $debug = null; + $debugHandle = null; if ($this->debug) { - $debug = fopen($this->debugFile, 'w+'); + $debugHandle = fopen($this->debugFile, 'w+'); } - $codeLength = strlen($this->code); - $parserState = new State($this->code); + $codeLength = strlen($code); + $parserState = new State($code); $parserState->setActiveFile($templatePath); $parserStatus = new Status($parserState); - $scanner = new Scanner($parserStatus->getState()); + $scanner = new Scanner($parserStatus->getState()); $parser = new phvolt_Parser($parserStatus); - $parser->phvolt_Trace($debug); + $parser->phvolt_Trace($debugHandle); + + $state = $parserStatus->getState(); + $scannerStatus = ScannerStatus::OK; - $state = $parserStatus->getState(); while (($scannerStatus = $scanner->scanForToken()) === ScannerStatus::OK) { - $this->token = $scanner->getToken(); - $parserStatus->setToken($this->token); + $token = $scanner->getToken(); + $parserStatus->setToken($token); $state->setStartLength($codeLength - $state->getCursor()); - $opcode = $this->token->opcode; + $opcode = $token->opcode; $state->setActiveToken($opcode); - switch ($opcode) { - case Compiler::PHVOLT_T_IGNORE: - break; - - case Compiler::PHVOLT_T_ADD: - $parser->phvolt_(Opcode::PLUS->value); - break; - - case Compiler::PHVOLT_T_SUB: - $parser->phvolt_(Opcode::MINUS->value); - break; - - case Compiler::PHVOLT_T_MUL: - $parser->phvolt_(Opcode::TIMES->value); - break; - - case Compiler::PHVOLT_T_DIV: - $parser->phvolt_(Opcode::DIVIDE->value); - break; - - case Compiler::PHVOLT_T_MOD: - $parser->phvolt_(Opcode::MOD->value); - break; - - case Compiler::PHVOLT_T_AND: - $parser->phvolt_(Opcode::AND->value); - break; - - case Compiler::PHVOLT_T_OR: - $parser->phvolt_(Opcode::OR->value); - break; - - case Compiler::PHVOLT_T_IS: - $parser->phvolt_(Opcode::IS->value); - break; - - case Compiler::PHVOLT_T_EQUALS: - $parser->phvolt_(Opcode::EQUALS->value); - break; - - case Compiler::PHVOLT_T_NOTEQUALS: - $parser->phvolt_(Opcode::NOTEQUALS->value); - break; - - case Compiler::PHVOLT_T_LESS: - $parser->phvolt_(Opcode::LESS->value); - break; - - case Compiler::PHVOLT_T_GREATER: - $parser->phvolt_(Opcode::GREATER->value); - break; - - case Compiler::PHVOLT_T_GREATEREQUAL: - $parser->phvolt_(Opcode::GREATEREQUAL->value); - break; - - case Compiler::PHVOLT_T_LESSEQUAL: - $parser->phvolt_(Opcode::LESSEQUAL->value); - break; - - case Compiler::PHVOLT_T_IDENTICAL: - $parser->phvolt_(Opcode::IDENTICAL->value); - break; - - case Compiler::PHVOLT_T_NOTIDENTICAL: - $parser->phvolt_(Opcode::NOTIDENTICAL->value); - break; - - case Compiler::PHVOLT_T_NOT: - $parser->phvolt_(Opcode::NOT->value); - break; - - case Compiler::PHVOLT_T_DOT: - $parser->phvolt_(Opcode::DOT->value); - break; - - case Compiler::PHVOLT_T_CONCAT: - $parser->phvolt_(Opcode::CONCAT->value); - break; - - case Compiler::PHVOLT_T_RANGE: - $parser->phvolt_(Opcode::RANGE->value); - break; - - case Compiler::PHVOLT_T_PIPE: - $parser->phvolt_(Opcode::PIPE->value); - break; - - case Compiler::PHVOLT_T_COMMA: - $parser->phvolt_(Opcode::COMMA->value); - break; - - case Compiler::PHVOLT_T_COLON: - $parser->phvolt_(Opcode::COLON->value); - break; - - case Compiler::PHVOLT_T_QUESTION: - $parser->phvolt_(Opcode::QUESTION->value); - break; - - case Compiler::PHVOLT_T_PARENTHESES_OPEN: - $parser->phvolt_(Opcode::PARENTHESES_OPEN->value); - break; - - case Compiler::PHVOLT_T_PARENTHESES_CLOSE: - $parser->phvolt_(Opcode::PARENTHESES_CLOSE->value); - break; - - case Compiler::PHVOLT_T_SBRACKET_OPEN: - $parser->phvolt_(Opcode::SBRACKET_OPEN->value); - break; - - case Compiler::PHVOLT_T_SBRACKET_CLOSE: - $parser->phvolt_(Opcode::SBRACKET_CLOSE->value); - break; - - case Compiler::PHVOLT_T_CBRACKET_OPEN: - $parser->phvolt_(Opcode::CBRACKET_OPEN->value); - break; - - case Compiler::PHVOLT_T_CBRACKET_CLOSE: - $parser->phvolt_(Opcode::CBRACKET_CLOSE->value); - break; - - case Compiler::PHVOLT_T_OPEN_DELIMITER: - $parser->phvolt_(Opcode::OPEN_DELIMITER->value); - break; - - case Compiler::PHVOLT_T_CLOSE_DELIMITER: - $parser->phvolt_(Opcode::CLOSE_DELIMITER->value); - break; - - case Compiler::PHVOLT_T_OPEN_EDELIMITER: - if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { - $this->createErrorMessage( - $parserStatus, - 'Child templates only may contain blocks' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $parser->phvolt_(Opcode::OPEN_EDELIMITER->value); - break; - - case Compiler::PHVOLT_T_CLOSE_EDELIMITER: - $parser->phvolt_(Opcode::CLOSE_EDELIMITER->value); - break; - - case Compiler::PHVOLT_T_NULL: - $parser->phvolt_(Opcode::NULL->value); - break; - - case Compiler::PHVOLT_T_TRUE: - $parser->phvolt_(Opcode::TRUE->value); - break; - - case Compiler::PHVOLT_T_FALSE: - $parser->phvolt_(Opcode::FALSE->value); - break; - - case Compiler::PHVOLT_T_INTEGER: - $this->phvoltParseWithToken( - $parser, - Compiler::PHVOLT_T_INTEGER, - Opcode::INTEGER - ); - break; - - case Compiler::PHVOLT_T_DOUBLE: - $this->phvoltParseWithToken( - $parser, - Compiler::PHVOLT_T_DOUBLE, - Opcode::DOUBLE - ); - break; - - case Compiler::PHVOLT_T_STRING: - $this->phvoltParseWithToken( - $parser, - Compiler::PHVOLT_T_STRING, - Opcode::STRING - ); - break; - - case Compiler::PHVOLT_T_IDENTIFIER: - $this->phvoltParseWithToken( - $parser, - Compiler::PHVOLT_T_IDENTIFIER, - Opcode::IDENTIFIER - ); - break; - - case Compiler::PHVOLT_T_IF: - if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { - $this->createErrorMessage( - $parserStatus, - 'Child templates only may contain blocks' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $state->incrementIfLevel(); - $state->incrementBlockLevel(); - - $parser->phvolt_(Opcode::IF->value); - break; - - case Compiler::PHVOLT_T_ELSE: - if ($state->getIfLevel() === 0 && $state->getForLevel() > 0) { - $parser->phvolt_(Opcode::ELSEFOR->value); - } else { - $parser->phvolt_(Opcode::ELSE->value); - } - break; - - case Compiler::PHVOLT_T_ELSEFOR: - $parser->phvolt_(Opcode::ELSEFOR->value); - break; - - case Compiler::PHVOLT_T_ELSEIF: - if ($state->getIfLevel() === 0) { - $this->createErrorMessage($parserStatus, 'Unexpected ENDIF'); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $parser->phvolt_(Opcode::ELSEIF->value); - break; - - case Compiler::PHVOLT_T_ENDIF: - $state->decrementBlockLevel(); - $state->decrementIfLevel(); - $parser->phvolt_(Opcode::ENDIF->value); - break; - - case Compiler::PHVOLT_T_FOR: - if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { - $this->createErrorMessage( - $parserStatus, - 'Child templates only may contain blocks' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $state->setOldIfLevel($state->getIfLevel()); - $state->setIfLevel(0); - $state->incrementForLevel(); - $state->incrementBlockLevel(); - $parser->phvolt_(Opcode::FOR->value); - break; - - case Compiler::PHVOLT_T_IN: - $parser->phvolt_(Opcode::IN->value); - break; - - case Compiler::PHVOLT_T_ENDFOR: - $state->decrementBlockLevel(); - $state->decrementForLevel(); - $state->setIfLevel($state->getOldIfLevel()); - $parser->phvolt_(Opcode::ENDFOR->value); - break; - - case Compiler::PHVOLT_T_SWITCH: - if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { - $this->createErrorMessage( - $parserStatus, - 'Child templates only may contain blocks' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } elseif ($state->getSwitchLevel() > 0) { - $this->createErrorMessage( - $parserStatus, - 'A nested switch detected. There is no nested switch-case statements support' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $state->setSwitchLevel(1); - $state->incrementBlockLevel(); - $parser->phvolt_(Opcode::SWITCH->value); - break; - - case Compiler::PHVOLT_T_CASE: - if ($state->getSwitchLevel() === 0) { - $this->createErrorMessage($parserStatus, 'Unexpected CASE'); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $parser->phvolt_(Opcode::CASE->value); - break; - - /* only for switch-case statements */ - case Compiler::PHVOLT_T_DEFAULT: - if ($state->getSwitchLevel() !== 0) { - $parser->phvolt_(Opcode::DEFAULT->value); - unset($this->token); - } else { - $this->phvoltParseWithToken( - $parser, - Compiler::PHVOLT_T_IDENTIFIER, - Opcode::IDENTIFIER, - ); - } - - break; - - case Compiler::PHVOLT_T_ENDSWITCH: - if ($state->getSwitchLevel() === 0) { - $this->createErrorMessage($parserStatus, 'Unexpected ENDSWITCH'); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $state->decrementBlockLevel(); - $state->setSwitchLevel(0); - $parser->phvolt_(Opcode::ENDSWITCH->value); - break; - - case Compiler::PHVOLT_T_RAW_FRAGMENT: - if ($this->token->length > 0) { - /** @var string $rawValue */ - $rawValue = $this->token->value ?? ''; - $value = trim($rawValue); - if ($value !== '' && $state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { - $this->createErrorMessage( - $parserStatus, - 'Child templates only may contain blocks' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - if (!$this->phvoltIsBlankString($this->token)) { - $state->incrementStatementPosition(); - } - - $this->phvoltParseWithToken( - $parser, - Compiler::PHVOLT_T_RAW_FRAGMENT, - Opcode::RAW_FRAGMENT - ); - } - break; - - case Compiler::PHVOLT_T_SET: - if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { - $this->createErrorMessage( - $parserStatus, - 'Child templates only may contain blocks' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $parser->phvolt_(Opcode::SET->value); - break; - - case Compiler::PHVOLT_T_ASSIGN: - $parser->phvolt_(Opcode::ASSIGN->value); - break; - - case Compiler::PHVOLT_T_ADD_ASSIGN: - $parser->phvolt_(Opcode::ADD_ASSIGN->value); - break; - - case Compiler::PHVOLT_T_SUB_ASSIGN: - $parser->phvolt_(Opcode::SUB_ASSIGN->value); - break; - - case Compiler::PHVOLT_T_MUL_ASSIGN: - $parser->phvolt_(Opcode::MUL_ASSIGN->value); - break; - - case Compiler::PHVOLT_T_DIV_ASSIGN: - $parser->phvolt_(Opcode::DIV_ASSIGN->value); - break; - - case Compiler::PHVOLT_T_INCR: - $parser->phvolt_(Opcode::INCR->value); - break; - - case Compiler::PHVOLT_T_DECR: - $parser->phvolt_(Opcode::DECR->value); - break; - - case Compiler::PHVOLT_T_BLOCK: - if ($state->getBlockLevel() > 0) { - $this->createErrorMessage( - $parserStatus, - 'Embedding blocks into other blocks is not supported' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $state->incrementBlockLevel(); - $parser->phvolt_(Opcode::BLOCK->value); - break; - - case Compiler::PHVOLT_T_ENDBLOCK: - $state->decrementBlockLevel(); - $parser->phvolt_(Opcode::ENDBLOCK->value); - break; - - case Compiler::PHVOLT_T_MACRO: - if ($state->getMacroLevel() > 0) { - $this->createErrorMessage( - $parserStatus, - 'Embedding macros into other macros is not allowed' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $state->incrementMacroLevel(); - $parser->phvolt_(Opcode::MACRO->value); - break; - - case Compiler::PHVOLT_T_ENDMACRO: - $state->decrementMacroLevel(); - $parser->phvolt_(Opcode::ENDMACRO->value); - break; - - case Compiler::PHVOLT_T_CALL: - $parser->phvolt_(Opcode::CALL->value); - break; - - case Compiler::PHVOLT_T_ENDCALL: - $parser->phvolt_(Opcode::ENDCALL->value); - break; - - case Compiler::PHVOLT_T_CACHE: - $parser->phvolt_(Opcode::CACHE->value); - break; - - case Compiler::PHVOLT_T_ENDCACHE: - $parser->phvolt_(Opcode::ENDCACHE->value); - break; - - case Compiler::PHVOLT_T_RAW: - $parser->phvolt_(Opcode::RAW->value); - $state->incrementForcedRawState(); - break; - - case Compiler::PHVOLT_T_ENDRAW: - $parser->phvolt_(Opcode::ENDRAW->value); - $state->decrementForcedRawState(); - break; - - case Compiler::PHVOLT_T_INCLUDE: - $parser->phvolt_(Opcode::INCLUDE->value); - break; - - case Compiler::PHVOLT_T_WITH: - $parser->phvolt_(Opcode::WITH->value); - break; - - case Compiler::PHVOLT_T_DEFINED: - $parser->phvolt_(Opcode::DEFINED->value); - break; - - case Compiler::PHVOLT_T_EMPTY: - $parser->phvolt_(Opcode::EMPTY->value); - break; - - case Compiler::PHVOLT_T_EVEN: - $parser->phvolt_(Opcode::EVEN->value); - break; - - case Compiler::PHVOLT_T_ODD: - $parser->phvolt_(Opcode::ODD->value); - break; - - case Compiler::PHVOLT_T_NUMERIC: - $parser->phvolt_(Opcode::NUMERIC->value); - break; - - case Compiler::PHVOLT_T_SCALAR: - $parser->phvolt_(Opcode::SCALAR->value); - break; - - case Compiler::PHVOLT_T_ITERABLE: - $parser->phvolt_(Opcode::ITERABLE->value); - break; - - case Compiler::PHVOLT_T_DO: - $parser->phvolt_(Opcode::DO->value); - break; - - case Compiler::PHVOLT_T_RETURN: - $parser->phvolt_(Opcode::RETURN->value); - break; - - case Compiler::PHVOLT_T_AUTOESCAPE: - $parser->phvolt_(Opcode::AUTOESCAPE->value); - break; - - case Compiler::PHVOLT_T_ENDAUTOESCAPE: - $parser->phvolt_(Opcode::ENDAUTOESCAPE->value); - break; - - case Compiler::PHVOLT_T_BREAK: - $parser->phvolt_(Opcode::BREAK->value); - break; - - case Compiler::PHVOLT_T_CONTINUE: - $parser->phvolt_(Opcode::CONTINUE->value); - break; - - case Compiler::PHVOLT_T_EXTENDS: - if ($state->getStatementPosition() !== 1) { - $this->createErrorMessage( - $parserStatus, - 'Extends statement must be placed at the first line in the template' - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } - - $state->setExtendsMode(1); - $parser->phvolt_(Opcode::EXTENDS->value); - break; - - default: - $this->createErrorMessage( - $parserStatus, - sprintf('Scanner: unknown opcode %d', $opcode) - ); - $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); - break; - } + match ($opcode) { + Compiler::PHVOLT_T_IGNORE => null, + Compiler::PHVOLT_T_ADD => $parser->phvolt_(Opcode::PLUS->value), + Compiler::PHVOLT_T_SUB => $parser->phvolt_(Opcode::MINUS->value), + Compiler::PHVOLT_T_MUL => $parser->phvolt_(Opcode::TIMES->value), + Compiler::PHVOLT_T_DIV => $parser->phvolt_(Opcode::DIVIDE->value), + Compiler::PHVOLT_T_MOD => $parser->phvolt_(Opcode::MOD->value), + Compiler::PHVOLT_T_AND => $parser->phvolt_(Opcode::AND->value), + Compiler::PHVOLT_T_OR => $parser->phvolt_(Opcode::OR->value), + Compiler::PHVOLT_T_IS => $parser->phvolt_(Opcode::IS->value), + Compiler::PHVOLT_T_EQUALS => $parser->phvolt_(Opcode::EQUALS->value), + Compiler::PHVOLT_T_NOTEQUALS => $parser->phvolt_(Opcode::NOTEQUALS->value), + Compiler::PHVOLT_T_LESS => $parser->phvolt_(Opcode::LESS->value), + Compiler::PHVOLT_T_GREATER => $parser->phvolt_(Opcode::GREATER->value), + Compiler::PHVOLT_T_GREATEREQUAL => $parser->phvolt_(Opcode::GREATEREQUAL->value), + Compiler::PHVOLT_T_LESSEQUAL => $parser->phvolt_(Opcode::LESSEQUAL->value), + Compiler::PHVOLT_T_IDENTICAL => $parser->phvolt_(Opcode::IDENTICAL->value), + Compiler::PHVOLT_T_NOTIDENTICAL => $parser->phvolt_(Opcode::NOTIDENTICAL->value), + Compiler::PHVOLT_T_NOT => $parser->phvolt_(Opcode::NOT->value), + Compiler::PHVOLT_T_DOT => $parser->phvolt_(Opcode::DOT->value), + Compiler::PHVOLT_T_CONCAT => $parser->phvolt_(Opcode::CONCAT->value), + Compiler::PHVOLT_T_RANGE => $parser->phvolt_(Opcode::RANGE->value), + Compiler::PHVOLT_T_PIPE => $parser->phvolt_(Opcode::PIPE->value), + Compiler::PHVOLT_T_COMMA => $parser->phvolt_(Opcode::COMMA->value), + Compiler::PHVOLT_T_COLON => $parser->phvolt_(Opcode::COLON->value), + Compiler::PHVOLT_T_QUESTION => $parser->phvolt_(Opcode::QUESTION->value), + Compiler::PHVOLT_T_PARENTHESES_OPEN => $parser->phvolt_(Opcode::PARENTHESES_OPEN->value), + Compiler::PHVOLT_T_PARENTHESES_CLOSE => $parser->phvolt_(Opcode::PARENTHESES_CLOSE->value), + Compiler::PHVOLT_T_SBRACKET_OPEN => $parser->phvolt_(Opcode::SBRACKET_OPEN->value), + Compiler::PHVOLT_T_SBRACKET_CLOSE => $parser->phvolt_(Opcode::SBRACKET_CLOSE->value), + Compiler::PHVOLT_T_CBRACKET_OPEN => $parser->phvolt_(Opcode::CBRACKET_OPEN->value), + Compiler::PHVOLT_T_CBRACKET_CLOSE => $parser->phvolt_(Opcode::CBRACKET_CLOSE->value), + Compiler::PHVOLT_T_OPEN_DELIMITER => $parser->phvolt_(Opcode::OPEN_DELIMITER->value), + Compiler::PHVOLT_T_CLOSE_DELIMITER => $parser->phvolt_(Opcode::CLOSE_DELIMITER->value), + Compiler::PHVOLT_T_OPEN_EDELIMITER => $this->handleOpenEdelimiter($parser, $parserStatus, $state), + Compiler::PHVOLT_T_CLOSE_EDELIMITER => $parser->phvolt_(Opcode::CLOSE_EDELIMITER->value), + Compiler::PHVOLT_T_NULL => $parser->phvolt_(Opcode::NULL->value), + Compiler::PHVOLT_T_TRUE => $parser->phvolt_(Opcode::TRUE->value), + Compiler::PHVOLT_T_FALSE => $parser->phvolt_(Opcode::FALSE->value), + Compiler::PHVOLT_T_INTEGER => $this->parseWithToken($parser, $token, Opcode::INTEGER), + Compiler::PHVOLT_T_DOUBLE => $this->parseWithToken($parser, $token, Opcode::DOUBLE), + Compiler::PHVOLT_T_STRING => $this->parseWithToken($parser, $token, Opcode::STRING), + Compiler::PHVOLT_T_IDENTIFIER => $this->parseWithToken($parser, $token, Opcode::IDENTIFIER), + Compiler::PHVOLT_T_IF => $this->handleIf($parser, $parserStatus, $state), + Compiler::PHVOLT_T_ELSE => $state->getIfLevel() === 0 && $state->getForLevel() > 0 + ? $parser->phvolt_(Opcode::ELSEFOR->value) + : $parser->phvolt_(Opcode::ELSE->value), + Compiler::PHVOLT_T_ELSEFOR => $parser->phvolt_(Opcode::ELSEFOR->value), + Compiler::PHVOLT_T_ELSEIF => $this->handleElseif($parser, $parserStatus, $state), + Compiler::PHVOLT_T_ENDIF => $this->handleEndif($parser, $state), + Compiler::PHVOLT_T_FOR => $this->handleFor($parser, $parserStatus, $state), + Compiler::PHVOLT_T_IN => $parser->phvolt_(Opcode::IN->value), + Compiler::PHVOLT_T_ENDFOR => $this->handleEndfor($parser, $state), + Compiler::PHVOLT_T_SWITCH => $this->handleSwitch($parser, $parserStatus, $state), + Compiler::PHVOLT_T_CASE => $this->handleCase($parser, $parserStatus), + Compiler::PHVOLT_T_DEFAULT => $this->handleDefault($parser, $parserStatus, $token, $state), + Compiler::PHVOLT_T_ENDSWITCH => $this->handleEndswitch($parser, $parserStatus, $state), + Compiler::PHVOLT_T_RAW_FRAGMENT => $this->handleRawFragment( + $parser, + $parserStatus, + $token, + $state + ), + Compiler::PHVOLT_T_SET => $this->handleSet($parser, $parserStatus, $state), + Compiler::PHVOLT_T_ASSIGN => $parser->phvolt_(Opcode::ASSIGN->value), + Compiler::PHVOLT_T_ADD_ASSIGN => $parser->phvolt_(Opcode::ADD_ASSIGN->value), + Compiler::PHVOLT_T_SUB_ASSIGN => $parser->phvolt_(Opcode::SUB_ASSIGN->value), + Compiler::PHVOLT_T_MUL_ASSIGN => $parser->phvolt_(Opcode::MUL_ASSIGN->value), + Compiler::PHVOLT_T_DIV_ASSIGN => $parser->phvolt_(Opcode::DIV_ASSIGN->value), + Compiler::PHVOLT_T_INCR => $parser->phvolt_(Opcode::INCR->value), + Compiler::PHVOLT_T_DECR => $parser->phvolt_(Opcode::DECR->value), + Compiler::PHVOLT_T_BLOCK => $this->handleBlock($parser, $parserStatus, $state), + Compiler::PHVOLT_T_ENDBLOCK => $this->handleEndblock($parser, $state), + Compiler::PHVOLT_T_MACRO => $this->handleMacro($parser, $parserStatus, $state), + Compiler::PHVOLT_T_ENDMACRO => $this->handleEndmacro($parser, $state), + Compiler::PHVOLT_T_CALL => $parser->phvolt_(Opcode::CALL->value), + Compiler::PHVOLT_T_ENDCALL => $parser->phvolt_(Opcode::ENDCALL->value), + Compiler::PHVOLT_T_CACHE => $parser->phvolt_(Opcode::CACHE->value), + Compiler::PHVOLT_T_ENDCACHE => $parser->phvolt_(Opcode::ENDCACHE->value), + Compiler::PHVOLT_T_RAW => $this->handleRaw($parser, $state), + Compiler::PHVOLT_T_ENDRAW => $this->handleEndraw($parser, $state), + Compiler::PHVOLT_T_INCLUDE => $parser->phvolt_(Opcode::INCLUDE->value), + Compiler::PHVOLT_T_WITH => $parser->phvolt_(Opcode::WITH->value), + Compiler::PHVOLT_T_DEFINED => $parser->phvolt_(Opcode::DEFINED->value), + Compiler::PHVOLT_T_EMPTY => $parser->phvolt_(Opcode::EMPTY->value), + Compiler::PHVOLT_T_EVEN => $parser->phvolt_(Opcode::EVEN->value), + Compiler::PHVOLT_T_ODD => $parser->phvolt_(Opcode::ODD->value), + Compiler::PHVOLT_T_NUMERIC => $parser->phvolt_(Opcode::NUMERIC->value), + Compiler::PHVOLT_T_SCALAR => $parser->phvolt_(Opcode::SCALAR->value), + Compiler::PHVOLT_T_ITERABLE => $parser->phvolt_(Opcode::ITERABLE->value), + Compiler::PHVOLT_T_DO => $parser->phvolt_(Opcode::DO->value), + Compiler::PHVOLT_T_RETURN => $parser->phvolt_(Opcode::RETURN->value), + Compiler::PHVOLT_T_AUTOESCAPE => $parser->phvolt_(Opcode::AUTOESCAPE->value), + Compiler::PHVOLT_T_ENDAUTOESCAPE => $parser->phvolt_(Opcode::ENDAUTOESCAPE->value), + Compiler::PHVOLT_T_BREAK => $parser->phvolt_(Opcode::BREAK->value), + Compiler::PHVOLT_T_CONTINUE => $parser->phvolt_(Opcode::CONTINUE->value), + Compiler::PHVOLT_T_EXTENDS => $this->handleExtends($parser, $parserStatus, $state), + default => $this->handleUnknownOpcode($parserStatus, $opcode), + }; if ($parserStatus->getStatus() !== Status::PHVOLT_PARSING_OK) { break; @@ -638,16 +197,24 @@ public function parseView(string $templatePath): array return $parser->getOutput(); } - /** - * @param Status $parserStatus - * @param string $message - * - * @return void - */ + public function setDebug(bool $debug): static + { + $this->debug = $debug; + + return $this; + } + + public function setDebugFile(string $debugFile): static + { + $this->debugFile = $debugFile; + + return $this; + } + private function createErrorMessage(Status $parserStatus, string $message): void { $length = 128 + strlen($parserStatus->getState()->getActiveFile()); - $str = sprintf( + $str = sprintf( "%s in %s on line %d", $message, $parserStatus->getState()->getActiveFile(), @@ -660,43 +227,282 @@ private function createErrorMessage(Status $parserStatus, string $message): void private function createScannerErrorMessage(Status $parserStatus): string { $state = $parserStatus->getState(); + if ($state->getStartLength() > 0) { if ($state->getStartLength() > 16) { $part = substr($state->getRawBuffer(), $state->getCursor(), 16); - $error = sprintf( + + return sprintf( "Scanning error before '%s...' in %s on line %d", $part, $state->getActiveFile(), $state->getActiveLine(), ); - } else { - $error = sprintf( - "Scanning error before '%s' in %s on line %d", - $state->getStart(), - $state->getActiveFile(), - $state->getActiveLine(), - ); } - } else { - $error = sprintf( - "Scanning error near to EOF in %s", + + return sprintf( + "Scanning error before '%s' in %s on line %d", + $state->getStart(), $state->getActiveFile(), + $state->getActiveLine(), ); } - return $error; + return sprintf( + "Scanning error near to EOF in %s", + $state->getActiveFile(), + ); } - /** - * @param Token $token - * - * @return bool - */ - private function phvoltIsBlankString(Token $token): bool + private function handleBlock(phvolt_Parser $parser, Status $parserStatus, State $state): void { - /** @var string $marker */ - $marker = $token->value ?? ''; - $len = strlen($marker); + if ($state->getBlockLevel() > 0) { + $this->createErrorMessage($parserStatus, 'Embedding blocks into other blocks is not supported'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $state->incrementBlockLevel(); + $parser->phvolt_(Opcode::BLOCK->value); + } + + private function handleCase(phvolt_Parser $parser, Status $parserStatus): void + { + if ($parserStatus->getState()->getSwitchLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Unexpected CASE'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $parser->phvolt_(Opcode::CASE->value); + } + + private function handleDefault( + phvolt_Parser $parser, + Status $parserStatus, + Token $token, + State $state + ): void { + if ($state->getSwitchLevel() !== 0) { + $parser->phvolt_(Opcode::DEFAULT->value); + + return; + } + + $newToken = new Token(Compiler::PHVOLT_T_IDENTIFIER, $token->value); + $parser->phvolt_(Opcode::IDENTIFIER->value, $newToken); + } + + private function handleElseif(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getIfLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Unexpected ENDIF'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $parser->phvolt_(Opcode::ELSEIF->value); + } + + private function handleEndblock(phvolt_Parser $parser, State $state): void + { + $state->decrementBlockLevel(); + $parser->phvolt_(Opcode::ENDBLOCK->value); + } + + private function handleEndfor(phvolt_Parser $parser, State $state): void + { + $state->decrementBlockLevel(); + $state->decrementForLevel(); + $state->setIfLevel($state->getOldIfLevel()); + $parser->phvolt_(Opcode::ENDFOR->value); + } + + private function handleEndif(phvolt_Parser $parser, State $state): void + { + $state->decrementBlockLevel(); + $state->decrementIfLevel(); + $parser->phvolt_(Opcode::ENDIF->value); + } + + private function handleEndmacro(phvolt_Parser $parser, State $state): void + { + $state->decrementMacroLevel(); + $parser->phvolt_(Opcode::ENDMACRO->value); + } + + private function handleEndraw(phvolt_Parser $parser, State $state): void + { + $parser->phvolt_(Opcode::ENDRAW->value); + $state->decrementForcedRawState(); + } + + private function handleEndswitch(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getSwitchLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Unexpected ENDSWITCH'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $state->decrementBlockLevel(); + $state->setSwitchLevel(0); + $parser->phvolt_(Opcode::ENDSWITCH->value); + } + + private function handleExtends(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getStatementPosition() !== 1) { + $this->createErrorMessage( + $parserStatus, + 'Extends statement must be placed at the first line in the template' + ); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $state->setExtendsMode(1); + $parser->phvolt_(Opcode::EXTENDS->value); + } + + private function handleFor(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Child templates only may contain blocks'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $state->setOldIfLevel($state->getIfLevel()); + $state->setIfLevel(0); + $state->incrementForLevel(); + $state->incrementBlockLevel(); + $parser->phvolt_(Opcode::FOR->value); + } + + private function handleIf(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Child templates only may contain blocks'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $state->incrementIfLevel(); + $state->incrementBlockLevel(); + $parser->phvolt_(Opcode::IF->value); + } + + private function handleMacro(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getMacroLevel() > 0) { + $this->createErrorMessage($parserStatus, 'Embedding macros into other macros is not allowed'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $state->incrementMacroLevel(); + $parser->phvolt_(Opcode::MACRO->value); + } + + private function handleOpenEdelimiter(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Child templates only may contain blocks'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $parser->phvolt_(Opcode::OPEN_EDELIMITER->value); + } + + private function handleRaw(phvolt_Parser $parser, State $state): void + { + $parser->phvolt_(Opcode::RAW->value); + $state->incrementForcedRawState(); + } + + private function handleRawFragment( + phvolt_Parser $parser, + Status $parserStatus, + Token $token, + State $state + ): void { + if ($token->length === 0) { + return; + } + + $value = trim((string)$token->value); + + if ($value !== '' && $state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Child templates only may contain blocks'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + if (!$this->isBlankString($token)) { + $state->incrementStatementPosition(); + } + + $this->parseWithToken($parser, $token, Opcode::RAW_FRAGMENT); + } + + private function handleSet(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Child templates only may contain blocks'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $parser->phvolt_(Opcode::SET->value); + } + + private function handleSwitch(phvolt_Parser $parser, Status $parserStatus, State $state): void + { + if ($state->getExtendsMode() === 1 && $state->getBlockLevel() === 0) { + $this->createErrorMessage($parserStatus, 'Child templates only may contain blocks'); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + if ($state->getSwitchLevel() > 0) { + $this->createErrorMessage( + $parserStatus, + 'A nested switch detected. There is no nested switch-case statements support' + ); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + + return; + } + + $state->setSwitchLevel(1); + $state->incrementBlockLevel(); + $parser->phvolt_(Opcode::SWITCH->value); + } + + private function handleUnknownOpcode(Status $parserStatus, int $opcode): void + { + $this->createErrorMessage($parserStatus, sprintf('Scanner: unknown opcode %d', $opcode)); + $parserStatus->setStatus(Status::PHVOLT_PARSING_FAILED); + } + + private function isBlankString(Token $token): bool + { + $marker = (string)$token->value; + $len = strlen($marker); for ($i = 0; $i < $len; $i++) { $ch = $marker[$i]; @@ -708,11 +514,9 @@ private function phvoltIsBlankString(Token $token): bool return true; } - private function phvoltParseWithToken(phvolt_Parser $parser, int $opcode, Opcode $parserCode): void + private function parseWithToken(phvolt_Parser $parser, Token $token, Opcode $parserCode): void { - $newToken = new Token($opcode, $this->token?->value); - - $this->token = $newToken; + $newToken = new Token($token->opcode, $token->value); $parser->phvolt_($parserCode->value, $newToken); } diff --git a/src/Scanner/Opcode.php b/src/Scanner/Opcode.php index b08b78a..fc68ebe 100644 --- a/src/Scanner/Opcode.php +++ b/src/Scanner/Opcode.php @@ -15,135 +15,135 @@ enum Opcode: int { - case ADD_ASSIGN = 46; - case AND = 6; - case ASSIGN = 45; - case AUTOESCAPE = 75; - case BLOCK = 64; - case BREAK = 77; - case CACHE = 66; - case CALL = 60; - case CASE = 42; - case CBRACKET_CLOSE = 88; - case CBRACKET_OPEN = 87; - case CLOSE_DELIMITER = 32; - case CLOSE_EDELIMITER = 63; - case COLON = 4; - case COMMA = 2; - case CONCAT = 23; - case CONTINUE = 78; - case DECR = 28; - case DEFAULT = 43; - case DEFINED = 80; - case DIVIDE = 18; - case DIV_ASSIGN = 49; - case DO = 73; - case DOT = 30; - case DOUBLE = 56; - case ELSE = 34; - case ELSEFOR = 36; - case ELSEIF = 35; - case EMPTY = 81; - case ENDAUTOESCAPE = 76; - case ENDBLOCK = 65; - case ENDCACHE = 67; - case ENDCALL = 61; - case ENDFOR = 39; - case ENDIF = 33; - case ENDMACRO = 53; - case ENDRAW = 69; - case ENDSWITCH = 41; - case EQUALS = 10; - case EVEN = 82; - case EXTENDS = 70; - case FALSE = 58; - case FOR = 37; - case GREATER = 13; - case GREATEREQUAL = 14; - case IDENTICAL = 16; - case IDENTIFIER = 38; - case IF = 31; - case IN = 8; - case INCLUDE = 71; - case INCR = 27; - case INTEGER = 54; - case IS = 9; - case ITERABLE = 86; - case LESS = 12; - case LESSEQUAL = 15; - case MACRO = 51; - case MINUS = 22; - case MOD = 20; - case MUL_ASSIGN = 48; - case NOT = 26; - case NOTEQUALS = 11; - case NOTIDENTICAL = 17; - case NULL = 57; - case NUMERIC = 84; - case ODD = 83; - case OPEN_DELIMITER = 1; - case OPEN_EDELIMITER = 62; - case OR = 7; + case ADD_ASSIGN = 46; + case AND = 6; + case ASSIGN = 45; + case AUTOESCAPE = 75; + case BLOCK = 64; + case BREAK = 77; + case CACHE = 66; + case CALL = 60; + case CASE = 42; + case CBRACKET_CLOSE = 88; + case CBRACKET_OPEN = 87; + case CLOSE_DELIMITER = 32; + case CLOSE_EDELIMITER = 63; + case COLON = 4; + case COMMA = 2; + case CONCAT = 23; + case CONTINUE = 78; + case DECR = 28; + case DEFAULT = 43; + case DEFINED = 80; + case DIVIDE = 18; + case DIV_ASSIGN = 49; + case DO = 73; + case DOT = 30; + case DOUBLE = 56; + case ELSE = 34; + case ELSEFOR = 36; + case ELSEIF = 35; + case EMPTY = 81; + case ENDAUTOESCAPE = 76; + case ENDBLOCK = 65; + case ENDCACHE = 67; + case ENDCALL = 61; + case ENDFOR = 39; + case ENDIF = 33; + case ENDMACRO = 53; + case ENDRAW = 69; + case ENDSWITCH = 41; + case EQUALS = 10; + case EVEN = 82; + case EXTENDS = 70; + case FALSE = 58; + case FOR = 37; + case GREATER = 13; + case GREATEREQUAL = 14; + case IDENTICAL = 16; + case IDENTIFIER = 38; + case IF = 31; + case IN = 8; + case INCLUDE = 71; + case INCR = 27; + case INTEGER = 54; + case IS = 9; + case ITERABLE = 86; + case LESS = 12; + case LESSEQUAL = 15; + case MACRO = 51; + case MINUS = 22; + case MOD = 20; + case MUL_ASSIGN = 48; + case NOT = 26; + case NOTEQUALS = 11; + case NOTIDENTICAL = 17; + case NULL = 57; + case NUMERIC = 84; + case ODD = 83; + case OPEN_DELIMITER = 1; + case OPEN_EDELIMITER = 62; + case OR = 7; case PARENTHESES_CLOSE = 52; - case PARENTHESES_OPEN = 29; - case PIPE = 25; - case PLUS = 21; - case QUESTION = 3; - case RANGE = 5; - case RAW = 68; - case RAW_FRAGMENT = 79; - case RETURN = 74; - case SBRACKET_CLOSE = 50; - case SBRACKET_OPEN = 24; - case SCALAR = 85; - case SET = 44; - case STRING = 55; - case SUB_ASSIGN = 47; - case SWITCH = 40; - case TIMES = 19; - case TRUE = 59; - case WITH = 72; + case PARENTHESES_OPEN = 29; + case PIPE = 25; + case PLUS = 21; + case QUESTION = 3; + case RANGE = 5; + case RAW = 68; + case RAW_FRAGMENT = 79; + case RETURN = 74; + case SBRACKET_CLOSE = 50; + case SBRACKET_OPEN = 24; + case SCALAR = 85; + case SET = 44; + case STRING = 55; + case SUB_ASSIGN = 47; + case SWITCH = 40; + case TIMES = 19; + case TRUE = 59; + case WITH = 72; public function label(): string { return match ($this) { - self::ADD_ASSIGN => '+=', - self::CBRACKET_CLOSE => '}', - self::CBRACKET_OPEN => '{', - self::CLOSE_DELIMITER => '%}', - self::CLOSE_EDELIMITER => '}}', - self::COLON => ':', - self::COMMA => ',', - self::CONCAT => '~', - self::DECR => '--', - self::DIVIDE => '/', - self::DIV_ASSIGN => '/=', - self::DOT => '.', - self::EQUALS => '=', - self::GREATER => '>', - self::GREATEREQUAL => '>=', - self::IDENTICAL => '===', - self::INCR => '++', - self::LESS => '<', - self::LESSEQUAL => '<=', - self::MINUS => '-', - self::MOD => '%', - self::MUL_ASSIGN => '*=', - self::NOT => '!', - self::NOTEQUALS => '!=', - self::NOTIDENTICAL => '!==', - self::OPEN_DELIMITER => '{%', - self::OPEN_EDELIMITER => '{{', + self::ADD_ASSIGN => '+=', + self::CBRACKET_CLOSE => '}', + self::CBRACKET_OPEN => '{', + self::CLOSE_DELIMITER => '%}', + self::CLOSE_EDELIMITER => '}}', + self::COLON => ':', + self::COMMA => ',', + self::CONCAT => '~', + self::DECR => '--', + self::DIVIDE => '/', + self::DIV_ASSIGN => '/=', + self::DOT => '.', + self::EQUALS => '=', + self::GREATER => '>', + self::GREATEREQUAL => '>=', + self::IDENTICAL => '===', + self::INCR => '++', + self::LESS => '<', + self::LESSEQUAL => '<=', + self::MINUS => '-', + self::MOD => '%', + self::MUL_ASSIGN => '*=', + self::NOT => '!', + self::NOTEQUALS => '!=', + self::NOTIDENTICAL => '!==', + self::OPEN_DELIMITER => '{%', + self::OPEN_EDELIMITER => '{{', self::PARENTHESES_CLOSE => ')', - self::PARENTHESES_OPEN => '(', - self::PIPE => '|', - self::PLUS => '+', - self::QUESTION => '?', - self::SBRACKET_CLOSE => ']', - self::SBRACKET_OPEN => '[', - self::SUB_ASSIGN => '-=', - self::TIMES => '*', - default => $this->name, + self::PARENTHESES_OPEN => '(', + self::PIPE => '|', + self::PLUS => '+', + self::QUESTION => '?', + self::SBRACKET_CLOSE => ']', + self::SBRACKET_OPEN => '[', + self::SUB_ASSIGN => '-=', + self::TIMES => '*', + default => $this->name, }; } } diff --git a/tests/unit/Parser/ParserTest.php b/tests/unit/Parser/ParserTest.php index 243fa29..3cf5c25 100644 --- a/tests/unit/Parser/ParserTest.php +++ b/tests/unit/Parser/ParserTest.php @@ -21,15 +21,15 @@ final class ParserTest extends TestCase { public function testEmptyTemplateReturnsEmptyArray(): void { - $parser = new Parser(''); + $parser = new Parser(); - $this->assertSame([], $parser->parseView('test.volt')); + $this->assertSame([], $parser->parse('', 'test.volt')); } public function testRawTextFragment(): void { - $parser = new Parser('Hello World'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('Hello World', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -39,8 +39,8 @@ public function testRawTextFragment(): void public function testEchoVariable(): void { - $parser = new Parser('{{ name }}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{{ name }}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -50,8 +50,8 @@ public function testEchoVariable(): void public function testIfStatement(): void { - $parser = new Parser('{% if active %}yes{% endif %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% if active %}yes{% endif %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -60,8 +60,8 @@ public function testIfStatement(): void public function testForLoop(): void { - $parser = new Parser('{% for item in items %}{{ item }}{% endfor %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% for item in items %}{{ item }}{% endfor %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -72,8 +72,8 @@ public function testForLoop(): void public function testSetStatement(): void { - $parser = new Parser('{% set x = 1 %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% set x = 1 %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -82,8 +82,11 @@ public function testSetStatement(): void public function testExtendsStatement(): void { - $parser = new Parser("{% extends 'base.volt' %}{% block content %}hello{% endblock %}"); - $result = $parser->parseView('child.volt'); + $parser = new Parser(); + $result = $parser->parse( + "{% extends 'base.volt' %}{% block content %}hello{% endblock %}", + 'child.volt' + ); $this->assertIsArray($result); } @@ -92,15 +95,15 @@ public function testSyntaxErrorThrowsException(): void { $this->expectException(Exception::class); - $parser = new Parser('{% endif %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% endif %}', 'test.volt'); } public function testTemplatePathInErrorMessage(): void { try { - $parser = new Parser('{% endif %}'); - $parser->parseView('mytemplate.volt'); + $parser = new Parser(); + $parser->parse('{% endif %}', 'mytemplate.volt'); $this->fail('Expected exception not thrown'); } catch (Exception $e) { $this->assertStringContainsString('mytemplate.volt', $e->getMessage()); @@ -111,26 +114,18 @@ public function testNestedSwitchThrowsException(): void { $this->expectException(Exception::class); - $parser = new Parser('{% switch x %}{% switch y %}{% endswitch %}{% endswitch %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% switch x %}{% switch y %}{% endswitch %}{% endswitch %}', 'test.volt'); } public function testDebugMode(): void { - $debugFile = sys_get_temp_dir() . '/volt_debug_test.txt'; - $parser = new Parser('{{ name }}'); - - $ref = new \ReflectionClass($parser); - - $debugProp = $ref->getProperty('debug'); - $debugProp->setAccessible(true); - $debugProp->setValue($parser, true); - - $fileProp = $ref->getProperty('debugFile'); - $fileProp->setAccessible(true); - $fileProp->setValue($parser, $debugFile); + $testsDir = dirname(__FILE__, 3); + $debugFile = $testsDir . '/_output/volt_debug_test.txt'; + $parser = new Parser(); + $parser->setDebug(true)->setDebugFile($debugFile); - $result = $parser->parseView('test.volt'); + $result = $parser->parse('{{ name }}', 'test.volt'); $this->assertIsArray($result); $this->assertFileExists($debugFile); @@ -140,8 +135,11 @@ public function testDebugMode(): void public function testElseForViaElse(): void { - $parser = new Parser('{% for x in items %}{{ x }}{% else %}empty{% endfor %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse( + '{% for x in items %}{{ x }}{% else %}empty{% endfor %}', + 'test.volt' + ); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -150,8 +148,11 @@ public function testElseForViaElse(): void public function testElsefor(): void { - $parser = new Parser('{% for x in items %}{{ x }}{% elsefor %}empty{% endfor %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse( + '{% for x in items %}{{ x }}{% elsefor %}empty{% endfor %}', + 'test.volt' + ); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -163,14 +164,14 @@ public function testElseifWithoutIfThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Unexpected ENDIF'); - $parser = new Parser('{% elseif x %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% elseif x %}', 'test.volt'); } public function testSwitchWithCase(): void { - $parser = new Parser('{% switch x %}{% case 1 %}one{% endswitch %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% switch x %}{% case 1 %}one{% endswitch %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -179,8 +180,11 @@ public function testSwitchWithCase(): void public function testSwitchWithDefault(): void { - $parser = new Parser('{% switch x %}{% default %}fallback{% endswitch %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse( + '{% switch x %}{% default %}fallback{% endswitch %}', + 'test.volt' + ); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -192,8 +196,8 @@ public function testCaseWithoutSwitchThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Unexpected CASE'); - $parser = new Parser('{% case 1 %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% case 1 %}', 'test.volt'); } public function testEndswitchWithoutSwitchThrowsException(): void @@ -201,8 +205,8 @@ public function testEndswitchWithoutSwitchThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Unexpected ENDSWITCH'); - $parser = new Parser('{% endswitch %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% endswitch %}', 'test.volt'); } public function testBlockInsideBlockThrowsException(): void @@ -210,8 +214,11 @@ public function testBlockInsideBlockThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Embedding blocks into other blocks is not supported'); - $parser = new Parser('{% block outer %}{% block inner %}{% endblock %}{% endblock %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse( + '{% block outer %}{% block inner %}{% endblock %}{% endblock %}', + 'test.volt' + ); } public function testMacroInsideMacroThrowsException(): void @@ -219,14 +226,20 @@ public function testMacroInsideMacroThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Embedding macros into other macros is not allowed'); - $parser = new Parser('{% macro outer() %}{% macro inner() %}{% endmacro %}{% endmacro %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse( + '{% macro outer() %}{% macro inner() %}{% endmacro %}{% endmacro %}', + 'test.volt' + ); } public function testMacroAndEndmacro(): void { - $parser = new Parser('{% macro greet(name) %}Hello {{ name }}{% endmacro %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse( + '{% macro greet(name) %}Hello {{ name }}{% endmacro %}', + 'test.volt' + ); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -235,8 +248,8 @@ public function testMacroAndEndmacro(): void public function testCallAndEndcall(): void { - $parser = new Parser('{% call greet() %}{% endcall %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% call greet() %}{% endcall %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -245,8 +258,8 @@ public function testCallAndEndcall(): void public function testRawAndEndraw(): void { - $parser = new Parser('{% raw %}{{ not_evaluated }}{% endraw %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% raw %}{{ not_evaluated }}{% endraw %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -255,8 +268,8 @@ public function testRawAndEndraw(): void public function testIncludeWith(): void { - $parser = new Parser('{% include "partial.volt" with vars %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% include "partial.volt" with vars %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -265,8 +278,11 @@ public function testIncludeWith(): void public function testReturnInMacro(): void { - $parser = new Parser('{% macro compute(x) %}{% return x %}{% endmacro %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse( + '{% macro compute(x) %}{% return x %}{% endmacro %}', + 'test.volt' + ); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -275,8 +291,8 @@ public function testReturnInMacro(): void public function testAddAssign(): void { - $parser = new Parser('{% set counter += 1 %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% set counter += 1 %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -285,8 +301,8 @@ public function testAddAssign(): void public function testSubAssign(): void { - $parser = new Parser('{% set counter -= 1 %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% set counter -= 1 %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -295,8 +311,8 @@ public function testSubAssign(): void public function testMulAssign(): void { - $parser = new Parser('{% set counter *= 2 %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% set counter *= 2 %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -305,8 +321,8 @@ public function testMulAssign(): void public function testDivAssign(): void { - $parser = new Parser('{% set counter /= 2 %}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{% set counter /= 2 %}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -318,8 +334,8 @@ public function testOpenEdelimiterInExtendsModeThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Child templates only may contain blocks'); - $parser = new Parser('{% extends "base.volt" %}{{ something }}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% extends "base.volt" %}{{ something }}', 'test.volt'); } public function testForInExtendsModeThrowsException(): void @@ -327,8 +343,8 @@ public function testForInExtendsModeThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Child templates only may contain blocks'); - $parser = new Parser('{% extends "base.volt" %}{% for x in y %}{% endfor %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% extends "base.volt" %}{% for x in y %}{% endfor %}', 'test.volt'); } public function testSwitchInExtendsModeThrowsException(): void @@ -336,8 +352,8 @@ public function testSwitchInExtendsModeThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Child templates only may contain blocks'); - $parser = new Parser('{% extends "base.volt" %}{% switch x %}{% endswitch %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% extends "base.volt" %}{% switch x %}{% endswitch %}', 'test.volt'); } public function testSetInExtendsModeThrowsException(): void @@ -345,8 +361,8 @@ public function testSetInExtendsModeThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Child templates only may contain blocks'); - $parser = new Parser('{% extends "base.volt" %}{% set x = 1 %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% extends "base.volt" %}{% set x = 1 %}', 'test.volt'); } public function testRawFragmentInExtendsModeThrowsException(): void @@ -354,8 +370,8 @@ public function testRawFragmentInExtendsModeThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Child templates only may contain blocks'); - $parser = new Parser('{% extends "base.volt" %}non-blank content'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% extends "base.volt" %}non-blank content', 'test.volt'); } public function testScannerErrorNearEof(): void @@ -363,10 +379,11 @@ public function testScannerErrorNearEof(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Scanning error'); - $parser = new Parser( - '{{ link_to("album/" ~ album.id ~ "/" ~ $album.uri, "test") }}' + $parser = new Parser(); + $parser->parse( + '{{ link_to("album/" ~ album.id ~ "/" ~ $album.uri, "test") }}', + 'test.volt' ); - $parser->parseView('test.volt'); } public function testIfInExtendsModeThrowsException(): void @@ -374,14 +391,14 @@ public function testIfInExtendsModeThrowsException(): void $this->expectException(Exception::class); $this->expectExceptionMessage('Child templates only may contain blocks'); - $parser = new Parser('{% extends "base.volt" %}{% if x %}yes{% endif %}'); - $parser->parseView('test.volt'); + $parser = new Parser(); + $parser->parse('{% extends "base.volt" %}{% if x %}yes{% endif %}', 'test.volt'); } public function testCurlyBracketEmptyExpression(): void { - $parser = new Parser('{{ {} }}'); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse('{{ {} }}', 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); @@ -390,11 +407,24 @@ public function testCurlyBracketEmptyExpression(): void public function testCurlyBracketDictExpression(): void { - $parser = new Parser("{{ {'key': 'value'} }}"); - $result = $parser->parseView('test.volt'); + $parser = new Parser(); + $result = $parser->parse("{{ {'key': 'value'} }}", 'test.volt'); $this->assertIsArray($result); $this->assertCount(1, $result); $this->assertSame(359, $result[0]['type']); // PHVOLT_T_ECHO } + + public function testParserIsReusable(): void + { + $parser = new Parser(); + + $result1 = $parser->parse('{{ name }}', 'first.volt'); + $result2 = $parser->parse('{% if active %}yes{% endif %}', 'second.volt'); + + $this->assertCount(1, $result1); + $this->assertCount(1, $result2); + $this->assertSame(359, $result1[0]['type']); // PHVOLT_T_ECHO + $this->assertSame(300, $result2[0]['type']); // PHVOLT_T_IF + } }