Skip to content

Commit

Permalink
pref: prepare some match pattern before parse
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Dec 30, 2022
1 parent 52d3f93 commit 9efc93f
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 45 deletions.
32 changes: 29 additions & 3 deletions src/Compiler/AbstractCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ abstract class AbstractCompiler implements CompilerInterface

public const PHP_TAG_CLOSE = '?>';

public const RAW_OUTPUT = 'raw';

public const VAR_PREFIX = '$';

public string $openTag = '{{';

public string $closeTag = '}}';

public const RAW_OUTPUT = 'raw';

/**
* @var string
*/
Expand Down Expand Up @@ -74,6 +76,8 @@ abstract class AbstractCompiler implements CompilerInterface
* custom directive, control statement token.
*
* -----
*
* ### Examples
* eg: implements: `include('parts/header.tpl')`
*
* ```php
Expand All @@ -86,7 +90,12 @@ abstract class AbstractCompiler implements CompilerInterface
*
* @var array{string, callable(string, string): string}
*/
public array $customDirectives = [];
protected array $customDirectives = [];

/**
* @var array
*/
protected array $directiveNames = [];

/**
* @return static
Expand Down Expand Up @@ -123,6 +132,8 @@ public function compileFile(string $tplFile): string
}

/**
* parse inline filters
*
* @param string $echoBody
*
* @return string
Expand All @@ -134,7 +145,12 @@ protected function parseInlineFilters(string $echoBody): string
}

$filters = Str::explode($echoBody, $this->filterSep);

// first is real echo body.
$newExpr = array_shift($filters);
if (CompileUtil::canAddVarPrefix($newExpr)) {
$newExpr = self::VAR_PREFIX . $newExpr;
}

foreach ($filters as $filter) {
if ($filter === self::RAW_OUTPUT) {
Expand Down Expand Up @@ -205,10 +221,20 @@ public function addFilter(string $name, string $callExpr): self
*/
public function addDirective(string $name, callable $handler): static
{
$this->directiveNames[] = $name;
$this->customDirectives[$name] = $handler;

return $this;
}

/**
* @return array
*/
public function getDirectiveNames(): array
{
return $this->directiveNames;
}

/**
* @return static
*/
Expand Down
68 changes: 68 additions & 0 deletions src/Compiler/CompileUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php declare(strict_types=1);

namespace PhpPkg\EasyTpl\Compiler;

/**
* class CompileUtil
*
* @author inhere
* @date 2022/12/30
*/
class CompileUtil
{
/**
* will match:
* - varName
* - top.subKey
*/
protected const REGEX_VAR_NAME = '@^[a-zA-Z_][\w.-]*$@';

/**
* Check is var output.
*
* @param string $line
*
* @return bool
*/
public static function canAddVarPrefix(string $line): bool
{
return $line[0] !== '$' && preg_match(self::REGEX_VAR_NAME, $line) === 1;
}

/**
* convert quick access array key path to php expression.
*
* - convert $ctx.top.sub to $ctx['top']['sub']
*
* @param string $line
*
* @return string
*/
public static function pathToArrayAccess(string $line): string
{
// - convert $ctx.top.sub to $ctx['top']['sub']
$pattern = '~(' . implode(')|(', [
'\$[\w.-]+\w', // array key path.
]) . ')~';

// https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
return preg_replace_callback($pattern, static function (array $matches) {
$varName = $matches[0];
// convert $ctx.top.sub to $ctx[top][sub]
if (str_contains($varName, '.')) {
$nodes = [];
foreach (explode('.', $varName) as $key) {
if ($key[0] === '$') {
$nodes[] = $key;
} else {
$nodes[] = is_numeric($key) ? "[$key]" : "['$key']";
}
}

$varName = implode('', $nodes);
}

return $varName;
}, $line);
}
}
81 changes: 41 additions & 40 deletions src/Compiler/PregCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@

namespace PhpPkg\EasyTpl\Compiler;

use Toolkit\Stdlib\Str;
use function addslashes;
use function array_keys;
use function explode;
use function implode;
use function is_numeric;
use function preg_match;
use function preg_replace;
use function preg_replace_callback;
Expand All @@ -35,6 +31,10 @@ class PregCompiler extends AbstractCompiler

private string $closeTagE = '\}\}';

private string $blockPattern = '';

private string $directivePattern = '';

/**
* @param string $open
* @param string $close
Expand Down Expand Up @@ -71,6 +71,8 @@ public function compile(string $tplCode): string
$tplCode = preg_replace("~$openTagE#.+?#$closeTagE~s", '', $tplCode);
}

$this->buildMatchPatterns();

$flags = 0;
// $flags = PREG_OFFSET_CAPTURE;
// $flags = PREG_PATTERN_ORDER | PREG_SET_ORDER;
Expand All @@ -89,14 +91,38 @@ function (array $matches) {
);
}

/**
* build match patterns
*
* @return void
*/
protected function buildMatchPatterns(): void
{
$this->blockPattern = Token::getBlockNamePattern();

$this->directivePattern = Token::buildDirectivePattern($this->directiveNames);
}

/**
* parse code block string.
*
* ### code blocks
*
* control stmt block
*
* - '=': echo
* - 'if'
* - 'for'
* - 'foreach'
* - 'switch'
* - ... more php keywords
*
* ### directives
*
* custom add added directives
*
* - 'include'
* - 'block'
*
* @param string $block
*
Expand All @@ -114,11 +140,8 @@ public function parseCodeBlock(string $block): string
return self::PHP_TAG_OPEN . ' } ' . self::PHP_TAG_CLOSE;
}

$isInline = !str_contains($trimmed, "\n");
$kwPattern = Token::getBlockNamePattern();

$directive = '';
$userPattern = Token::buildDirectivePattern(array_keys($this->customDirectives));
$isInline = !str_contains($trimmed, "\n");

// default is define statement.
$type = Token::T_DEFINE;
Expand Down Expand Up @@ -147,7 +170,7 @@ public function parseCodeBlock(string $block): string
if ($type === Token::T_ELSE) {
$close = ': ' . self::PHP_TAG_CLOSE;
}
} elseif (preg_match($kwPattern, $trimmed, $matches)) {
} elseif (preg_match($this->blockPattern, $trimmed, $matches)) {
// control block: if, for, foreach, define vars, etc
$type = $matches[1];
$open = self::PHP_TAG_OPEN . ' ';
Expand All @@ -161,7 +184,7 @@ public function parseCodeBlock(string $block): string
$close = ': ' . self::PHP_TAG_CLOSE;
}
}
} elseif ($userPattern && preg_match($userPattern, $trimmed, $matches)) {
} elseif ($this->directivePattern && preg_match($this->directivePattern, $trimmed, $matches)) {
// support user add special directives.
$directive = $type = $matches[1];
$handlerFn = $this->customDirectives[$directive];
Expand All @@ -180,43 +203,21 @@ public function parseCodeBlock(string $block): string

// inline echo support filters
if ($isInline && $type === Token::T_ECHO) {
// auto append var prefix: $
if ($trimmed[0] !== '$' && Str::isVarName($trimmed)) {
$trimmed = '$' . $trimmed;
}

$endChar = $endChar ?: $trimmed[strlen($trimmed) - 1];

// with filters
if ($endChar !== ';' && str_contains($trimmed, $this->filterSep)) {
$echoBody = substr($trimmed, strlen($eKws));
$trimmed = $eKws . $this->parseInlineFilters($echoBody);
} elseif (CompileUtil::canAddVarPrefix($trimmed)) {
// auto append var prefix: $
$trimmed = self::VAR_PREFIX . $trimmed;
}
}

// handle
// - convert $ctx.top.sub to $ctx[top][sub]
$pattern = '~(' . implode(')|(', [
'\$[\w.-]+\w', // array key path.
]) . ')~';

// https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php
$trimmed = preg_replace_callback($pattern, static function (array $matches) {
$varName = $matches[0];
// convert $ctx.top.sub to $ctx[top][sub]
if (str_contains($varName, '.')) {
$nodes = [];
foreach (explode('.', $varName) as $key) {
if ($key[0] === '$') {
$nodes[] = $key;
} else {
$nodes[] = is_numeric($key) ? "[$key]" : "['$key']";
}
}

$varName = implode('', $nodes);
}

return $varName;
}, $trimmed);
// handle quick access array key.
// - convert $ctx.top.sub to $ctx['top']['sub']
$trimmed = CompileUtil::pathToArrayAccess($trimmed);

return $open . $trimmed . $close;
}
Expand Down
31 changes: 31 additions & 0 deletions test/Compiler/CompileUtilTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace PhpPkg\EasyTplTest\Compiler;

use PhpPkg\EasyTpl\Compiler\CompileUtil;
use PhpPkg\EasyTplTest\BaseTestCase;

/**
* class CompileUtilTest
*
* @author inhere
* @date 2022/12/30
*/
class CompileUtilTest extends BaseTestCase
{
public function testCanAddVarPrefix(): void
{
$this->assertTrue(CompileUtil::canAddVarPrefix('abc'));
$this->assertTrue(CompileUtil::canAddVarPrefix('top.abc'));
$this->assertTrue(CompileUtil::canAddVarPrefix('top.sub-key'));

$this->assertFalse(CompileUtil::canAddVarPrefix("top['abc']"));
$this->assertFalse(CompileUtil::canAddVarPrefix('abc()'));
}

public function testPathToArrayAccess(): void
{
$this->assertEquals('ctx.top.sub', CompileUtil::pathToArrayAccess('ctx.top.sub'));
$this->assertEquals("\$ctx['top']['sub']", CompileUtil::pathToArrayAccess('$ctx.top.sub'));
}
}
15 changes: 13 additions & 2 deletions test/Compiler/PregCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,21 @@ public function testCompileCode_inline_echo():void
['{{ $name ?: "inhere" }}', '<?= $name ?: "inhere" ?>'],
['{{ $name ?? "inhere" }}', '<?= $name ?? "inhere" ?>'],
['{{ $name ?? "inhere"; }}', '<?= $name ?? "inhere"; ?>'],
// sub key
['{{ ctx.pkgName }}', '<?= $ctx[\'pkgName\'] ?>'],
['{{ $ctx.pkgName }}', '<?= $ctx[\'pkgName\'] ?>'],
['{{ $ctx.top1.pkgName }}', '<?= $ctx[\'top1\'][\'pkgName\'] ?>'],
['{{ $ctx.pkgName ?? "default" }}', '<?= $ctx[\'pkgName\'] ?? "default" ?>'],
['{{ ctx.pkg-name }}', '<?= $ctx[\'pkg-name\'] ?>'],
['{{ $ctx.pkg-name }}', '<?= $ctx[\'pkg-name\'] ?>'],
['{{ ctx.pkg_name }}', '<?= $ctx[\'pkg_name\'] ?>'],
['{{ $ctx.pkg_name }}', '<?= $ctx[\'pkg_name\'] ?>'],
// multi parts
['{{ ctx.top1.pkg-name }}', '<?= $ctx[\'top1\'][\'pkg-name\'] ?>'],
['{{ $ctx.top1.pkg-name }}', '<?= $ctx[\'top1\'][\'pkg-name\'] ?>'],
['{{ $ctx.top1.pkgName }}', '<?= $ctx[\'top1\'][\'pkgName\'] ?>'],
['{{ ctx.top-node.pkg-name }}', '<?= $ctx[\'top-node\'][\'pkg-name\'] ?>'],
['{{ $ctx.top-node.pkg-name }}', '<?= $ctx[\'top-node\'][\'pkg-name\'] ?>'],
['{{ $ctx.pkg_name }}', '<?= $ctx[\'pkg_name\'] ?>'],
// func
['{{ some_func() }}', '<?= some_func() ?>'],
['{{ some_func(); }}', '<?= some_func(); ?>'],
['{{ $this->include("header.tpl") }}', '<?= $this->include("header.tpl") ?>'],
Expand Down Expand Up @@ -126,7 +134,10 @@ public function testCompile_inline_echo_with_filters():void

$tests = [
['{{ "a" . "b" }}', '<?= "a" . "b" ?>'],
['{{ name | ucfirst }}', '<?= htmlspecialchars((string)ucfirst($name)) ?>'],
['{{ $name | ucfirst }}', '<?= htmlspecialchars((string)ucfirst($name)) ?>'],
['{{ user.name | ucfirst }}', '<?= htmlspecialchars((string)ucfirst($user[\'name\'])) ?>'],
['{{ user.first-name | ucfirst }}', '<?= htmlspecialchars((string)ucfirst($user[\'first-name\'])) ?>'],
[
'{{ $name ?: "inhere" | substr:0,3 }}',
'<?= htmlspecialchars((string)substr($name ?: "inhere", 0,3)) ?>'
Expand Down

0 comments on commit 9efc93f

Please sign in to comment.