- First commit of first draft

This commit is contained in:
Dave M. 2019-08-21 16:11:39 -04:00
commit 66c7b349d1
36 changed files with 820 additions and 0 deletions

18
composer.json Normal file
View File

@ -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/"
}
}
}

37
src/Builder.php Normal file
View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Picea;
class Builder
{
protected string $templatePath = "";
public function __construct(string $templatePath = "./Builder/ClassTemplate.php")
{
$this->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);
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace %NAMESPACE%;
%USE%
class %CLASSNAME% %EXTENDS% {
protected array $__section_list = [];
protected array $__variables_list = [];
public function output() : void
{
extract($this->__variables_list); ?>%CONTENT%<?php
}
public function __renderSection($name) : void
{
foreach([ 'prepend', 'default', 'append' ] as $item) {
usort($this->__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();
}
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace Picea\Builder;
class SourceFileBuilder {
public array $section = [];
protected string $fileName = "";
protected string $filePath = "";
protected string $namespace = "";
protected string $code = "";
public function fileInfo(string $name, string $path) : self
{
$this->fileName = $name;
$this->filePath = $path;
return $this;
}
}

84
src/Compiler.php Normal file
View File

@ -0,0 +1,84 @@
<?php declare(strict_types=1);
namespace Picea;
class Compiler
{
protected string $sourceCode = "";
protected array $syntaxObjectList = [];
protected array $tagList = [];
protected string $tagTokenOpen = "\{%";
protected string $tagTokenClose = "%\}";
protected array $extensionList = [];
public function compile(Compiler\Context $context) : string
{
$context->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;
}
}

View File

@ -0,0 +1,5 @@
<?php declare(strict_types=1);
namespace Picea\Compiler;
class BaseContext extends Context { }

44
src/Compiler/Context.php Normal file
View File

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace Picea\Compiler;
use Picea\Compiler;
abstract class Context {
public string $namespace = "";
public string $extendFrom = "";
public array $switchStack = [];
public array $iterateStack = [];
public array $useStack = [];
public array $hooks = [];
public Compiler $compiler;
public function hook(string $path, Callable $callback) : self
{
$this->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 ?? []);
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class BreakToken implements ControlStructure {
public string $token = "break";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
return "<?php break; ?>";
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Picea\ControlStructure;
class CaseToken implements ControlStructure {
public string $token = "case";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
$output = "";
if ( $context->switchStack ) {
array_pop($context->switchStack);
}
else {
$output = "<?php ";
}
return ( $output ?? "" ) . "case $arguments: ?>";
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Picea\ControlStructure;
interface ControlStructure {
public function parse(\Picae\Compiler\Context &$context, string $sourceCode);
}

View File

@ -0,0 +1,21 @@
<?php
namespace Picea\ControlStructure;
class DefaultToken implements ControlStructure {
public string $token = "default";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
$output = "";
if ( $context->switchStack ) {
array_pop($context->switchStack);
}
else {
$output = "<?php ";
}
return ( $output ?? "" ) . "default: ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class ElseIfToken implements ControlStructure {
public string $token = "elseif";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
return "<?php elseif ($arguments): ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class ElseToken implements ControlStructure {
public string $token = "else";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
return "<?php else: ?>";
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Picea\ControlStructure;
class EndCaseToken extends BreakToken {
public string $token = "endcase";
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class EndIfToken implements ControlStructure {
public string $token = "endif";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
return "<?php endif ?>";
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Picea\ControlStructure;
class EndSectionToken implements ControlStructure {
public string $token = "endsection";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
if ( empty($context->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 "<?php }]; $build?>";
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Picea\ControlStructure;
class EndSwitchToken implements ControlStructure {
public string $token = "endswitch";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
return "<?php endswitch; ?>";
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Picea\ControlStructure;
class EndforToken implements ControlStructure {
public string $token = "endfor";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
if ( end($context->iterationStack)['or'] === false ) {
$output = "<?php endfor; ?>";
}
else {
$output = "<?php endif; ?>";
}
array_pop($context->iterationStack);
return $output;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Picea\ControlStructure;
class EndforeachToken implements ControlStructure {
public string $token = "endforeach";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
if ( end($context->iterationStack)['or'] === false ) {
$output = "<?php endforeach; ?>";
}
else {
$output = "<?php endif; ?>";
}
array_pop($context->iterationStack);
return $output;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Picea\ControlStructure;
class ExtendsToken implements ControlStructure {
public string $token = "extends";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $path) {
# Triming string's quotes
$path = trim($path, "\"\' \t");
$context->extendFrom = $path;
return "";
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Picea\ControlStructure;
class ForToken implements ControlStructure {
public string $token = "for";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
$uid = "$".uniqid("for_");
$context->iterationStack[] = [
'or' => false,
'uid' => $uid,
'token' => 'endfor',
];
return "<?php for ($arguments): {$uid} = 1; ?>";
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Picea\ControlStructure;
class ForeachToken implements ControlStructure {
public string $token = "foreach";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
$uid = "$".uniqid("foreach_");
$context->iterationStack[] = [
'or' => false,
'uid' => $uid,
'token' => 'endforeach',
];
return "<?php foreach ($arguments): {$uid} = 1; ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class IfToken implements ControlStructure {
public string $token = "if";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
return "<?php if ($arguments): ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class NamespaceToken implements ControlStructure {
public string $token = "namespace";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
$context->namespace = $arguments;
return "";
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Picea\ControlStructure;
class OrToken implements ControlStructure {
public string $token = "or";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
if ( empty($context->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 "<?php {$context->iterationStack[$key]['token']}; if(false === {$context->iterationStack[$key]['uid']} ?? false): ?>";
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Picea\ControlStructure;
class SectionToken implements ControlStructure {
public string $token = "section";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
list($name, $options) = array_pad(explode(',', $arguments), 2, null);
if ( $options ?? false ) {
$options = eval($options);
}
if ( ! ctype_alnum(str_replace([".", "\"", "'"], "", $name)) ) {
throw new \RuntimeException("Your section named `{$name}` contains invalid character. Allowed are only letters, numbers and dots");
}
$context->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 "<?php \$this->__section_list[$name] ??= [ 'prepend' => [], 'append' => [], 'default' => [] ];
\$this->__section_list[$name]['$action'][] = [ 'order' => $order, 'callback' => function() {
extract(\$this->__variables_list); ?>";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class SwitchToken implements ControlStructure {
public string $token = "switch";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
$context->switchStack[] = true;
return "<?php switch($arguments):";
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Picea\ControlStructure;
class UseToken implements ControlStructure {
public string $token = "use";
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
$context->useStack[] = $arguments;
return "";
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Picea\Extension;
interface Extension {
public function parse(\Picae\Compiler\Context &$context, string $sourceCode);
}

View File

@ -0,0 +1,14 @@
<?php
namespace Picea\Extension;
class JsonExtension implements Extension {
public string $token = "json";
public int $flags = JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_QUOT | \JSON_THROW_ON_ERROR;
public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments) {
return "<?php echo json_encode($arguments, {$this->flags}) ?>";
}
}

120
src/Picea.php Normal file
View File

@ -0,0 +1,120 @@
<?php declare(strict_types=1);
namespace Picea;
class Picea
{
public Compiler\Context $context;
public function __construct(?Context $context = null)
{
$this->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;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Picea\Syntax;
class CommentToken implements Syntax {
protected string $tokenOpen = "\{\#";
protected string $tokenClose = "\#\}";
public function parse(/*\Picae\Compiler\Context*/ &$context, string &$sourceCode)
{
$sourceCode = preg_replace("#({$this->tokenOpen})(.*?)({$this->tokenClose})#s", "", $sourceCode);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Picea\Syntax;
class EchoRawToken implements Syntax {
public string $tokenOpen = "\{\{=";
public string $tokenClose = "\}\}";
public function parse(/*\Picae\Compiler\Context*/ &$content, string &$sourceCode)
{
$sourceCode = preg_replace_callback("#({$this->tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) {
return "<?php echo {$matches[2]} ?>";
}, $sourceCode);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Picea\Syntax;
class EchoSafeToken implements Syntax {
public string $tokenOpen = "\{\{";
public string $tokenClose = "\}\}";
public int $flag = \ENT_QUOTES;
public string $encoding;
public bool $doubleEncode = true;
public function __construct() {
$this->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 "<?php echo htmlspecialchars({$matches[2]}, {$this->flag}, '{$this->encoding}', " . ($this->doubleEncode ? "true" : "false") . ") ?>";
}, $sourceCode);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Picea\Syntax;
class PhpTagToken implements Syntax {
public string $tokenOpen = "\{\?";
public string $tokenClose = "\?\}";
public function parse(/*\Picae\Compiler\Context*/ &$context, string &$sourceCode)
{
$sourceCode = preg_replace_callback("#({$this->tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) {
return "<?php {$matches[2]} ?>";
}, $sourceCode);
}
}

7
src/Syntax/Syntax.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace Picea\Syntax;
interface Syntax {
public function parse(\Picae\Compiler\Context &$context, string &$sourceCode);
}