diff --git a/docs/00-intro.md b/docs/00-intro.md new file mode 100644 index 0000000..246a8bc --- /dev/null +++ b/docs/00-intro.md @@ -0,0 +1,47 @@ +# Picea + +Welcome to the official Picea documentation. + +This library offers modern features from templating language, while simply generating normal PHP after compilation, such as view inheritance, definable blocks, 'or' token on unrunned looped, etc... + +Picea uses the same delimiters that Twig uses, which are `{% %}`, `{{ }}` and `{# #}`. + +The first `{% %}` is used for most **control structure** and **extensions**. + +The `{{ }}` delimiter is used to **echo escaped content** in a page. (see *01-echoing*) + +The `{# #}` is exclusively used as a **comment** enclosure. (see *01-comment*) + +## Quick start + +Render a simple Picea view: + +```php +$picea = new Picea\Picea(); +$picea->renderHtml('view/path/hello_world'); +``` + +And the view content could look like: + +*path/hello_world* +```html + + + + Picea's simple + + + + + {{ $someText }} + + +``` + + diff --git a/docs/01-comment.md b/docs/01-comment.md new file mode 100644 index 0000000..b2be90f --- /dev/null +++ b/docs/01-comment.md @@ -0,0 +1,26 @@ +# Control structures - Comments + +There is no single-line token to comment code within Picea, it's a tag working as such : + +**[PICEA]** So, using this code: + +```html +{# This is how you comment in a Picea view #} + +
+ {# Is it hello or good bye ? not sure on this ! #} + Good bye world ! +
+ +{# This is a multi-line comment +
This part will not be rendered
+#} +``` + +**[HTML]** Would render as such: + +```html +
+ Good bye world ! +
+``` \ No newline at end of file diff --git a/docs/01-echoing.md b/docs/01-echoing.md new file mode 100644 index 0000000..0d4e63d --- /dev/null +++ b/docs/01-echoing.md @@ -0,0 +1,73 @@ +# Echoing text or data + +There is two token groups that you can use to print content into a view, `echo` or `print`. + +Escaped by default : `{% echo $argument %}` = `{{ $argument }}` = `{% print $argument %}` + +Raw when needed: `{% echo.raw $argument %}` = `{{= $argument }}` = `{% print.raw $argument %}` + +## Outputing content to a view using `echo` / `{{ }}` / `print`, `echo.raw` / `{{= }}` / `print.raw` + +Using `print` or `echo`, which are, by default, made safer by using PHP's native `htmlspecialchars`. + +**[PICEA]** So, using this code: + +```html +{{ "Hello World !" }} + +{% echo "This is another way to output content" %} + +{% print "This is the third way to output content" %} + +
+ +{% echo.raw $someHTML %} + +{% print.raw $someOtherHTML %} + +{{= $someMoreHTML }} +``` + +**[PHP]** Would yield internally: + +```html + + + + + + +
+ + + + + + +``` + +## Using string format variant `printf` / `printf.raw` + +Those tokens represents the equivalent of the printf() function from PHP. + +**[PICEA]** So, using this code: + +```html +{% php + $num = 5; + $location = 'tree'; +%} +{% printf 'There are %d monkeys in the %s', $num, $location %} +{% printf.raw 'There are %d monkeys in the %s', $num, $location %} +``` + +**[PHP]** Would render internally as : + +```html + + + +``` \ No newline at end of file diff --git a/docs/02-control-structure-comparisons.md b/docs/02-control-structure-comparisons.md new file mode 100644 index 0000000..4afb49a --- /dev/null +++ b/docs/02-control-structure-comparisons.md @@ -0,0 +1,86 @@ +# Control structure - Comparisons + +Most control structures used within PHP view templates are supported natively in Picea. + +The goal for this project always has been to not loose original feature while removing some limitations with PHP's original templating syntax. + +## Comparison using `if` / `then` / `else` / `elseif` / `endif` + +Comparisons works the same way as using PHP's alternative syntax. + +This is also how they are rendered in the compilation process. + +**[PICEA]** So, using this code: + +```html +{% php $search = 'Pack my box with five dozen liquor jugs' %} + +{% if strpos($search, 'beer') !== false %} + Found 'beer' into {{ $search }} ! +{% elseif strpos($search, 'liquor') !== false %} + Found 'liquor' into {{ $search }} ! +{% else %} + Neither 'beer' or 'liquor' were found in {{ $search }} +{% endif %} +``` + +**[PHP]** Would yield: + +```php + + + + Found 'beer' into ! + + Found 'liquor' into ! + + Neither 'beer' or 'liquor' were found in + +``` + +And then, the code would be runned through PHP's native `include` mechanic. + +## Comparison using `switch` / `case` / `break` / `endswitch` + +Using switches within HTML in plain PHP can be quite cumbersome because of it's limitation +disallowing any output (including whitespace) between it's statement. + +Picea will allow some more complex (and readable) switches within your views. + +**[PICEA]** So, using this code: + +```php +{% php $selected = random_int(0,5) %} + +{% switch $selected %} + +{% case 1 %} + {{ "One person selected" }} +{% break %} + +{% case 2 %} +{% default %} + {{ "Multiple person ($selected) selected" }} +{% break %} + +{% endswitch %} +``` + +**[PHP]** Would render as such: + +```php + + + + + + + + + + + + +``` \ No newline at end of file diff --git a/docs/02-control-structure-extends-section.md b/docs/02-control-structure-extends-section.md new file mode 100644 index 0000000..ef1b1ad --- /dev/null +++ b/docs/02-control-structure-extends-section.md @@ -0,0 +1,122 @@ +# Control structure - `extends` / `section` + +A nice feature of most robust templating engine is the ability to inherit from other view. + +Picea follows a similar train of tought, since it's also possible create a chain of inheritance +using `extends` which have definable parts you can declare using `section`. + +## Basic `extends` (string $path) + +You must provide a valid `$path` from which the template will be inherited. + +**[PICEA]** So, using this code: + +*path/base/layout.phtml* +```html + + + + {% section "head" %} + + + {{ title() }} - AnExampleApp + {% section %} + + + +
{% section "main" %}{% endsection %}
+
+ {% section "footer" %} + © Copyright {{ date('Y') }} + {% endsection %} +
+ + +``` + +*path/home.phtml* +```html +{% extends "path/base/layout" %} + +{% title "Home page" %} + +{% section "main" %} +

Welcome !

+ {# @TODO integrate our new blog engine below ! #} +
+ This is our new blog ! We hope you are gonna enjoy your stay on our new platform ! +
+{% endsection %} +``` + +**[HTML]** Would render as such : +```html + + + + + + Home page - AnExampleApp + + + +
+

Welcome !

+ +
+ This is our new blog ! We hope you are gonna enjoy your stay on our new platform ! +
+
+
+ © Copyright 2022 +
+ + +``` + +### Inherit an already extended view + +We could use the previous file `path/home` and generate, let's say, the same page, but without a navigation menu. + +**[PICEA]** So, using this code: + +*path/home-navless.phtml* +```html +{% extends "path/home" %} + +{% section "header" %}{% endsection %} +``` + +**[HTML]** Would render as such : +```html + + + + + + Home page - AnExampleApp + + + +
+

Welcome !

+ +
+ This is our new blog ! We hope you are gonna enjoy your stay on our new platform ! +
+
+
+ © Copyright 2022 +
+ + +``` + +Notice that the `
` tag is now empty, since we've redeclared it in our navless view. \ No newline at end of file diff --git a/docs/02-control-structure-function.md b/docs/02-control-structure-function.md new file mode 100644 index 0000000..bab4066 --- /dev/null +++ b/docs/02-control-structure-function.md @@ -0,0 +1,42 @@ +# Control structure - `function` + +Sometimes comes a need to have a small subset of code which is gonna be used twice (or more) +in the same view. Instead of simply duplicating the code (with everything that can then go wrong if you ever need +to play with it later), you could create a `function`. + +Functions are declared exactly like vanilla PHP. + +**[PICEA]** So, using this code: + +```html + +{% use Psr\Http\Message\ServerRequestInterface %} + +{% title "My generic title" %} + +{% function printCustomTitle(ServerRequestInterface $request) : bool %} + {% if $request->getAttribute('lean.route')->name === 'home' %} +

This is a custom title !

+ + {% return true %} + {% endif %} + + {% return false %} +{% endfunction %} + +{% if ! printCustomTitle($request) %} +

{{ title() }}

+{% endif %} +``` + +**[HTML]** Would yield: + +*page **is** 'home'* +```php +

This is a custom title !

+``` + +*page **is not** 'home'* +```php +

My generic title

+``` diff --git a/docs/02-control-structure-loops.md b/docs/02-control-structure-loops.md new file mode 100644 index 0000000..50f1f29 --- /dev/null +++ b/docs/02-control-structure-loops.md @@ -0,0 +1,193 @@ +# Control structure - Loops + +Picea's loop works the same as PHP original alternative syntax. + +There is, however, some minors improvments and a new `{% or %}` clause working much like the `{% else %}` clause in a comparison. + +## Loop using `for` / `or` / `continue` / `break` / `endfor` + +The simplest of sequence loop, `for` / `endfor` simply iterate over a given counter (or whatever your needs). + +**[PICEA]** So, using this code: + +```html +{% php $users = array_slice([ 'Tom', 'Sam', 'Mario', 'Steve' ], 0, random_int(0, 4)) %} + +

User list

+ +
    + {% for $i = 0; $i < count($users); $i++ %} + {% if $users[$i] === 'Steve' %} + {# Will leave the loop if user's name is Steve #} + {% break %} + {% else %} +
  • {{ $users[$i] }}
  • + {% endif %} + {% or %} +
  • Given user list was empty
  • + {% endfor %} +
+``` + +**[PHP]** Would compile like : + +```php + + +

User list

+ +
    + + + +
  • + +
  • Given user list was empty
  • +
+``` + +**[HTML]** And would render as such given random_int() returns a **3**: + +```html +

User list

+ +
    +
  • Tom
  • +
  • Sam
  • +
  • Mario
  • +
+``` + +**[HTML]** Or would render as such given random_int() returns a **0**: + +```html +

User list

+ +
    +
  • Tom
  • +
+``` + +## Loop using `foreach` / `or` / `continue` / `break` / `endforeach` + +The more complex `foreach` / `endforeach` allows to iterate over keys and values of an array. + +**[PICEA]** So, using this code: +```html +{# Generate a random list of 0 to 4 names #} +{% php $users = array_slice([ 'Tom', 'Sam', 'Mario', 'Steve', 'Joan' ], 0, random_int(0, 5)) %} + +

Random User list

+ +
    + {% foreach $users as $index => $name %} + {% if $name === 'Steve' %} + {# We skip Steve, but allows Joan #} + {% continue %} + {% endif %} +
  • {{ $name }}
  • + {% or %} +
  • Given user list was empty
  • + {% endforeach %} +
+``` + +**[HTML]** Could render as such if prior random_int() returns '**2**': +```html +

User list

+ +
    +
  • Tom
  • +
  • Sam
  • +
+``` + +**[HTML]** Could render as such given random_int() returns '**0**': +```html +

User list

+ +
    +``` + +## Loop using `while` / `or` / `continue` / `break` / `endwhile` + +This syntax allows to loop on a computed iteration count. + +**[PICEA]** So, using this code: + +```html +{% php $fruits = [ 'apple', 'pear', 'banana', 'tomato' ] %} + +

    Grocery list

    + +
      + {% while $item = array_pop($fruits) %} +
    • {{ ucfirst($item) }}
    • + {% or %} +
    • We should never see this, since the list is always populated !
    • + {% endwhile %} +
    +``` + +**[HTML]** Would render as such: + +```html +

    Grocery list

    + +
      +
    • Apple
    • +
    • Pear
    • +
    • Banana
    • +
    • Tomato
    • +
    +``` + +## Loop using `do` / `continue` / `break` / `while` + +This syntax do not have an alternative syntax in vanilla PHP. + +Picea allows for this unusual syntax to be used the same way you would with any other control structure. + +There is, however, no support for the `or` clause for this structure since it will iterate at least once. + +**[PICEA]** So, using this code: + +```html +

    Random number generator

    + +
      + {% do %} + {% php $number = random_int(1, 25); %} + + {% if $number === 10 %} + {# For a reason I'm not entirely sure why, 10 must not be displayed ! #} + {% continue %} + {% endif %} + +
    • {{ str_pad($number, 2, '0' ,STR_PAD_LEFT) }}
    • + {% while $number !== 15 %} {# Loop stops whenever $number is equal to 15 #} +
    +``` + +**[HTML]** Could render as such: + +```html +

    Random number generator

    + +
      +
    • 02
    • +
    • 12
    • +
    • 07
    • +
    • 13
    • +
    • 04
    • +
    • 07
    • +
    • 01
    • +
    • 16
    • +
    • 14
    • +
    • 24
    • +
    • 14
    • +
    • 01
    • +
    • 15
    • +
    +``` diff --git a/docs/02-control-structure-use.md b/docs/02-control-structure-use.md new file mode 100644 index 0000000..e9d7ef5 --- /dev/null +++ b/docs/02-control-structure-use.md @@ -0,0 +1,34 @@ +# Control structure - `Use` + +While working with inside a template, it might be needed that you reference some other namespaces. + +The use clause works exaclty like PHP's vanilla top of the page `use`. + +**[PICEA]** So, using this code: + +```html +{% use AnyVendor\My\Super\Randomizer %} + +{# One chance out of ten to end this loop %} +{% while Randomizer::run(1, 10) === 10 %} + {% if Randomizer::run(1, 100) < 50 %} + Lower than 50 ! + {% else %} + Greater than 50 ! + {% endif %} +{% endwhile %} + +

    DONE !

    +``` + +**[HTML]** Could render as such output: + +```html +Lower than 50 ! +Greater than 50 ! +Lower than 50 ! +Greater than 50 ! +Lower than 50 ! +Lower than 50 ! +

    DONE !

    +``` \ No newline at end of file diff --git a/docs/02-control-structure-view-include-block.md b/docs/02-control-structure-view-include-block.md new file mode 100644 index 0000000..a4be19c --- /dev/null +++ b/docs/02-control-structure-view-include-block.md @@ -0,0 +1,112 @@ +# Control structure - View (or `include`) / Block + +There is three ways to include other views into your current view `{% view %} / {% include %}` or +using `{% block %}{% endblock %}`. + +The order of loading depends on the order given within your directory configuration; the first file found from it +will be used. + +## Inline `view` (string $path, array|null $arguments = null) + +You must provide a valid `$path` from which the template will be loaded. If no `$arguments` are provided, defined +variables from the caller's scope will be given (from `get_defined_vars()`). + +**[PICEA]** So, using this code: + +*path/base/nav.phtml* +```html + +``` + +*path/base/nav-item.phtml* +```html +{{ $item->name }}" +``` + +**[HTML]** Could render such as : +```html + +``` + +## `include` (string $path) content from external file + +Whenever you need to `include` a raw file from one of your view directories + +## Reusable `block` (string $path, ...$arguments) / `endblock` + +A better way to achieve this behaviour could be to create a `block` which you can define as needed. + +**[PICEA]** So, using this code: + +*path/base/nav.phtml* +```html + +``` + +*path/base/nav-item-block.phtml* +```html +{% arguments string $name, string $anchor, int $index = 0 %} + +{{ $name }}" +``` + +**[HTML]** Would render the same as the `view` example : +```html + +``` + +### Extending a `block` using `define` and `slot` + +You might need to define some custom content inside of a `block`. + +You can do so by using `define` and `slot`. + +**[PICEA]** So, using this code: + +*path/base/nav.phtml* +```html + +``` + +*path/base/nav-item-block.phtml* +```html +{% arguments string $name, string $anchor, int $index = 0 %} + +{% define slot %} + +{{ $name }}" +``` + +**[HTML]** Would render the same as the `view` example : +```html + +``` \ No newline at end of file diff --git a/docs/03-method-request.md b/docs/03-method-request.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/10-extension-custom.md b/docs/10-extension-custom.md new file mode 100644 index 0000000..a51b621 --- /dev/null +++ b/docs/10-extension-custom.md @@ -0,0 +1,4 @@ +# Extension - Creating an extension + + + diff --git a/docs/10-extension-json.md b/docs/10-extension-json.md new file mode 100644 index 0000000..984bbbb --- /dev/null +++ b/docs/10-extension-json.md @@ -0,0 +1,4 @@ +# Extension - `json`, `json.pretty`, `json.html` + +This extension + diff --git a/docs/10-extension-language.md b/docs/10-extension-language.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/10-extension-money.md b/docs/10-extension-money.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/10-extension-php.md b/docs/10-extension-php.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/10-extension-title.md b/docs/10-extension-title.md new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/docs/10-extension-title.md @@ -0,0 +1 @@ + function($c) { + return new Picea($c->get(Context::class), $c->get(Cache::class), $c->get(Compiler::class), null, $c->get(FileFetcher::class), null, getenv("DEBUG")); + }, + + Context::class => function($c) { + return new BaseContext( $c->get(Lean\Lean::class)->getPiceaContext() ); + }, + + Compiler::class => function($c) { + return new Compiler(new class(array_merge([ + $c->get(LanguageExtension::class), + $c->get(TitleExtension::class), + $c->get(MoneyExtension::class), + $c->get(UrlExtension::class), + $c->get(Method\Form::class), + $c->get(Method\Pagination::class), + $c->get(Request::class), + ], class_exists(\Taxus\Picea\Extension::class) ? [ $c->get(\Taxus\Picea\Extension::class) ] : [], + array_map(fn($class) => $c->get($class), $c->get(Lean\Lean::class)->getPiceaExtensions() ))) extends DefaultRegistrations { + + public function registerAll(Compiler $compiler) : void + { + parent::registerAll($compiler); + ( new Ui() )->registerFormExtension($compiler); + } + + }); + }, + + Request::class => autowire(Request::class), + + Method\Form::class => autowire(Method\Form::class), + + Method\Pagination::class => autowire(Method\Pagination::class), + + LanguageExtension::class => create(LanguageExtension::class)->constructor(get(LanguageHandler::class)), + + LanguageHandler::class => function($c) { + return new class( $c->get(Tell\I18n::class) ) implements LanguageHandler { + public Tell\I18n $tell; + + public function __construct(Tell\I18n $tell) { + $this->tell = $tell; + } + + public function languageFromKey(string $key, array $variables = []) : array|string + { + return $this->tell->fromKey($key, $variables) ?: ""; + } + }; + }, + + TitleExtension::class => autowire(TitleExtension::class), + + MoneyExtension::class => autowire(MoneyExtension::class), + + UrlExtension::class => create(UrlExtension::class)->constructor(get(Context::class), getenv("URL_BASE"), get('git.commit')), + + Cache::class => create(Opcache::class)->constructor(getenv("CACHE_PATH"), get(Context::class)), + + FileFetcher::class => function($c) { + return new FileFetcher($c->get(Lean\Lean::class)->getViewPaths()); + }, +]; +``` + diff --git a/src/Builder.php b/src/Builder.php index 69c2930..a541802 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -4,7 +4,7 @@ namespace Picea; class Builder { - const TEMPLATE_CLASSNAME_PREFIX = "PiceaTemplate_"; + public const TEMPLATE_CLASSNAME_PREFIX = "PiceaTemplate_"; protected string $templatePath = ""; diff --git a/src/Builder/ClassTemplate.php b/src/Builder/ClassTemplate.php index ab7ffa1..7190d48 100644 --- a/src/Builder/ClassTemplate.php +++ b/src/Builder/ClassTemplate.php @@ -8,10 +8,13 @@ namespace %NAMESPACE%; if (! class_exists("%NAMESPACE%\%CLASSNAME%", false) ) { class %CLASSNAME% %EXTENDS% { + public array $blockList = []; public array $sectionList = []; + public array $sectionStack = []; + public array $variableList = []; public ?object $thisProxy = null; @@ -24,6 +27,8 @@ if (! class_exists("%NAMESPACE%\%CLASSNAME%", false) ) { public bool $renderingInsideSection = false; + public int $depth = 0; + public function __construct(\Picea\Picea $picea, array $variablesList = [], ?object $thisProxy = null) { $this->picea = $picea; $this->variableList = $variablesList; @@ -37,15 +42,26 @@ if (! class_exists("%NAMESPACE%\%CLASSNAME%", false) ) { public function output(array $variablesList = []) : void { + $__event = new \Picea\Builder\ClassTemplateEvent(); + $this->rendering = true; - ( function($___class__template, $___global_variables, $___variables, $picea) { + $this->depth++; + + $__event->eventExecute(\Picea\Event\Builder\ClassTemplateOutputing::class, $variablesList); + + ( function($___class__template, $___global_variables, $___variables, $__event, $picea) { extract($___global_variables); extract($___variables, \EXTR_OVERWRITE); ?>%CONTENT%call($this->thisProxy ?? new class(){}, $this, $this->variableList, $variablesList, $this->picea); + } )->call($this->thisProxy ?? new class() {}, $this, $this->variableList, $variablesList, $__event, $this->picea); + + $__event->eventExecute(\Picea\Event\Builder\ClassTemplateOutputDone::class, $variablesList); + %PARENT_OUTPUT% + $this->depth--; + $this->rendering = false; } @@ -85,6 +101,14 @@ if (! class_exists("%NAMESPACE%\%CLASSNAME%", false) ) { public static function getSourceLineFromException(int $sourceLine) : ? int { + $selfSource = file_get_contents(__FILE__); + + foreach(explode("\n", $selfSource) as $line => $content) { + if ( strpos($content, str_replace('$', '%', '/*$EXCEPTION_LINE_BASE$*/')) !== false ) { + return $sourceLine - $line; + } + } + $sourceFile = file_get_contents("%TEMPLATE%"); if ( $sourceFile ) { diff --git a/src/Builder/ClassTemplateEvent.php b/src/Builder/ClassTemplateEvent.php new file mode 100644 index 0000000..dcbcdfb --- /dev/null +++ b/src/Builder/ClassTemplateEvent.php @@ -0,0 +1,8 @@ +cachePath($viewPath)) ) { $this->compiled[$viewPath] = include($path); - - # if ( $this->compiled[$viewPath]['extends'] ?? false ) { - # $this->compiled($this->compiled[$viewPath]['extends']); - # } - + return true; } @@ -86,7 +81,7 @@ class Opcache implements Cache { return true; } - protected function cachePath(string $fileName = "") : string + public function cachePath(string $fileName = "") : string { return implode(DIRECTORY_SEPARATOR, array_filter([ $this->cachePath, $this->cacheFolder, $fileName ? str_replace([ "/", DIRECTORY_SEPARATOR ], "~", Builder::generateClassName($fileName) . ".php") : null ])); } @@ -95,7 +90,7 @@ class Opcache implements Cache { spl_autoload_register(function ($class) use ($namespace) { $prefix = "$namespace\\"; - $baseDir = $this->cachePath() . "/"; + $baseDir = $this->cachePath() . DIRECTORY_SEPARATOR; $len = strlen($prefix); @@ -103,11 +98,24 @@ class Opcache implements Cache { return; } - $file = $baseDir . str_replace('\\', '/', substr($class, $len)) . '.php'; + $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, $len)) . '.php'; if ( file_exists($file) ) { require $file; } }); } + + public function getFilelist() : array + { + return glob($this->cachePath() . DIRECTORY_SEPARATOR . Builder::TEMPLATE_CLASSNAME_PREFIX . "*"); + } + + public function purge() : void + { + foreach($this->getFilelist() as $dir) + { + dump($dir); + } + } } diff --git a/src/Compiler.php b/src/Compiler.php index f725e0f..edb8fff 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -122,14 +122,19 @@ class Compiler } + public function __toString() : string + { + return "WHATAFAK"; + } + public function getExtensionFromToken(string $name) : Extension\Extension { - if ( false === $this->extensionList[$name] ?? false ) { + if ( ! isset($this->extensionList[$name]) ) { throw new \InvalidArgumentException(<<extensionList[$name]; } diff --git a/src/Compiler/Context.php b/src/Compiler/Context.php index 0cbf307..b490c40 100644 --- a/src/Compiler/Context.php +++ b/src/Compiler/Context.php @@ -24,6 +24,8 @@ abstract class Context { public array $useStack = []; + public int $functions = 0; + public array $functionStack = []; public array $hooks = []; diff --git a/src/ControlStructure/AbstractLoop.php b/src/ControlStructure/AbstractLoop.php new file mode 100644 index 0000000..285f854 --- /dev/null +++ b/src/ControlStructure/AbstractLoop.php @@ -0,0 +1,47 @@ +iterationStack ?? [], function($item) { + return ! $item['or']; + }); + + $count = count($stack); + + if ( $count > 0 ) { + $name .= "[" . end($stack)['uid'] . "]"; + } + + $context->iterationStack[] = [ + 'or' => false, + 'uid' => $name, + 'token' => "end{$token}", + ]; + + return ""; + + case "endwhile": + case "endforeach": + $last = end($context->iterationStack); + + if ( $last['or'] === false ) { + $output = ""; + } + else { + $output = ""; + } + + array_pop($context->iterationStack); + + return $output; + } + } +} diff --git a/src/ControlStructure/BlockToken.php b/src/ControlStructure/BlockToken.php index e369781..5999a2f 100644 --- a/src/ControlStructure/BlockToken.php +++ b/src/ControlStructure/BlockToken.php @@ -6,7 +6,7 @@ class BlockToken implements ControlStructure { public array $token = [ "arguments", "block", "endblock", "define", "slot", "endslot", "using" ]; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { static $slotDefinitions = []; switch($token) { @@ -33,7 +33,7 @@ class BlockToken implements ControlStructure { sprintf('A block awaiting arguments `%s` instead received `%s` with values `%s`', '$arguments', implode(', ', array_map('gettype', \$inlineVariables ?? [])), json_encode(\$inlineVariables)) ); } - ?> + /*%EXCEPTION_LINE_BASE%*/?> PHP; case "define": @@ -49,7 +49,9 @@ class BlockToken implements ControlStructure { $def = end($slotDefinitions); list($name, $definition) = array_pad(explode(',', $arguments, 2), 2, ""); - + + $loops = count($context->iterationStack ?? []) ? ",". implode(', ', array_filter(array_column($context->iterationStack, 'uid'), fn($e) => strpos($e, '[') === false)) : null; + if ($def->hasDefinitions() ) { $slotName = eval("return $name;"); $def->currentSlot = $slotName; @@ -62,7 +64,7 @@ class BlockToken implements ControlStructure { } return <<printSlot($name, function($definition array \$___using = []) use (\$picea) { extract(\$___using, \EXTR_SKIP); ?> + printSlot($name, function($definition array \$___using = []) use (\$picea $loops) { extract(\$___using, \EXTR_SKIP); ?> PHP; } else { @@ -71,7 +73,7 @@ class BlockToken implements ControlStructure { } return <<slotIsSet($name) || \$___block->setSlot($name, function($definition array \$___using = []) use (\$picea) { extract(\$___using, \EXTR_SKIP); ?> + slotIsSet($name) || \$___block->setSlot($name, function($definition array \$___using = []) use (\$picea $loops) { extract(\$___using, \EXTR_SKIP); ?> PHP; } diff --git a/src/ControlStructure/BreakToken.php b/src/ControlStructure/BreakToken.php index 8f815ac..06427ce 100644 --- a/src/ControlStructure/BreakToken.php +++ b/src/ControlStructure/BreakToken.php @@ -6,7 +6,7 @@ class BreakToken implements ControlStructure { public string $token = "break"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { return ""; } diff --git a/src/ControlStructure/CaseToken.php b/src/ControlStructure/CaseToken.php index 127e3fa..d3c1c3a 100644 --- a/src/ControlStructure/CaseToken.php +++ b/src/ControlStructure/CaseToken.php @@ -6,7 +6,7 @@ class CaseToken implements ControlStructure { public string $token = "case"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { $output = ""; if ( $context->switchStack ) { diff --git a/src/ControlStructure/ContinueToken.php b/src/ControlStructure/ContinueToken.php index 97a971c..f8e8034 100644 --- a/src/ControlStructure/ContinueToken.php +++ b/src/ControlStructure/ContinueToken.php @@ -6,7 +6,7 @@ class ContinueToken implements ControlStructure { public string $token = "continue"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { return ""; } diff --git a/src/ControlStructure/ControlStructure.php b/src/ControlStructure/ControlStructure.php index 51c0add..71a7b72 100644 --- a/src/ControlStructure/ControlStructure.php +++ b/src/ControlStructure/ControlStructure.php @@ -3,5 +3,5 @@ namespace Picea\ControlStructure; interface ControlStructure { - public function parse(\Picae\Compiler\Context &$context, string $sourceCode, string $token); + public function parse(\Picea\Compiler\Context &$context, string $sourceCode, string $token); } diff --git a/src/ControlStructure/DefaultToken.php b/src/ControlStructure/DefaultToken.php index cb6b6c9..37ed7d0 100644 --- a/src/ControlStructure/DefaultToken.php +++ b/src/ControlStructure/DefaultToken.php @@ -6,7 +6,7 @@ class DefaultToken implements ControlStructure { public string $token = "default"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { $output = ""; if ( $context->switchStack ) { diff --git a/src/ControlStructure/EchoToken.php b/src/ControlStructure/EchoToken.php new file mode 100644 index 0000000..f20c841 --- /dev/null +++ b/src/ControlStructure/EchoToken.php @@ -0,0 +1,28 @@ +"; + + case "echo.raw": + return ""; + } + } + + public static function echoRaw($arguments) : string + { + return ""; + } + + public static function echoSafe($arguments) : string + { + return ""; + } +} diff --git a/src/ControlStructure/EndRawToken.php b/src/ControlStructure/EndRawToken.php deleted file mode 100644 index a705f02..0000000 --- a/src/ControlStructure/EndRawToken.php +++ /dev/null @@ -1,12 +0,0 @@ -iterationStack ?? [], function($item) { - return ! $item['or']; - }); - - $count = count($stack); - - if ( $count > 0 ) { - $name .= "[" . end($stack)['uid'] . "]"; - } - - $context->iterationStack[] = [ - 'or' => false, - 'uid' => $name, - 'token' => 'endforeach', - ]; - - return ""; - - case "endforeach": - $last = end($context->iterationStack); - - if ( $last['or'] === false ) { - $output = ""; - } - else { - $output = ""; - } - - array_pop($context->iterationStack); - - return $output; - } - } } diff --git a/src/ControlStructure/FunctionToken.php b/src/ControlStructure/FunctionToken.php index da82ea7..06fa788 100644 --- a/src/ControlStructure/FunctionToken.php +++ b/src/ControlStructure/FunctionToken.php @@ -6,9 +6,11 @@ class FunctionToken implements ControlStructure { public array $token = [ "function", "endfunction", "return" ]; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { switch($token) { case "function": + $context->functions++; + return $this->printFunction($context, $arguments); case "return": @@ -31,7 +33,6 @@ class FunctionToken implements ControlStructure { protected function printFunction($context, ?string $arguments) : string { - $context->functions++; return ""; } diff --git a/src/ControlStructure/IfToken.php b/src/ControlStructure/IfToken.php index 9c86446..1a35fcd 100644 --- a/src/ControlStructure/IfToken.php +++ b/src/ControlStructure/IfToken.php @@ -6,7 +6,7 @@ class IfToken implements ControlStructure { public array $token = [ "if", "else", "elseif", "endif" ]; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { switch($token) { case "if": return ""; diff --git a/src/ControlStructure/IncludeToken.php b/src/ControlStructure/IncludeToken.php index b75d253..2fd91e0 100644 --- a/src/ControlStructure/IncludeToken.php +++ b/src/ControlStructure/IncludeToken.php @@ -6,7 +6,7 @@ class IncludeToken implements ControlStructure { public string $token = "include"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { - return "picea->inlineContent($arguments); ?>"; + public function parse(\Picea\Compiler\Context &$context, ? string $viewPath, string $token) { + return "picea->inlineContent($viewPath); ?>"; } } diff --git a/src/ControlStructure/NamespaceToken.php b/src/ControlStructure/NamespaceToken.php index d0b9cd7..77859e3 100644 --- a/src/ControlStructure/NamespaceToken.php +++ b/src/ControlStructure/NamespaceToken.php @@ -6,7 +6,7 @@ class NamespaceToken implements ControlStructure { public string $token = "namespace"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { $context->namespace = $arguments; return ""; } diff --git a/src/ControlStructure/OrToken.php b/src/ControlStructure/OrToken.php index beecda0..99ad56f 100644 --- a/src/ControlStructure/OrToken.php +++ b/src/ControlStructure/OrToken.php @@ -6,9 +6,9 @@ class OrToken implements ControlStructure { public string $token = "or"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { if ( empty($context->iterationStack) ) { - throw new \LogicException("Token `or` was used outside of iterator. Make sure your `for` or `foreach` declaration are properly made."); + throw new \LogicException("Token `or` was used outside of iterator. Make sure your `for`, `foreach`, `while`, `do/until` declaration are properly made."); } $key = count( $context->iterationStack ) - 1; diff --git a/src/ControlStructure/RawToken.php b/src/ControlStructure/RawToken.php deleted file mode 100644 index 8834bca..0000000 --- a/src/ControlStructure/RawToken.php +++ /dev/null @@ -1,12 +0,0 @@ -printSection($context, $arguments); @@ -46,14 +46,26 @@ class SectionToken implements ControlStructure { $order = $options['order'] ?? "count(\$___class__template->sectionList[$name]['$action'])"; return "sectionList[$name] ??= [ 'prepend' => [], 'append' => [], 'default' => [] ];". - "\$___class__template->sectionList[$name]['$action'][] = [ 'order' => $order, 'callback' => function() use (\$picea, \$___class__template, \$___global_variables, \$___variables) {". - "extract(\$___global_variables); extract(\$___variables, \EXTR_OVERWRITE); \$___class__template->sectionStack[] = '$name'; ?>"; + "\$___class__template->sectionList[$name]['$action'][] = [ + 'order' => $order, + 'callback' => function() use (\$picea, \$___class__template, \$___global_variables, \$___variables, \$__event) {". + "extract(\$___global_variables); extract(\$___variables, \EXTR_OVERWRITE); + \$___class__template->sectionStack[] = $name; + \$__event->eventExecute(\Picea\Event\Builder\ClassTemplateRenderSection::class, $name);?>"; } protected function printEndSection($context) : string { $section = array_pop($context->sections); - $build = $context->extendFrom ? "!empty(\$___class__template->sectionStack) && \$___class__template->renderSection({$section['name']});" : "\$___class__template->renderSection({$section['name']});"; - return "sectionStack); }]; $build?>"; + $build = $context->extendFrom ? "!empty(\$___class__template->sectionStack) && \$___class__template->renderSection({$section['name']}, false);" : "\$___class__template->renderSection({$section['name']}, false);"; + + return <<eventExecute(\Picea\Event\Builder\ClassTemplateRenderSectionDone::class, {$section['name']}); + array_pop(\$___class__template->sectionStack); }]; + $build + ?> + PHP; + } } \ No newline at end of file diff --git a/src/ControlStructure/SwitchToken.php b/src/ControlStructure/SwitchToken.php index a8643f7..f21d39d 100644 --- a/src/ControlStructure/SwitchToken.php +++ b/src/ControlStructure/SwitchToken.php @@ -6,7 +6,7 @@ class SwitchToken implements ControlStructure { public array $token = [ "switch", "case", "endswitch" ]; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { switch($token) { case "switch": $context->switchStack[] = true; diff --git a/src/ControlStructure/UseToken.php b/src/ControlStructure/UseToken.php index 25a3243..a721ff4 100644 --- a/src/ControlStructure/UseToken.php +++ b/src/ControlStructure/UseToken.php @@ -6,7 +6,7 @@ class UseToken implements ControlStructure { public string $token = "use"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { $context->useStack[] = $arguments; return ""; } diff --git a/src/ControlStructure/ViewToken.php b/src/ControlStructure/ViewToken.php index e6c01b0..8094bc9 100644 --- a/src/ControlStructure/ViewToken.php +++ b/src/ControlStructure/ViewToken.php @@ -6,7 +6,7 @@ class ViewToken implements ControlStructure { public string $token = "view"; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { # The way this is ordered, if you provide a second arguments, being an array of variables, get_defined_vars() will not be pushed inside the view return "picea->inlineHtml(\$this, $arguments, get_defined_vars()); ?>"; } diff --git a/src/ControlStructure/WhileToken.php b/src/ControlStructure/WhileToken.php new file mode 100644 index 0000000..1549808 --- /dev/null +++ b/src/ControlStructure/WhileToken.php @@ -0,0 +1,32 @@ +iterationStack[] = [ + 'or' => false, + 'token' => "do", + ]; + + return ""; + + case "while": + if ( $context->iterationStack ?? false ) { + if ( end($context->iterationStack)['token'] === 'do' ) { + array_pop($context->iterationStack); + + return ""; + } + } + } + + return parent::parse($context, $arguments, $token); + } +} + + diff --git a/src/Event/Builder/ClassTemplateOutputDone.php b/src/Event/Builder/ClassTemplateOutputDone.php new file mode 100644 index 0000000..e44e74a --- /dev/null +++ b/src/Event/Builder/ClassTemplateOutputDone.php @@ -0,0 +1,7 @@ +_eventList[] = $event; + } + + public function eventFromType(string $type) : array + { + return array_filter($this->_eventList, fn($ev) => $ev instanceof $type); + } + + public function eventExecute(string $type, ...$arguments) : void + { + foreach($this->eventFromType($type) as $event) { + $this->_returnList[$event::class][] = call_user_func_array([ $event, $this->_eventTraitMethod ], $arguments); + } + } +} \ No newline at end of file diff --git a/src/Exception/RenderHtmlException.php b/src/Exception/RenderHtmlException.php new file mode 100644 index 0000000..799e4c0 --- /dev/null +++ b/src/Exception/RenderHtmlException.php @@ -0,0 +1,52 @@ +picea = $picea; + + $this->defineError($previous, $compiledObject); + } + + protected function defineError(\Throwable $previous, object $compiledObject) : void + { + $loadedTemplates = array_flip($this->picea->loadedTemplateFile); + + foreach($previous->getTrace() as $trace) { + if ( isset($trace['file'], $loadedTemplates[$trace['file']]) ) { + $class = $loadedTemplates[ $trace['file'] ]; + + $content = include($trace['file']); + + $this->file = $content['view']; + $this->line = $class::getSourceLineFromException($trace['line']); + + return; + } + } + } + + protected function getTemplateFile(string $filePath) : ? array + { + $content = null; + + if ( is_array($content) && isset($content['classname'], $content['namespace'], $content['view'], $content['extends']) ) { + return $content; + } + + return null; + } +} diff --git a/src/Exception/RenderingError.php b/src/Exception/RenderingError.php deleted file mode 100644 index a31acca..0000000 --- a/src/Exception/RenderingError.php +++ /dev/null @@ -1,129 +0,0 @@ - - */ -class RenderingError extends \Exception -{ - /** - * Constructor. - * - * By default, automatic guessing is enabled. - * - * @param string $message The error message - * @param int $lineno The template line where the error occurred - * @param Source|null $source The source context where the error occurred - */ - public function __construct(string $message, Source $source = null, \Exception $previous = null) - { - parent::__construct('', 0, $previous); - - if (null === $source) { - $name = null; - } else { - $name = $source->getName(); - $this->sourceCode = $source->getCode(); - $this->sourcePath = $source->getPath(); - } - - $this->lineno = $lineno; - $this->name = $name; - $this->rawMessage = $message; - - - } - - public static function generateFromCompiledClass(\Throwable $exception) : self - { - - - return $exception; - } - - private function readTemplateInformation(): void - { - $template = null; - $templateClass = null; - - $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT); - foreach ($backtrace as $trace) { - if (isset($trace['object']) && $trace['object'] instanceof Template) { - $currentClass = \get_class($trace['object']); - $isEmbedContainer = 0 === strpos($templateClass, $currentClass); - if (null === $this->name || ($this->name == $trace['object']->getTemplateName() && !$isEmbedContainer)) { - $template = $trace['object']; - $templateClass = \get_class($trace['object']); - } - } - } - - // update template name - if (null !== $template && null === $this->name) { - $this->name = $template->getTemplateName(); - } - - // update template path if any - if (null !== $template && null === $this->sourcePath) { - $src = $template->getSourceContext(); - $this->sourceCode = $src->getCode(); - $this->sourcePath = $src->getPath(); - } - - if (null === $template || $this->lineno > -1) { - return; - } - - $r = new \ReflectionObject($template); - $file = $r->getFileName(); - - $exceptions = [$e = $this]; - while ($e = $e->getPrevious()) { - $exceptions[] = $e; - } - - while ($e = array_pop($exceptions)) { - $traces = $e->getTrace(); - array_unshift($traces, ['file' => $e->getFile(), 'line' => $e->getLine()]); - - while ($trace = array_shift($traces)) { - if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { - continue; - } - - foreach ($template->getDebugInfo() as $codeLine => $templateLine) { - if ($codeLine <= $trace['line']) { - // update template line - $this->lineno = $templateLine; - - return; - } - } - } - } - } -} diff --git a/src/Extension/Extension.php b/src/Extension/Extension.php index 2176a56..a5dbb7c 100644 --- a/src/Extension/Extension.php +++ b/src/Extension/Extension.php @@ -3,5 +3,5 @@ namespace Picea\Extension; interface Extension { - public function parse(\Picae\Compiler\Context &$context, string $sourceCode, string $token); + public function parse(\Picea\Compiler\Context &$context, string $sourceCode, string $token); } diff --git a/src/Extension/JsonExtension.php b/src/Extension/JsonExtension.php index 260dc2a..1643daa 100644 --- a/src/Extension/JsonExtension.php +++ b/src/Extension/JsonExtension.php @@ -10,7 +10,7 @@ class JsonExtension implements Extension, FunctionExtension { public int $flags = JSON_HEX_TAG | \JSON_HEX_QUOT | \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_UNICODE; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { $flag = $this->flags; @@ -20,7 +20,7 @@ class JsonExtension implements Extension, FunctionExtension { break; case "json.html": - return "flags}), ENT_QUOTES, 'UTF-8') ?>"; + return ""; } $cls = static::class; @@ -32,7 +32,7 @@ class JsonExtension implements Extension, FunctionExtension { { return [ 'json' => function($arguments, ? int $flags = null) { - return json_encode($arguments, \JSON_FORCE_OBJECT); + return json_encode($arguments, $flags ?? $this->flags); }, ]; } diff --git a/src/Extension/LanguageExtension.php b/src/Extension/LanguageExtension.php index 8bd3f50..fbbb7c4 100644 --- a/src/Extension/LanguageExtension.php +++ b/src/Extension/LanguageExtension.php @@ -3,6 +3,7 @@ namespace Picea\Extension; use Picea\Compiler\Context; +use Picea\Event\Builder\ClassTemplateRenderSectionDone; class LanguageExtension implements Extension, FunctionExtension { @@ -24,11 +25,26 @@ class LanguageExtension implements Extension, FunctionExtension { ]; } - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) : string { switch($token) { case "language.set": - return "compiler->getExtensionFromToken('$token')->currentLanguage = $arguments; ?>"; + $cls = $this::class; + + return <<getParam('extends'); + + if ( \$___class__template->depth === 1 || \$___class__template->sectionStack ) { + \$ext->currentLanguage = $arguments; + + # @TODO Make sure this event is only registered when we output() a template, if we are in a section, we must attach it a view/section/block output event instead ! + \$__event->eventRegister(\\$cls::outputDoneEvent(\$ext)); + } + })(\$picea->compiler->getExtensionFromToken('$token')); + ?> + PHP; case "lang": return "compiler->getExtensionFromToken('$token')->absoluteLang($arguments), \ENT_QUOTES, ini_get('default_charset'), true) ?>"; @@ -59,4 +75,21 @@ class LanguageExtension implements Extension, FunctionExtension { { return $this->languageHandler->languageFromKey($key, $variables); } + + public static function outputDoneEvent(LanguageExtension $languageExtension) : ClassTemplateRenderSectionDone + { + return new class($languageExtension) implements ClassTemplateRenderSectionDone { + + protected string $current; + + public function __construct(protected LanguageExtension $languageExtension) { + $this->current = $this->languageExtension->currentLanguage; + } + + public function execute(string $name) : mixed + { + return $this->current ? $this->languageExtension->currentLanguage = $this->current : null; + } + }; + } } diff --git a/src/Extension/MoneyExtension.php b/src/Extension/MoneyExtension.php index 48c62c0..f572271 100644 --- a/src/Extension/MoneyExtension.php +++ b/src/Extension/MoneyExtension.php @@ -4,7 +4,7 @@ namespace Picea\Extension; use Picea\Compiler\Context; -class MoneyExtension implements Extension { +class MoneyExtension implements Extension, FunctionExtension { public string $token = "money"; @@ -14,18 +14,19 @@ class MoneyExtension implements Extension { public \NumberFormatter $formatter; - public function __construct(Context $context) { - $this->register($context); + public function __construct() { $this->locale = explode('.', \Locale::getDefault())[0]; $this->formatter = new \NumberFormatter($this->locale, \NumberFormatter::CURRENCY); } - public function register(Context $context) : void + public function exportFunctions(): array { - $context->pushFunction("money", [ $this, 'money' ]); + return [ + "money" => [ $this, 'money' ] + ]; } - - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { return ""; } diff --git a/src/Extension/PhpExtension.php b/src/Extension/PhpExtension.php index 6c363e0..b4eccbe 100644 --- a/src/Extension/PhpExtension.php +++ b/src/Extension/PhpExtension.php @@ -6,7 +6,7 @@ class PhpExtension implements Extension { public array $token = [ "php" ]; - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { return ""; } } diff --git a/src/Extension/PrintExtension.php b/src/Extension/PrintExtension.php index cd262df..a9baf2e 100644 --- a/src/Extension/PrintExtension.php +++ b/src/Extension/PrintExtension.php @@ -6,7 +6,7 @@ use Picea\Compiler\Context; class PrintExtension implements Extension { - public array $token = [ "print", "print.safe", "print.raw" ]; + public array $token = [ "echo", "echo.raw", "print", "print.raw", "printf", "printf.raw" ]; public int $flag = \ENT_QUOTES; @@ -18,15 +18,22 @@ class PrintExtension implements Extension { $this->encoding = ini_get("default_charset"); } - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) : string { switch($token) { + case 'echo': case 'print': - case "print.safe": - return "flag}, '{$this->encoding}', " . ($this->doubleEncode ? "true" : "false") . ") ?>"; + return "flag}, '{$this->encoding}', " . ($this->doubleEncode ? "true" : "false") . ") ?>"; + case 'echo.raw': case "print.raw": - return ""; + return ""; + + case 'printf': + return "flag}, '{$this->encoding}', " . ($this->doubleEncode ? "true" : "false") . ") ?>"; + + case 'printf.raw': + return ""; } } diff --git a/src/Extension/TitleExtension.php b/src/Extension/TitleExtension.php index 6389159..03b4ad3 100644 --- a/src/Extension/TitleExtension.php +++ b/src/Extension/TitleExtension.php @@ -2,25 +2,27 @@ namespace Picea\Extension; -use Picea\Compiler\Context; - -class TitleExtension implements Extension { +class TitleExtension implements Extension, FunctionExtension { public string $token = "title"; public string $title = ""; - - public function __construct(Context $context) { - $this->register($context); + + public function exportFunctions(): array + { + return [ + "title" => [$this, 'handleTitle'], + ]; } - public function register(Context $context) : void - { - $context->pushFunction("title", [ $this, 'handleTitle' ]); - } - - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) { - return ""; + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) { + return << + PHP; } public function handleTitle(? string $set = null, ...$arguments) : ? string diff --git a/src/Extension/UrlExtension.php b/src/Extension/UrlExtension.php index ce2ca0b..694bff9 100644 --- a/src/Extension/UrlExtension.php +++ b/src/Extension/UrlExtension.php @@ -4,7 +4,15 @@ namespace Picea\Extension; use Picea\Compiler\Context; -class UrlExtension implements Extension { +class UrlExtension implements Extension, FunctionExtension { + + public const URLIZE_PATTERN_URL = <<)(?=[^\w/._\-&])~s + PATTERN; + + public const URLIZE_PATTERN_EMAIL = <<urlBase = trim($urlBase, "/"); $this->assetToken = $assetToken; - $this->register($context); } - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : ?string + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) : ?string { switch($token) { case "asset": @@ -41,19 +48,24 @@ class UrlExtension implements Extension { return "compiler->getExtensionFromToken('$token')->setUrlParameters($arguments) ?>"; case "slug": - return \Transliterator::createFromRules(':: Any-Latin;:: NFD;:: [:Nonspacing Mark:] Remove;:: NFC;:: [:Punctuation:] Remove;:: Lower();[:Separator:] > \'-\'')->transliterate( $arguments ); + return "compiler->getExtensionFromToken('$token')->slug($arguments) ?>"; + + #return \Transliterator::createFromRules(':: Any-Latin;:: NFD;:: [:Nonspacing Mark:] Remove;:: NFC;:: [:Punctuation:] Remove;:: Lower();[:Separator:] > \'-\'')->transliterate( $arguments ); } return null; } - - public function register(Context $context) : void + + public function exportFunctions(): array { - $context->pushFunction("url", [ $this, 'buildUrl' ]); - $context->pushFunction("current_url", [ $this, 'currentUrl' ]); - $context->pushFunction("asset", [ $this, 'buildAssetUrl' ]); - $context->pushFunction("route", [ $this, 'buildRouteUrl' ]); - $context->pushFunction("slug", [ $this, 'slug' ]); + return [ + "url" => [ $this, 'buildUrl' ], + "current_url" => [ $this, 'currentUrl' ], + "asset" => [ $this, 'buildAssetUrl' ], + "route" => [ $this, 'buildRouteUrl' ], + "slug" => [ $this, 'slug' ], + "urlize" => [ $this, 'urlize' ] + ]; } public function getRouteList(bool $full = false) : array @@ -66,25 +78,36 @@ class UrlExtension implements Extension { return $url . ( $parameters ? "?" . http_build_query($parameters) : "" ); } + public function urlize(string $string) : string + { + # Normal URL patterns + $string = preg_replace(static::URLIZE_PATTERN_URL, '$0', $string); + + # Email patterns + $string = preg_replace(static::URLIZE_PATTERN_EMAIL, '$0', $string); + + return $string; + } + public function currentUrl(array $parameters = []) : string { return $this->buildUrl($this->uri(), $parameters); } - public function buildUrl(string $uri = "", array $parameters = []) : string + public function buildUrl(string $uri = "", array $parameters = [], bool $appendVersion = false) : string { - return $this->setUrlParameters($this->url() . "/" . ltrim($uri, "/"), $parameters); + return $this->setUrlParameters($this->url() . "/" . ltrim($uri, "/"), $appendVersion ? array_replace([ 'v' => $this->assetToken ], $parameters) : $parameters); } - public function buildAssetUrl(string $uri, array $parameters = []) : string + public function buildAssetUrl(string $uri, array $parameters = [], bool $appendVersion = true) : string { - return $this->buildUrl($uri, array_replace([ 'v' => $this->assetToken ], $parameters)); + return $this->buildUrl($uri, $parameters, $appendVersion); } - public function buildRouteUrl(string $name, array $parameters = []) : string + public function buildRouteUrl(string $name, array $parameters = [], bool $appendVersion = false) : string { if ( false !== ( $route = $this->routes[$name] ?? false ) ) { - return $this->buildUrl($this->prepareRoute($route['route'], $parameters), $parameters); + return $this->buildUrl($this->prepareRoute($route['route'], $parameters), $parameters, $appendVersion); } $routeList = json_encode($this->routes, \JSON_PRETTY_PRINT); @@ -100,9 +123,14 @@ class UrlExtension implements Extension { return $this->scheme() . $this->domain() . $this->base(); } + # src: https://stackoverflow.com/a/14550919 public static function slug(string $text, string $separator = '-') : string { - return str_replace('-', $separator, \Transliterator::createFromRules(':: Any-Latin;:: NFD;:: [:Nonspacing Mark:] Remove;:: NFC;:: [:Punctuation:] Remove;:: Lower();[:Separator:] > \'-\'')->transliterate(str_replace('-', ' ', $text))); + $clean = iconv('UTF-8', 'ASCII//TRANSLIT', $text); + $clean = preg_replace("/[^a-zA-Z0-9\/_| -]/", '', $clean); + + return preg_replace("/[\/_| -]+/", $separator, strtolower(trim($clean, '-'))); + } public function registerRoute(string $name, string $route, string $class, string $method, array $routeMethods) : void @@ -174,12 +202,12 @@ class UrlExtension implements Extension { $_SERVER['Front-End-Https'] ?? "", $_SERVER['X-Forwarded-Proto'] ?? "", $_SERVER['X-Forwarded-Protocol'] ?? "", - $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? "", + $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? "", $_SERVER['HTTP_X_FORWARDED_PROTOCOL'] ?? "", ])) || isset($_SERVER['HTTP_X_ARR_SSL']); return $https - || ( "443" === ( $_SERVER['SERVER_PORT'] ?? "" ) ) + || ( "443" === ( $_SERVER['SERVER_PORT'] ?? "" ) ) || ( "443" === ( $_SERVER['HTTP_X_FORWARDED_PORT'] ?? "" ) ) || ( "off" !== ( strtolower($_SERVER['HTTPS'] ?? $_SERVER['HTTP_X_FORWARDED_SSL'] ?? $_SERVER['X-Forwarded-Ssl'] ?? "off")) ); } @@ -193,7 +221,7 @@ class UrlExtension implements Extension { list($variable, $default) = explode('=', $item[1]); } elseif (strpos($item[1], ":") !== false) { - list($variable, $type) = explode(':', $item[1]); + list($variable, ) = explode(':', $item[1]); } else { $variable = $item[1]; @@ -201,7 +229,6 @@ class UrlExtension implements Extension { if ( array_key_exists($variable, $arguments) ) { $value = $arguments[ $variable ]; - unset($arguments[ $variable ]); } else { @@ -209,6 +236,7 @@ class UrlExtension implements Extension { } $search[ $item[0] ] = $value; + } $route = str_replace(array_keys($search), array_values($search), $route); @@ -226,5 +254,4 @@ class UrlExtension implements Extension { return $route; } - } diff --git a/src/FileFetcher.php b/src/FileFetcher.php index ec4b760..46e87ff 100644 --- a/src/FileFetcher.php +++ b/src/FileFetcher.php @@ -2,6 +2,8 @@ namespace Picea; +use RecursiveIteratorIterator, RecursiveDirectoryIterator; + class FileFetcher { protected array $folderList = []; @@ -12,13 +14,13 @@ class FileFetcher public function __construct(?array $folderList = null) { if ( $folderList !== null ) { - $this->folderList = $folderList; + $this->addFolders($folderList); } } public function addFolder(string $folder, int $order = 100) : void { - $folder = rtrim($folder, DIRECTORY_SEPARATOR); + $folder = $this->normalizeFolder($folder); $this->folderList[$folder] = [ 'path' => $folder, @@ -28,7 +30,14 @@ class FileFetcher public function addFolders(array $folderList) : void { - $this->folderList = array_replace($this->folderList, $folderList); + foreach($folderList as $folder) { + $this->addFolder($folder['path'], $folder['order']); + } + } + + protected function normalizeFolder(string $path) : string + { + return rtrim($path, '/\\'); } public function folderList(?array $set = null) : ?array @@ -40,7 +49,7 @@ class FileFetcher { usort($this->folderList, fn($a, $b) => $a['order'] <=> $b['order']); - foreach($this->folderList as $folder) { + foreach($this->folderList() as $folder) { foreach($this->supportedExtensionList as $extension) { $file = $folder['path'] . DIRECTORY_SEPARATOR . "$fileName.$extension"; $file = str_replace([ '\\', '/' ], DIRECTORY_SEPARATOR, $file); @@ -56,7 +65,7 @@ class FileFetcher } # Fallback on full-path - foreach($this->folderList as $folder) { + foreach($this->folderList() as $folder) { $file = $folder['path'] . DIRECTORY_SEPARATOR . $fileName; $file = str_replace([ '\\', '/' ], DIRECTORY_SEPARATOR, $file); @@ -72,6 +81,30 @@ class FileFetcher throw new \RuntimeException("Given view file `$fileName` can not be found within given folder list.."); } + public function getFileList() : array + { + usort($this->folderList, fn($a, $b) => $a['order'] <=> $b['order']); + + $list = []; + + foreach($this->folderList() as $folder) { + $path = $folder['path'] . "/"; + $list[$path] = []; + + if ( \file_exists($path) ) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST, RecursiveIteratorIterator::CATCH_GET_CHILD); + + foreach ($iterator as $file) { + if ($file->isFile() && in_array($file->getExtension(), $this->supportedExtensionList)) { + $list[$path][] = $file; + } + } + } + } + + return $list; + } + public function getFilePath(string $fileName) : string { return $this->findFile($fileName); @@ -81,4 +114,5 @@ class FileFetcher { return file_get_contents($this->getFilePath($fileName)); } + } diff --git a/src/Language/DefaultRegistrations.php b/src/Language/DefaultRegistrations.php index cd5166a..3719ad7 100644 --- a/src/Language/DefaultRegistrations.php +++ b/src/Language/DefaultRegistrations.php @@ -28,7 +28,6 @@ class DefaultRegistrations implements LanguageRegistration public function registerSyntax(Compiler $compiler) : void { - $compiler->registerSyntax(new \Picea\Syntax\PhpTagToken()); $compiler->registerSyntax(new \Picea\Syntax\CommentToken()); $compiler->registerSyntax(new \Picea\Syntax\EchoRawToken()); $compiler->registerSyntax(new \Picea\Syntax\EchoSafeToken()); @@ -45,6 +44,7 @@ class DefaultRegistrations implements LanguageRegistration $compiler->registerControlStructure(new \Picea\ControlStructure\IfToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\ForeachToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\ForToken()); + $compiler->registerControlStructure(new \Picea\ControlStructure\WhileToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\OrToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\SwitchToken()); $compiler->registerControlStructure(new \Picea\ControlStructure\DefaultToken()); diff --git a/src/Method/Request.php b/src/Method/Request.php index 1cfff22..ed41b9d 100644 --- a/src/Method/Request.php +++ b/src/Method/Request.php @@ -7,10 +7,11 @@ use Picea\Extension\Extension, use Picea\Compiler\Context; +use Picea\Extension\FunctionExtension; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; -class Request implements Extension { +class Request implements Extension, FunctionExtension { use ExtensionTrait; public array $tokens; @@ -21,20 +22,21 @@ class Request implements Extension { public function __construct(ServerRequestInterface $request, Context $context) { $this->request = $request; - $this->register($context); } - public function parse(/*\Picae\Compiler\Context*/ &$context, ?string $arguments, string $token) : string { } + public function parse(\Picea\Compiler\Context &$context, ?string $arguments, string $token) : string { } - public function register(Context $context) : void + public function exportFunctions(): array { - $context->pushFunction("cookie", [ $this, 'cookie' ]); - $context->pushFunction("get", [ $this, 'get' ]); - $context->pushFunction("post", [ $this, 'post' ]); - $context->pushFunction("request", [ $this, 'request' ]); - $context->pushFunction("server", [ $this, 'server' ]); + return [ + "cookie" => [ $this, 'cookie' ], + "get" => [ $this, 'get' ], + "post" => [ $this, 'post' ], + "request" => [ $this, 'request' ], + "server" => [ $this, 'server' ], + ]; } - + public function cookie(? string $variableName = null, $default = null) { return $variableName === null ? $this->request->getCookieParams() : static::arrayGet($this->request->getCookieParams(), $variableName) ?? $default; diff --git a/src/Picea.php b/src/Picea.php index 201ced9..5879360 100644 --- a/src/Picea.php +++ b/src/Picea.php @@ -20,8 +20,6 @@ class Picea implements LanguageRegistration public string $builderTemplatePath; - public Closure $responseHtml; - public Caching\Cache $cache; public FileFetcher $fileFetcher; @@ -35,7 +33,6 @@ class Picea implements LanguageRegistration public array $compiled = []; public function __construct( - ? Closure $responseHtml = null, ? Compiler\Context $context = null, ? Caching\Cache $cache = null, ? Compiler $compiler = null, @@ -44,7 +41,6 @@ class Picea implements LanguageRegistration ? string $builderTemplatePath = null, bool $debug = false ){ - $this->response = $responseHtml; $this->cache = $cache ?? new Caching\Memory(""); $this->context = $context ?? new Compiler\BaseContext(); $this->languageRegistration = $languageRegistration ?? new Language\DefaultRegistrations(); @@ -58,7 +54,7 @@ class Picea implements LanguageRegistration $this->renderContext($this->context); } - public function gatherTemplateObject(string $viewPath, array $variables = [], ? object $proxy = null) : ? object + public function gatherCompiledObject(string $viewPath, array $variables = [], ? object $proxy = null) : ? object { if ( null === $object = $this->fetchFromCache($viewPath, $this->globalVariables + $variables, $proxy) ) { throw new \RuntimeException("An error occured while trying to save a compiled template."); @@ -69,78 +65,47 @@ class Picea implements LanguageRegistration public function renderHtml(string $viewPath, array $variables = [], ?object $proxy = null) : ? string { + $object = $this->gatherCompiledObject($viewPath, $variables, $proxy); + try { - return call_user_func($this->gatherTemplateObject($viewPath, $variables, $proxy)); + return call_user_func($object); } catch(\Throwable $ex) { - # Temporary class for an experiment - throw $ex; - throw new class($object, $this, "An error occurred trying to render HTML view `$viewPath` : " . $ex->getMessage(), 911, $ex) extends \Exception { - - protected Picea $picea; - - public function __construct(object $compiledObject, Picea $picea, string $message, int $code, \Throwable $previous) - { - parent::__construct($message, $code, $previous); - - $this->picea = $picea; - - # $template = $this->getTemplateFile( $previous->getFile() ); - $this->defineError($previous, $compiledObject); - } - - protected function defineError(\Throwable $previous, object $compiledObject) : void - { - $loadedTemplates = array_flip($this->picea->loadedTemplateFile); - - foreach($previous->getTrace() as $trace) { - if ( isset($trace['file'], $loadedTemplates[$trace['file']]) ) { - $class = $loadedTemplates[ $trace['file'] ]; - - $content = include($trace['file']); - - $this->file = $content['view']; - $this->line = $class::getSourceLineFromException($trace['line']); - - return; - } - } - } - - protected function getTemplateFile(string $filePath) : ? array - { - $content = null; - - if ( is_array($content) && isset($content['classname'], $content['namespace'], $content['view'], $content['extends']) ) { - return $content; - } - - return null; - } - - }; + if (! $ex instanceof Exception\RenderHtmlException ) { + throw new Exception\RenderHtmlException($object, $this, "An error occurred trying to render HTML view `$viewPath` : " . $ex->getMessage(), 911, $ex); + } + else { + throw $ex; + } } + + exit(); } /** * Method used by Block and View tokens - * @param object $proxy - * @param string $viewPath - * @param array $variables - * @return type */ public function inlineHtml(? object $proxy, string $viewPath, array $variables) { return $this->renderHtml($viewPath, $this->globalVariables + $variables, $proxy); } + /** + * Allows block to be called from templates + */ public function inlineBlock(? object $proxy, string $viewPath, ... $variables) { return $this->renderHtml($viewPath, [ 'inlineVariables' => $variables, 'globalVariables' => $this->globalVariables ], $proxy); } - + + /** + * Push view content inline + */ public function inlineContent(string $viewPath) { return $this->fileFetcher->getFileContent($viewPath); } - + + /** + * Renders a compile context + */ public function renderContext(Compiler\Context $context) : object { if ( null === $object = $this->contextFromCache($context) ) { diff --git a/src/Syntax/CommentToken.php b/src/Syntax/CommentToken.php index e895023..25f0bef 100644 --- a/src/Syntax/CommentToken.php +++ b/src/Syntax/CommentToken.php @@ -10,7 +10,7 @@ class CommentToken implements Syntax { protected string $tokenClose = "\#\}"; - public function parse(/*\Picae\Compiler\Context*/ &$context, string &$sourceCode) + public function parse(\Picea\Compiler\Context &$context, string &$sourceCode) { $sourceCode = preg_replace("#({$this->tokenOpen})(.*?)({$this->tokenClose})#s", "", $sourceCode); } diff --git a/src/Syntax/EchoRawToken.php b/src/Syntax/EchoRawToken.php index 0a92328..da25c86 100644 --- a/src/Syntax/EchoRawToken.php +++ b/src/Syntax/EchoRawToken.php @@ -8,7 +8,7 @@ class EchoRawToken implements Syntax { public string $tokenClose = "\}\}"; - public function parse(/*\Picae\Compiler\Context*/ &$content, string &$sourceCode) + public function parse(\Picea\Compiler\Context &$content, string &$sourceCode) { $sourceCode = preg_replace_callback("#({$this->tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) { $line = trim($matches[2], " \t\n\r\0\x0B;"); diff --git a/src/Syntax/EchoSafeToken.php b/src/Syntax/EchoSafeToken.php index 1b832b4..4da643e 100644 --- a/src/Syntax/EchoSafeToken.php +++ b/src/Syntax/EchoSafeToken.php @@ -18,12 +18,12 @@ class EchoSafeToken implements Syntax { $this->encoding = ini_get("default_charset"); } - public function parse(/*\Picae\Compiler\Context*/ &$context, string &$sourceCode) + public function parse(\Picea\Compiler\Context &$context, string &$sourceCode) { $sourceCode = preg_replace_callback("#({$this->tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) { $line = trim($matches[2], " \t\n\r\0\x0B;"); - return "flag}, '{$this->encoding}', " . ($this->doubleEncode ? "true" : "false") . ") ?>"; + return "flag}, '{$this->encoding}', " . ($this->doubleEncode ? "true" : "false") . ") ?>"; }, $sourceCode); } diff --git a/src/Syntax/PhpTagToken.php b/src/Syntax/PhpTagToken.php deleted file mode 100644 index 3dc0c6a..0000000 --- a/src/Syntax/PhpTagToken.php +++ /dev/null @@ -1,18 +0,0 @@ -tokenOpen})(.*?)({$this->tokenClose})#s", function ($matches) { - return ""; - }, $sourceCode); - } - -} diff --git a/src/Syntax/Syntax.php b/src/Syntax/Syntax.php index fe1992f..1cf661b 100644 --- a/src/Syntax/Syntax.php +++ b/src/Syntax/Syntax.php @@ -3,5 +3,5 @@ namespace Picea\Syntax; interface Syntax { - public function parse(\Picae\Compiler\Context &$context, string &$sourceCode); + public function parse(\Picea\Compiler\Context &$context, string &$sourceCode); }