├── .gitignore ├── demo ├── footer_inner.html ├── footer.html ├── demo.php └── demo.html ├── .travis.yml ├── composer.json ├── licence ├── tests └── TonicTest.php ├── README.md └── src └── Tonic.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/** 2 | composer.lock -------------------------------------------------------------------------------- /demo/footer_inner.html: -------------------------------------------------------------------------------- 1 | by {$user.name} 2 | -------------------------------------------------------------------------------- /demo/footer.html: -------------------------------------------------------------------------------- 1 |
Tonic© {$now.date("Y")}
2 | {include footer_inner.html} 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | 10 | matrix: 11 | fast_finish: true 12 | allow_failures: 13 | - php: 7.0 14 | 15 | script: 16 | - phpunit tests/TonicTest 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgamba/tonic", 3 | "type": "library", 4 | "description": "Super fast and powerful PHP template engine", 5 | "keywords": ["php","template","template engine"], 6 | "homepage": "https://github.com/rgamba/tonic", 7 | "license": "BSD", 8 | "authors": [ 9 | { 10 | "name": "Ricardo Gamba", 11 | "email": "rgamba@gmail.com", 12 | "homepage": "http://github.com/rgamba", 13 | "role": "Lead" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=5.3.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Tonic\\": "src" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /licence: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Ricardo Gamba 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Tonic nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /demo/demo.php: -------------------------------------------------------------------------------- 1 | @date_create(), 14 | "context" => array( 15 | "post" => $_POST, 16 | "get" => $_GET 17 | ) 18 | )); 19 | // Create a custom modifier 20 | Tonic::extendModifier("myModifier",function($input, $prepend, $append = ""){ 21 | // $input will hold the current variable value, it's mandatory that your lambda 22 | // function has an input receiver, all other arguments are optional 23 | // We can perform input validations 24 | if(empty($prepend)) { 25 | throw new \InvalidArgumentException("prepend is required"); 26 | } 27 | return $prepend . $input . $append; 28 | }); 29 | 30 | $tpl = new Tonic("demo.html"); 31 | 32 | // Uncomment the following 2 lines to enable caching 33 | //$tpl->enable_content_cache = true; 34 | //$tpl->cache_dir = './cache/'; 35 | // Assign a variable to the template 36 | $tpl->user_role = "member"; 37 | 38 | // Another method to assign variables: 39 | $tpl->assign("currency","USD"); 40 | 41 | // Assign arrays to the template 42 | $tpl->user = array( 43 | "name" => "Ricardo", 44 | "last_name" => "Gamba", 45 | "email" => "rgamba@gmail.com", 46 | "extra" => "This is a large description of the user" 47 | ); 48 | 49 | // Assign a more complex array 50 | $tpl->users = array( 51 | array( 52 | "name" => "rocio 'lavin'", 53 | "email" => "rlavin@gmail.com", 54 | "role" => "admin" 55 | ), 56 | array( 57 | "name" => "roberto lopez", 58 | "email" => "rlopex@gmail.com", 59 | "role" => "member" 60 | ), 61 | array( 62 | "name" => "rodrigo gomez", 63 | "email" => "rgomez@gmail.com", 64 | "role" => "member" 65 | ) 66 | ); 67 | 68 | $tpl->number = 10; 69 | 70 | $tpl->js = '{"name" : "Ricardo", "last_name": "Gamba"}'; 71 | $tpl->array = array( 72 | "name" => "Ricardo", 73 | "last_name" => "Gamba" 74 | ); 75 | $tpl->js_text = "Ricardo"; 76 | $tpl->ilegal_js = "javascript: alert('Hello');"; 77 | 78 | // Render the template 79 | echo $tpl->render(); 80 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | 20 | 26 | 27 |

Simple variables

28 | Link with href context 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Name: {$user.name}
Last name: {$user.last_name}
Email: {$user.email}
41 | 42 |
{$js}
43 | 44 |

Context

45 | 46 |

Print 'test' GET variable: {$context.get.test.default("test variable not set")}

47 | 48 |

Easy way to check if variables are set or empty

49 | 50 | {if $context.get.test} 51 |

Context GET variable EXISTS

52 | {else} 53 |

Context GET variables IS EMPTY

54 | {endif} 55 |

Modifiers

56 | 57 |

Date management: {$now.toLocal().date("Y-m-d h:i a")}

58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
Name: {$user.name.upper()}
Nickname: {$user.name.lower().if("admin", "administrator", $user_role).capitalize()}
Extra: {$user.extra.truncate(20)}
70 | 71 |

Modifiers with errors

72 | 73 |

Can't sha1() a date object: {$now.sha1()}

74 |

Or when missing a required parameter: {$user_role.truncate()}

75 |

Custom modifier

76 |

{$user.name.myModifier("Hello ", " goodbye")}

77 | 78 |

Control structures

79 | 80 |

If conditionals

81 | 82 | {if $user_role.upper() eq "ADMIN"} 83 |

Welcome Administrator

84 | {elseif $user_role.upper() eq "MEMBER" or $user_role.upper() eq "TEAM"} 85 |

Welcome member of the community

86 | {else} 87 |

You are not a registered user

88 | {endif} 89 | 90 |

Which is the same as doing it with macros

91 | 92 |

Welcome Administrator

93 |

Welcome member of the community

94 |

You are not a registered user

95 | 96 |

Loops

97 | 104 |
105 |

There are no users

106 |
107 | 108 | {include footer.html} 109 | 110 | -------------------------------------------------------------------------------- /tests/TonicTest.php: -------------------------------------------------------------------------------- 1 | 'Ricardo', 8 | 'last_name' => 'Gamba', 9 | 'array' => array( 10 | 'one' => 'first item string', 11 | 'two' => 2 12 | ), 13 | 'json_string' => '{"name" : "Ricardo", "last_name": "Gamba"}', 14 | 'inline_js' => "javascript: alert('Hello');", 15 | 'empty' => '', 16 | 'ten' => 10, 17 | 'users' => array( 18 | array( 19 | "name" => "rocio 'lavin'", 20 | "email" => "rlavin@gmail.com", 21 | "role" => "admin" 22 | ), 23 | array( 24 | "name" => "roberto lopez", 25 | "email" => "rlopex@gmail.com", 26 | "role" => "member" 27 | ), 28 | array( 29 | "name" => "rodrigo gomez", 30 | "email" => "rgomez@gmail.com", 31 | "role" => "member" 32 | ) 33 | ) 34 | ); 35 | 36 | public function __construct(){ 37 | $this->vars['object'] = (object)$this->vars['array']; 38 | $this->vars['date'] = date_create('2016-11-30'); 39 | } 40 | 41 | public function testVariables() { 42 | $tpl = array( 43 | array('

{$name}

', '

Ricardo

'), 44 | array('

{ $last_name }

', '

Gamba

'), 45 | array('

{$array.one}

', '

first item string

'), 46 | array('

{$object->one}

', '

first item string

'), 47 | ); 48 | foreach($tpl as $i => $t) { 49 | $template = new Tonic(); 50 | $template->loadFromString($t[0])->setContext($this->vars); 51 | $this->assertEquals($t[1], $template->render()); 52 | } 53 | } 54 | 55 | public function testModifiers() { 56 | // TODO: Test all modifiers 57 | Tonic::extendModifier("myModifier",function($input, $prepend, $append = ""){ 58 | if(empty($prepend)) { 59 | throw new \InvalidArgumentException("prepend is required"); 60 | } 61 | return $prepend . $input . $append; 62 | }); 63 | $tpl = array( 64 | array('

{$name.upper()}

', '

RICARDO

'), 65 | array('

{$array.one.capitalize().truncate(10)}

', '

First Item...

'), 66 | array('

{$empty.default("No value")}

', '

No value

'), 67 | array('

{$date.date("d-m-Y")}

', '

30-11-2016

'), 68 | array('

{$name.myModifier("Mr. ")}

', '

Mr. Ricardo

') 69 | ); 70 | foreach($tpl as $i => $t) { 71 | $template = new Tonic(); 72 | $template->loadFromString($t[0])->setContext($this->vars); 73 | $this->assertEquals($t[1], $template->render()); 74 | } 75 | } 76 | 77 | public function testIf() { 78 | $tpl = array( 79 | array('{if $name eq "Ricardo"}YES{else}NO{endif}', 'YES'), 80 | array('{if $name.lower() == "ricardo"}YES{else}NO{endif}', 'YES'), 81 | array('{ if $name.lower() == "ricardo" }YES{ else }NO{ endif }', 'YES'), 82 | array('{ if $name == $last_name }YES{ else }NO{ endif }', 'NO') 83 | ); 84 | foreach($tpl as $i => $t) { 85 | $template = new Tonic(); 86 | $template->loadFromString($t[0])->setContext($this->vars); 87 | $this->assertEquals($t[1], $template->render()); 88 | } 89 | } 90 | 91 | public function testIfMacros() { 92 | $tpl = array( 93 | array('

YES

', '

YES

'), 94 | array('

YES

', '

YES

'), 95 | array('

YES

', ''), 96 | array('

YES

', '') 97 | ); 98 | foreach($tpl as $i => $t) { 99 | $template = new Tonic(); 100 | $template->loadFromString($t[0])->setContext($this->vars); 101 | $this->assertEquals($t[1], $template->render()); 102 | } 103 | } 104 | 105 | public function testLoops() { 106 | $tpl = array( 107 | array('{loop $i, $user in $users}{$i}:{$user.name}
{endloop}', '0:rocio 'lavin'
1:roberto lopez
2:rodrigo gomez
'), 108 | array('{loop $u in $users}{$u.name}
{endloop}', 'rocio 'lavin'
roberto lopez
rodrigo gomez
') 109 | ); 110 | 111 | foreach($tpl as $i => $t) { 112 | $template = new Tonic(); 113 | $template->loadFromString($t[0])->setContext($this->vars); 114 | $this->assertEquals($t[1], $template->render()); 115 | } 116 | } 117 | 118 | public function testLoopsMacros() { 119 | $tpl = array( 120 | array('
  • {$i}:{$user.name}
  • ', '
  • 0:rocio 'lavin'
  • 1:roberto lopez
  • 2:rodrigo gomez
  • '), 121 | array('
  • {$u.name}
  • ', '
  • rocio 'lavin'
  • roberto lopez
  • rodrigo gomez
  • ') 122 | ); 123 | 124 | foreach($tpl as $i => $t) { 125 | $template = new Tonic(); 126 | $template->loadFromString($t[0])->setContext($this->vars); 127 | $this->assertEquals($t[1], $template->render()); 128 | } 129 | } 130 | 131 | public function testContextAwareness() { 132 | $tpl = array( 133 | array('

    Test

    ', '

    Test

    '), 134 | array('
    {$inline_js}
    ', '
    javascript: alert('Hello');
    '), 135 | array('Click', 'Click'), 136 | array('Link', 'Link'), 137 | array('

    {$inline_js.ignoreContext()}

    ', '

    javascript: alert(\'Hello\');

    ') 138 | ); 139 | 140 | foreach($tpl as $i => $t) { 141 | $template = new Tonic(); 142 | $template->loadFromString($t[0])->setContext($this->vars); 143 | $this->assertEquals($t[1], $template->render()); 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tonic 2 | 3 | [![Build Status](https://travis-ci.org/rgamba/tonic.svg?branch=master)](https://travis-ci.org/rgamba/tonic) 4 | 5 | Super fast and powerful template engine. Pure PHP, zero dependencies. 6 | 7 | ## Usage 8 | Using Tonic is pretty straight forward. 9 | ```php 10 | use Tonic\Tonic; 11 | $tpl = new Tonic("demo.html"); 12 | $tpl->user_role = "member"; 13 | echo $tpl->render(); 14 | ``` 15 | It's also very flexible. The above code can also be written like: 16 | ```php 17 | $tpl = new Tonic(); 18 | echo $tpl->load("demo.html")->assign("user_role","member")->render(); 19 | ``` 20 | ## Show me the syntax 21 | Using Tonic 22 | ```html 23 | 24 |

    Welcome {$user.name.capitalize().truncate(50)}

    25 | User role: {$role.lower().if("admin","administrator").capitalize()} 26 | 27 | ``` 28 | vs. writting all in PHP 29 | ```html 30 | 31 |

    Welcome 50 ? substr(ucwords($user["name"]),0,50)."..." : ucwords($user["name"])) ?>

    32 | User role: 33 | 34 | ``` 35 | ## Installation 36 | Install using composer 37 | ``` 38 | $ composer require rgamba/tonic 39 | ``` 40 | ## Caching 41 | All tonic templates are compiled back to native PHP code. It's highly recommended that you use the caching functionality so that the same template doesn't need to be compiled over and over again increasing the CPU usage on server side. 42 | ```php 43 | $tpl = new Tonic(); 44 | $tpl->cache_dir = "./cache/"; // Be sure this directory exists and has writing permissions 45 | $tpl->enable_content_cache = true; // Importante to set this to true! 46 | ``` 47 | ## Modifiers 48 | Modifiers are functions that modify the output variable in various ways. All modifiers must be preceded by a variable and can be chained with other modifiers. Example: 49 | ```html 50 | {$variable.modifier1().modifier2().modifier3()} 51 | ``` 52 | We can also use modifiers in the same way when using associative arrays: 53 | ```html 54 | {$my_array.item.sub_item.modifier1().modifier2().modifier3()} 55 | ``` 56 | ## Working with dates 57 | It's easy to handle and format dates inside a Tonic template. 58 | ```php 59 | Tonic::$local_tz = 'America/New_york'; // Optionaly set the user's local tz 60 | $tpl = new Tonic(); 61 | $tpl->my_date = date_create(); 62 | ``` 63 | And the template 64 | ```html 65 |

    Today is {$my_date.date("Y-m-d h:i a")}

    66 | ``` 67 | Working with timezones 68 | ```html 69 |

    The local date is {$my_date.toLocal().date("Y-m-d h:i a")}

    70 | ``` 71 | Which will render `$my_date` to the timezone configured in ` Tonic::$local_tz` 72 | ### Custom timezone 73 | ```html 74 |

    The local date is {$my_date.toTz("America/Mexico_city").date("Y-m-d h:i a")}

    75 | ``` 76 | ### List of modifiers 77 | 78 | Name | Description 79 | --- | --- 80 | `upper()` | Uppercase 81 | `lower()` | Lowercase 82 | `capitalize()` | Capitalize words (ucwords) 83 | `abs()` | Absolute value 84 | `truncate(len)` | Truncate and add "..." if string is larger than "len" 85 | `count()` | Alias to count() 86 | `length()` | alias to count() 87 | `date(format)` | Format date like date(format) 88 | `nl2br()` | Alias to nl2br 89 | `stripSlashes()` | Alias to stripSlashes() 90 | `sum(value)` | Sums value to the current variable 91 | `substract(value)` | Substracts value to the current variable 92 | `multiply(value)` | Multiply values 93 | `divide(value)` | Divide values 94 | `addSlashes()` | Alias of addSlashes() 95 | `encodeTags()` | Encode the htmls tags inside the variable 96 | `decodeTags()` | Decode the tags inside the variable 97 | `stripTags()` | Alias of strip_tags() 98 | `urldecode()` | Alias of urldecode() 99 | `trim()` | Alias of trim() 100 | `sha1()` | Returns the sha1() of the variable 101 | `numberFormat(decimals)` | Alias of number_format() 102 | `lastIndex()` | Returns the last array's index of the variable 103 | `lastValue()` | Returns the array's last element 104 | `jsonEncode()` | Alias of json_encode() 105 | `replace(find,replace)` | Alias of str_replace() 106 | `default(value)` | In case variable is empty, assign it value 107 | `ifEmpty(value [,else_value])` | If variable is empty assign it value, else if else_value is set, set it to else_value 108 | `if(value, then_value [,else_value [,comparisson_operator]] )` | Conditionally set the variable's value. All arguments can be variables 109 | `preventTagEncode()` | If ESCAPE_TAGS_IN_VARS = true, this prevents the variable's value to be encoded 110 | 111 | ### Creating custom modifiers 112 | If you need a custom modifier you can extend the list and create your own. 113 | ```php 114 | // This function will only prepend and append some text to the variable 115 | Tonic::extendModifier("myFunction",function($input, $prepend, $append = ""){ 116 | // $input will hold the current variable value, it's mandatory that your lambda 117 | // function has an input receiver, all other arguments are optional 118 | // We can perform input validations 119 | if(empty($prepend)) { 120 | throw new \Exception("prepend is required"); 121 | } 122 | return $prepend . $input . $append; 123 | }); 124 | ``` 125 | And you can easily use this modifier: 126 | ```html 127 |

    {$name.myFunction("hello "," goodbye")}

    128 | ``` 129 | ### Anonymous modifiers 130 | Sometimes you just need to call functions directly from inside the template whose return value is constantly changing and therefore it can't be linked to a static variable. Also, it's value is not dependant on any variable. In those cases you can use anonymous modifiers. 131 | To do that, you need to create a custom modifier, IGNORE the `$input` parameter in case you need to use other parameters. 132 | ```php 133 | Tonic::extendModifier("imagesDir", function($input){ 134 | // Note that $input will always be empty when called this modifier anonymously 135 | return "/var/www/" . $_SESSION["theme"] . "/images"; 136 | }); 137 | ``` 138 | Then you can call it directly from the template 139 | ```html 140 | 141 | ``` 142 | ## Context awareness 143 | Tonic prevents you from escaping variables in your app that could led to possible attacks. Each variable that's going to be displayed to the user should be carefully escaped, and it sould be done acoardingly to it's context. 144 | For example, a variable in a href attr of a link should be escaped in a different way from some variable in a javascript tag or a `

    ` tag. 145 | The good news is that tonic does all this work for you. 146 | ```php 147 | $tonic->assign("array",array("Name" => "Ricardo", "LastName", "Gamba")); 148 | $tonic->assign("ilegal_js","javascript: alert('Hello');"); 149 | ``` 150 | And the HTML 151 | ```html 152 | Click me 153 | 154 |

    The ilegal js is: {$ilegal_js}

    155 | 156 | Valid link generated 157 | 158 |

    We can also ignore the context awareness: {$ilegal_js.ignoreContext()}

    159 | ``` 160 | ## Include templates 161 | You can include a template inside another template 162 | ```html 163 | {include footer.html} 164 | ``` 165 | We can also fetch and external page and load it into our current template 166 | ```html 167 | {include http://mypage.com/static.html} 168 | ``` 169 | Templates includes support nested calls, but beware that infinite loop can happen in including a template "A" inside "A" template. 170 | ## Control structures 171 | ### If / else 172 | Making conditionals is very easy 173 | ```html 174 | {if $user.role eq "admin"} 175 |

    Hello admin

    176 | {elseif $user.role.upper() eq "MEMBER" or $user.role.upper() eq "CLIENT"} 177 |

    Hello member

    178 | {else} 179 |

    Hello guest

    180 | {endif} 181 | ``` 182 | You can use regular logic operators (==, !=, >, <, >=, <=, ||, &&) or you can use the following 183 | 184 | Operator | Equivalent 185 | --- | --- 186 | eq | == 187 | neq | != 188 | gt | > 189 | lt | < 190 | gte | >= 191 | lte | <= 192 | 193 | ### Loops 194 | ```html 195 | 200 | ``` 201 | Or if the array key is needed 202 | ```html 203 | 208 | ``` 209 | ### Working with macros 210 | Both if structures and loop constructs can be written in a more HTML-friendly way so your code can be more readable. Here's an example: 211 | ```html 212 | 215 | ``` 216 | Which is exactly the same as: 217 | ```html 218 | {if $users} 219 | 224 | {endif} 225 | ``` 226 | 227 | ## Template inheritance 228 | Tonic supports single template inheritance. The idea behind this is to keep things nice and simple. Multiple inheritance can lead to complicated views difficult to maintain. 229 | 230 | In Tonic, template inheritance is based on `blocks`. Suppose we have the following base template: 231 | 232 | **base.html** 233 | ```html 234 | 235 | 236 | Tonic 237 | 238 | 239 |
    240 |

    Default welcome message!

    241 |
    242 |
    243 |
    244 |

    This is the default content.

    245 |
    246 |
    247 |
    Tonic 2016
    248 | 249 | ``` 250 | 251 | Then you have several partial templates or views and you would like to reuse the main `base.html` as a "skeleton". To do that, we work with `blocks`. 252 | Each block is defined by the tag `{block name}{endblock}` and/or by the html attribute `tn-block="name"` which effectively encloses the HTML element with the attibute as the block with the name __name__. 253 | 254 | **inner.html** 255 | ```html 256 | { extends base.html } 257 |
    258 |

    Welcome to my inner page!

    259 |
    260 |

    This content WON´T be rendered at all!

    261 |
    262 |

    This is the content of my inner view. 263 |

    264 | ``` 265 | 266 | As a result we will have the following view: 267 | ```html 268 | 269 | 270 | Tonic 271 | 272 | 273 |
    274 |

    Welcome to my inner page!

    275 |
    276 |
    277 |
    278 |

    This is the content of my inner view. 279 |

    280 |
    281 |
    Tonic 2016
    282 | 283 | ``` 284 | 285 | There are several keys here: 286 | * The `{ extend }` tag. Which first and only argument should be the template file relative to `Tonic::$root` (by default `./`). 287 | * The `tn-block="header"` html attribute that defines the block and is enclosed by the closing matching tag of the HTML element. 288 | * All the blocks found in the child template (inner.html) will effectively replace the matching blocks on the parent template (base.html). If there is a block in the child template that is not defined in the parent template, **that block won´t be rendered at all**. 289 | * Block names must only by alphanumerical with and must not contain `$` or any special characters or spaces. 290 | * The parent template (base.html) inherits the context (scope, variables) of the child template. 291 | * You can only extend 1 template. 292 | 293 | **NOTE** It is also possible to define blocks using the `{block header}{endblock}` notation. We prefer to use HTML attributes as it is cleaner. 294 | Example: 295 | ```html 296 | {block myBlock}

    Welcome

    {endblock} 297 | ``` 298 | is exactly the same as: 299 | ```html 300 |

    Welcome

    301 | ``` 302 | 303 | 304 | ## Changelog 305 | * 11-10-2016 - 3.1.0 - Added support for template inheritance 306 | * 25-03-2015 - 3.0.0 - Added Context Awareness and Maco Syntax for ifs and loops 307 | * 23-03-2015 - 2.2.0 - Added namespace support and added modifier exceptions 308 | * 20-03-2015 - 2.1.0 - Added the option to extend modifiers. 309 | * 19-03-2015 - 2.0.0 - IMPORTANT update. The syntax of most structures has changed slightly, it's not backwards compatible with previous versions. 310 | -------------------------------------------------------------------------------- /src/Tonic.php: -------------------------------------------------------------------------------- 1 | 8 | * @license BSD 3-Clause License 9 | */ 10 | namespace Tonic; 11 | 12 | class Tonic{ 13 | /** 14 | * Enable context awareness 15 | */ 16 | public static $context_aware = true; 17 | /** 18 | * Local timezone (to use with toLocal() modifier) 19 | */ 20 | public static $local_tz = 'GMT'; 21 | /** 22 | * Include path 23 | */ 24 | public static $root=''; 25 | /** 26 | * Enable template caching 27 | */ 28 | public static $enable_content_cache = false; 29 | /** 30 | * Caching directory (must have write permissions) 31 | */ 32 | public static $cache_dir = "cache/"; 33 | /** 34 | * Cache files lifetime 35 | */ 36 | public $cache_lifetime = 86400; 37 | /** 38 | * Default extension for includes 39 | */ 40 | public $default_extension='.html'; 41 | 42 | private $file; 43 | private $assigned=array(); 44 | private $output=""; 45 | private $source; 46 | private $content; 47 | private $or_content; 48 | private $is_php = false; 49 | private $cur_context = null; 50 | private static $modifiers = null; 51 | private static $globals=array(); 52 | private $blocks = array(); 53 | private $blocks_override = array(); 54 | private $base = null; 55 | 56 | /** 57 | * Object constructor 58 | * @param $file template file to load 59 | */ 60 | public function __construct($file=NULL){ 61 | self::initModifiers(); 62 | if(!empty($file)){ 63 | $this->file=$file; 64 | $this->load(); 65 | } 66 | } 67 | 68 | /** 69 | * Create a new custom modifier 70 | * @param Name of the modifier 71 | * @param Lambda function, modifier function 72 | */ 73 | public static function extendModifier($name, $func){ 74 | if(!empty(self::$modifiers[$name])) 75 | return false; 76 | if(!is_callable($func)) 77 | return false; 78 | self::$modifiers[$name] = $func; 79 | return true; 80 | } 81 | 82 | /** 83 | * Set the global environment variables for all templates 84 | * @param associative array with the global variables 85 | */ 86 | public static function setGlobals($g=array()){ 87 | if(!is_array($g)) 88 | return false; 89 | self::$globals=$g; 90 | } 91 | 92 | /** 93 | * Load the desired template 94 | * @param $file 95 | * @return 96 | */ 97 | public function load($file=NULL){ 98 | if($file!=NULL) 99 | $this->file=$file; 100 | if(empty($this->file)) return false; 101 | $ext = explode('.',$file); 102 | $ext = $ext[count($ext)-1]; 103 | if($ext == "php"){ 104 | $this->is_php = true; 105 | }else{ 106 | if(!file_exists(self::$root . $this->file)) { 107 | echo "tonic: unable to load file '".self::$root . $this->file."'"; 108 | return false; 109 | } 110 | $this->source=file_get_contents(self::$root . $this->file); 111 | $this->content=&$this->source; 112 | } 113 | $this->or_content = $this->content; 114 | return $this; 115 | } 116 | 117 | /** 118 | * Load from string instead of file 119 | * 120 | * @param mixed $str 121 | */ 122 | public function loadFromString($str){ 123 | $this->source=$str; 124 | $this->content=&$this->source; 125 | return $this; 126 | } 127 | 128 | /** 129 | * Assign value to a variable inside the template 130 | * @param $var 131 | * @param $val 132 | */ 133 | public function assign($var,$val){ 134 | $this->assigned[$var]=$val; 135 | return $this; 136 | } 137 | 138 | public function getContext(){ 139 | return $this->assigned; 140 | } 141 | 142 | /** 143 | * Magic method alias for self::assign 144 | * @param $k 145 | * @param $v 146 | */ 147 | public function __set($k,$v){ 148 | $this->assign($k,$v); 149 | } 150 | 151 | /** 152 | * Assign multiple variables at once 153 | * This method should always receive get_defined_vars() 154 | * as the first argument 155 | * @param get_defined_vars() 156 | * @return 157 | */ 158 | public function setContext($vars){ 159 | if(!is_array($vars)) 160 | return false; 161 | foreach($vars as $k => $v){ 162 | $this->assign($k,$v); 163 | } 164 | return $this; 165 | } 166 | 167 | /** 168 | * Return compiled template 169 | * @return 170 | */ 171 | public function render($replace_cache=false){ 172 | if($replace_cache) 173 | if(file_exists(self::$cache_dir.sha1($this->file))) 174 | unlink(self::$cache_dir.sha1($this->file)); 175 | if(!$this->is_php){ 176 | if(!$this->getFromCache()){ 177 | $this->assignGlobals(); 178 | $this->handleExtends(); 179 | $this->handleBlockMacros(); 180 | $this->handleBlocks(); 181 | $this->handleIncludes(); 182 | $this->handleIfMacros(); 183 | $this->handleLoopMacros(); 184 | $this->handleLoops(); 185 | $this->handleIfs(); 186 | $this->handleVars(); 187 | $this->compile(); 188 | } 189 | }else{ 190 | $this->renderPhp(); 191 | } 192 | if($this->base != null) { 193 | // This template has inheritance 194 | $parent = new Tonic($this->base); 195 | $parent->setContext($this->assigned); 196 | $parent->overrideBlocks($this->blocks); 197 | return $parent->render(); 198 | } 199 | 200 | return $this->output; 201 | } 202 | 203 | /** 204 | * For internal use only for template inheritance. 205 | */ 206 | public function overrideBlocks($blocks) { 207 | $this->blocks_override = $blocks; 208 | } 209 | 210 | /** 211 | * Backwards compatibility for cache. 212 | */ 213 | public function __get($var) { 214 | switch($var) { 215 | case 'enable_content_cache': 216 | // Backwards compatibility support 217 | return self::$enable_content_cache; 218 | break; 219 | case 'cache_dir': 220 | return self::$cache_dir; 221 | break; 222 | default: 223 | throw new \Exception("Tried to access invalid property " . $var); 224 | } 225 | } 226 | 227 | private function getFromCache(){ 228 | if(self::$enable_content_cache!=true || !file_exists(self::$cache_dir.sha1($this->file))) 229 | return false; 230 | $file_expiration = filemtime(self::$cache_dir.sha1($this->file)) + (int)$this->cache_lifetime; 231 | if($file_expiration < time()){ 232 | unlink(self::$cache_dir.sha1($this->file)); 233 | return false; 234 | } 235 | $this->assignGlobals(); 236 | foreach($this->assigned as $var => $val) 237 | ${$var}=$val; 238 | ob_start(); 239 | include_once(self::$cache_dir.sha1($this->file)); 240 | $this->output=ob_get_clean(); 241 | return true; 242 | } 243 | 244 | private function renderPhp(){ 245 | $this->assignGlobals(); 246 | if(!file_exists($this->file)) 247 | die("TemplateEngine::renderPhp() - File not found (".$this->file.")"); 248 | ob_start(); 249 | Sys::get('module_controller')->includeView($this->file,$this->assigned); 250 | $this->output=ob_get_clean(); 251 | return true; 252 | } 253 | 254 | private function assignGlobals(){ 255 | self::$globals['__func'] = null; 256 | $this->setContext(self::$globals); 257 | } 258 | 259 | private function compile(){ 260 | foreach($this->assigned as $var => $val){ 261 | ${$var}=$val; 262 | } 263 | if(self::$enable_content_cache==true){ 264 | $this->saveCache(); 265 | } 266 | ob_start(); 267 | $e=eval('?>'.$this->content); 268 | $this->output=ob_get_clean(); 269 | if($e===false){ 270 | die("Error: ".$this->output."
    ".$this->content); 271 | } 272 | } 273 | 274 | private function saveCache(){ 275 | $file_name=sha1($this->file); 276 | $cache=@fopen(self::$cache_dir.$file_name,'w'); 277 | @fwrite($cache,$this->content); 278 | @fclose($cache); 279 | } 280 | 281 | private function removeWhiteSpaces($str) { 282 | $in = false; 283 | $escaped = false; 284 | $ws_string = ""; 285 | for($i = 0; $i <= strlen($str)-1; $i++) { 286 | $char = substr($str,$i,1); 287 | $je = false; 288 | $continue = false; 289 | switch($char) { 290 | case '\\': 291 | $je = true; 292 | $escaped = true; 293 | break; 294 | case '"': 295 | if(!$escaped) { 296 | $in = !$in; 297 | } 298 | break; 299 | case " ": 300 | if(!$in) { 301 | $continue = true; 302 | } 303 | break; 304 | } 305 | if (!$je) { 306 | $escaped = false; 307 | } 308 | if(!$continue) { 309 | $ws_string .= $char; 310 | } 311 | } 312 | return $ws_string; 313 | } 314 | 315 | private function handleIncludes(){ 316 | $matches=array(); 317 | preg_match_all('/\{\s*include\s*(.+?)\s*}/',$this->content,$matches); 318 | if(!empty($matches)){ 319 | foreach($matches[1] as $i => $include){ 320 | $include=trim($include); 321 | 322 | $include=explode(',',$include); 323 | $params=array(); 324 | if(count($include)>1){ 325 | $inc=$include[0]; 326 | unset($include[0]); 327 | foreach($include as $kv){ 328 | @list($key,$val)=@explode('=',$kv); 329 | $params[$key]=empty($val) ? true : $val; 330 | } 331 | $include=$inc; 332 | }else 333 | $include=$include[0]; 334 | 335 | if (substr($include,0,4) == "http") { 336 | $rep = file_get_contents($include); 337 | } else { 338 | ob_start(); 339 | $inc = new Tonic($include); 340 | $inc->setContext($this->assigned); 341 | $rep = $inc->render(); 342 | $err = ob_get_clean(); 343 | if(!empty($err)) 344 | $rep = $err; 345 | } 346 | $this->content=str_replace($matches[0][$i],$rep,$this->content); 347 | } 348 | } 349 | } 350 | 351 | private function getParams($params){ 352 | $i=0; 353 | $p=array(); 354 | $escaped=false; 355 | $in_str=false; 356 | $act=""; 357 | while($i$args[0]: ".$e->getMessage().""); 405 | } 406 | return $ret; 407 | } 408 | 409 | private function applyModifiers(&$var,$mod,$match = ""){ 410 | $context = null; 411 | if(self::$context_aware == true) { 412 | if(!empty($match) && !in_array("ignoreContext()",$mod)) { 413 | $context = $this->getVarContext($match, $this->cur_context); 414 | switch($context["tag"]){ 415 | default: 416 | if($context["in_tag"]){ 417 | array_push($mod,"contextTag(".$context["in_str"].")"); 418 | } else { 419 | array_push($mod,"contextOutTag()"); 420 | } 421 | break; 422 | case "script": 423 | array_push($mod, 'contextJs('.$context["in_str"].')'); 424 | break; 425 | } 426 | 427 | } 428 | } 429 | $this->cur_context = $context; 430 | if(count($mod) <= 0){ 431 | return; 432 | } 433 | $ov=$var; 434 | foreach($mod as $name){ 435 | $modifier=explode('(',$name,2); 436 | $name=$modifier[0]; 437 | $params=substr($modifier[1],0,-1); 438 | $params=$this->getParams($params); 439 | foreach(self::$modifiers as $_name => $mod) { 440 | if($_name != $name) 441 | continue; 442 | $ov = 'self::callModifier("'.$_name.'",'.$ov.(!empty($params) ? ',"'.implode('","',$params).'"' : "").')'; 443 | } 444 | continue; 445 | } 446 | $var=$ov; 447 | } 448 | 449 | private function getVarContext($str, $context = null){ 450 | if($context == null) { 451 | $cont = $this->content; 452 | $in_str = false; 453 | $str_char = ""; 454 | $in_tag = false; 455 | $prev_tag = ""; 456 | $prev_char = ""; 457 | } else { 458 | $cont = substr($this->content,$context['offset']); 459 | $in_str = $context["in_str"]; 460 | $str_char = $context["str_char"]; 461 | $in_tag = $context["in_tag"]; 462 | $prev_tag = $context["tag"]; 463 | $prev_char = $context["prev_char"]; 464 | } 465 | 466 | $i = strpos($cont, $str); 467 | if($i === false){ 468 | return false; 469 | } 470 | $escaped = false; 471 | $capturing_tag_name = false; 472 | $char = ""; 473 | for($j = 0; $j <= $i; $j++){ 474 | $prev_char = $char; 475 | $char = substr($cont, $j, 1); 476 | switch($char){ 477 | case "\\": 478 | $escaped = true; 479 | continue; 480 | break; 481 | case "'": 482 | case '"': 483 | if(!$escaped){ 484 | if($in_str && $char == $str_char) { 485 | $str_char = $char; 486 | } 487 | $in_str = !$in_str; 488 | } 489 | break; 490 | case ">": 491 | if(!$in_str){ 492 | if($prev_char == "?"){ 493 | continue; 494 | } 495 | $in_tag = false; 496 | if($capturing_tag_name) { 497 | $capturing_tag_name = false; 498 | } 499 | } 500 | break; 501 | case "<": 502 | if(!$in_str){ 503 | if(substr($cont, $j+1, 1) == "?"){ 504 | continue; 505 | } 506 | $prev_tag = ""; 507 | $in_tag = true; 508 | $capturing_tag_name = true; 509 | continue; 510 | } 511 | break; 512 | case " ": 513 | if($capturing_tag_name){ 514 | $capturing_tag_name = false; 515 | } 516 | default: 517 | if($capturing_tag_name){ 518 | $prev_tag .= $char; 519 | } 520 | } 521 | if($escaped) { 522 | $escaped = false; 523 | } 524 | } 525 | return array( 526 | "tag" => $prev_tag, 527 | "in_tag" => $in_tag, 528 | "in_str" => $in_str, 529 | "offset" => $i + (int)$context['offset'], 530 | "str_char" => $str_char, 531 | "prev_char" => $prev_char 532 | ); 533 | } 534 | 535 | private function escapeCharsInString($str, $escapeChar, $repChar, $strDelimiter='"') { 536 | $ret = ""; 537 | $inQuote = false; 538 | $escaped = false; 539 | for($i = 0; $i <= strlen($str); $i++) { 540 | $char = substr($str, $i, 1); 541 | switch($char) { 542 | case '\\': 543 | $escaped = true; 544 | $ret .= $char; 545 | break; 546 | case $strDelimiter: 547 | if(!$escaped) { 548 | $inQuote = !$inQuote; 549 | } 550 | $ret .= $char; 551 | break; 552 | default: 553 | if($inQuote && $char == $escapeChar) { 554 | $ret .= $repChar; 555 | } else { 556 | $ret .= $char; 557 | } 558 | } 559 | if($escaped) { 560 | $escaped = false; 561 | } 562 | } 563 | return $ret; 564 | } 565 | 566 | private function handleVars(){ 567 | $matches=array(); 568 | preg_match_all('/\{\s*\$(.+?)\s*\}/',$this->content,$matches); 569 | if(!empty($matches)){ 570 | foreach($matches[1] as $i => $var_name){ 571 | $prev_tag=strpos($var_name,'preventTag') === false ? false : true; 572 | $var_name = $this->escapeCharsInString($var_name, '.', '**dot**'); 573 | $var_name=explode('.',$var_name); 574 | if(count($var_name)>1){ 575 | $vn=$var_name[0]; 576 | if(empty($vn)){ 577 | $vn = "__func"; 578 | } 579 | unset($var_name[0]); 580 | $mod=array(); 581 | foreach($var_name as $j => $index){ 582 | $index = str_replace('**dot**', '.', $index); 583 | $index=explode('->',$index,2); 584 | $obj=''; 585 | if(count($index)>1){ 586 | $obj='->'.$index[1]; 587 | $index=$index[0]; 588 | }else 589 | $index=$index[0]; 590 | if(substr($index,-1,1)==")"){ 591 | $mod[]=$index.$obj; 592 | }else{ 593 | if(substr($index,0,1)=='$') 594 | $vn.="[$index]$obj"; 595 | else 596 | $vn.="['$index']$obj"; 597 | } 598 | } 599 | $var_name='$'.$vn; 600 | $mod=$this->applyModifiers($var_name,$mod,$matches[0][$i]); 601 | }else{ 602 | $var_name='$'.$var_name[0]; 603 | $mod=$this->applyModifiers($var_name,array(),$matches[0][$i]); 604 | } 605 | $rep='getMessage(); } ?>'; 606 | $this->content=$this->str_replace_first($matches[0][$i],$rep,$this->content); 607 | } 608 | } 609 | } 610 | 611 | private function str_replace_first($find, $replace, $string) { 612 | $pos = strpos($string,$find); 613 | if ($pos !== false) { 614 | return substr_replace($string,$replace,$pos,strlen($find)); 615 | } 616 | return ""; 617 | } 618 | 619 | private function findVarInString(&$string){ 620 | return self::findVariableInString($string); 621 | } 622 | 623 | private static function findVariableInString(&$string){ 624 | $var_match=array(); 625 | preg_match_all('/\$([a-zA-Z0-9_\-\(\)\.\",>]+)/',$string,$var_match); 626 | if(!empty($var_match[0])){ 627 | foreach($var_match[1] as $j => $var){ 628 | $_var_name=explode('.',$string); 629 | if(count($_var_name)>1){ 630 | $vn=$_var_name[0]; 631 | unset($_var_name[0]); 632 | $mod=array(); 633 | foreach($_var_name as $k => $index){ 634 | $index=explode('->',$index,2); 635 | $obj=''; 636 | if(count($index)>1){ 637 | $obj='->'.$index[1]; 638 | $index=$index[0]; 639 | }else 640 | $index=$index[0]; 641 | if(substr($index,-1,1)==")"){ 642 | $mod[]=$index.$obj; 643 | }else{ 644 | $vn.="['$index']$obj"; 645 | } 646 | } 647 | $_var_name='$'.$vn; 648 | $this->applyModifiers($_var_name,$mod); 649 | }else{ 650 | $_var_name='$'.$_var_name[0]; 651 | } 652 | $string=str_replace(@$var_match[0][$j],'".'.$_var_name.'."',$string); 653 | } 654 | } 655 | } 656 | 657 | private function handleIfMacros(){ 658 | $match = $this->matchTags('/<([a-xA-Z_\-0-9]+).+?tn-if\s*=\s*"(.+?)".*?>/','{endif}'); 659 | if (empty($match)) { 660 | return false; 661 | } 662 | $this->content = preg_replace('/<([a-xA-Z_\-0-9]+)(.+?)tn-if\s*=\s*"(.+?)"(.*?)>/','{if $3}<$1$2$4>',$this->content); 663 | } 664 | 665 | private function handleLoopMacros(){ 666 | $match = $this->matchTags('/<([a-xA-Z_\-0-9]+).+?tn-loop\s*=\s*"(.+?)".*?>/','{endloop}'); 667 | if (empty($match)) { 668 | return false; 669 | } 670 | $this->content = preg_replace('/<([a-xA-Z_\-0-9]+)(.+?)tn-loop\s*=\s*"(.+?)"(.*?)>/','{loop $3}<$1$2$4>',$this->content); 671 | } 672 | 673 | private function handleBlockMacros(){ 674 | $match = $this->matchTags('/<([a-xA-Z_\-0-9]+).+?tn-block\s*=\s*"(.+?)".*?>/','{endblock}'); 675 | if (empty($match)) { 676 | return false; 677 | } 678 | $this->content = preg_replace('/<([a-xA-Z_\-0-9]+)(.+?)tn-block\s*=\s*"(.+?)"(.*?)>/','{block $3}<$1$2$4>',$this->content); 679 | } 680 | 681 | private function matchTags($regex, $append=""){ 682 | $matches = array(); 683 | if (!preg_match_all($regex,$this->content,$matches)) { 684 | return false; 685 | } 686 | $offset = 0; 687 | $_offset = 0; 688 | $ret = array(); 689 | foreach($matches[0] as $k => $match){ 690 | $_cont = substr($this->content,$offset); 691 | $in_str = false; 692 | $escaped = false; 693 | $i = strpos($_cont, $match); 694 | $tag = $matches[1][$k]; 695 | $len_match = strlen($match); 696 | $offset += $i + $len_match; 697 | $str_char = ""; 698 | $lvl = 1; 699 | $prev_char = ""; 700 | $prev_tag = ""; 701 | $struct = ""; 702 | $in_tag = false; 703 | $capturing_tag_name = false; 704 | $_m = array(); 705 | foreach($matches as $z => $v){ 706 | $_m[$z] = $matches[$z][$k]; 707 | } 708 | 709 | $ret[$k] = array( 710 | "match" => $match, 711 | "matches" => $_m, 712 | "all" => $match, 713 | "inner" => "", 714 | "starts_at" => $offset - $len_match, 715 | "ends_at" => 0, 716 | ); 717 | 718 | for($j = $i + strlen($match); $j <= strlen($_cont); $j++) { 719 | $char = substr($_cont, $j, 1); 720 | $prev_char = $char; 721 | $struct .= $char; 722 | $break = false; 723 | switch ($char) { 724 | case "\\": 725 | $escaped = true; 726 | continue; 727 | break; 728 | case "'": 729 | case '"': 730 | if(!$escaped){ 731 | if($in_str && $char == $str_char) { 732 | $str_char = $char; 733 | } 734 | $in_str = !$in_str; 735 | } 736 | break; 737 | case ">": 738 | if(!$in_str){ 739 | if($in_tag) { 740 | $in_tag = false; 741 | if( $prev_tag == "/".$tag){ 742 | $lvl--; 743 | if($lvl <= 0) { 744 | $break=true; 745 | } 746 | } else if(substr($prev_tag,0,1) == "/"){ 747 | $lvl--; 748 | } else { 749 | if($prev_char != "/" && !in_array(str_replace("/","",$prev_tag), array('area','base','br','col','command','embed','hr','img','input','keygen','link','meta','param','source','track','wbr'))){ 750 | $lvl++; 751 | } 752 | } 753 | if($capturing_tag_name) { 754 | $capturing_tag_name = false; 755 | } 756 | } 757 | } 758 | break; 759 | case "<": 760 | if($in_tag){ 761 | continue; 762 | } 763 | if(!$in_str){ 764 | $prev_tag = ""; 765 | $in_tag = true; 766 | $capturing_tag_name = true; 767 | continue; 768 | } 769 | break; 770 | case " ": 771 | if($capturing_tag_name){ 772 | $capturing_tag_name = false; 773 | } 774 | default: 775 | if($capturing_tag_name){ 776 | $prev_tag .= $char; 777 | } 778 | } 779 | if($escaped) { 780 | $escaped = false; 781 | } 782 | if($break){ 783 | break; 784 | } 785 | } 786 | $ret[$k]["all"] .= $struct; 787 | $struct_len = strlen($struct); 788 | $ret[$k]["inner"] = substr($struct,0,$struct_len - strlen($tag)-3); 789 | $ret[$k]["ends_at"] = $ret[$k]["starts_at"] + $struct_len + $len_match; 790 | if($break && !empty($append)){ 791 | $this->content = substr_replace($this->content,$append,$ret[$k]["ends_at"],0); 792 | } 793 | } 794 | return $ret; 795 | } 796 | 797 | private function handleExtends(){ 798 | $matches=array(); 799 | preg_match_all('/\{\s*(extends )\s*(.+?)\s*\}/',$this->content,$matches); 800 | $base = $matches[2]; 801 | if(count($base) <= 0) 802 | return; 803 | if(count($base)>1) 804 | throw new \Exception("Each template can extend 1 parent at the most"); 805 | $base = $base[0]; 806 | if(substr($base, 0, 1) == '"') { 807 | $base = substr($base, 1); 808 | } 809 | if(substr($base, -1) == '"') { 810 | $base = substr($base, 0, -1); 811 | } 812 | $base = self::$root . $base; 813 | if(!file_exists($base)) { 814 | throw new \Exception("Unable to extend base template ". $base); 815 | } 816 | $this->base = $base; 817 | $this->content = str_replace($matches[0][0], "", $this->content); 818 | } 819 | 820 | private function handleIfs(){ 821 | $matches=array(); 822 | preg_match_all('/\{\s*(if|elseif)\s*(.+?)\s*\}/',$this->content,$matches); 823 | if(!empty($matches)){ 824 | foreach($matches[2] as $i => $condition){ 825 | $condition=trim($condition); 826 | $condition=str_replace(array( 827 | 'eq', 828 | 'gt', 829 | 'lt', 830 | 'neq', 831 | 'or', 832 | 'gte', 833 | 'lte' 834 | ),array( 835 | '==', 836 | '>', 837 | '<', 838 | '!=', 839 | '||', 840 | '>=', 841 | '<=' 842 | 843 | ),$condition); 844 | $var_match=array(); 845 | preg_match_all('/\$([a-zA-Z0-9_\-\(\)\.]+)/',$condition,$var_match); 846 | if(!empty($var_match)){ 847 | foreach($var_match[1] as $j => $var){ 848 | $var_name=explode('.',$var); 849 | if(count($var_name)>1){ 850 | $vn=$var_name[0]; 851 | unset($var_name[0]); 852 | $mod=array(); 853 | foreach($var_name as $k => $index){ 854 | $index=explode('->',$index,2); 855 | $obj=''; 856 | if(count($index)>1){ 857 | $obj='->'.$index[1]; 858 | $index=$index[0]; 859 | }else 860 | $index=$index[0]; 861 | if(substr($index,-1,1)==")"){ 862 | $mod[]=$index.$obj; 863 | }else{ 864 | $vn.="['$index']$obj"; 865 | } 866 | } 867 | $var_name='$'.$vn; 868 | $this->applyModifiers($var_name,$mod); 869 | }else{ 870 | $var_name='$'.$var_name[0]; 871 | } 872 | $condition=str_replace(@$var_match[0][$j],$var_name,$condition); 873 | } 874 | } 875 | $rep=''; 876 | $this->content=str_replace($matches[0][$i],$rep,$this->content); 877 | } 878 | } 879 | $this->content=preg_replace('/\{\s*(\/if|endif)\s*\}/','',$this->content); 880 | $this->content=preg_replace('/\{\s*else\s*\}/','',$this->content); 881 | 882 | } 883 | 884 | private function handleBlocks(){ 885 | $matches=array(); 886 | preg_match_all('/\{\s*(block)\s*(.+?)\s*\}/',$this->content,$matches); 887 | $blocks = $matches[2]; 888 | if(count($blocks) <= 0) 889 | return; 890 | foreach($blocks as $i => $block) { 891 | $block = trim($block); 892 | $rv = ''; 893 | $this->content = str_replace($matches[0][$i], $rv, $this->content); 894 | } 895 | $this->content=preg_replace('/\{\s*endblock\s*\}/','',$this->content); 896 | } 897 | 898 | public function __call($name, $args) { 899 | $n = explode('_', $name); 900 | if($n[0] == 'ob') { 901 | $this->blocks[$n[1]] = $args[0]; 902 | } 903 | if($this->base != null) 904 | return ""; 905 | 906 | return empty($this->blocks_override[$n[1]]) ? $args[0] : $this->blocks_override[$n[1]]; 907 | } 908 | 909 | private function handleLoops(){ 910 | $matches=array(); 911 | preg_match_all('/\{\s*(loop|for)\s*(.+?)\s*\}/',$this->content,$matches); 912 | if(!empty($matches)){ 913 | foreach($matches[2] as $i => $loop){ 914 | $loop = str_replace(' in ', '**in**', $loop); 915 | $loop = $this->removeWhiteSpaces($loop); 916 | $loop_det=explode('**in**',$loop); 917 | $loop_name=$loop_det[1]; 918 | unset($loop_det[1]); 919 | $loop_name=explode('.',$loop_name); 920 | if(count($loop_name)>1){ 921 | $ln=$loop_name[0]; 922 | unset($loop_name[0]); 923 | foreach($loop_name as $j => $suffix) 924 | $ln.="['$suffix']"; 925 | $loop_name=$ln; 926 | }else{ 927 | $loop_name=$loop_name[0]; 928 | } 929 | $key=NULL; 930 | $val=NULL; 931 | 932 | $loop_vars = explode(",",$loop_det[0]); 933 | if (count($loop_vars) > 1) { 934 | $key = $loop_vars[0]; 935 | $val = $loop_vars[1]; 936 | } else { 937 | $val = $loop_vars[0]; 938 | } 939 | 940 | foreach($loop_det as $j => $_val){ 941 | @list($k,$v)=explode(',',$_val); 942 | if($k=="key"){ 943 | $key=$v; 944 | continue; 945 | } 946 | if($k=="item"){ 947 | $val=$v; 948 | continue; 949 | } 950 | } 951 | $rep=' '.$val : ' '.$val).'): ?>'; 952 | $this->content=str_replace($matches[0][$i],$rep,$this->content); 953 | } 954 | } 955 | $this->content=preg_replace('/\{\s*(\/loop|endloop|\/for|endfor)\s*\}/','',$this->content); 956 | } 957 | 958 | public static function removeSpecialChars($text){ 959 | $find = array('á','é','í','ó','ú','Á','É','Í','Ó','Ú','ñ','Ñ',' ','"',"'"); 960 | $rep = array('a','e','i','o','u','A','E','I','O','U','n','N','-',"",""); 961 | return str_replace($find,$rep,$text); 962 | return(strtr($text,$tofind,$replac)); 963 | } 964 | 965 | public static function zeroFill($text,$digits){ 966 | if(strlen($text)<$digits){ 967 | $ceros=$digits-strlen($text); 968 | for($i=0;$i<=$ceros-1;$i++){ 969 | $ret.="0"; 970 | } 971 | $ret=$ret.$text; 972 | return $ret; 973 | }else{ 974 | return $text; 975 | } 976 | } 977 | 978 | private static function initModifiers(){ 979 | self::extendModifier("upper", function($input) { 980 | if(!is_string($input)){ 981 | return $input; 982 | } 983 | return strtoupper($input); 984 | }); 985 | self::extendModifier("lower", function($input) { 986 | if(!is_string($input)){ 987 | return $input; 988 | } 989 | return strtolower($input); 990 | }); 991 | self::extendModifier("capitalize", function($input) { 992 | if(!is_string($input)){ 993 | return $input; 994 | } 995 | return ucwords($input); 996 | }); 997 | self::extendModifier("abs", function($input) { 998 | if(!is_numeric($input)){ 999 | return $input; 1000 | } 1001 | return abs($input); 1002 | }); 1003 | self::extendModifier("isEmpty", function($input) { 1004 | return empty($input); 1005 | }); 1006 | self::extendModifier("truncate", function($input,$len) { 1007 | if(empty($len)) { 1008 | throw new \Exception("length parameter is required"); 1009 | } 1010 | return substr($input,0,$len).(strlen($input) > $len ? "..." : ""); 1011 | }); 1012 | self::extendModifier("count", function($input) { 1013 | return count($input); 1014 | }); 1015 | self::extendModifier("length", function($input) { 1016 | return count($input); 1017 | }); 1018 | self::extendModifier("toLocal", function($input) { 1019 | if(!is_object($input)){ 1020 | throw new \Exception("variable is not a valid date"); 1021 | } 1022 | return date_timezone_set($input, timezone_open(self::$local_tz)); 1023 | }); 1024 | self::extendModifier("toTz", function($input,$tz) { 1025 | if(!is_object($input)){ 1026 | throw new \Exception("variable is not a valid date"); 1027 | } 1028 | return date_timezone_set($input, timezone_open($tz)); 1029 | }); 1030 | self::extendModifier("toGMT", function($input,$tz) { 1031 | if(!is_object($input)){ 1032 | throw new \Exception("variable is not a valid date"); 1033 | } 1034 | if(empty($tz)){ 1035 | throw new \Exception("timezone is required"); 1036 | } 1037 | return date_timezone_set($input, timezone_open("GMT")); 1038 | }); 1039 | self::extendModifier("date", function($input,$format) { 1040 | if(!is_object($input)){ 1041 | throw new \Exception("variable is not a valid date"); 1042 | } 1043 | if(empty($format)){ 1044 | throw new \Exception("date format is required"); 1045 | } 1046 | return date_format($input,$format); 1047 | }); 1048 | self::extendModifier("nl2br", function($input) { 1049 | return nl2br($input); 1050 | }); 1051 | self::extendModifier("stripSlashes", function($input) { 1052 | if(!is_string($input)){ 1053 | return $input; 1054 | } 1055 | return stripslashes($input); 1056 | }); 1057 | self::extendModifier("sum", function($input,$val) { 1058 | if(!is_numeric($input) || !is_numeric($val)){ 1059 | throw new \Exception("input and value must be numeric"); 1060 | } 1061 | return $input + (float)$val; 1062 | }); 1063 | self::extendModifier("substract", function($input,$val) { 1064 | if(!is_numeric($input) || !is_numeric($val)){ 1065 | throw new \Exception("input and value must be numeric"); 1066 | } 1067 | return $input - (float)$val; 1068 | }); 1069 | self::extendModifier("multiply", function($input,$val) { 1070 | if(!is_numeric($input) || !is_numeric($val)){ 1071 | throw new \Exception("input and value must be numeric"); 1072 | } 1073 | return $input * (float)$val; 1074 | }); 1075 | self::extendModifier("divide", function($input,$val) { 1076 | if(!is_numeric($input) || !is_numeric($val)){ 1077 | throw new \Exception("input and value must be numeric"); 1078 | } 1079 | return $input / (float)$val; 1080 | }); 1081 | self::extendModifier("mod", function($input,$val) { 1082 | if(!is_numeric($input) || !is_numeric($val)){ 1083 | throw new \Exception("input and value must be numeric"); 1084 | } 1085 | return $input % (float)$val; 1086 | }); 1087 | self::extendModifier("encodeTags", function($input) { 1088 | if(!is_string($input)){ 1089 | return $input; 1090 | } 1091 | return htmlspecialchars($input,ENT_NOQUOTES); 1092 | }); 1093 | self::extendModifier("decodeTags", function($input) { 1094 | if(!is_string($input)){ 1095 | return $input; 1096 | } 1097 | return htmlspecialchars_decode($input); 1098 | }); 1099 | self::extendModifier("stripTags", function($input) { 1100 | if(!is_string($input)){ 1101 | return $input; 1102 | } 1103 | return strip_tags($input); 1104 | }); 1105 | self::extendModifier("urlDecode", function($input) { 1106 | if(!is_string($input)){ 1107 | return $input; 1108 | } 1109 | return urldecode($input); 1110 | }); 1111 | self::extendModifier("addSlashes", function($input){ 1112 | return addslashes($input); 1113 | }); 1114 | self::extendModifier("urlFriendly", function($input) { 1115 | if(!is_string($input)){ 1116 | return $input; 1117 | } 1118 | return urlencode(self::removeSpecialChars(strtolower($input))); 1119 | }); 1120 | self::extendModifier("trim", function($input) { 1121 | if(!is_string($input)){ 1122 | return $input; 1123 | } 1124 | return trim($input); 1125 | }); 1126 | self::extendModifier("sha1", function($input) { 1127 | if(!is_string($input)){ 1128 | throw new \Exception("input must be string"); 1129 | } 1130 | return sha1($input); 1131 | }); 1132 | self::extendModifier("safe", function($input) { 1133 | return htmlentities($input, ENT_QUOTES); 1134 | }); 1135 | self::extendModifier("numberFormat", function($input,$precision = 2) { 1136 | if(!is_numeric($input)){ 1137 | throw new \Exception("input must be numeric"); 1138 | } 1139 | return number_format($input,(int)$precision); 1140 | }); 1141 | self::extendModifier("lastIndex", function($input) { 1142 | if(!is_array($input)){ 1143 | throw new \Exception("input must be an array"); 1144 | } 1145 | return current(array_reverse(array_keys($input))); 1146 | }); 1147 | self::extendModifier("lastValue", function($input) { 1148 | if(!is_array($input)){ 1149 | throw new \Exception("input must be an array"); 1150 | } 1151 | return current(array_reverse($input)); 1152 | }); 1153 | self::extendModifier("jsonEncode", function($input) { 1154 | return json_encode($input); 1155 | }); 1156 | self::extendModifier("substr", function($input,$a = 0,$b = 0) { 1157 | return substr($input,$a,$b); 1158 | }); 1159 | self::extendModifier("join", function($input,$glue) { 1160 | if(!is_array($input)){ 1161 | throw new \Exception("input must be an array"); 1162 | } 1163 | if(empty($glue)){ 1164 | throw new \Exception("string glue is required"); 1165 | } 1166 | return implode($glue,$input); 1167 | }); 1168 | self::extendModifier("explode", function($input,$del) { 1169 | if(!is_string($input)){ 1170 | throw new \Exception("input must be a string"); 1171 | } 1172 | if(empty($del)){ 1173 | throw new \Exception("delimiter is required"); 1174 | } 1175 | return explode($del,$input); 1176 | }); 1177 | self::extendModifier("replace", function($input,$search,$replace) { 1178 | if(!is_string($input)){ 1179 | throw new \Exception("input must be a string"); 1180 | } 1181 | if(empty($search)){ 1182 | throw new \Exception("search is required"); 1183 | } 1184 | if(empty($replace)){ 1185 | throw new \Exception("replace is required"); 1186 | } 1187 | return str_replace($search,$replace,$input); 1188 | }); 1189 | self::extendModifier("preventTagEncode", function($input) { 1190 | return $input; 1191 | }); 1192 | self::extendModifier("default", function($input,$default) { 1193 | return (empty($input) ? $default : $input); 1194 | }); 1195 | self::extendModifier("contextJs", function($input,$in_str) { 1196 | if( (is_object($input) || is_array($input)) && !$in_str){ 1197 | return json_encode($input); 1198 | } else if(is_numeric($input) || is_bool($input)){ 1199 | return $input; 1200 | } else if(is_null($input)) { 1201 | return "null"; 1202 | } else { 1203 | if(!$in_str){ 1204 | return '"' . addslashes($input) .'"'; 1205 | } else { 1206 | if(is_object($input) || is_array($input)) { 1207 | $input = json_encode($input); 1208 | } 1209 | return addslashes($input); 1210 | } 1211 | 1212 | } 1213 | }); 1214 | self::extendModifier("contextOutTag", function($input) { 1215 | if(is_object($input) || is_array($input)){ 1216 | return var_dump($input); 1217 | } else { 1218 | return htmlentities($input,ENT_QUOTES); 1219 | } 1220 | }); 1221 | self::extendModifier("contextTag", function($input, $in_str) { 1222 | if((is_object($input) || is_array($input)) && $in_str){ 1223 | return http_build_query($input); 1224 | } else { 1225 | if($in_str) { 1226 | return urlencode($input); 1227 | } else { 1228 | return htmlentities($input,ENT_QUOTES); 1229 | } 1230 | 1231 | } 1232 | }); 1233 | self::extendModifier("addDoubleQuotes", function($input){ 1234 | return '"' . $input . '"'; 1235 | }); 1236 | self::extendModifier("ifEmpty", function($input,$true_val, $false_val = null) { 1237 | if(empty($true_val)){ 1238 | throw new \Exception("true value is required"); 1239 | } 1240 | $ret = $input; 1241 | if(empty($ret)) { 1242 | $ret = $true_val; 1243 | } else if($false_val) { 1244 | $ret = $false_val; 1245 | } 1246 | return $ret; 1247 | }); 1248 | self::extendModifier("if", function($input, $condition, $true_val, $false_val = null, $operator = "eq") { 1249 | if(empty($true_val)){ 1250 | throw new \Exception("true value is required"); 1251 | } 1252 | switch($operator){ 1253 | case '': 1254 | case '==': 1255 | case '=': 1256 | case 'eq': 1257 | default: 1258 | $operator="=="; 1259 | break; 1260 | case '<': 1261 | case 'lt': 1262 | $operator="<"; 1263 | break; 1264 | case '>': 1265 | case 'gt': 1266 | $operator=">"; 1267 | break; 1268 | case '<=': 1269 | case 'lte': 1270 | $operator="<="; 1271 | break; 1272 | case '>=': 1273 | case 'gte': 1274 | $operator=">="; 1275 | break; 1276 | case 'neq': 1277 | $operator = "!="; 1278 | break; 1279 | } 1280 | $ret = $input; 1281 | if(eval('return ("'.$condition.'"'.$operator.'"'.$input.'");')) { 1282 | $ret = $true_val; 1283 | } else if($false_val) { 1284 | $ret = $false_val; 1285 | } 1286 | return $ret; 1287 | }); 1288 | 1289 | } 1290 | } 1291 | --------------------------------------------------------------------------------