├── .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 | | Name: | {$user.name} |
33 |
34 |
35 | | Last name: | {$user.last_name} |
36 |
37 |
38 | | Email: | {$user.email} |
39 |
40 |
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 | | Name: | {$user.name.upper()} |
62 |
63 |
64 | | Nickname: | {$user.name.lower().if("admin", "administrator", $user_role).capitalize()} |
65 |
66 |
67 | | Extra: | {$user.extra.truncate(20)} |
68 |
69 |
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 |
98 | -
99 | User: {$user.name.capitalize()}
100 | Email: {$user.email}
101 | Role: {$user.role.if("admin","administrator").upper()}
102 |
103 |
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 lopez2:rodrigo gomez'),
121 | array('{$u.name}', 'rocio 'lavin'roberto lopezrodrigo 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 | [](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 |
196 | {loop $user in $users}
197 | - {$user.name.capitalize()}
198 | {endloop}
199 |
200 | ```
201 | Or if the array key is needed
202 | ```html
203 |
204 | {loop $i,$user in $users}
205 | - {$i} - {$user.name.capitalize()}
206 | {endloop}
207 |
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 |
213 | - Hello {$user}
214 |
215 | ```
216 | Which is exactly the same as:
217 | ```html
218 | {if $users}
219 |
220 | {loop $user in $users}
221 | - Hello {$user}
222 | {endloop}
223 |
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 |
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 |
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 |
276 |
277 |
278 |
This is the content of my inner view.
279 |
280 |
281 |
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 |
--------------------------------------------------------------------------------