Skip to content

Commit

Permalink
Merge pull request #13 from SerhiiCho/v2.6.0
Browse files Browse the repository at this point in the history
V2.6.0
  • Loading branch information
SerhiiCho authored Nov 21, 2023
2 parents cd575dc + 971509b commit a2a1fc2
Show file tree
Hide file tree
Showing 21 changed files with 205 additions and 19 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tests/files/before/* linguist-vendored
tests/files/expect/* linguist-vendored
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ This package is not for creating a full-featured template engine. It's just a si

## What Goodbye HTML has?
- [x] Variables
- [x] Assigning variables
- [x] Using variables
- [x] Printing variables
- [x] If/Else-If/Else statements
- [x] Ternary expressions
- [x] Loops
Expand Down Expand Up @@ -157,6 +160,7 @@ Infix operators are used to perform math operations or string concatenation. For
| Divide | / | 6 / 3 | int, float |
| Modulo | % | 5 % 2 | int, float |
| Concatenate | . | 'Hello' . ' world' | string |
| Assigning | = | {{ $a = 5 }} | all the types |

## All the available syntax in html/text file

Expand Down
13 changes: 12 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@

----

## v2.6.0 (2023-11-21)

- Added `elseif` statements to a BNF grammar
- Added `.gitattributes` file to ignore HTML files in `tests/files` directory
- Updated code to level 9 of the PHP Stan static analysis tool
- Fixed a typo in the change log file
- Added variable declaration statement support. Now you can declare variables like this: `{{ $name = 'Anna' }}`. Variable declaration is a statement, and must be surrounded with curly braces
- 🐛 Bug fix, `$index` variable was accessible outside of the loop. Now, it will throw an error that variable $index is undefined.

----

## v2.5.0 (2023-11-19)

- Added support for `elseif (<expression>)` and `else if (<expression>)` statements like we have in PHP. You can use them like this: `{{ if true }}<h1>True</h1>{{ elseif false }}<h1>False</h1>{{ else }}<h1>Something else</h1>{{ endif }}`
- Added support for `elseif (<expression>)` and `else if (<expression>)` statements like we have in PHP. You can use them like this: `{{ if true }}<h1>True</h1>{{ elseif false }}<h1>False</h1>{{ else }}<h1>Something else</h1>{{ end }}`
- Added **PHP Stan** static analysis tool
- Added **CS Fixer** code style fixer
- 🐛 Bug fixes in the `Parser.php` class related to readonly properties being set later in the code
Expand Down
6 changes: 5 additions & 1 deletion docs/goodbye-html.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
<loop-statement>
::= "{{" "loop" <expression> "," <expression> "}}" <block-statement>* "{{" "end" "}}"

<else-if-statement>
::= "{{" "else" "if" <expression> "}}" <block-statement>*

<if-statement>
::= "{{" "if" <expression> "}}" <block-statement>* "{{" "end" "}}"
| "{{" "if" <expression> "}}" <block-statement>* "{{" "else" "}}" <block-statement>* "{{" "end" "}}"
| "{{" "if" <expression> "}}" <block-statement>* "{{" "else" "}}" <block-statement>* "{{" "end" "}}"
| "{{" "if" <expression> "}}" <block-statement>* <else-if-statement>* "{{" "end" "}}"
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
parameters:
level: 8
level: 9
paths:
- src
2 changes: 1 addition & 1 deletion src/Ast/Literals/StringLiteral.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public function tokenLiteral(): string

public function string(): string
{
return '"' . $this->value . '"';
return sprintf("'%s'", $this->value);
}
}
29 changes: 29 additions & 0 deletions src/Ast/Statements/AssignStatement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Serhii\GoodbyeHtml\Ast\Statements;

use Serhii\GoodbyeHtml\Ast\Expressions\Expression;
use Serhii\GoodbyeHtml\Ast\Expressions\VariableExpression;
use Serhii\GoodbyeHtml\Token\Token;

readonly class AssignStatement implements Statement
{
public function __construct(
public Token $token,
public VariableExpression $variable,
public Expression $value,
) {
}

public function tokenLiteral(): string
{
return $this->token->literal;
}

public function string(): string
{
return sprintf("{{ %s = %s }}", $this->variable->string(), $this->value->string());
}
}
2 changes: 1 addition & 1 deletion src/Ast/Statements/IfStatement.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
readonly class IfStatement implements Statement
{
/**
* @param array<int,IfStatement> $elseIfBlocks
* @param list<IfStatement> $elseIfBlocks
*/
public function __construct(
public Token $token,
Expand Down
29 changes: 26 additions & 3 deletions src/CoreParser/CoreParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Serhii\GoodbyeHtml\Ast\Literals\IntegerLiteral;
use Serhii\GoodbyeHtml\Ast\Literals\NullLiteral;
use Serhii\GoodbyeHtml\Ast\Literals\StringLiteral;
use Serhii\GoodbyeHtml\Ast\Statements\AssignStatement;
use Serhii\GoodbyeHtml\Ast\Statements\BlockStatement;
use Serhii\GoodbyeHtml\Ast\Statements\ExpressionStatement;
use Serhii\GoodbyeHtml\Ast\Statements\HtmlStatement;
Expand Down Expand Up @@ -49,12 +50,12 @@ class CoreParser
private Token $peekToken;

/**
* @var array<string,Closure>
* @var array<string, Closure(): Expression>
*/
private array $prefixParseFns = [];

/**
* @var array<string,Closure>
* @var array<string, Closure(Expression): Expression>
*/
private array $infixParseFns = [];

Expand Down Expand Up @@ -183,10 +184,32 @@ private function parseEmbeddedCode(): Statement
return match ($this->curToken->type) {
TokenType::IF => $this->parseIfStatement(),
TokenType::LOOP => $this->parseLoopStatement(),
TokenType::VAR => $this->parseAssignStatement(),
default => $this->parseExpressionStatement(),
};
}

