├── .gitignore
├── .travis.yml
├── README.md
├── composer.json
├── lib
└── Lex
│ ├── ArrayableInterface.php
│ ├── ArrayableObjectExample.php
│ ├── Parser.php
│ └── ParsingException.php
├── phpunit.xml
└── tests
└── ParserTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | coverage
3 | vendor
4 | composer.lock
5 | *~
6 | *.bak
7 | Thumbs.db
8 | desktop.ini
9 | .buildpath
10 | .project
11 | .settings
12 | nbproject/
13 | .idea
14 | *.tmproj
15 | *.sublime-project
16 | *.sublime-workspace
17 | index.php
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 5.3
5 | - 5.4
6 | - 5.5
7 | - 5.6
8 | - hhvm
9 |
10 | before_script:
11 | - composer install
12 |
13 | script: phpunit --coverage-text --configuration phpunit.xml
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Lex
2 | ===
3 |
4 | [](http://travis-ci.org/pyrocms/lex)
5 |
6 | Lex is a lightweight template parser.
7 |
8 | _Lex is released under the MIT License and is Copyrighted 2011 - 2014 PyroCMS Team._
9 |
10 | Change Log
11 | ==========
12 |
13 | 2.3.2
14 | -----
15 |
16 | * Convert objects with `->toArray()` at the beginning of the `parser()` method only.
17 | * As much as we want to say goodbye to PHP 5.3, we brought it back for now.
18 |
19 | 2.3.1
20 | -----
21 |
22 | * Added an ArrayableInterface that automatically converts objects to arrays. This allows an opportunity to define how an object will be converted to an array by adding a `->toArray()` method.
23 | * Looped data is now checked if it is an array or if it implements \IteratorAggregate. This prevents "invalid arguments in foreach" errors while allowing arrays and Collection objects to be iterated.
24 | * Dropped support for PHP 5.3
25 |
26 | 2.3.0
27 | -----
28 |
29 | * Added support for self-closing Callback Tags (e.g. `{{ foo.bar /}}` instead of `{{ foo.bar }}{{ /foo.bar }}`).
30 | * Rolled back the change to make Callback Tags less greedy. Callback Tags are now greedy again. If you want to use both Single and Block tags in the same template, you must close the Single tags.
31 |
32 | 2.2.3
33 | -----
34 |
35 | * Fixes issue which caused all callbacks to be processed as single tags, even if they were a block.
36 |
37 | 2.2.2
38 | -----
39 |
40 | * Fixed #7 - Conditionals inside looped data tags now work as expected.
41 |
42 | 2.2.1
43 | -----
44 |
45 | * Changed injectNoparse to be static.
46 |
47 | 2.2.0
48 | -----
49 |
50 | * Fixed a test which was PHP 5.4 only.
51 | * Added PHPUnit as a composer dev requirement.
52 | * Added a Lex\ParsingException class which is thrown when a parsing exception occurs.
53 |
54 | 2.1.1
55 | -----
56 |
57 | * Fixed an issue where strings returned by callbacks inside a comparison conditional were being processed incorrectly, causing the conditional to always fail.
58 |
59 | 2.1.0
60 | -----
61 |
62 | * Undefined variables in a conditional now evaluate to NULL, so `{{ if foo }}` now works properly.
63 | * Added the `exists` keyword.
64 | * Added the `not` keyword.
65 |
66 | 2.0.3
67 | -----
68 |
69 | * Fixes composer autoloading.
70 | * Moved classes into lib folder.
71 |
72 | 2.0.2
73 | -----
74 |
75 | * Fixed a bug introduced in 2.0.1 where NULL variables were not being displayed.
76 |
77 | 2.0.1
78 | -----
79 |
80 | * Fixed a bug where variables with a "falsey" (e.g. 0, "0", -1, etc.) value were not displayed.
81 |
82 | 2.0.0
83 | -----
84 |
85 | * All code now follows PSR-0, 1 and 2.
86 | * Lex_Parser has been moved to the `Lex` namespace and renamed to `Parser`.
87 | * Lex_Autoloader has been removed. It is now PSR-0 compliant.
88 | * Added the support for `{{ unless }}` and `{{ elseunless }}`.
89 |
90 |
91 |
92 | Basic Usage
93 | ===========
94 |
95 | Using Lex
96 | -------------
97 |
98 | Lex is a Composer package named `pyrocms/lex`. To use it, simply add it to the `require` section of you `composer.json` file.
99 |
100 | {
101 | "require": {
102 | "pyrocms/lex": "2.2.*"
103 | }
104 | }
105 |
106 | After adding Lex to your `composer.json` file, simply use the class as normal.
107 |
108 | $parser = new Lex\Parser();
109 |
110 | Using Lex
111 | ---------
112 |
113 | Basic parsing of a file:
114 |
115 | $parser = new Lex\Parser();
116 | $template = $parser->parse(file_get_contents('template.lex'), $data);
117 |
118 | You can also set the Scope Glue (see "Scope Glue" under Syntax below):
119 |
120 | $parser = new Lex\Parser();
121 | $parser->scopeGlue(':');
122 | $template = $parser->parse(file_get_contents('template.lex'), $data);
123 |
124 | To allow noparse extractions to accumulate so they don't get parsed by a later call to the parser set cumulativeNoparse to true:
125 |
126 | $parser = new Lex\Parser();
127 | $parser->cumulativeNoparse(true);
128 | $template = $parser->parse(file_get_contents('template.lex'), $data);
129 | // Second parse on the same text somewhere else in your app
130 | $template = $parser->parse($template, $data);
131 | // Now that all parsing is done we inject the contents between the {{ noparse }} tags back into the template text
132 | Lex\Parser::injectNoparse($template);
133 |
134 | If you only want to parse a data array and not worry about callback tags or comments, you can do use the `parseVariables()` method:
135 |
136 | $parser = new Lex\Parser();
137 | $template = $parser->parseVariables(file_get_contents('template.lex'), $data);
138 |
139 |
140 | ### PHP in Templates
141 |
142 | By default PHP is encoded, and not executed. This is for security reasons. However, you may at times want to enable it. To do that simply send `true` as the fourth parameter to your `parse()` call.
143 |
144 | $parser = new Lex\Parser();
145 | $template = $parser->parse(file_get_contents('template.lex'), $data, $callback, true);
146 |
147 | Syntax
148 | ======
149 |
150 | General
151 | -------
152 |
153 | All Lex code is delimeted by double curly braces (`{{ }}`). These delimeters were chosen to reduce the chance of conflicts with JavaScript and CSS.
154 |
155 | Here is an example of some Lex template code:
156 |
157 | Hello, {{name}}
158 |
159 |
160 | Scope Glue
161 | ----------
162 |
163 | Scope Glue is/are the character(s) used by Lex to trigger a scope change. A scope change is what happens when, for instance, you are accessing a nested variable inside and array/object, or when scoping a custom callback tag.
164 |
165 | By default a dot (`.`) is used as the Scope Glue, although you can select any character(s).
166 |
167 | `Setting Scope Glue`
168 |
169 | $parser->scopeGlue(':');
170 |
171 | Whitespace
172 | ----------
173 |
174 | Whitespace before or after the delimeters is allowed, however, in certain cases, whitespace within the tag is prohibited (explained in the following sections).
175 |
176 | **Some valid examples:**
177 |
178 | {{ name }}
179 | {{name }}
180 | {{ name}}
181 | {{ name }}
182 | {{
183 | name
184 | }}
185 |
186 | **Some invalid examples:**
187 |
188 | {{ na me }}
189 | { {name} }
190 |
191 |
192 | Comments
193 | --------
194 |
195 | You can add comments to your templates by wrapping the text in `{{# #}}`.
196 |
197 | **Example**
198 |
199 | {{# This will not be parsed or shown in the resulting HTML #}}
200 |
201 | {{#
202 | They can be multi-line too.
203 | #}}
204 |
205 | Prevent Parsing
206 | ---------------
207 |
208 | You can prevent the parser from parsing blocks of code by wrapping it in `{{ noparse }}{{ /noparse }}` tags.
209 |
210 | **Example**
211 |
212 | {{ noparse }}
213 | Hello, {{ name }}!
214 | {{ /noparse }}
215 |
216 | Variable Tags
217 | -------------
218 |
219 | When dealing with variables, you can: access single variables, access deeply nested variables inside arrays/objects, and loop over an array. You can even loop over nested arrays.
220 |
221 | ### Simple Variable Tags
222 |
223 | For our basic examples, lets assume you have the following array of variables (sent to the parser):
224 |
225 | array(
226 | 'title' => 'Lex is Awesome!',
227 | 'name' => 'World',
228 | 'real_name' => array(
229 | 'first' => 'Lex',
230 | 'last' => 'Luther',
231 | )
232 | )
233 |
234 | **Basic Example:**
235 |
236 | {{# Parsed: Hello, World! #}}
237 | Hello, {{ name }}!
238 |
239 | {{# Parsed:
Lex is Awesome!
#}}
240 | {{ title }}
241 |
242 | {{# Parsed: My real name is Lex Luther! #}}
243 | My real name is {{ real_name.first }} {{ real_name.last }}
244 |
245 | The `{{ real_name.first }}` and `{{ real_name.last }}` tags check if `real_name` exists, then check if `first` and `last` respectively exist inside the `real_name` array/object then returns it.
246 |
247 | ### Looped Variable Tags
248 |
249 | Looped Variable tags are just like Simple Variable tags, except they correspond to an array of arrays/objects, which is looped over.
250 |
251 | A Looped Variable tag is a closed tag which wraps the looped content. The closing tag must match the opening tag exactly, except it must be prefixed with a forward slash (`/`). There can be **no** whitespace between the forward slash and the tag name (whitespace before the forward slash is allowed).
252 |
253 | **Valid Example:**
254 |
255 | {{ projects }} Some Content Here {{ /projects }}
256 |
257 | **Invalid Example:**
258 |
259 | {{ projects }} Some Content Here {{/ projects }}
260 |
261 | The looped content is what is contained between the opening and closing tags. This content is looped through and output for every item in the looped array.
262 |
263 | When in a Looped Tag you have access to any sub-variables for the current element in the loop.
264 |
265 | In the following example, let's assume you have the following array/object of variables:
266 |
267 | array(
268 | 'title' => 'Current Projects',
269 | 'projects' => array(
270 | array(
271 | 'name' => 'Acme Site',
272 | 'assignees' => array(
273 | array('name' => 'Dan'),
274 | array('name' => 'Phil'),
275 | ),
276 | ),
277 | array(
278 | 'name' => 'Lex',
279 | 'contributors' => array(
280 | array('name' => 'Dan'),
281 | array('name' => 'Ziggy'),
282 | array('name' => 'Jerel')
283 | ),
284 | ),
285 | ),
286 | )
287 |
288 | In the template, we will want to display the title, followed by a list of projects and their assignees.
289 |
290 | {{ title }}
291 | {{ projects }}
292 | {{ name }}
293 | Assignees
294 |
295 | {{ assignees }}
296 | - {{ name }}
297 | {{ /assignees }}
298 |
299 | {{ /projects }}
300 |
301 | As you can see inside each project element we have access to that project's assignees. You can also see that you can loop over sub-values, exactly like you can any other array.
302 |
303 | Conditionals
304 | -------------
305 |
306 | Conditionals in Lex are simple and easy to use. It allows for the standard `if`, `elseif`, and `else` but it also adds `unless` and `elseunless`.
307 |
308 | The `unless` and `elseunless` are the EXACT same as using `{{ if ! (expression) }}` and `{{ elseif ! (expression) }}` respectively. They are added as a nicer, more understandable syntax.
309 |
310 | All `if` blocks must be closed with the `{{ endif }}` tag.
311 |
312 | Variables inside of if Conditionals, do not, and should not, use the Tag delimeters (it will cause wierd issues with your output).
313 |
314 | A Conditional can contain any Comparison Operators you would do in PHP (`==`, `!=`, `===`, `!==`, `>`, `<`, `<=`, `>=`). You can also use any of the Logical Operators (`!`, `not`, `||`, `&&`, `and`, `or`).
315 |
316 | **Examples**
317 |
318 | {{ if show_name }}
319 | My name is {{real_name.first}} {{real_name.last}}
320 | {{ endif }}
321 |
322 | {{ if user.group == 'admin' }}
323 | You are an Admin!
324 | {{ elseif user.group == 'user' }}
325 | You are a normal User.
326 | {{ else }}
327 | I don't know what you are.
328 | {{ endif }}
329 |
330 | {{ if show_real_name }}
331 | My name is {{real_name.first}} {{real_name.last}}
332 | {{ else }}
333 | My name is John Doe
334 | {{ endif }}
335 |
336 | {{ unless age > 21 }}
337 | You are to young.
338 | {{ elseunless age < 80 }}
339 | You are to old...it'll kill ya!
340 | {{ else }}
341 | Go ahead and drink!
342 | {{ endif }}
343 |
344 | ### The `not` Operator
345 |
346 | The `not` operator is equivilent to using the `!` operator. They are completely interchangable (in-fact `not` is translated to `!` prior to compilation).
347 |
348 | ### Undefined Variables in Conditionals
349 |
350 | Undefined variables in conditionals are evaluated to `null`. This means you can do things like `{{ if foo }}` and not have to worry if the variable is defined or not.
351 |
352 | ### Checking if a Variable Exists
353 |
354 | To check if a variable exists in a conditional, you use the `exists` keyword.
355 |
356 | **Examples**
357 |
358 | {{ if exists foo }}
359 | Foo Exists
360 | {{ elseif not exists foo }}
361 | Foo Does Not Exist
362 | {{ endif }}
363 |
364 | You can also combine it with other conditions:
365 |
366 | {{ if exists foo and foo !== 'bar' }}
367 | Something here
368 | {{ endif }}
369 |
370 | The expression `exists foo` evaluates to either `true` or `false`. Therefore something like this works as well:
371 |
372 | {{ if exists foo == false }}
373 | {{ endif }}
374 |
375 | ### Callback Tags in Conditionals
376 |
377 | Using a callback tag in a conditional is simple. Use it just like any other variable except for one exception. When you need to provide attributes for the callback tag, you are required to surround the tag with a ***single*** set of braces (you can optionally use them for all callback tags).
378 |
379 | **Note: When using braces inside of a conditional there CANNOT be any whitespace after the opening brace, or before the closing brace of the callback tag within the conditional. Doing so will result in errors.**
380 |
381 | **Examples**
382 |
383 | {{ if user.logged_in }} {{ endif }}
384 |
385 | {{ if user.logged_in and {user.is_group group="admin"} }} {{ endif }}
386 |
387 |
388 | Callback Tags
389 | -------------
390 |
391 | Callback tags allow you to have tags with attributes that get sent through a callback. This makes it easy to create a nice plugin system.
392 |
393 | Here is an example
394 |
395 | {{ template.partial name="navigation" }}
396 |
397 | You can also close the tag to make it a **Callback Block**:
398 |
399 | {{ template.partial name="navigation" }}
400 | {{ /template.partial }}
401 |
402 | Note that attributes are not required. When no attributes are given, the tag will first be checked to see if it is a data variable, and then execute it as a callback.
403 |
404 | {{ template.partial }}
405 |
406 | ### The Callback
407 |
408 | The callback can be any valid PHP callable. It is sent to the `parse()` method as the third parameter:
409 |
410 | $parser->parse(file_get_contents('template.lex'), $data, 'my_callback');
411 |
412 | The callback must accept the 3 parameters below (in this order):
413 |
414 | $name - The name of the callback tag (it would be "template.partial" in the above examples)
415 | $attributes - An associative array of the attributes set
416 | $content - If it the tag is a block tag, it will be the content contained, else a blank string
417 |
418 | The callback must also return a string, which will replace the tag in the content.
419 |
420 | **Example**
421 |
422 | function my_callback($name, $attributes, $content)
423 | {
424 | // Do something useful
425 | return $result;
426 | }
427 |
428 | ### Closing Callback Tags
429 |
430 | If a Callback Tag can be used in single or block form, then when using it in it's singular form, it must be closed (just like HTML).
431 |
432 | **Example**
433 |
434 | {{ foo.bar.baz }}{{ /foo.bar.baz }}
435 |
436 | {{ foo.bar.baz }}
437 | Content
438 | {{ /foo.bar.baz }}
439 |
440 | #### Self Closing Callback Tags
441 |
442 | You can shorten the above by using self-closing tags, just like in HTML. You simply put a `/` at the end of the tag (there MUST be NO space between the `/` and the `}}`).
443 |
444 | **Example**
445 |
446 | {{ foo.bar.baz /}}
447 |
448 | {{ foo.bar.baz }}
449 | Content
450 | {{ /foo.bar.baz }}
451 |
452 |
453 | Recursive Callback Blocks
454 | -------------
455 |
456 | The recursive callback tag allows you to loop through a child's element with the same output as the main block. It is triggered
457 | by using the ***recursive*** keyword along with the array key name. The two words must be surrounded by asterisks as shown in the example below.
458 |
459 | **Example**
460 |
461 | function my_callback($name, $attributes, $content)
462 | {
463 | $data = array(
464 | 'url' => 'url_1',
465 | 'title' => 'First Title',
466 | 'children' => array(
467 | array(
468 | 'url' => 'url_2',
469 | 'title' => 'Second Title',
470 | 'children' => array(
471 | array(
472 | 'url' => 'url_3',
473 | 'title' => 'Third Title'
474 | )
475 | )
476 | ),
477 | array(
478 | 'url' => 'url_4',
479 | 'title' => 'Fourth Title',
480 | 'children' => array(
481 | array(
482 | 'url' => 'url_5',
483 | 'title' => 'Fifth Title'
484 | )
485 | )
486 | )
487 | )
488 | );
489 |
490 | $parser = new Lex\Parser();
491 | return $parser->parse($content, $data);
492 | }
493 |
494 |
495 | In the template set it up as shown below. If `children` is not empty Lex will
496 | parse the contents between the `{{ navigation }}` tags again for each of `children`'s arrays.
497 | The resulting text will then be inserted in place of `{{ *recursive children* }}`. This can be done many levels deep.
498 |
499 |
500 | {{ navigation }}
501 | - {{ title }}
502 | {{ if children }}
503 |
504 | {{ *recursive children* }}
505 |
506 | {{ endif }}
507 |
508 | {{ /navigation }}
509 |
510 |
511 |
512 | **Result**
513 |
514 |
531 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pyrocms/lex",
3 | "type": "library",
4 | "description": "A lightweight template parser.",
5 | "keywords": ["template","parser"],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Dan Horrigan",
10 | "email": "dan@dhorrigan.com",
11 | "role": "Lead Developer"
12 | }
13 | ],
14 | "replace": {
15 | "fuel/lex": "*"
16 | },
17 | "require": {
18 | "php": ">=5.3.0"
19 | },
20 | "autoload": {
21 | "psr-0": { "Lex": "lib/" }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/Lex/ArrayableInterface.php:
--------------------------------------------------------------------------------
1 | 'bar');
19 |
20 | /**
21 | * Define how the object will be converted to an array
22 | *
23 | * @return array
24 | */
25 | public function toArray()
26 | {
27 | return $this->attributes;
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/lib/Lex/Parser.php:
--------------------------------------------------------------------------------
1 | array(),
38 | );
39 |
40 | protected static $data = null;
41 | protected static $callbackData = array();
42 |
43 | /**
44 | * The main Lex parser method. Essentially acts as dispatcher to
45 | * all of the helper parser methods.
46 | *
47 | * @param string $text Text to parse
48 | * @param array|object $data Array or object to use
49 | * @param mixed $callback Callback to use for Callback Tags
50 | * @return string
51 | */
52 | public function parse($text, $data = array(), $callback = false, $allowPhp = false)
53 | {
54 |
55 | $this->setupRegex();
56 | $this->allowPhp = $allowPhp;
57 |
58 | // Only convert objects to arrays
59 | if (is_object($data)) {
60 | $data = $this->toArray($data);
61 | }
62 |
63 | // Is this the first time parse() is called?
64 | if (self::$data === null) {
65 | // Let's store the local data array for later use.
66 | self::$data = $data;
67 | } else {
68 | // Let's merge the current data array with the local scope variables
69 | // So you can call local variables from within blocks.
70 | $data = array_merge(self::$data, $data);
71 |
72 | // Since this is not the first time parse() is called, it's most definately a callback,
73 | // let's store the current callback data with the the local data
74 | // so we can use it straight after a callback is called.
75 | self::$callbackData = $data;
76 | }
77 |
78 | // The parseConditionals method executes any PHP in the text, so clean it up.
79 | if (! $allowPhp) {
80 | $text = str_replace(array('', '?>'), array('<?', '?>'), $text);
81 | }
82 |
83 | $text = $this->parseComments($text);
84 | $text = $this->extractNoparse($text);
85 | $text = $this->extractLoopedTags($text, $data, $callback);
86 |
87 | // Order is important here. We parse conditionals first as to avoid
88 | // unnecessary code from being parsed and executed.
89 | $text = $this->parseConditionals($text, $data, $callback);
90 | $text = $this->injectExtractions($text, 'looped_tags');
91 | $text = $this->parseVariables($text, $data, $callback);
92 | $text = $this->injectExtractions($text, 'callback_blocks');
93 |
94 | if ($callback) {
95 | $text = $this->parseCallbackTags($text, $data, $callback);
96 | }
97 |
98 | // To ensure that {{ noparse }} is never parsed even during consecutive parse calls
99 | // set $cumulativeNoparse to true and use self::injectNoparse($text); immediately
100 | // before the final output is sent to the browser
101 | if (! $this->cumulativeNoparse) {
102 | $text = $this->injectExtractions($text);
103 | }
104 |
105 | return $text;
106 | }
107 |
108 | /**
109 | * Removes all of the comments from the text.
110 | *
111 | * @param string $text Text to remove comments from
112 | * @return string
113 | */
114 | public function parseComments($text)
115 | {
116 | $this->setupRegex();
117 |
118 | return preg_replace('/\{\{#.*?#\}\}/s', '', $text);
119 | }
120 |
121 | /**
122 | * Recursivly parses all of the variables in the given text and
123 | * returns the parsed text.
124 | *
125 | * @param string $text Text to parse
126 | * @param array|object $data Array or object to use
127 | * @return string
128 | */
129 | public function parseVariables($text, $data, $callback = null)
130 | {
131 | $this->setupRegex();
132 |
133 | /**
134 | * $data_matches[][0][0] is the raw data loop tag
135 | * $data_matches[][0][1] is the offset of raw data loop tag
136 | * $data_matches[][1][0] is the data variable (dot notated)
137 | * $data_matches[][1][1] is the offset of data variable
138 | * $data_matches[][2][0] is the content to be looped over
139 | * $data_matches[][2][1] is the offset of content to be looped over
140 | */
141 | if (preg_match_all($this->variableLoopRegex, $text, $data_matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE)) {
142 | foreach ($data_matches as $index => $match) {
143 | if ($loop_data = $this->getVariable($match[1][0], $data)) {
144 | $looped_text = '';
145 | if (is_array($loop_data) or ($loop_data instanceof \IteratorAggregate)) {
146 | foreach ($loop_data as $item_data) {
147 | $str = $this->extractLoopedTags($match[2][0], $item_data, $callback);
148 | $str = $this->parseConditionals($str, $item_data, $callback);
149 | $str = $this->injectExtractions($str, 'looped_tags');
150 | $str = $this->parseVariables($str, $item_data, $callback);
151 | if ($callback !== null) {
152 | $str = $this->parseCallbackTags($str, $item_data, $callback);
153 | }
154 |
155 | $looped_text .= $str;
156 | }
157 | }
158 | $text = preg_replace('/'.preg_quote($match[0][0], '/').'/m', addcslashes($looped_text, '\\$'), $text, 1);
159 | } else { // It's a callback block.
160 | // Let's extract it so it doesn't conflict
161 | // with the local scope variables in the next step.
162 | $text = $this->createExtraction('callback_blocks', $match[0][0], $match[0][0], $text);
163 | }
164 | }
165 | }
166 |
167 | /**
168 | * $data_matches[0] is the raw data tag
169 | * $data_matches[1] is the data variable (dot notated)
170 | */
171 | if (preg_match_all($this->variableTagRegex, $text, $data_matches)) {
172 | foreach ($data_matches[1] as $index => $var) {
173 | if (($val = $this->getVariable($var, $data, '__lex_no_value__')) !== '__lex_no_value__') {
174 | $text = str_replace($data_matches[0][$index], $val, $text);
175 | }
176 | }
177 | }
178 |
179 | return $text;
180 | }
181 |
182 | /**
183 | * Parses all Callback tags, and sends them through the given $callback.
184 | *
185 | * @param string $text Text to parse
186 | * @param mixed $callback Callback to apply to each tag
187 | * @param bool $inConditional Whether we are in a conditional tag
188 | * @return string
189 | */
190 | public function parseCallbackTags($text, $data, $callback)
191 | {
192 | $this->setupRegex();
193 | $inCondition = $this->inCondition;
194 |
195 | if ($inCondition) {
196 | $regex = '/\{\s*('.$this->variableRegex.')(\s+.*?)?\s*\}/ms';
197 | } else {
198 | $regex = '/\{\{\s*('.$this->variableRegex.')(\s+.*?)?\s*(\/)?\}\}/ms';
199 | }
200 | /**
201 | * $match[0][0] is the raw tag
202 | * $match[0][1] is the offset of raw tag
203 | * $match[1][0] is the callback name
204 | * $match[1][1] is the offset of callback name
205 | * $match[2][0] is the parameters
206 | * $match[2][1] is the offset of parameters
207 | * $match[3][0] is the self closure
208 | * $match[3][1] is the offset of closure
209 | */
210 | while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
211 | $selfClosed = false;
212 | $parameters = array();
213 | $tag = $match[0][0];
214 | $start = $match[0][1];
215 | $name = $match[1][0];
216 | if (isset($match[2])) {
217 | $cb_data = $data;
218 | if ( !empty(self::$callbackData)) {
219 | $data = $this->toArray($data);
220 | $cb_data = array_merge(self::$callbackData, $data);
221 | }
222 | $raw_params = $this->injectExtractions($match[2][0], '__cond_str');
223 | $parameters = $this->parseParameters($raw_params, $cb_data, $callback);
224 | }
225 |
226 | if (isset($match[3])) {
227 | $selfClosed = true;
228 | }
229 | $content = '';
230 |
231 | $temp_text = substr($text, $start + strlen($tag));
232 | if (preg_match('/\{\{\s*\/'.preg_quote($name, '/').'\s*\}\}/m', $temp_text, $match, PREG_OFFSET_CAPTURE) && ! $selfClosed) {
233 |
234 | $content = substr($temp_text, 0, $match[0][1]);
235 | $tag .= $content.$match[0][0];
236 |
237 | // Is there a nested block under this one existing with the same name?
238 | $nested_regex = '/\{\{\s*('.preg_quote($name, '/').')(\s.*?)\}\}(.*?)\{\{\s*\/\1\s*\}\}/ms';
239 | if (preg_match($nested_regex, $content.$match[0][0], $nested_matches)) {
240 | $nested_content = preg_replace('/\{\{\s*\/'.preg_quote($name, '/').'\s*\}\}/m', '', $nested_matches[0]);
241 | $content = $this->createExtraction('nested_looped_tags', $nested_content, $nested_content, $content);
242 | }
243 | }
244 |
245 | $replacement = call_user_func_array($callback, array($name, $parameters, $content));
246 | $replacement = $this->parseRecursives($replacement, $content, $callback);
247 |
248 | if ($inCondition) {
249 | $replacement = $this->valueToLiteral($replacement);
250 | }
251 | $text = preg_replace('/'.preg_quote($tag, '/').'/m', addcslashes($replacement, '\\$'), $text, 1);
252 | $text = $this->injectExtractions($text, 'nested_looped_tags');
253 | }
254 |
255 | return $text;
256 | }
257 |
258 | /**
259 | * Parses all conditionals, then executes the conditionals.
260 | *
261 | * @param string $text Text to parse
262 | * @param mixed $data Data to use when executing conditionals
263 | * @param mixed $callback The callback to be used for tags
264 | * @return string
265 | */
266 | public function parseConditionals($text, $data, $callback)
267 | {
268 | $this->setupRegex();
269 | preg_match_all($this->conditionalRegex, $text, $matches, PREG_SET_ORDER);
270 |
271 | $this->conditionalData = $data;
272 |
273 | /**
274 | * $matches[][0] = Full Match
275 | * $matches[][1] = Either 'if', 'unless', 'elseif', 'elseunless'
276 | * $matches[][2] = Condition
277 | */
278 | foreach ($matches as $match) {
279 | $this->inCondition = true;
280 |
281 | $condition = $match[2];
282 |
283 | // Extract all literal string in the conditional to make it easier
284 | if (preg_match_all('/(["\']).*?(?createExtraction('__cond_str', $m, $m, $condition);
287 | }
288 | }
289 | $condition = preg_replace($this->conditionalNotRegex, '$1!$2', $condition);
290 |
291 | if (preg_match_all($this->conditionalExistsRegex, $condition, $existsMatches, PREG_SET_ORDER)) {
292 | foreach ($existsMatches as $m) {
293 | $exists = 'true';
294 | if ($this->getVariable($m[2], $data, '__doesnt_exist__') === '__doesnt_exist__') {
295 | $exists = 'false';
296 | }
297 | $condition = $this->createExtraction('__cond_exists', $m[0], $m[1].$exists.$m[3], $condition);
298 | }
299 | }
300 |
301 | $condition = preg_replace_callback('/\b('.$this->variableRegex.')\b/', array($this, 'processConditionVar'), $condition);
302 |
303 | if ($callback) {
304 | $condition = preg_replace('/\b(?!\{\s*)('.$this->callbackNameRegex.')(?!\s+.*?\s*\})\b/', '{$1}', $condition);
305 | $condition = $this->parseCallbackTags($condition, $data, $callback);
306 | }
307 |
308 | // Re-extract the strings that have now been possibly added.
309 | if (preg_match_all('/(["\']).*?(?createExtraction('__cond_str', $m, $m, $condition);
312 | }
313 | }
314 |
315 |
316 | // Re-process for variables, we trick processConditionVar so that it will return null
317 | $this->inCondition = false;
318 | $condition = preg_replace_callback('/\b('.$this->variableRegex.')\b/', array($this, 'processConditionVar'), $condition);
319 | $this->inCondition = true;
320 |
321 | // Re-inject any strings we extracted
322 | $condition = $this->injectExtractions($condition, '__cond_str');
323 | $condition = $this->injectExtractions($condition, '__cond_exists');
324 |
325 | $conditional = '';
336 |
337 | $text = preg_replace('/'.preg_quote($match[0], '/').'/m', addcslashes($conditional, '\\$'), $text, 1);
338 | }
339 |
340 | $text = preg_replace($this->conditionalElseRegex, '', $text);
341 | $text = preg_replace($this->conditionalEndRegex, '', $text);
342 |
343 | $text = $this->parsePhp($text);
344 | $this->inCondition = false;
345 |
346 | return $text;
347 | }
348 |
349 | /**
350 | * Goes recursively through a callback tag with a passed child array.
351 | *
352 | * @param string $text - The replaced text after a callback.
353 | * @param string $orig_text - The original text, before a callback is called.
354 | * @param mixed $callback
355 | * @return string $text
356 | */
357 | public function parseRecursives($text, $orig_text, $callback)
358 | {
359 | // Is there a {{ *recursive [array_key]* }} tag here, let's loop through it.
360 | if (preg_match($this->recursiveRegex, $text, $match)) {
361 | $array_key = $match[1];
362 | $tag = $match[0];
363 | $next_tag = null;
364 | $children = self::$callbackData[$array_key];
365 | $child_count = count($children);
366 | $count = 1;
367 |
368 | // Is the array not multi-dimensional? Let's make it multi-dimensional.
369 | if ($child_count == count($children, COUNT_RECURSIVE)) {
370 | $children = array($children);
371 | $child_count = 1;
372 | }
373 |
374 | foreach ($children as $child) {
375 | $has_children = true;
376 |
377 | // If this is a object let's convert it to an array.
378 | $child = $this->toArray($child);
379 |
380 | // Does this child not contain any children?
381 | // Let's set it as empty then to avoid any errors.
382 | if ( ! array_key_exists($array_key, $child)) {
383 | $child[$array_key] = array();
384 | $has_children = false;
385 | }
386 |
387 | $replacement = $this->parse($orig_text, $child, $callback, $this->allowPhp);
388 |
389 | // If this is the first loop we'll use $tag as reference, if not
390 | // we'll use the previous tag ($next_tag)
391 | $current_tag = ($next_tag !== null) ? $next_tag : $tag;
392 |
393 | // If this is the last loop set the next tag to be empty
394 | // otherwise hash it.
395 | $next_tag = ($count == $child_count) ? '' : md5($tag.$replacement);
396 |
397 | $text = str_replace($current_tag, $replacement.$next_tag, $text);
398 |
399 | if ($has_children) {
400 | $text = $this->parseRecursives($text, $orig_text, $callback);
401 | }
402 | $count++;
403 | }
404 | }
405 |
406 | return $text;
407 | }
408 |
409 | /**
410 | * Gets or sets the Scope Glue
411 | *
412 | * @param string|null $glue The Scope Glue
413 | * @return string
414 | */
415 | public function scopeGlue($glue = null)
416 | {
417 | if ($glue !== null) {
418 | $this->regexSetup = false;
419 | $this->scopeGlue = $glue;
420 | }
421 |
422 | return $this->scopeGlue;
423 | }
424 |
425 | /**
426 | * Sets the noparse style. Immediate or cumulative.
427 | *
428 | * @param bool $mode
429 | * @return void
430 | */
431 | public function cumulativeNoparse($mode)
432 | {
433 | $this->cumulativeNoparse = $mode;
434 | }
435 |
436 | /**
437 | * Injects noparse extractions.
438 | *
439 | * This is so that multiple parses can store noparse
440 | * extractions and all noparse can then be injected right
441 | * before data is displayed.
442 | *
443 | * @param string $text Text to inject into
444 | * @return string
445 | */
446 | public static function injectNoparse($text)
447 | {
448 | if (isset(self::$extractions['noparse'])) {
449 | foreach (self::$extractions['noparse'] AS $hash => $replacement) {
450 | if (strpos($text, "noparse_{$hash}") !== FALSE) {
451 | $text = str_replace("noparse_{$hash}", $replacement, $text);
452 | }
453 | }
454 | }
455 |
456 | return $text;
457 | }
458 |
459 | /**
460 | * This is used as a callback for the conditional parser. It takes a variable
461 | * and returns the value of it, properly formatted.
462 | *
463 | * @param array $match A match from preg_replace_callback
464 | * @return string
465 | */
466 | protected function processConditionVar($match)
467 | {
468 | $var = is_array($match) ? $match[0] : $match;
469 | if (in_array(strtolower($var), array('true', 'false', 'null', 'or', 'and')) or
470 | strpos($var, '__cond_str') === 0 or
471 | strpos($var, '__cond_exists') === 0 or
472 | is_numeric($var))
473 | {
474 | return $var;
475 | }
476 |
477 | $value = $this->getVariable($var, $this->conditionalData, '__processConditionVar__');
478 |
479 | if ($value === '__processConditionVar__') {
480 | return $this->inCondition ? $var : 'null';
481 | }
482 |
483 | return $this->valueToLiteral($value);
484 | }
485 |
486 | /**
487 | * This is used as a callback for the conditional parser. It takes a variable
488 | * and returns the value of it, properly formatted.
489 | *
490 | * @param array $match A match from preg_replace_callback
491 | * @return string
492 | */
493 | protected function processParamVar($match)
494 | {
495 | return $match[1].$this->processConditionVar($match[2]);
496 | }
497 |
498 | /**
499 | * Takes a value and returns the literal value for it for use in a tag.
500 | *
501 | * @param string $value Value to convert
502 | * @return string
503 | */
504 | protected function valueToLiteral($value)
505 | {
506 | if (is_object($value) and is_callable(array($value, '__toString'))) {
507 | return var_export((string) $value, true);
508 | } elseif (is_array($value)) {
509 | return !empty($value) ? "true" : "false";
510 | } else {
511 | return var_export($value, true);
512 | }
513 | }
514 |
515 | /**
516 | * Sets up all the global regex to use the correct Scope Glue.
517 | *
518 | * @return void
519 | */
520 | protected function setupRegex()
521 | {
522 | if ($this->regexSetup) {
523 | return;
524 | }
525 | $glue = preg_quote($this->scopeGlue, '/');
526 |
527 | $this->variableRegex = $glue === '\\.' ? '[a-zA-Z0-9_'.$glue.']+' : '[a-zA-Z0-9_\.'.$glue.']+';
528 | $this->callbackNameRegex = $this->variableRegex.$glue.$this->variableRegex;
529 | $this->variableLoopRegex = '/\{\{\s*('.$this->variableRegex.')\s*\}\}(.*?)\{\{\s*\/\1\s*\}\}/ms';
530 | $this->variableTagRegex = '/\{\{\s*('.$this->variableRegex.')\s*\}\}/m';
531 |
532 | $this->callbackBlockRegex = '/\{\{\s*('.$this->variableRegex.')(\s.*?)\}\}(.*?)\{\{\s*\/\1\s*\}\}/ms';
533 |
534 | $this->recursiveRegex = '/\{\{\s*\*recursive\s*('.$this->variableRegex.')\*\s*\}\}/ms';
535 |
536 | $this->noparseRegex = '/\{\{\s*noparse\s*\}\}(.*?)\{\{\s*\/noparse\s*\}\}/ms';
537 |
538 | $this->conditionalRegex = '/\{\{\s*(if|unless|elseif|elseunless)\s*((?:\()?(.*?)(?:\))?)\s*\}\}/ms';
539 | $this->conditionalElseRegex = '/\{\{\s*else\s*\}\}/ms';
540 | $this->conditionalEndRegex = '/\{\{\s*endif\s*\}\}/ms';
541 | $this->conditionalExistsRegex = '/(\s+|^)exists\s+('.$this->variableRegex.')(\s+|$)/ms';
542 | $this->conditionalNotRegex = '/(\s+|^)not(\s+|$)/ms';
543 |
544 | $this->regexSetup = true;
545 |
546 | // This is important, it's pretty unclear by the documentation
547 | // what the default value is on <= 5.3.6
548 | ini_set('pcre.backtrack_limit', 1000000);
549 | }
550 |
551 | /**
552 | * Extracts the noparse text so that it is not parsed.
553 | *
554 | * @param string $text The text to extract from
555 | * @return string
556 | */
557 | protected function extractNoparse($text)
558 | {
559 | /**
560 | * $matches[][0] is the raw noparse match
561 | * $matches[][1] is the noparse contents
562 | */
563 | if (preg_match_all($this->noparseRegex, $text, $matches, PREG_SET_ORDER)) {
564 | foreach ($matches as $match) {
565 | $text = $this->createExtraction('noparse', $match[0], $match[1], $text);
566 | }
567 | }
568 |
569 | return $text;
570 | }
571 |
572 | /**
573 | * Extracts the looped tags so that we can parse conditionals then re-inject.
574 | *
575 | * @param string $text The text to extract from
576 | * @return string
577 | */
578 | protected function extractLoopedTags($text, $data = array(), $callback = null)
579 | {
580 | /**
581 | * $matches[][0] is the raw match
582 | */
583 | if (preg_match_all($this->callbackBlockRegex, $text, $matches, PREG_SET_ORDER)) {
584 | foreach ($matches as $match) {
585 | // Does this callback block contain parameters?
586 | if ($this->parseParameters($match[2], $data, $callback)) {
587 | // Let's extract it so it doesn't conflict with local variables when
588 | // parseVariables() is called.
589 | $text = $this->createExtraction('callback_blocks', $match[0], $match[0], $text);
590 | } else {
591 | $text = $this->createExtraction('looped_tags', $match[0], $match[0], $text);
592 | }
593 | }
594 | }
595 |
596 | return $text;
597 | }
598 |
599 | /**
600 | * Extracts text out of the given text and replaces it with a hash which
601 | * can be used to inject the extractions replacement later.
602 | *
603 | * @param string $type Type of extraction
604 | * @param string $extraction The text to extract
605 | * @param string $replacement Text that will replace the extraction when re-injected
606 | * @param string $text Text to extract out of
607 | * @return string
608 | */
609 | protected function createExtraction($type, $extraction, $replacement, $text)
610 | {
611 | $hash = md5($replacement);
612 | self::$extractions[$type][$hash] = $replacement;
613 |
614 | return str_replace($extraction, "{$type}_{$hash}", $text);
615 | }
616 |
617 | /**
618 | * Injects all of the extractions.
619 | *
620 | * @param string $text Text to inject into
621 | * @return string
622 | */
623 | protected function injectExtractions($text, $type = null)
624 | {
625 | if ($type === null) {
626 | foreach (self::$extractions as $type => $extractions) {
627 | foreach ($extractions as $hash => $replacement) {
628 | if (strpos($text, "{$type}_{$hash}") !== false) {
629 | $text = str_replace("{$type}_{$hash}", $replacement, $text);
630 | unset(self::$extractions[$type][$hash]);
631 | }
632 | }
633 | }
634 | } else {
635 | if ( ! isset(self::$extractions[$type])) {
636 | return $text;
637 | }
638 |
639 | foreach (self::$extractions[$type] as $hash => $replacement) {
640 | if (strpos($text, "{$type}_{$hash}") !== false) {
641 | $text = str_replace("{$type}_{$hash}", $replacement, $text);
642 | unset(self::$extractions[$type][$hash]);
643 | }
644 | }
645 | }
646 |
647 | return $text;
648 | }
649 |
650 | /**
651 | * Takes a dot-notated key and finds the value for it in the given
652 | * array or object.
653 | *
654 | * @param string $key Dot-notated key to find
655 | * @param array|object $data Array or object to search
656 | * @param mixed $default Default value to use if not found
657 | * @return mixed
658 | */
659 | protected function getVariable($key, $data, $default = null)
660 | {
661 | if (strpos($key, $this->scopeGlue) === false) {
662 | $parts = explode('.', $key);
663 | } else {
664 | $parts = explode($this->scopeGlue, $key);
665 | }
666 | foreach ($parts as $key_part) {
667 | if (is_array($data)) {
668 | if ( ! array_key_exists($key_part, $data)) {
669 | return $default;
670 | }
671 |
672 | $data = $data[$key_part];
673 | } elseif (is_object($data)) {
674 | if ( ! isset($data->{$key_part})) {
675 | return $default;
676 | }
677 |
678 | $data = $data->{$key_part};
679 | } else {
680 | return $default;
681 | }
682 | }
683 |
684 | return $data;
685 | }
686 |
687 | /**
688 | * Evaluates the PHP in the given string.
689 | *
690 | * @param string $text Text to evaluate
691 | * @return string
692 | */
693 | protected function parsePhp($text)
694 | {
695 | ob_start();
696 | $result = eval('?>'.$text.'', 'conditionalData = $data;
716 | $this->inCondition = true;
717 | // Extract all literal string in the conditional to make it easier
718 | if (preg_match_all('/(["\']).*?(?createExtraction('__param_str', $m, $m, $parameters);
721 | }
722 | }
723 |
724 | $parameters = preg_replace_callback(
725 | '/(.*?\s*=\s*(?!__))('.$this->variableRegex.')/is',
726 | array($this, 'processParamVar'),
727 | $parameters
728 | );
729 | if ($callback) {
730 | $parameters = preg_replace('/(.*?\s*=\s*(?!\{\s*)(?!__))('.$this->callbackNameRegex.')(?!\s*\})\b/', '$1{$2}', $parameters);
731 | $parameters = $this->parseCallbackTags($parameters, $data, $callback);
732 | }
733 |
734 | // Re-inject any strings we extracted
735 | $parameters = $this->injectExtractions($parameters, '__param_str');
736 | $this->inCondition = false;
737 |
738 | if (preg_match_all('/(.*?)\s*=\s*(\'|"|?\w+;)(.*?)(? $attr) {
741 | $return[trim($matches[1][$i])] = stripslashes($matches[3][$i]);
742 | }
743 |
744 | return $return;
745 | }
746 |
747 | return array();
748 | }
749 |
750 | /**
751 | * Convert objects to arrays
752 | *
753 | * @param mixed $data
754 | * @return array
755 | */
756 | public function toArray($data = array())
757 | {
758 | if ($data instanceof ArrayableInterface) {
759 | $data = $data->toArray();
760 | }
761 |
762 | // Objects to arrays
763 | is_array($data) or $data = (array) $data;
764 |
765 | // lower case array keys
766 | if (is_array($data)) {
767 | $data = array_change_key_case($data, CASE_LOWER);
768 | }
769 |
770 | return $data;
771 | }
772 | }
--------------------------------------------------------------------------------
/lib/Lex/ParsingException.php:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | tests
11 |
12 |
13 |
--------------------------------------------------------------------------------
/tests/ParserTest.php:
--------------------------------------------------------------------------------
1 | parser = new Lex\Parser();
9 | }
10 |
11 | public function templateDataProvider()
12 | {
13 | return array(
14 | array(
15 | array(
16 | 'name' => 'Lex',
17 | 'filters' => array(
18 | 'enable' => true,
19 | ),
20 | ),
21 | ),
22 | );
23 | }
24 |
25 | public function testCanSetScopeGlue()
26 | {
27 | $this->parser->scopeGlue('~');
28 | $scopeGlue = new ReflectionProperty($this->parser, 'scopeGlue');
29 |
30 | $this->assertTrue($scopeGlue->isProtected());
31 |
32 | $scopeGlue->setAccessible(true);
33 | $this->assertEquals('~', $scopeGlue->getValue($this->parser));
34 | }
35 |
36 | public function testCanGetScopeGlue()
37 | {
38 | $this->parser->scopeGlue('~');
39 | $this->assertEquals('~', $this->parser->scopeGlue());
40 | }
41 |
42 | public function testValueToLiteral()
43 | {
44 | $method = new ReflectionMethod($this->parser, 'valueToLiteral');
45 |
46 | $this->assertTrue($method->isProtected());
47 |
48 | $method->setAccessible(true);
49 |
50 | $this->assertSame("NULL", $method->invoke($this->parser, null));
51 | $this->assertSame("true", $method->invoke($this->parser, true));
52 | $this->assertSame("false", $method->invoke($this->parser, false));
53 | $this->assertSame("'some_string'", $method->invoke($this->parser, "some_string"));
54 | $this->assertSame("24", $method->invoke($this->parser, 24));
55 | $this->assertSame("true", $method->invoke($this->parser, array('foo')));
56 | $this->assertSame("false", $method->invoke($this->parser, array()));
57 |
58 | $mock = $this->getMock('stdClass', array('__toString'));
59 | $mock->expects($this->any())
60 | ->method('__toString')
61 | ->will($this->returnValue('obj_string'));
62 |
63 | $this->assertSame("'obj_string'", $method->invoke($this->parser, $mock));
64 | }
65 |
66 | /**
67 | * @dataProvider templateDataProvider
68 | */
69 | public function testGetVariable($data)
70 | {
71 | $method = new ReflectionMethod($this->parser, 'getVariable');
72 |
73 | $this->assertTrue($method->isProtected());
74 |
75 | $method->setAccessible(true);
76 |
77 | $this->assertEquals('Lex', $method->invoke($this->parser, 'name', $data));
78 | $this->assertEquals(null, $method->invoke($this->parser, 'age', $data));
79 | $this->assertEquals(false, $method->invoke($this->parser, 'age', $data, false));
80 |
81 | $this->assertEquals(true, $method->invoke($this->parser, 'filters.enable', $data));
82 | $this->assertEquals(null, $method->invoke($this->parser, 'filters.name', $data));
83 | $this->assertEquals(false, $method->invoke($this->parser, 'filters.name', $data, false));
84 |
85 | }
86 |
87 | /**
88 | * Regression test for https://www.pyrocms.com/forums/topics/view/19686
89 | */
90 | public function testFalseyVariableValuesParseProperly()
91 | {
92 | $data = array(
93 | 'zero_num' => 0,
94 | 'zero_string' => "0",
95 | 'zero_float' => 0.0,
96 | 'empty_string' => "",
97 | 'null_value' => null,
98 | 'simplexml_empty_node' => simplexml_load_string(''),
99 | );
100 |
101 | $text = "{{zero_num}},{{zero_string}},{{zero_float}},{{empty_string}},{{null_value}},{{simplexml_empty_node}}";
102 | $expected = '0,0,0,,,';
103 |
104 | $result = $this->parser->parseVariables($text, $data);
105 |
106 | $this->assertEquals($expected, $result);
107 | }
108 |
109 | /**
110 | * @dataProvider templateDataProvider
111 | */
112 | public function testExists($data)
113 | {
114 | $result = $this->parser->parse("{{ if exists name }}1{{ else }}0{{ endif }}", $data);
115 | $this->assertEquals('1', $result);
116 |
117 | $result = $this->parser->parse("{{ if not exists age }}0{{ else }}1{{ endif }}", $data);
118 | $this->assertEquals('0', $result);
119 | }
120 |
121 | /**
122 | * Regression test for https://github.com/fuelphp/lex/issues/2
123 | *
124 | * @dataProvider templateDataProvider
125 | */
126 | public function testUndefinedInConditional($data)
127 | {
128 | $result = $this->parser->parse("{{ if age }}0{{ else }}1{{ endif }}", $data);
129 | $this->assertEquals('1', $result);
130 | }
131 |
132 | /**
133 | * Regression test for https://github.com/pyrocms/pyrocms/issues/1906
134 | */
135 | public function testCallbacksInConditionalComparison()
136 | {
137 | $result = $this->parser->parse("{{ if foo.bar.baz == 'yes' }}Yes{{ else }}No{{ endif }}", array(), function ($name, $attributes, $content) {
138 | if ($name == 'foo.bar.baz') {
139 | return 'yes';
140 | }
141 | return 'no';
142 | });
143 | $this->assertEquals('Yes', $result);
144 | }
145 |
146 | /**
147 | * Test for https://github.com/pyrocms/pyrocms/issues/2104
148 | *
149 | * Failing IF statements multiple levels deep
150 | * - IS_JUL Tests 'text' == 'text'
151 | * - TOTAL_GT_0 Tests total > 0
152 | * - HAS_ENTRIES Tests isset(entries)
153 | */
154 | public function testDeepCallbacksInConditionalComparison()
155 | {
156 | $data = array(
157 | 'pagination' => null,
158 | 'total' => 172,
159 | 'years' => array(
160 | 2012 => array(
161 | 'year' => '2012',
162 | 'months' => array(
163 | '01' => array(
164 | 'month' => 'jan',
165 | 'month_num' => '01',
166 | 'date' => 946713600,
167 | 'total' => 3,
168 | 'entries' => array(
169 | 1326787200 => array(),
170 | 1326355200 => array(),
171 | 1325577600 => array(),
172 | ),
173 | ),
174 | '02' => array(
175 | 'month' => 'feb',
176 | 'month_num' => '02',
177 | 'date' => 949392000,
178 | 'total' => 0,
179 | ),
180 | '07' => array(
181 | 'month' => 'jul',
182 | 'month_num' => '07',
183 | 'date' => 962434800,
184 | 'total' => 1,
185 | 'entries' => array(
186 | 1343026800 => array(),
187 | ),
188 | ),
189 | 10 => array(
190 | 'month' => 'oct',
191 | 'month_num' => '10',
192 | 'date' => 970383600,
193 | 'total' => 2,
194 | 'entries' => array(
195 | 1350543600 => array(),
196 | 1350457200 => array(),
197 | ),
198 | ),
199 | 11 => array(
200 | 'month' => 'nov',
201 | 'month_num' => '11',
202 | 'date' => 973065600,
203 | 'total' => 4,
204 | 'entries' => array(
205 | 1354003200 => array(),
206 | 1353398400 => array(),
207 | 1352707200 => array(),
208 | ),
209 | ),
210 | 12 => array(
211 | 'month' => 'dec',
212 | 'month_num' => '12',
213 | 'date' => 975657600,
214 | 'total' => 0,
215 | ),
216 | ),
217 | ),
218 | 2011 => array(
219 | 'year' => '2011',
220 | 'months' => array(
221 | '01' => array(
222 | 'month' => 'jan',
223 | 'month_num' => '01',
224 | 'date' => 946713600,
225 | 'total' => 0,
226 | ),
227 | '04' => array(
228 | 'month' => 'apr',
229 | 'month_num' => '04',
230 | 'date' => 954576000,
231 | 'total' => 13,
232 | 'entries' => array(
233 | 1303974000 => array(),
234 | 1303887600 => array(),
235 | ),
236 | ),
237 | '07' => array(
238 | 'month' => 'jul',
239 | 'month_num' => '07',
240 | 'date' => 962434800,
241 | 'total' => 0,
242 | ),
243 | '08' => array(
244 | 'month' => 'aug',
245 | 'month_num' => '08',
246 | 'date' => 965113200,
247 | 'total' => 8,
248 | 'entries' => array(
249 | 1313391600 => array(),
250 | 1313046000 => array(),
251 | 1312354800 => array(),
252 | ),
253 | ),
254 | ),
255 | ),
256 | ),
257 | );
258 |
259 | $html = << 0 }}true{{ else }}false{{ endif }}, HAS_ENTRIES: {{ if entries }}true{{ else }}false{{ endif }}
266 |
267 | {{ /months }}
268 | )
269 | {{ /years }}
270 | )
271 | HTML;
272 |
273 | $expected_html = <<parser->parse($html, $data);
298 |
299 | $this->assertEquals($expected_html, $result);
300 |
301 | }
302 |
303 | public function testSelfClosingTag()
304 | {
305 | $self = $this;
306 | $result = $this->parser->parse("{{ foo.bar.baz /}}Here{{ foo.bar.baz }}Content{{ /foo.bar.baz }}", array(), function ($name, $attributes, $content) use ($self) {
307 | if ($content == '') {
308 | return 'DanWas';
309 | } else {
310 | return '';
311 | }
312 | });
313 | $this->assertEquals('DanWasHere', $result);
314 | }
315 |
316 | /**
317 | * Test that the toArray method converts an standard object to an array
318 | */
319 | public function testObjectToArray()
320 | {
321 | $data = new stdClass;
322 | $data->foo = 'bar';
323 |
324 | $result = $this->parser->toArray($data);
325 |
326 | $this->assertEquals(array('foo' => 'bar'), $result);
327 | }
328 |
329 | /**
330 | * Test that the toArray method converts an object that implements ArrayableInterface to an array
331 | */
332 | public function testArrayableInterfaceToArray()
333 | {
334 | $data = new Lex\ArrayableObjectExample;
335 |
336 | $result = $this->parser->toArray($data);
337 |
338 | $this->assertEquals(array('foo' => 'bar'), $result);
339 | }
340 |
341 | /**
342 | * Test that the toArray method converts an integer to an array
343 | */
344 | public function testIntegerToArray()
345 | {
346 | $data = 1;
347 |
348 | $result = $this->parser->toArray($data);
349 |
350 | $this->assertEquals(true, is_array($result));
351 | }
352 |
353 | /**
354 | * Test that the toArray method converts an string to an array
355 | */
356 | public function testStringToArray()
357 | {
358 | $data = 'Hello World';
359 |
360 | $result = $this->parser->toArray($data);
361 |
362 | $this->assertEquals(true, is_array($result));
363 | }
364 |
365 | /**
366 | * Test that the toArray method converts an boolean to an array
367 | */
368 | public function testBooleanToArray()
369 | {
370 | $data = true;
371 |
372 | $result = $this->parser->toArray($data);
373 |
374 | $this->assertEquals(true, is_array($result));
375 | }
376 |
377 | /**
378 | * Test that the toArray method converts an null value to an array
379 | */
380 | public function testNullToArray()
381 | {
382 | $data = null;
383 |
384 | $result = $this->parser->toArray($data);
385 |
386 | $this->assertEquals(true, is_array($result));
387 | }
388 |
389 | /**
390 | * Test that the toArray method converts an float value to an array
391 | */
392 | public function testFloatToArray()
393 | {
394 | $data = 1.23456789;
395 |
396 | $result = $this->parser->toArray($data);
397 |
398 | $this->assertEquals(true, is_array($result));
399 | }
400 | }
401 |
--------------------------------------------------------------------------------