diff --git a/README.md b/README.md index 23a60e7..d2372a6 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ $variables = [ 'show_container' => false, ]; -// Absolute file path or file content as a string +// Absolute file path to a text file $file_path = __DIR__ . '/hello.html'; $parser = new Parser($file_path, $variables); @@ -110,7 +110,8 @@ Parsed HTML to a PHP string ``` -## Same example but for WordPress shortcode + +### Same example but for WordPress shortcode ```php use Serhii\GoodbyeHtml\Parser; @@ -127,6 +128,21 @@ function shortcode_callback() { } ``` +## Options + +The instance of `Parser` class takes the third argument as a `ParserOption` enum. You can pass it to the constructor of the `Parser` class as a third argument. For now, it has only a single options: + +#### `ParserOption::PARSE_TEXT` +If you pass this option, the parser, instead of getting the content of the provided file path, will parse the provided string. This option is useful when you want to parse a string instead of a file. + +```php +$parser = new Parser('
{{ $title }}
', [ + 'title' => 'Hello world' +], ParserOption::PARSE_TEXT); + +// output:
Hello world
+``` + ## Supported types Types that you can pass to the parser to include them in the `html/text` file. Note that not all PHP types are supported for know. More types will be added in next releases. @@ -294,7 +310,16 @@ Loop takes 2 integer arguments. The first argument is from what number start loo ``` +#### Assigning statements + +You can assign values to variables inside your text files using curly braces. For example if you want to assign value 5 to variable `$a`, you can do it like this `{{ $a = 5 }}`. You can also use prefix operators to change the value of the variable. For example if you want to assign value false to variable `$is_smoking`, you can do it like this `{{ $is_smoking = !true }}`. + +```html +
{{ $age = 33 }}
+``` + ## Getting started + ```bash $ composer require serhii/goodbye-html ``` diff --git a/docs/changelog.md b/docs/changelog.md index b404b4b..5dc665d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,15 @@ ---- +## v2.7.0 (2023-11-22) + +- Added more tests to make sure that everything works as expected +- Added more info to the `README.md` file +- Added added assign statement to the BNF grammar +- Added a third parameter to a `Parser.php` which excepts `ParserOption` ENUM + +---- + ## v2.6.0 (2023-11-21) - Added `elseif` statements to a BNF grammar diff --git a/docs/goodbye-html.bnf b/docs/goodbye-html.bnf index f9f2286..f91db61 100644 --- a/docs/goodbye-html.bnf +++ b/docs/goodbye-html.bnf @@ -5,7 +5,7 @@ | | | - | + | ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" @@ -29,6 +29,8 @@ ::= + ::= "=" + ::= ::= "?" ":" diff --git a/src/Ast/Expressions/PrefixExpression.php b/src/Ast/Expressions/PrefixExpression.php index 81cc834..bcf1d5f 100644 --- a/src/Ast/Expressions/PrefixExpression.php +++ b/src/Ast/Expressions/PrefixExpression.php @@ -8,6 +8,9 @@ readonly class PrefixExpression implements Expression { + /** + * @param non-empty-string $operator + */ public function __construct( public Token $token, public string $operator, @@ -20,6 +23,9 @@ public function tokenLiteral(): string return $this->token->literal; } + /** + * @return non-empty-string + */ public function string(): string { return sprintf('(%s%s)', $this->operator, $this->right->string()); diff --git a/src/Ast/Literals/NullLiteral.php b/src/Ast/Literals/NullLiteral.php index 730bfde..3b77189 100644 --- a/src/Ast/Literals/NullLiteral.php +++ b/src/Ast/Literals/NullLiteral.php @@ -20,6 +20,6 @@ public function tokenLiteral(): string public function string(): string { - return ''; + return 'null'; } } diff --git a/src/CoreParser/CoreParser.php b/src/CoreParser/CoreParser.php index 736317f..0bef9db 100644 --- a/src/CoreParser/CoreParser.php +++ b/src/CoreParser/CoreParser.php @@ -310,6 +310,10 @@ private function parsePrefixExpression(): Expression $right = $this->parseExpression(Precedence::PREFIX); + if ($operator === '') { + throw new CoreParserException(ParserError::prefixOperatorNotFound()); + } + return new PrefixExpression($token, $operator, $right); } diff --git a/src/CoreParser/ParserError.php b/src/CoreParser/ParserError.php index a2d6779..f02b7e9 100644 --- a/src/CoreParser/ParserError.php +++ b/src/CoreParser/ParserError.php @@ -11,6 +11,9 @@ abstract class ParserError { private const PREFIX = '[PARSER_ERROR]:'; + /** + * @return non-empty-string + */ public static function noPrefixParseFunction(Token $token): string { return sprintf( @@ -20,6 +23,9 @@ public static function noPrefixParseFunction(Token $token): string ); } + /** + * @return non-empty-string + */ public static function expectNextTokenToBeDifferent(TokenType $token, Token $peek): string { return sprintf( @@ -30,6 +36,9 @@ public static function expectNextTokenToBeDifferent(TokenType $token, Token $pee ); } + /** + * @return non-empty-string + */ public static function elseIfBlockWrongPlace(): string { return sprintf( @@ -37,4 +46,12 @@ public static function elseIfBlockWrongPlace(): string self::PREFIX, ); } + + /** + * @return non-empty-string + */ + public static function prefixOperatorNotFound(): string + { + return sprintf('%s prefix operator not found', self::PREFIX); + } } diff --git a/src/Exceptions/CoreParserException.php b/src/Exceptions/CoreParserException.php index 639a8be..7effdca 100644 --- a/src/Exceptions/CoreParserException.php +++ b/src/Exceptions/CoreParserException.php @@ -8,4 +8,11 @@ class CoreParserException extends Exception { + /** + * @param non-empty-string $message + */ + public function __construct(string $message) + { + parent::__construct($message); + } } diff --git a/src/Exceptions/EvaluatorException.php b/src/Exceptions/EvaluatorException.php index bbf793f..71f8352 100644 --- a/src/Exceptions/EvaluatorException.php +++ b/src/Exceptions/EvaluatorException.php @@ -8,4 +8,11 @@ class EvaluatorException extends Exception { + /** + * @param non-empty-string $message + */ + public function __construct(string $message) + { + parent::__construct($message); + } } diff --git a/src/Exceptions/ParserException.php b/src/Exceptions/ParserException.php index 3a830d8..f2cd831 100644 --- a/src/Exceptions/ParserException.php +++ b/src/Exceptions/ParserException.php @@ -8,4 +8,11 @@ class ParserException extends Exception { + /** + * @param non-empty-string $message + */ + public function __construct(string $message) + { + parent::__construct($message); + } } diff --git a/src/Lexer/Lexer.php b/src/Lexer/Lexer.php index 2d78cb8..c73edd8 100644 --- a/src/Lexer/Lexer.php +++ b/src/Lexer/Lexer.php @@ -11,10 +11,30 @@ class Lexer { private const LAST_CHAR = 'ΓΈ'; - private readonly string $input; + /** + * The position of the current character in the input (points to current char) + * + * @var int<0, max> + */ private int $position = 0; + + /** + * The position of the next character in the input (points to next char) + * + * @var int<0, max> + */ private int $nextPosition = 0; + + private readonly string $input; + + /** + * The current character under examination + */ private string $char = ''; + + /** + * Tells us whether we are in HTML or in embedded code + */ private bool $isHtml = true; public function __construct(string $input) @@ -74,7 +94,7 @@ private function readEmbeddedCodeToken(): Token } if ($this->isVariableStart()) { - $this->advanceChar(); + $this->advanceChar(); // skip "$" return new Token(TokenType::VAR, $this->readIdentifier()); } @@ -147,12 +167,16 @@ private function advanceChar(): void ++$this->nextPosition; } + /** + * @return non-empty-string + */ private function peekChar(): string { if ($this->nextPosition >= strlen($this->input)) { return self::LAST_CHAR; } + /** @var non-empty-string */ return $this->input[$this->nextPosition]; } @@ -184,6 +208,9 @@ private function isNumber(string $number): bool return preg_match('/[0-9.]/', $number) === 1; } + /** + * @return non-empty-string + */ private function readIdentifier(): string { $position = $this->position; @@ -192,6 +219,7 @@ private function readIdentifier(): string $this->advanceChar(); } + /** @var non-empty-string $result */ $result = substr($this->input, $position, $this->position - $position); // Handle case when identifier is "else if" @@ -206,6 +234,9 @@ private function readIdentifier(): string return $result; } + /** + * @return non-empty-string + */ private function readNumber(): string { $position = $this->position; @@ -214,6 +245,7 @@ private function readNumber(): string $this->advanceChar(); } + /** @var non-empty-string */ return substr($this->input, $position, $this->position - $position); } diff --git a/src/Obj/ErrorObj.php b/src/Obj/ErrorObj.php index 0b2069b..9208279 100644 --- a/src/Obj/ErrorObj.php +++ b/src/Obj/ErrorObj.php @@ -6,6 +6,9 @@ readonly class ErrorObj extends Obj { + /** + * @param non-empty-string $message + */ public function __construct(public string $message) { } @@ -15,6 +18,9 @@ public function type(): ObjType return ObjType::ERROR_OBJ; } + /** + * @return non-empty-string + */ public function value(): string { return $this->message; diff --git a/src/Parser.php b/src/Parser.php index aeadc37..a879141 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -24,12 +24,14 @@ class Parser private string $html_content; /** - * @param string $file_path Absolute file path or the file content itself - * @param array|null $variables Associative array ['var_name' => 'will be inserted'] + * @param string $file_path Absolute file path + * @param array|null $variables + * @param ParserOption|null $options */ public function __construct( private readonly string $file_path, private readonly array|null $variables = null, + private readonly ParserOption|null $options = null, ) { $this->html_content = ''; } @@ -45,6 +47,10 @@ public function __construct( */ public function parseHtml(): string { + if ($this->file_path === '') { + return ''; + } + $this->setHtmlContent(); if (!$this->hasVariables()) { @@ -69,7 +75,7 @@ public function parseHtml(): string */ private function setHtmlContent(): void { - if (!str_starts_with($this->file_path, '/')) { + if ($this->options && $this->options === ParserOption::PARSE_TEXT) { $this->html_content = $this->file_path; return; } diff --git a/src/ParserOption.php b/src/ParserOption.php new file mode 100644 index 0000000..87f6ff0 --- /dev/null +++ b/src/ParserOption.php @@ -0,0 +1,10 @@ +{{ $his_age }}', '

33

'], ['{{ $lang = "PHP" }}{{ $lang="Go" }}{{ $lang }}', 'Go'], // test overriding ['{{ if true }}{{ $platform = "Mac" }}{{ $platform }}{{ end }}', 'Mac'], - ['{{ $platform = "Linux" }}{{ if true }}{{ $platform }}{{ end }}', 'Linux'], + ['{{ $platform = "Linux" . " platform" }}{{ if true }}{{ $platform }}{{ end }}', 'Linux platform'], + ['{{ $sum = 22 + 8 }}{{ $sum }}', '30'], + ['{{ $isGood = !true }}{{ $isGood ? "Good" : "Bad" }}', 'Bad'], ]; } @@ -400,6 +408,15 @@ public static function providerForTestVariableIsUndefinedOutOfScopes(): array ['{{ if false }}{{ else }}{{ $age = 33 }}{{ end }}{{ $age }}', 'age'], ['{{ loop 0, 1 }}{{ $index }}{{ end }}{{ $index }}', 'index'], ['{{ loop -1, 3 }}{{ $age = 33 }}{{ end }}{{ $age }}', 'age'], + [ + << null, 'name' => 'Elliot', 'movie' => 'Mr. Robot', - ]); + ], ParserOption::PARSE_TEXT); $this->assertSame($expect, $parser->parseHtml()); } + + public function testParserReturnsEmptyStringWithEmptyFilePath(): void + { + $parser = new Parser(''); + $this->assertEmpty($parser->parseHtml()); + } }