private function parseAssignStatement(): Statement
{
if (!$this->peekTokenIs(TokenType::ASSIGN)) {
return $this->parseExpressionStatement();
}

$token = $this->curToken;

$variable = $this->parseVariableExpression();

$this->expectPeek(TokenType::ASSIGN); // skip variable

$this->nextToken(); // skip "="

return new AssignStatement(
token: $token,
variable: $variable,
value: $this->parseExpression(Precedence::LOWEST),
);
}

private function parseHtmlStatement(): HtmlStatement
{
return new HtmlStatement($this->curToken);
Expand Down Expand Up @@ -238,7 +261,7 @@ private function parseExpression(Precedence $precedence): Expression
return $leftExp;
}

private function parseVariableExpression(): Expression
private function parseVariableExpression(): VariableExpression
{
return new VariableExpression(
$this->curToken,
Expand Down
18 changes: 18 additions & 0 deletions src/Evaluator/Evaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Serhii\GoodbyeHtml\Ast\Literals\NullLiteral;
use Serhii\GoodbyeHtml\Ast\Literals\StringLiteral;
use Serhii\GoodbyeHtml\Ast\Node;
use Serhii\GoodbyeHtml\Ast\Statements\AssignStatement;
use Serhii\GoodbyeHtml\Ast\Statements\BlockStatement;
use Serhii\GoodbyeHtml\Ast\Statements\ExpressionStatement;
use Serhii\GoodbyeHtml\Ast\Statements\HtmlStatement;
Expand Down Expand Up @@ -47,6 +48,7 @@ public function eval(Node $node, Env $env): Obj
BlockStatement::class => $this->evalBlockStatement($node, $env),
LoopStatement::class => $this->evalLoopStatement($node, $env),
ExpressionStatement::class => $this->eval($node->expression, $env),
AssignStatement::class => $this->evalAssignStatement($node, $env),
PrefixExpression::class => $this->evalPrefixExpression($node, $env),
InfixExpression::class => $this->evalInfixExpression($node, $env),
VariableExpression::class => $this->evalVariableExpression($node, $env),
Expand Down Expand Up @@ -154,6 +156,7 @@ private function evalIfStatement(IfStatement $node, Env $env): Obj
}

$isTrue = $condition->value();
$env = Env::newEnclosedEnv($env);

if ($isTrue) {
return $this->eval($node->block, $env);
Expand Down Expand Up @@ -230,6 +233,8 @@ private function evalLoopStatement(LoopStatement $node, Env $env): Obj
return EvalError::wrongArgumentType('loop', ObjType::INTEGER_OBJ, $to);
}

$env = Env::newEnclosedEnv($env);

for ($i = $from->value; $i <= $to->value; $i++) {
$env->set('index', new IntegerObj($i));

Expand All @@ -245,6 +250,19 @@ private function evalLoopStatement(LoopStatement $node, Env $env): Obj
return new HtmlObj($html);
}

private function evalAssignStatement(AssignStatement $node, Env $env): Obj
{
$value = $this->eval($node->value, $env);

if ($value instanceof ErrorObj) {
return $value;
}

$env->set($node->variable->value, $value);

return new NullObj();
}

private function evalMinusPrefixOperatorExpression(Obj $right): Obj
{
$value = $right->value();
Expand Down
1 change: 1 addition & 0 deletions src/Lexer/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ private function readEmbeddedCodeToken(): Token
':' => $this->createTokenAndAdvanceChar(TokenType::COLON),
'!' => $this->createTokenAndAdvanceChar(TokenType::BANG),
'.' => $this->createTokenAndAdvanceChar(TokenType::PERIOD),
'=' => $this->createTokenAndAdvanceChar(TokenType::ASSIGN),
default => false,
};

Expand Down
5 changes: 2 additions & 3 deletions src/Obj/Env.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
class Env
{
/**
* @param array<string,Obj> $store
* @param array<string, Obj> $store
*/
public function __construct(
private array $store = [],
private readonly ?self $outer = null,
) {
}

// todo: it's going to be used in the next version when I implement scopes
public static function newEnclosedEnv(Env $outer): self
{
return new self([], $outer);
Expand All @@ -36,7 +35,7 @@ public function set(string $key, Obj $value): void
}

/**
* @param array<string,mixed> $arr
* @param array<string, Obj> $arr
*/
public static function fromArray(array $arr): self
{
Expand Down
5 changes: 2 additions & 3 deletions src/Obj/Obj.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ abstract public function value(): int|string|bool|float|null;
*/
public static function fromNative(mixed $value, string $name): self
{
$type = gettype($value);

switch ($type) {
switch (gettype($value)) {
case 'string':
return new StringObj($value);
case 'integer':
Expand All @@ -31,6 +29,7 @@ public static function fromNative(mixed $value, string $name): self
case 'NULL':
return new NullObj();
default:
$type = gettype($value);
$msg = sprintf('[PARSER_ERROR] Provided variable "%s" has unsupported type "%s"', $name, $type);
throw new ParserException($msg);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Parser

/**
* @param string $file_path Absolute file path or the file content itself
* @param array<string,mixed>|null $variables Associative array ['var_name' => 'will be inserted']
* @param array<string, mixed>|null $variables Associative array ['var_name' => 'will be inserted']
*/
public function __construct(
private readonly string $file_path,
Expand Down
1 change: 1 addition & 0 deletions src/Token/TokenType.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum TokenType: string
case MODULO = '%';
case PERIOD = '.';
case BANG = "!";
case ASSIGN = '=';

// Delimiters
case LBRACES = '{{';
Expand Down
19 changes: 16 additions & 3 deletions tests/CoreParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Serhii\GoodbyeHtml\Ast\Literals\IntegerLiteral;
use Serhii\GoodbyeHtml\Ast\Literals\NullLiteral;
use Serhii\GoodbyeHtml\Ast\Literals\StringLiteral;
use Serhii\GoodbyeHtml\Ast\Statements\AssignStatement;
use Serhii\GoodbyeHtml\Ast\Statements\ExpressionStatement;
use Serhii\GoodbyeHtml\Ast\Statements\HtmlStatement;
use Serhii\GoodbyeHtml\Ast\Statements\IfStatement;
Expand Down Expand Up @@ -80,7 +81,7 @@ private static function testBoolean($bool, $val): void
self::assertSame($val, $bool->value, "Boolean must have value '{$val}', got: '{$bool->value}'");
}

public function testParsingVariables(): void
public function testParsingVariable(): void
{
$input = '{{ $userName }}';

Expand Down Expand Up @@ -248,7 +249,7 @@ public function testParsingLoopStatement(): void

$this->assertCount(3, $loop->body->statements, 'Loop body must contain 3 statements');

/** @var array<int,HtmlStatement|ExpressionStatement> $stmts */
/** @var list<HtmlStatement|ExpressionStatement> $stmts */
$stmts = $loop->body->statements;

$this->testVariable($stmts[1]->expression, 'index');
Expand All @@ -264,7 +265,7 @@ public function testParsingStrings(): void
/** @var ExpressionStatement $stmt */
$stmt = $this->createProgram($input)->statements[0];

/** @var StringLiteral $var */
/** @var StringLiteral $str */
$str = $stmt->expression;

$this->testString($str, 'hello');
Expand Down Expand Up @@ -401,4 +402,16 @@ public function testWhenParsingIfStatementWithElseBlockBeforeElseIfBlockGivesErr

$this->createProgram("{{ if true }}1{{ else }}2{{ elseif true }}3{{ end }}");
}

public function testParsingAssignStatement(): void
{
$input = "{{ \$herName = 'Anna' }}";

/** @var AssignStatement $stmt */
$stmt = $this->createProgram($input)->statements[0];

$this->testVariable($stmt->variable, 'herName');
$this->testString($stmt->value, 'Anna');
$this->assertSame("{{ \$herName = 'Anna' }}", $stmt->string());
}
}
45 changes: 45 additions & 0 deletions tests/EvaluatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -357,4 +357,49 @@ public static function providerForTestEvalInfixExpressions(): array
["{{ 'She\'s ' . 25 }}", "She's 25"],
];
}

#[DataProvider('providerForTestAssignStatement')]
public function testAssignStatement(string $input, string $output): void
{
$evaluated = $this->testEval($input);

if ($evaluated instanceof ErrorObj) {
$this->fail($evaluated->message);
}

$this->assertSame($output, $evaluated->value());
}

public static function providerForTestAssignStatement(): array
{
return [
['{{ $herName = "Anna" }}{{ $herName }}', 'Anna'],
['{{ $his_age = 33 }}<h1>{{ $his_age }}</h1>', '<h1>33</h1>'],
['{{ $lang = "PHP" }}{{ $lang="Go" }}{{ $lang }}', 'Go'], // test overriding
['{{ if true }}{{ $platform = "Mac" }}{{ $platform }}{{ end }}', 'Mac'],
['{{ $platform = "Linux" }}{{ if true }}{{ $platform }}{{ end }}', 'Linux'],
];
}

#[DataProvider('providerForTestVariableIsUndefinedOutOfScopes')]
public function testVariableIsUndefinedOutOfScopes(string $input, string $varName): void
{
$expect = EvalError::variableIsUndefined(
new VariableExpression(new Token(TokenType::VAR, $varName), $varName)
)->message;

$evaluated = $this->testEval($input);

$this->assertSame($expect, $evaluated->value());
}

public static function providerForTestVariableIsUndefinedOutOfScopes(): array
{
return [
['{{ if true }}{{ $name = "Anna" }}{{ end }}{{ $name }}', 'name'],
['{{ if false }}{{ else }}{{ $age = 33 }}{{ end }}{{ $age }}', 'age'],
['{{ loop 0, 1 }}{{ $index }}{{ end }}{{ $index }}', 'index'],
['{{ loop -1, 3 }}{{ $age = 33 }}{{ end }}{{ $age }}', 'age'],
];
}
}
Loading

0 comments on commit a2a1fc2

Please sign in to comment.