From afdf5078e989a770de807c811b48eca6fae0cb30 Mon Sep 17 00:00:00 2001 From: Inhere Date: Sun, 7 Nov 2021 04:16:14 +0800 Subject: [PATCH] feature: complete some new easy template logic --- app/Lib/Template/EasyTemplate.php | 299 ++++++++++++++++++ app/Lib/Template/HtmlTemplate.php | 4 +- app/Lib/Template/README.md | 5 + app/Lib/Template/SimpleTemplate.php | 24 +- app/Lib/Template/TemplateInterface.php | 10 +- app/Lib/Template/TextTemplate.php | 96 ++++-- app/Lib/Template/easy-template.md | 42 +++ .../templates/gen-by-parse/gen-go-funcs2.tpl | 14 + test/testdata/full-demo.tpl.php | 45 +++ test/testdata/full-demo2.tpl | 0 test/testdata/use_echo_foreach.tpl | 8 + .../Lib/Template/EasyTemplateTest.php | 179 +++++++++++ .../Lib/Template/TextTemplateTest.php | 41 +++ 13 files changed, 724 insertions(+), 43 deletions(-) create mode 100644 app/Lib/Template/EasyTemplate.php create mode 100644 app/Lib/Template/README.md create mode 100644 app/Lib/Template/easy-template.md create mode 100644 resource/templates/gen-by-parse/gen-go-funcs2.tpl create mode 100644 test/testdata/full-demo.tpl.php create mode 100644 test/testdata/full-demo2.tpl create mode 100644 test/testdata/use_echo_foreach.tpl create mode 100644 test/unittest/Lib/Template/EasyTemplateTest.php create mode 100644 test/unittest/Lib/Template/TextTemplateTest.php diff --git a/app/Lib/Template/EasyTemplate.php b/app/Lib/Template/EasyTemplate.php new file mode 100644 index 0000000..e91ad26 --- /dev/null +++ b/app/Lib/Template/EasyTemplate.php @@ -0,0 +1,299 @@ +'; + + /** + * @var string[] + */ + protected array $allowExt = ['.php', '.tpl']; + + public string $openTag = '{{'; + public string $closeTag = '}}'; + + // add slashes tag name + private string $openTagE = '\{\{'; + private string $closeTagE = '\}\}'; + + /** + * @param string $open + * @param string $close + * + * @return $this + */ + public function setOpenCloseTag(string $open, string $close): self + { + $this->openTag = $open; + $this->openTagE = addslashes($open); + + $this->closeTag = $close; + $this->closeTagE = addslashes($close); + + return $this; + } + + /** + * @param string $tplCode + * @param array $tplVars + * + * @return string + */ + public function renderString(string $tplCode, array $tplVars): string + { + $tplCode = $this->compileCode($tplCode); + + return parent::renderString($tplCode, $tplVars); + } + + /** + * @param string $tplFile + * @param array $tplVars + * + * @return string + */ + public function renderFile(string $tplFile, array $tplVars): string + { + $phpFile = $this->compileFile($tplFile); + + return $this->doRenderFile($phpFile, $tplVars); + } + + /** + * @param string $tplFile + * + * @return string + */ + public function compileFile(string $tplFile): string + { + if (!file_exists($tplFile)) { + throw new InvalidArgumentException('no such template file:' . $tplFile); + } + + $tplCode = File::readAll($tplFile); + // compile contents + $tplCode = $this->compileCode($tplCode); + + // generate temp php file + return $this->genTempPhpFile($tplCode); + } + + /** + * compile contents + * + * @param string $code + * + * @return string + */ + public function compileCode(string $code): string + { + // Not contains open tag + if (!str_contains($code, $this->openTag)) { + return $code; + } + + $openTagE = $this->openTagE; + $closeTagE = $this->closeTagE; + // $pattern = "/$openTagE\s*(.+)$closeTagE/"; + + $limit = -1; + $flags = 0; + // $flags = PREG_OFFSET_CAPTURE; + // $flags = PREG_PATTERN_ORDER | PREG_SET_ORDER; + + // TIP: `.+` -> `.+?` + // `?` - 非贪婪匹配; 若不加,会导致有多个相同标签时,第一个开始会匹配到最后一个的关闭 + return preg_replace_callback( + "~$openTagE\s*(.+?)$closeTagE~s", // Amixu, iu, s + function (array $matches) { + // vdump($matches); + return $this->parseCodeBlock($matches[1]); + }, + $code, + $limit, + $count, + $flags + ); + } + + public const T_ECHO = 'echo'; + public const T_IF = 'if'; + public const T_FOR = 'for'; + public const T_FOREACH = 'foreach'; + public const T_SWITCH = 'switch'; + + public const BLOCK_TOKENS = [ + 'foreach', + 'endforeach', + 'for', + 'endfor', + 'if', + 'elseif', + 'else', + 'endif', + ]; + + /** + * parse code block string. + * + * - '=': echo + * - '-': trim + * - 'if' + * - 'for' + * - 'foreach' + * - 'switch' + * + * @param string $block + * + * @return string + */ + public function parseCodeBlock(string $block): string + { + if (!$trimmed = trim($block)) { + return $block; + } + + $type = $trimmed[0]; + $left = self::PHP_TAG_OPEN . ' '; + $right = self::PHP_TAG_CLOSE; + + $isInline = !str_contains($block, "\n"); + // ~^(if|elseif|else|endif|for|endfor|foreach|endforeach)~ + $kwPattern = '~^(' . implode('|', self::BLOCK_TOKENS) . ')~'; + + // echo + if ($type === '=' ) { + $type = self::T_ECHO; + $left = self::PHP_TAG_ECHO; + } elseif (preg_match($kwPattern, $trimmed, $matches)) { // other: if, for, foreach, define vars, etc + $type = $matches[1]; + } elseif ($isInline && !str_contains($block, '=')) { + // auto add echo + $type = self::T_ECHO; + $left = self::PHP_TAG_ECHO1; + } +vdump($type); + // else code is define block + + $pattern = '~(' . implode(')|(', [ + '\$[\w.]+\w', // array key path. + ]) . ')~'; + + // https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php + $block = 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; + }, $block); + + return $left . $block . $right; + } + + + /** + * inside the if/elseif/else/for/foreach + * + * @var bool + */ + private bool $insideIfFor = false; + + /** + * inside the php tag + * + * @var bool + */ + private bool $insideTag = false; + + /** + * compile contents + * + * @param string $code + * + * @return string + */ + public function compileCodeV2(string $code): string + { + // Not contains open tag + if (!str_contains($code, $this->openTag)) { + return $code; + } + + $compiled = []; + foreach (explode("\n", $code) as $line) { + // empty line + if (!$line || !trim($line)) { + $compiled[] = $line; + continue; + } + + if ( + !$this->insideTag + && (!str_contains($line, $this->openTag) || !str_contains($line, $this->closeTag)) + ) { + $compiled[] = $line; + continue; + } + + // parse line + $compiled[] = $this->analyzeLineChars($line); + } + + return implode("\n", $compiled); + } + + /** + * @param string $line + * + * @return string + */ + public function analyzeLineChars(string $line): string + { + $chars = preg_split('//u', $line, -1, PREG_SPLIT_NO_EMPTY); + + $prev = $next = 0; + foreach ($chars as $i => $char) { + + } + + return ''; + } + +} diff --git a/app/Lib/Template/HtmlTemplate.php b/app/Lib/Template/HtmlTemplate.php index 3ca5704..ae0e993 100644 --- a/app/Lib/Template/HtmlTemplate.php +++ b/app/Lib/Template/HtmlTemplate.php @@ -17,12 +17,12 @@ class HtmlTemplate extends TextTemplate /** * @var string */ - protected $viewsDir; + protected string $viewsDir = ''; /** * @var string[] */ - protected $allowExt = ['.html', '.phtml', '.php']; + protected array $allowExt = ['.html', '.phtml', '.php']; /** * manual set view files diff --git a/app/Lib/Template/README.md b/app/Lib/Template/README.md new file mode 100644 index 0000000..73c3bdf --- /dev/null +++ b/app/Lib/Template/README.md @@ -0,0 +1,5 @@ +# Template + + +## Text2Template + diff --git a/app/Lib/Template/SimpleTemplate.php b/app/Lib/Template/SimpleTemplate.php index 7a0543d..4197961 100644 --- a/app/Lib/Template/SimpleTemplate.php +++ b/app/Lib/Template/SimpleTemplate.php @@ -20,7 +20,7 @@ class SimpleTemplate extends AbstractTemplate /** * @var string */ - protected $varTpl = '{{%s}}'; + protected string $varTpl = '{{%s}}'; /** * Class constructor. @@ -33,36 +33,36 @@ public function __construct(array $config = []) } /** - * @param string $tempFile - * @param array $vars + * @param string $tplFile + * @param array $tplVars * * @return string */ - public function renderFile(string $tempFile, array $vars): string + public function renderFile(string $tplFile, array $tplVars): string { - if (!file_exists($tempFile)) { - throw new InvalidArgumentException('the template file is not exist. file:' . $tempFile); + if (!file_exists($tplFile)) { + throw new InvalidArgumentException('the template file is not exist. file:' . $tplFile); } - $tplCode = file_get_contents($tempFile); + $tplCode = file_get_contents($tplFile); - return $this->renderString($tplCode, $vars); + return $this->renderString($tplCode, $tplVars); } /** * @param string $tplCode - * @param array $vars + * @param array $tplVars * * @return string */ - public function renderString(string $tplCode, array $vars): string + public function renderString(string $tplCode, array $tplVars): string { if ($this->globalVars) { - $vars = array_merge($this->globalVars, $vars); + $tplVars = array_merge($this->globalVars, $tplVars); } $fmtVars = []; - foreach ($vars as $name => $var) { + foreach ($tplVars as $name => $var) { $name = sprintf($this->varTpl, (string)$name); // add $fmtVars[$name] = $var; diff --git a/app/Lib/Template/TemplateInterface.php b/app/Lib/Template/TemplateInterface.php index 2b4beab..018fa7e 100644 --- a/app/Lib/Template/TemplateInterface.php +++ b/app/Lib/Template/TemplateInterface.php @@ -10,18 +10,18 @@ interface TemplateInterface { /** - * @param string $tempFile - * @param array $vars + * @param string $tplFile + * @param array $tplVars * * @return string */ - public function renderFile(string $tempFile, array $vars): string; + public function renderFile(string $tplFile, array $tplVars): string; /** * @param string $tplCode - * @param array $vars + * @param array $tplVars * * @return string */ - public function renderString(string $tplCode, array $vars): string; + public function renderString(string $tplCode, array $tplVars): string; } diff --git a/app/Lib/Template/TextTemplate.php b/app/Lib/Template/TextTemplate.php index de2884f..0361fed 100644 --- a/app/Lib/Template/TextTemplate.php +++ b/app/Lib/Template/TextTemplate.php @@ -15,6 +15,8 @@ use function md5; use function ob_get_clean; use function ob_start; +use function sprintf; +use const EXTR_OVERWRITE; use const PHP_EOL; /** @@ -27,7 +29,21 @@ class TextTemplate extends AbstractTemplate /** * @var string[] */ - protected $allowExt = ['.php']; + protected array $allowExt = ['.php']; + + /** + * The dir for auto generated temp php file + * + * @var string + */ + public string $tmpDir = ''; + + /** + * The auto generated temp php file + * + * @var string + */ + private string $tmpPhpFile = ''; /** * Class constructor. @@ -40,67 +56,91 @@ public function __construct(array $config = []) } /** - * @param string $tempFile - * @param array $vars + * @param string $tplCode + * @param array $tplVars + * + * @return string + */ + public function renderString(string $tplCode, array $tplVars): string + { + $tempFile = $this->genTempPhpFile($tplCode); + + return $this->doRenderFile($tempFile, $tplVars); + } + + /** + * @param string $tplFile + * @param array $tplVars * * @return string */ - public function renderFile(string $tempFile, array $vars): string + public function renderFile(string $tplFile, array $tplVars): string { - if (!file_exists($tempFile)) { - throw new InvalidArgumentException('the template file is not exist. file:' . $tempFile); + return $this->doRenderFile($tplFile, $tplVars); + } + + /** + * @param string $tplFile + * @param array $tplVars + * + * @return string + */ + protected function doRenderFile(string $tplFile, array $tplVars): string + { + if (!file_exists($tplFile)) { + throw new InvalidArgumentException('no such template file:' . $tplFile); } if ($this->globalVars) { - $vars = array_merge($this->globalVars, $vars); + $tplVars = array_merge($this->globalVars, $tplVars); } ob_start(); - extract($vars, \EXTR_OVERWRITE); - // eval($tplCode . "\n"); + extract($tplVars, EXTR_OVERWRITE); // require \BASE_PATH . '/runtime/go-snippets-0709.tpl.php'; - /** @noinspection PhpIncludeInspection */ - require $tempFile; + require $tplFile; return ob_get_clean(); } /** + * generate temp php file + * * @param string $tplCode - * @param array $vars * * @return string */ - public function renderString(string $tplCode, array $vars): string + protected function genTempPhpFile(string $tplCode): string { - $tempDir = Sys::getTempDir() . '/kitegen'; - $fileHash = md5($tplCode); - $tempFile = $tempDir . '/' . date('ymd') . "-{$fileHash}.php"; + $tmpDir = $this->tmpDir ?: Sys::getTempDir() . '/php-tpl-gen'; + $tmpFile = sprintf('%s/%s-%s.php', $tmpDir, date('ymd'), md5($tplCode)); - if (!file_exists($tempFile)) { - Dir::create($tempDir); + if (!file_exists($tmpFile)) { + Dir::create($tmpDir); // write contents - $num = file_put_contents($tempFile, $tplCode . PHP_EOL); + $num = file_put_contents($tmpFile, $tplCode . PHP_EOL); if ($num < 1) { throw new RuntimeException('write template contents to temp file error'); } } - return $this->renderFile($tempFile, $vars); + $this->tmpPhpFile = $tmpFile; + return $tmpFile; } /** + * TIP: must be all php codes + * * @param string $tplCode * @param array $vars * * @return string */ - private function renderByEval(string $tplCode, array $vars): string + public function renderByEval(string $tplCode, array $vars): string { - \vdump($tplCode); ob_start(); - extract($vars, \EXTR_OVERWRITE); - // eval($tplCode . "\n"); + extract($vars, EXTR_OVERWRITE); + eval($tplCode . "\n"); // require \BASE_PATH . '/runtime/go-snippets-0709.tpl.php'; return ob_get_clean(); } @@ -121,4 +161,12 @@ public function setAllowExt(array $allowExt): void $this->allowExt = $allowExt; } + /** + * @return string + */ + public function getTmpPhpFile(): string + { + return $this->tmpPhpFile; + } + } diff --git a/app/Lib/Template/easy-template.md b/app/Lib/Template/easy-template.md new file mode 100644 index 0000000..996f974 --- /dev/null +++ b/app/Lib/Template/easy-template.md @@ -0,0 +1,42 @@ +# Text2Template + +## Using the filters + +You can use the filters in any of your blade templates. + +#### Regular usage: + +```php +{{ 'john' | ucfirst }} // John +``` + +#### Chained usage: + +```php +{{ 'john' | ucfirst | substr:0,1 }} // J +{{ '1999-12-31' | date:'Y/m/d' }} // 1999/12/31 +``` + +#### Passing non-static values: + +```php +{{ $name | ucfirst | substr:0,1 }} +{{ $user['name'] | ucfirst | substr:0,1 }} +{{ $currentUser->name | ucfirst | substr:0,1 }} +{{ getName() | ucfirst | substr:0,1 }} +``` + +#### Passing variables as filter parameters: + +```php +$currency = 'HUF' +{{ '12.75' | currency:$currency }} // HUF 12.75 +``` + +#### Built-in Laravel functionality: + +```php +{{ 'This is a title' | slug }} // this-is-a-title +{{ 'This is a title' | title }} // This Is A Title +{{ 'foo_bar' | studly }} // FooBar +``` \ No newline at end of file diff --git a/resource/templates/gen-by-parse/gen-go-funcs2.tpl b/resource/templates/gen-by-parse/gen-go-funcs2.tpl new file mode 100644 index 0000000..b9e8414 --- /dev/null +++ b/resource/templates/gen-by-parse/gen-go-funcs2.tpl @@ -0,0 +1,14 @@ +# +# usage: kite gen parse @resource-tpl/gen-by-parse/gen-go-funcs.tpl +# +vars=[Info, Error, Warn] + +### + +{{ foreach ($vars as $var): }} +// {{= $var }}f print message with {{= $var }} style +func {{= $var }}f(format string, a ...interface{}) { + {{= $var }}.Printf(format, a...) +} + +{{ endforeach; }} \ No newline at end of file diff --git a/test/testdata/full-demo.tpl.php b/test/testdata/full-demo.tpl.php new file mode 100644 index 0000000..f0fe24a --- /dev/null +++ b/test/testdata/full-demo.tpl.php @@ -0,0 +1,45 @@ + + /** + * getTitle(true) ?> + * + * @throws Throwable + * @api getUrlInfo()->getPath(true) ?> + */ + public function actiongetUrlInfo()->getShortName(true)?>(): void + { + $post = Yii::$app->request->post(); + +getArrayCopy() as $key => $val) : ?> + $ = Validate::validategetValType($val); echo ucfirst($typ === 'array' ? 'arrayValue' : $typ) +?>($post, ''); + + + + + + at if + 5) : ?> + at elseif + + at else + + + $logic = new getString('resName'))?>Logic(); + + $result = $logic->getShortName()?>($id, $params); + ResponseHelper::outputJson($result); + } diff --git a/test/testdata/full-demo2.tpl b/test/testdata/full-demo2.tpl new file mode 100644 index 0000000..e69de29 diff --git a/test/testdata/use_echo_foreach.tpl b/test/testdata/use_echo_foreach.tpl new file mode 100644 index 0000000..805cab8 --- /dev/null +++ b/test/testdata/use_echo_foreach.tpl @@ -0,0 +1,8 @@ + +{{ foreach ($vars as $var): }} +// {{= $var }}f print message with {{= $var }} style +func {{= $var }}f(format string, a ...interface{}) { + {{= $var }}.Printf(format, a...) +} + +{{ endforeach; }} diff --git a/test/unittest/Lib/Template/EasyTemplateTest.php b/test/unittest/Lib/Template/EasyTemplateTest.php new file mode 100644 index 0000000..ef41397 --- /dev/null +++ b/test/unittest/Lib/Template/EasyTemplateTest.php @@ -0,0 +1,179 @@ + ['Info', 'Error', 'Warn']]; + \vdump($tplFile); + + $result = $t->renderFile($tplFile, $tplVars); + + vdump($result); + } + + public function testCompileFile_use_echo_foreach():void + { + $t = new EasyTemplate(); + + $tplFile = Kite::resolve('@testdata/use_echo_foreach.tpl'); + $phpFile = $t->compileFile($tplFile); + + $this->assertNotEmpty($phpFile); + + $genCode = File::readAll($phpFile); + + $this->assertStringContainsString('assertStringContainsString('assertStringNotContainsString('{{', $genCode); + $this->assertStringNotContainsString('}}', $genCode); + // vdump($genCode); + } + + public function testCompileCode_check():void + { + $t2 = new EasyTemplate(); + + $compiled = $t2->compileCode(''); + $this->assertEquals('', $compiled); + + $compiled = $t2->compileCode('no tpl tags'); + $this->assertEquals('no tpl tags', $compiled); + } + + private $tplVars = [ + 'int' => 23, + 'str' => 'a string', + 'arr' => [ + 'hello', + 'world', + ], + 'map' => [ + 'key0' => 'map-val0', + 'key1' => 'map-val1', + ], + ]; + + public function testCompileCode_inline_echo():void + { + $t2 = new EasyTemplate(); + + $simpleTests = [ + ['{{ "a" . "b" }}', ''], + ['{{ $name }}', ''], + ['{{ $name ?: "inhere" }}', ''], + ['{{ $name ?? "inhere" }}', ''], + ]; + foreach ($simpleTests as [$in, $out]) { + $this->assertEquals($out, $t2->compileCode($in)); + } + + $tplCode = <<<'TPL' + +{{= $ctx.pkgName ?? "org.example.entity" }} + +TPL; + $compiled = $t2->compileCode($tplCode); + // vdump($tplCode, $compiled); + $this->assertNotEmpty($compiled); + $this->assertEquals(<<<'CODE' + + + +CODE +,$compiled); + + $tplCode = <<<'TPL' +{{= $ctx->pkgName ?? "org.example.entity" }} +TPL; + $compiled = $t2->compileCode($tplCode); + // vdump($tplCode, $compiled); + $this->assertNotEmpty($compiled); + $this->assertEquals(<<<'CODE' +pkgName ?? "org.example.entity" ?> +CODE +,$compiled); + + } + + public function testCompileCode_ml_block():void + { + $t2 = new EasyTemplate(); + + $code = <<<'CODE' +{{ + +$a = random_int(1, 10); +}} +CODE; + $compiled = $t2->compileCode($code); + $this->assertEquals(<<<'CODE' + +CODE + ,$compiled); + } + + public function testV2Render_vars():void + { + // inline + $code = ' +{{= $ctx.pkgName ?? "org.example.entity" }} +'; + + $tokens1 = token_get_all($code); + vdump($tokens1); + + $tokens2 = PhpToken::tokenize($code); + vdump($tokens2); + + $tplVars = ['vars' => ['Info', 'Error', 'Warn']]; + $t = new EasyTemplate(); + + $result = $t->renderString($code, $tplVars); + + vdump($result); + } + + public function testV2Render_ifElse():void + { + $t = new EasyTemplate(); + + $tplFile = Kite::resolve('@resource-tpl/gen-by-parse/gen-go-funcs2.tpl'); + $tplVars = ['vars' => ['Info', 'Error', 'Warn']]; + \vdump($tplFile); + + $result = $t->renderFile($tplFile, $tplVars); + + vdump($result); + } + + public function testV2Render_foreach():void + { + $t = new EasyTemplate(); + + $tplFile = Kite::resolve('@resource-tpl/gen-by-parse/gen-go-funcs2.tpl'); + $tplVars = ['vars' => ['Info', 'Error', 'Warn']]; + \vdump($tplFile); + + $result = $t->renderFile($tplFile, $tplVars); + + vdump($result); + } +} diff --git a/test/unittest/Lib/Template/TextTemplateTest.php b/test/unittest/Lib/Template/TextTemplateTest.php new file mode 100644 index 0000000..e839a1a --- /dev/null +++ b/test/unittest/Lib/Template/TextTemplateTest.php @@ -0,0 +1,41 @@ + ['Info', 'Error', 'Warn']]; + \vdump($tplCode); + + $result = $t->renderByEval($tplCode, $tplVars); + + vdump($result); + } + + public function testRenderByRequire():void + { + $t = new TextTemplate(); + + $tplFile = Kite::alias('@resource-tpl/gen-by-parse/gen-go-funcs.tpl'); + $tplVars = ['vars' => ['Info', 'Error', 'Warn']]; + \vdump($tplFile); + + $result = $t->renderFile($tplFile, $tplVars); + + vdump($result); + } +}