diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 71353b0..22b47d4 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: true matrix: - php: [8.1, 8.2] + php: [8.1, 8.2, 8.3] os: [ubuntu-latest, macOS-latest] # windows-latest, # include: # will not testing on php 7.2 # - os: 'ubuntu-latest' diff --git a/phpunit.xml b/phpunit.xml index 53e86d0..ee130c0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,15 +1,14 @@ - - - - app - - - - - test/ - - + + + + + test/ + + + + + app + + diff --git a/src/Compiler/CompileUtil.php b/src/Compiler/CompileUtil.php index 8ff4362..c5df41a 100644 --- a/src/Compiler/CompileUtil.php +++ b/src/Compiler/CompileUtil.php @@ -3,6 +3,7 @@ namespace PhpPkg\EasyTpl\Compiler; use function defined; +use function preg_match; use function str_contains; use function str_starts_with; @@ -53,42 +54,64 @@ public static function canAddVarPrefix(string $line): bool */ private static ?string $matchKeyPath = null; + private const ONLY_PATH_PATTERN = '/^\$?[a-zA-Z_][\w.-]+\w$/'; + /** * convert access array key path to php array access expression. * - * - convert $ctx.top.sub to $ctx['top']['sub'] + * - convert `$ctx.top.sub` to `$ctx['top']['sub']` + * - convert `ctx.top.sub` to `$ctx['top']['sub']` * - * @param string $line var line. must start with $ + * @param string $line var line. NOT: line must start with $ * * @return string */ public static function toArrayAccessStmt(string $line): string { - if (self::$matchKeyPath === null) { - // - convert $ctx.top.sub to $ctx['top']['sub'] + $hasSpace = str_contains($line, ' '); + + // only key path. + if (!$hasSpace && preg_match(self::ONLY_PATH_PATTERN, $line) === 1) { + return self::handleMatch($line); + } + + // with space and key path at first node. like: ctx.top.sub ?? "fallback" + if ($hasSpace) { + [$first, $last] = explode(' ', $line, 2); + if (preg_match(self::ONLY_PATH_PATTERN, $first) === 1 && !str_contains($last, '.')) { + return self::handleMatch($first) . " $last"; + } + } + + // with fallback statement. like: $ctx.top.sub ?? "fallback" + if (!self::$matchKeyPath) { self::$matchKeyPath = '~(' . implode(')|(', [ '\$[\w.-]+\w', // array key path. ]) . ')~'; } // https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php - return preg_replace_callback(self::$matchKeyPath, static function (array $matches) { + return preg_replace_callback(self::$matchKeyPath, static function (array $matches) use ($line) { $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 self::handleMatch($varName); } - return $varName; }, $line); } + + private static function handleMatch(string $varName): string + { + $nodes = []; + foreach (explode('.', $varName) as $i => $key) { + if ($i === 0) { + $nodes[] = $key[0] === '$' ? $key : '$' . $key; + } else { + $nodes[] = is_numeric($key) ? "[$key]" : "['$key']"; + } + } + + return implode('', $nodes); + } } diff --git a/src/Compiler/PregCompiler.php b/src/Compiler/PregCompiler.php index 3eec883..999234e 100644 --- a/src/Compiler/PregCompiler.php +++ b/src/Compiler/PregCompiler.php @@ -229,8 +229,9 @@ public function parseCodeBlock(string $trimmed): string // handle quick access array key. // - convert $ctx.top.sub to $ctx['top']['sub'] - $trimmed = CompileUtil::toArrayAccessStmt($trimmed); - + if (str_contains($trimmed, '.')) { + $trimmed = CompileUtil::toArrayAccessStmt($trimmed); + } return $open . $trimmed . $close; } diff --git a/src/EasyTemplate.php b/src/EasyTemplate.php index 6355a6b..418c384 100644 --- a/src/EasyTemplate.php +++ b/src/EasyTemplate.php @@ -144,7 +144,10 @@ public static function newRaw(array $config = []): self /** * Class constructor. * - * @param array{tmpDir: string, compiler: class-string|CompilerInterface} $config + * @param array{ + * tmpDir: string, + * tplDir: string, + * compiler: class-string|CompilerInterface } $config */ public function __construct(array $config = []) { diff --git a/src/SimpleTemplate.php b/src/SimpleTemplate.php index ccc5dfd..a829cf0 100644 --- a/src/SimpleTemplate.php +++ b/src/SimpleTemplate.php @@ -113,20 +113,11 @@ public function renderString(string $tplCode, array $tplVars = []): string */ protected function renderVars(string $tplCode, array $vars): string { - $fmtVars = Arr::flattenMap($vars, Arr\ArrConst::FLAT_DOT_JOIN_INDEX); - - // convert array value to string. - foreach ($vars as $name => $val) { - if (is_array($val)) { - $fmtVars[$name] = Arr::toStringV2($val); - } - } - if (!$this->pattern) { $this->parseFormat($this->format); } - return preg_replace_callback($this->pattern, function (array $match) use ($fmtVars) { + return preg_replace_callback($this->pattern, function (array $match) use ($vars) { $var = trim($match[1]); if (!$var) { return $match[0]; @@ -137,9 +128,10 @@ protected function renderVars(string $tplCode, array $vars): string [$var, $filters] = Str::explode($var, '|', 2); } - if (isset($fmtVars[$var])) { - $val = $fmtVars[$var]; - return $filters ? $this->pipeFilter->applyStringRules($val, $filters) : $val; + $value = Arr::getByPath($vars, $var); + if ($value !== null) { + $value = is_array($value) ? Arr::toStringV2($value) : (string)$value; + return $filters ? $this->pipeFilter->applyStringRules($value, $filters) : $value; } return $match[0]; diff --git a/src/TextTemplate.php b/src/TextTemplate.php index 68a702a..f788a5c 100644 --- a/src/TextTemplate.php +++ b/src/TextTemplate.php @@ -9,29 +9,21 @@ namespace PhpPkg\EasyTpl; -use PhpPkg\EasyTpl\Contract\CompilerInterface; - /** * class TextTemplate + * - will disable echo filter for default. * * @author inhere - * @deprecated please use {@see EasyTemplate::textTemplate() } */ -class TextTemplate extends EasyTemplate +class TextTemplate { /** - * @var string[] + * @param array $config + * + * @return EasyTemplate */ - protected array $allowExt = ['.php', '.tpl']; - - /** - * @param CompilerInterface $compiler - */ - protected function initCompiler(CompilerInterface $compiler): void + public static function new(array $config = []): EasyTemplate { - parent::initCompiler($compiler); - - // use raw echo for text template - $compiler->disableEchoFilter(); + return EasyTemplate::newTexted($config); } } diff --git a/test/Compiler/CompileUtilTest.php b/test/Compiler/CompileUtilTest.php index 10da2c5..6a9c0ef 100644 --- a/test/Compiler/CompileUtilTest.php +++ b/test/Compiler/CompileUtilTest.php @@ -27,7 +27,28 @@ public function testCanAddVarPrefix(): void public function testPathToArrayAccess(): void { - $this->assertEquals('ctx.top.sub', CompileUtil::toArrayAccessStmt('ctx.top.sub')); + $this->assertEquals('$varName', CompileUtil::toArrayAccessStmt('varName')); + $this->assertEquals("= varName", CompileUtil::toArrayAccessStmt('= varName')); + + $this->assertEquals("\$ctx['top']['sub']", CompileUtil::toArrayAccessStmt('ctx.top.sub')); $this->assertEquals("\$ctx['top']['sub']", CompileUtil::toArrayAccessStmt('$ctx.top.sub')); + + // with fallback statement + $ret = <<assertEquals($ret, CompileUtil::toArrayAccessStmt('ctx.top.sub ?? "fallback"')); + $this->assertEquals($ret, CompileUtil::toArrayAccessStmt('$ctx.top.sub ?? "fallback"')); + + $ret = <<assertEquals($ret, CompileUtil::toArrayAccessStmt('= $ctx.top.sub ?? "fallback"')); + + $ret = <<assertEquals($ret, CompileUtil::toArrayAccessStmt('= $ctx.top.sub ?? "fall.back"')); } } diff --git a/test/EasyTemplateTest.php b/test/EasyTemplateTest.php index 99d6474..e458c20 100644 --- a/test/EasyTemplateTest.php +++ b/test/EasyTemplateTest.php @@ -93,7 +93,7 @@ public function testRenderFile_use_all_token(): void $t = $this->newTemplate(); $tplFile = $this->getTestTplFile('testdata/use_all_token.tpl'); - $result = $t->renderFile($tplFile, $this->tplVars); + $result = $t->renderFile($tplFile, $this->tplVars); $this->assertNotEmpty($result); $this->assertStringNotContainsString('{{', $result); @@ -102,16 +102,41 @@ public function testRenderFile_use_all_token(): void public function testRender_array_value_use_keyPath(): void { - $t = new EasyTemplate(); + $t = $this->newTemplate(); // inline $code = ' {{= $ctx.pkgName ?? "org.example.entity" }} '; - $tplVars = ['ctx' => ['pkgName' => 'MyPKG']]; - $result = $t->renderString($code, $tplVars); - // vdump($result); + $tplVars = ['str' => 'hello', 'ctx' => ['pkgName' => 'MyPKG']]; + $result = $t->renderString($code, $tplVars); $this->assertEquals("\nMyPKG\n", $result); + + $code = '{{ $ctx.pkgName }}'; + $result = $t->renderString($code, $tplVars); + $this->assertStringContainsString('MyPKG', $result); + + $tests = [ + ['tpl' => '{{= $str }}', 'result' => 'hello'], + ['tpl' => '{{ str }}', 'result' => 'hello'], + ['tpl' => '{{= $ctx.pkgName ?? "org.example.entity" }}', 'result' => 'MyPKG'], + ['tpl' => '{{ $ctx.pkgName ?? "org.example.entity" }}', 'result' => 'MyPKG'], + // ['tpl' => '{{ ctx.pkgName ?? "org.example.entity" }}', 'result' => 'MyPKG'], + ['tpl' => '{{ $ctx.pkgName }}', 'result' => 'MyPKG'], + ['tpl' => '{{ ctx.pkgName }}', 'result' => 'MyPKG'], + ['tpl' => '{{ $ctx.notExist ?? "fallback" }}', 'result' => 'fallback'], + ['tpl' => '{{ ctx.notExist ?? "fallback" }}', 'result' => 'fallback'], + ]; + foreach ($tests as $test) { + $result = $t->renderString($test['tpl'], $test['vars'] ?? $tplVars); + $this->assertStringContainsString($test['result'], $result); + } + + $e = $this->runAndGetException(function () use ($t, $tplVars) { + $tpl = '{{ ctx.pkgName ?? "org.example.entity" }}'; + $t->renderString($tpl, $tplVars); + }); + $this->assertStringContainsString('Undefined constant "ctx"', $e->getMessage()); } public function testPhpFuncAsFilter_compile_render(): void @@ -127,21 +152,21 @@ public function testAddFilters_compile_render(): void { $t = new EasyTemplate(); $t->addFilters([ - 'upper' => 'strtoupper', + 'upper' => 'strtoupper', 'myFilter' => function (string $str) { return substr($str, 3); }, ]); $code = '{{ $name | upper }}'; - $out = ''; + $out = ''; $this->assertEquals($out, $t->compileCode($code)); $this->assertEquals('INHERE', $t->renderString($code, [ 'name' => 'inhere', ])); $code = '{{ $name | myFilter }}'; - $out = <<<'CODE' + $out = <<<'CODE' applyFilter('myFilter', $name)) ?> CODE; $this->assertEquals($out, $t->compileCode($code)); @@ -150,7 +175,7 @@ public function testAddFilters_compile_render(): void ])); $code = '{{ $name | upper | myFilter }}'; - $out = <<<'CODE' + $out = <<<'CODE' applyFilter('myFilter', strtoupper($name))) ?> CODE; $this->assertEquals($out, $t->compileCode($code)); @@ -167,12 +192,12 @@ public function testAddFilter_setFilterSep(): void // bad $code = '{{ $name |upper }}'; - $out = ''; + $out = ''; $this->assertEquals($out, $t->compileCode($code)); // goods $code = '{{ $name | upper }}'; - $out = ''; + $out = ''; $this->assertEquals($out, $t->compileCode($code)); } @@ -186,7 +211,7 @@ public function testRender_define(): void }} {{ $name | upper }} CODE; - $result = $t->renderString($tplCode); + $result = $t->renderString($tplCode); $this->assertNotEmpty($result); $this->assertEquals('INHERE', $result); } @@ -255,13 +280,13 @@ public function testRender_include_file(): void public function testEasy_textTemplate(): void { - $t = $this->newTexted(); + $t = $this->newTexted(); $vs = [ 'name' => 'inhere', ]; $code = '{{ $name | upper }}'; - $out = ''; + $out = ''; $this->assertEquals($out, $t->compileCode($code)); $this->assertEquals('INHERE', $t->renderString($code, $vs));