From 66c7b349d1a97f4326697250b2743c92c938c06c Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Wed, 21 Aug 2019 16:11:39 -0400 Subject: [PATCH] - First commit of first draft --- composer.json | 18 ++++ src/Builder.php | 37 +++++++ src/Builder/ClassTemplate.php | 38 +++++++ src/Builder/SourceFileBuilder.php | 23 +++++ src/Compiler.php | 84 +++++++++++++++ src/Compiler/BaseContext.php | 5 + src/Compiler/Context.php | 44 ++++++++ src/ControlStructure/BreakToken.php | 13 +++ src/ControlStructure/CaseToken.php | 21 ++++ src/ControlStructure/ControlStructure.php | 7 ++ src/ControlStructure/DefaultToken.php | 21 ++++ src/ControlStructure/ElseIfToken.php | 13 +++ src/ControlStructure/ElseToken.php | 13 +++ src/ControlStructure/EndCaseToken.php | 7 ++ src/ControlStructure/EndIfToken.php | 13 +++ src/ControlStructure/EndSectionToken.php | 18 ++++ src/ControlStructure/EndSwitchToken.php | 12 +++ src/ControlStructure/EndforToken.php | 20 ++++ src/ControlStructure/EndforeachToken.php | 20 ++++ src/ControlStructure/ExtendsToken.php | 17 +++ src/ControlStructure/ForToken.php | 20 ++++ src/ControlStructure/ForeachToken.php | 20 ++++ src/ControlStructure/IfToken.php | 13 +++ src/ControlStructure/NamespaceToken.php | 13 +++ src/ControlStructure/OrToken.php | 19 ++++ src/ControlStructure/SectionToken.php | 37 +++++++ src/ControlStructure/SwitchToken.php | 13 +++ src/ControlStructure/UseToken.php | 13 +++ src/Extension/Extension.php | 7 ++ src/Extension/JsonExtension.php | 14 +++ src/Picea.php | 120 ++++++++++++++++++++++ src/Syntax/CommentToken.php | 16 +++ src/Syntax/EchoRawToken.php | 18 ++++ src/Syntax/EchoSafeToken.php | 28 +++++ src/Syntax/PhpTagToken.php | 18 ++++ src/Syntax/Syntax.php | 7 ++ 36 files changed, 820 insertions(+) create mode 100644 composer.json create mode 100644 src/Builder.php create mode 100644 src/Builder/ClassTemplate.php create mode 100644 src/Builder/SourceFileBuilder.php create mode 100644 src/Compiler.php create mode 100644 src/Compiler/BaseContext.php create mode 100644 src/Compiler/Context.php create mode 100644 src/ControlStructure/BreakToken.php create mode 100644 src/ControlStructure/CaseToken.php create mode 100644 src/ControlStructure/ControlStructure.php create mode 100644 src/ControlStructure/DefaultToken.php create mode 100644 src/ControlStructure/ElseIfToken.php create mode 100644 src/ControlStructure/ElseToken.php create mode 100644 src/ControlStructure/EndCaseToken.php create mode 100644 src/ControlStructure/EndIfToken.php create mode 100644 src/ControlStructure/EndSectionToken.php create mode 100644 src/ControlStructure/EndSwitchToken.php create mode 100644 src/ControlStructure/EndforToken.php create mode 100644 src/ControlStructure/EndforeachToken.php create mode 100644 src/ControlStructure/ExtendsToken.php create mode 100644 src/ControlStructure/ForToken.php create mode 100644 src/ControlStructure/ForeachToken.php create mode 100644 src/ControlStructure/IfToken.php create mode 100644 src/ControlStructure/NamespaceToken.php create mode 100644 src/ControlStructure/OrToken.php create mode 100644 src/ControlStructure/SectionToken.php create mode 100644 src/ControlStructure/SwitchToken.php create mode 100644 src/ControlStructure/UseToken.php create mode 100644 src/Extension/Extension.php create mode 100644 src/Extension/JsonExtension.php create mode 100644 src/Picea.php create mode 100644 src/Syntax/CommentToken.php create mode 100644 src/Syntax/EchoRawToken.php create mode 100644 src/Syntax/EchoSafeToken.php create mode 100644 src/Syntax/PhpTagToken.php create mode 100644 src/Syntax/Syntax.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d387c1b --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "mcnd/picea", + "description": "A templating engine based on Twig's syntax and Plates's native uses of PHP.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Dave Mc Nicoll", + "email": "mcndave@gmail.com" + } + ], + "require": {}, + "autoload": { + "psr-4": { + "Picea\\": "src/" + } + } +} diff --git a/src/Builder.php b/src/Builder.php new file mode 100644 index 0000000..854f061 --- /dev/null +++ b/src/Builder.php @@ -0,0 +1,37 @@ +templatePath = $templatePath; + } + + public function build(Compiler\Context &$context, string $compiledSource) : array + { + $className = "PiceaTemplate_" . $this->generateClassUID($compiledSource); + + $replace = [ + '%NAMESPACE%' => $context->namespace, + '%USE%' => $context->renderUses(), + '%CLASSNAME%' => $className, + '%EXTENDS%' => '', + '%CONTENT%' => $compiledSource, + ]; + + $classContent = str_replace(array_keys($replace), array_values($replace), file_get_contents($this->templatePath)); + + return [ + $context->namespace, $className, $classContent, + ]; + } + + public function generateClassUID(string $filePath) : string + { + return md5($filePath); + } +} diff --git a/src/Builder/ClassTemplate.php b/src/Builder/ClassTemplate.php new file mode 100644 index 0000000..1a866ec --- /dev/null +++ b/src/Builder/ClassTemplate.php @@ -0,0 +1,38 @@ +__variables_list); ?>%CONTENT%__section_list[$name][$item], fn($a, $b) => $a['order'] <=> $b['order']); + + foreach($this->__section_list[$name][$item] as $section) { + $section['callback'](); + + if ( $item === 'default' ) { + break 1; + } + } + } + } + + public function __renderTemplate() { + ob_start(); + $this->output(); + return ob_get_clean(); + } +} diff --git a/src/Builder/SourceFileBuilder.php b/src/Builder/SourceFileBuilder.php new file mode 100644 index 0000000..1c4984f --- /dev/null +++ b/src/Builder/SourceFileBuilder.php @@ -0,0 +1,23 @@ +fileName = $name; + $this->filePath = $path; + return $this; + } +} diff --git a/src/Compiler.php b/src/Compiler.php new file mode 100644 index 0000000..7cf3b03 --- /dev/null +++ b/src/Compiler.php @@ -0,0 +1,84 @@ +compiler = $this; + + /** + * Syntaxes such as {{ echo }} & {{= echo raw }} && {# comment #} + */ + foreach($this->syntaxObjectList as $syntax) { + $syntax->parse($context, $this->sourceCode); + } + + /** + * Control structures & functions {% functionName arguments %} + */ + $replace = "#({$this->tagTokenOpen})(.*?)({$this->tagTokenClose})#s"; + + $this->sourceCode = preg_replace_callback($replace, function ($matches) use (&$context) { + $matches[2] = trim($matches[2]); + + list($token, $arguments) = array_pad(array_filter(explode(' ', $matches[2], 2), 'strlen'), 2, null); + + # @TODO Refractor this parts to allows registration to the tag's name + if ( $this->tagList[$token] ?? false ) { + return $this->tagList[$token]->parse($context, $arguments); + } + elseif ( $this->extensionList[$token] ?? false ) { + return $this->extensionList[$token]->parse($context, $arguments); + } + else { + throw new \LogicException("Impossible to find token `$token` declared in `{$matches[2]}`. Perhapse you forgot to add a custom token to Picea's engine ?"); + } + + }, $this->sourceCode); + + return $this->sourceCode; + } + + public function loadSourceCode($html) : self + { + $this->sourceCode = $html; + return $this; + } + + public function registerExtension(Extension\Extension $extension) : self + { + $this->extensionList[$extension->token] = $extension; + return $this; + } + + public function registerCaching(CachingStrategy $cachingStrategy) : self + { + return $this; + } + + public function registerSyntax(Syntax\Syntax $syntaxObject) : self + { + $this->syntaxObjectList[] = $syntaxObject; + return $this; + } + + public function registerControlStructure(ControlStructure\ControlStructure $controlStructureObject) : self + { + $this->tagList[$controlStructureObject->token] = $controlStructureObject; + return $this; + } +} diff --git a/src/Compiler/BaseContext.php b/src/Compiler/BaseContext.php new file mode 100644 index 0000000..deea870 --- /dev/null +++ b/src/Compiler/BaseContext.php @@ -0,0 +1,5 @@ +hooks[$path] ??= []; + $this->hooks[$path][] = $callback; + + return $this; + } + + public function run(string $path, ...$arguments) : self + { + if ( $this->hooks[$path] ?? false ) { + while ( $callback = array_pop($this->hooks[$path]) ) { + $callback(...$arguments); + } + } + } + + public function renderUses() : string + { + return implode(",", $context->useStack ?? []); + } +} diff --git a/src/ControlStructure/BreakToken.php b/src/ControlStructure/BreakToken.php new file mode 100644 index 0000000..adcd8d7 --- /dev/null +++ b/src/ControlStructure/BreakToken.php @@ -0,0 +1,13 @@ +"; + } + +} diff --git a/src/ControlStructure/CaseToken.php b/src/ControlStructure/CaseToken.php new file mode 100644 index 0000000..0dffdbc --- /dev/null +++ b/src/ControlStructure/CaseToken.php @@ -0,0 +1,21 @@ +switchStack ) { + array_pop($context->switchStack); + } + else { + $output = ""; + } +} diff --git a/src/ControlStructure/ControlStructure.php b/src/ControlStructure/ControlStructure.php new file mode 100644 index 0000000..cd5a25d --- /dev/null +++ b/src/ControlStructure/ControlStructure.php @@ -0,0 +1,7 @@ +switchStack ) { + array_pop($context->switchStack); + } + else { + $output = ""; + } +} diff --git a/src/ControlStructure/ElseIfToken.php b/src/ControlStructure/ElseIfToken.php new file mode 100644 index 0000000..8c2f3d2 --- /dev/null +++ b/src/ControlStructure/ElseIfToken.php @@ -0,0 +1,13 @@ +"; + } + +} diff --git a/src/ControlStructure/ElseToken.php b/src/ControlStructure/ElseToken.php new file mode 100644 index 0000000..1789dab --- /dev/null +++ b/src/ControlStructure/ElseToken.php @@ -0,0 +1,13 @@ +"; + } + +} diff --git a/src/ControlStructure/EndCaseToken.php b/src/ControlStructure/EndCaseToken.php new file mode 100644 index 0000000..4a6a039 --- /dev/null +++ b/src/ControlStructure/EndCaseToken.php @@ -0,0 +1,7 @@ +"; + } + +} diff --git a/src/ControlStructure/EndSectionToken.php b/src/ControlStructure/EndSectionToken.php new file mode 100644 index 0000000..4ed4a21 --- /dev/null +++ b/src/ControlStructure/EndSectionToken.php @@ -0,0 +1,18 @@ +sections) ) { + throw new \RuntimeException("A section closing tag {% endsection %} was found without an opening {% section %} tag"); + } + + $section = array_pop($context->sections); + $build = $context->extendFrom ? "" : "\$this->__renderSection({$section['name']});"; + return ""; + } +} diff --git a/src/ControlStructure/EndSwitchToken.php b/src/ControlStructure/EndSwitchToken.php new file mode 100644 index 0000000..d346293 --- /dev/null +++ b/src/ControlStructure/EndSwitchToken.php @@ -0,0 +1,12 @@ +"; + } +} diff --git a/src/ControlStructure/EndforToken.php b/src/ControlStructure/EndforToken.php new file mode 100644 index 0000000..07156a2 --- /dev/null +++ b/src/ControlStructure/EndforToken.php @@ -0,0 +1,20 @@ +iterationStack)['or'] === false ) { + $output = ""; + } + else { + $output = ""; + } + + array_pop($context->iterationStack); + return $output; + } +} diff --git a/src/ControlStructure/EndforeachToken.php b/src/ControlStructure/EndforeachToken.php new file mode 100644 index 0000000..a407c08 --- /dev/null +++ b/src/ControlStructure/EndforeachToken.php @@ -0,0 +1,20 @@ +iterationStack)['or'] === false ) { + $output = ""; + } + else { + $output = ""; + } + + array_pop($context->iterationStack); + return $output; + } +} diff --git a/src/ControlStructure/ExtendsToken.php b/src/ControlStructure/ExtendsToken.php new file mode 100644 index 0000000..648a91a --- /dev/null +++ b/src/ControlStructure/ExtendsToken.php @@ -0,0 +1,17 @@ +extendFrom = $path; + + return ""; + } +} diff --git a/src/ControlStructure/ForToken.php b/src/ControlStructure/ForToken.php new file mode 100644 index 0000000..972d5be --- /dev/null +++ b/src/ControlStructure/ForToken.php @@ -0,0 +1,20 @@ +iterationStack[] = [ + 'or' => false, + 'uid' => $uid, + 'token' => 'endfor', + ]; + + return ""; + } +} diff --git a/src/ControlStructure/ForeachToken.php b/src/ControlStructure/ForeachToken.php new file mode 100644 index 0000000..1420086 --- /dev/null +++ b/src/ControlStructure/ForeachToken.php @@ -0,0 +1,20 @@ +iterationStack[] = [ + 'or' => false, + 'uid' => $uid, + 'token' => 'endforeach', + ]; + + return ""; + } +} diff --git a/src/ControlStructure/IfToken.php b/src/ControlStructure/IfToken.php new file mode 100644 index 0000000..db22919 --- /dev/null +++ b/src/ControlStructure/IfToken.php @@ -0,0 +1,13 @@ +"; + } + +} diff --git a/src/ControlStructure/NamespaceToken.php b/src/ControlStructure/NamespaceToken.php new file mode 100644 index 0000000..157c757 --- /dev/null +++ b/src/ControlStructure/NamespaceToken.php @@ -0,0 +1,13 @@ +namespace = $arguments; + return ""; + } +} diff --git a/src/ControlStructure/OrToken.php b/src/ControlStructure/OrToken.php new file mode 100644 index 0000000..955e8f3 --- /dev/null +++ b/src/ControlStructure/OrToken.php @@ -0,0 +1,19 @@ +iterationStack) ) { + throw new \LogicException("Token `or` was used outside of iterator. Make sure your `for` or `foreach` declaration are properly made."); + } + + $key = count( $context->iterationStack ) - 1; + $context->iterationStack[$key]['or'] = true; + return "iterationStack[$key]['token']}; if(false === {$context->iterationStack[$key]['uid']} ?? false): ?>"; + } + +} diff --git a/src/ControlStructure/SectionToken.php b/src/ControlStructure/SectionToken.php new file mode 100644 index 0000000..56c600d --- /dev/null +++ b/src/ControlStructure/SectionToken.php @@ -0,0 +1,37 @@ +sections[] = [ + 'name' => $name, + 'options' => $options, + ]; + + $action = $options['action'] ?? "default"; + + if (! in_array($action, ['prepend', 'append', 'default'])) { + throw new \RuntimeException("An unsupported action `$action` was given as an option of a {% section %} tag"); + } + + $order = $options['order'] ?? "count(\$this->__section_list[$name]['$action'])"; + + return "__section_list[$name] ??= [ 'prepend' => [], 'append' => [], 'default' => [] ]; + \$this->__section_list[$name]['$action'][] = [ 'order' => $order, 'callback' => function() { + extract(\$this->__variables_list); ?>"; + } +} diff --git a/src/ControlStructure/SwitchToken.php b/src/ControlStructure/SwitchToken.php new file mode 100644 index 0000000..02f79bd --- /dev/null +++ b/src/ControlStructure/SwitchToken.php @@ -0,0 +1,13 @@ +switchStack[] = true; + return "useStack[] = $arguments; + return ""; + } +} diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php new file mode 100644 index 0000000..773602f --- /dev/null +++ b/src/Extension/Extension.php @@ -0,0 +1,7 @@ +flags}) ?>"; + } + +} diff --git a/src/Picea.php b/src/Picea.php new file mode 100644 index 0000000..aea30c1 --- /dev/null +++ b/src/Picea.php @@ -0,0 +1,120 @@ +context = $context ?? new Compiler\BaseContext(); + } + + public function compileSource(string $source) : array + { + return [ + 'context' => $context = clone $this->context, + 'source' => $this->instanciateCompiler($source)->compile($context) + ]; + } + + public function compileFile(string $filePath) : array + { + if (! file_exists($filePath) ) { + throw new \InvalidArgumentException("File `$filePath` cannot be found or it is a permission problem"); + } + + if ( false === $fileContent = file_get_contents($filePath) ) { + throw new \ErrorException("Given file could not be opened `$filePath`. This could indicate a permission misconfiguration on your file or folder"); + } + + return [ + 'context' => $context = clone $this->context, + 'source' => $this->instanciateCompiler($fileContent)->compile($context) + ]; + } + + public function buildFromSource(string $source) : array + { + $tmpFolder = sys_get_temp_dir(); + $builder = $this->instanciateBuilder(); + $compiledSource = $this->compileSource($source); + list($namespace, $className, $compiledSource) = $builder->build($compiledSource['context'], $compiledSource['source']) ; + + $path = "$tmpFolder/$className.php"; + file_put_contents($path, $compiledSource); + + return [ + 'path' => $path, + 'namespace' => $namespace, + 'className' => $className, + ]; + } + + public function buildFromPath(string $path) : string + { + + } + + public function instanciateCompiler(string $sourceCode) : Compiler + { + $compiler = new Compiler(); + $compiler->loadSourceCode($sourceCode); + + $this->registerSyntax($compiler) + ->registerControlStructure($compiler) + ->registerExtension($compiler); + + return $compiler; + } + + public function instanciateBuilder() : Builder + { + $builder = new Builder(dirname(__FILE__) . "/Builder/ClassTemplate.php"); + return $builder; + } + + protected function registerSyntax(Compiler $compiler) : self + { + $compiler->registerSyntax( new \Picea\Syntax\PhpTagToken() ); + $compiler->registerSyntax( new \Picea\Syntax\CommentToken() ); + $compiler->registerSyntax( new \Picea\Syntax\EchoRawToken() ); + $compiler->registerSyntax( new \Picea\Syntax\EchoSafeToken() ); + + return $this; + } + + protected function registerControlStructure(Compiler $compiler) : self + { + $compiler->registerControlStructure(new \Picea\ControlStructure\NamespaceToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\UseToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\IfToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\ElseToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\ElseIfToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\EndIfToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\ForeachToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\ForToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\OrToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\EndforeachToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\EndforToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\SwitchToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\CaseToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\DefaultToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\BreakToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\EndCaseToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\EndSwitchToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\ExtendsToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\SectionToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\EndSectionToken()); + + return $this; + } + + protected function registerExtension(Compiler $compiler) : self + { + $compiler->registerExtension(new \Picea\Extension\JsonExtension()); + + return $this; + } +} diff --git a/src/Syntax/CommentToken.php b/src/Syntax/CommentToken.php new file mode 100644 index 0000000..07bb3eb --- /dev/null +++ b/src/Syntax/CommentToken.php @@ -0,0 +1,16 @@ +tokenOpen})(.*?)({$this->tokenClose})#s", "", $sourceCode); + } + +} diff --git a/src/Syntax/EchoRawToken.php b/src/Syntax/EchoRawToken.php new file mode 100644 index 0000000..68c4b41 --- /dev/null +++ b/src/Syntax/EchoRawToken.php @@ -0,0 +1,18 @@ +tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) { + return ""; + }, $sourceCode); + } + +} diff --git a/src/Syntax/EchoSafeToken.php b/src/Syntax/EchoSafeToken.php new file mode 100644 index 0000000..13c8936 --- /dev/null +++ b/src/Syntax/EchoSafeToken.php @@ -0,0 +1,28 @@ +encoding = ini_get("default_charset"); + } + + public function parse(/*\Picae\Compiler\Context*/ &$context, string &$sourceCode) + { + $sourceCode = preg_replace_callback("#({$this->tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) { + return "flag}, '{$this->encoding}', " . ($this->doubleEncode ? "true" : "false") . ") ?>"; + }, $sourceCode); + } + +} diff --git a/src/Syntax/PhpTagToken.php b/src/Syntax/PhpTagToken.php new file mode 100644 index 0000000..3dc0c6a --- /dev/null +++ b/src/Syntax/PhpTagToken.php @@ -0,0 +1,18 @@ +tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) { + return ""; + }, $sourceCode); + } + +} diff --git a/src/Syntax/Syntax.php b/src/Syntax/Syntax.php new file mode 100644 index 0000000..fe1992f --- /dev/null +++ b/src/Syntax/Syntax.php @@ -0,0 +1,7 @@ +