├── .git-blame-ignore-revs
├── .php-cs-fixer.dist.php
├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── infection.json.dist
├── phpunit.xml.dist
└── src
└── Liquid
├── AbstractBlock.php
├── AbstractTag.php
├── Cache.php
├── Cache
├── Apc.php
├── File.php
└── Local.php
├── Context.php
├── CustomFilters.php
├── Decision.php
├── Document.php
├── Drop.php
├── Exception
├── CacheException.php
├── FilesystemException.php
├── MissingFilesystemException.php
├── NotFoundException.php
├── ParseException.php
├── RenderException.php
└── WrongArgumentException.php
├── FileSystem.php
├── FileSystem
├── Local.php
└── Virtual.php
├── Filterbank.php
├── Liquid.php
├── LiquidException.php
├── LocalFileSystem.php
├── Regexp.php
├── StandardFilters.php
├── Tag
├── TagAssign.php
├── TagBlock.php
├── TagBreak.php
├── TagCapture.php
├── TagCase.php
├── TagComment.php
├── TagContinue.php
├── TagCycle.php
├── TagDecrement.php
├── TagExtends.php
├── TagFor.php
├── TagIf.php
├── TagIfchanged.php
├── TagInclude.php
├── TagIncrement.php
├── TagPaginate.php
├── TagRaw.php
├── TagTablerow.php
└── TagUnless.php
├── Template.php
└── Variable.php
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
2 | # https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt
3 |
4 | 72c7e3f4e34d2ed474b1399f2a7ab427fb6f3b14
5 | d837112369c0193def1c27636da1b9a8df5c98f3
6 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
24 | ->setRiskyAllowed(true)
25 | ->setRules([
26 | '@PSR2' => true,
27 | 'psr_autoloading' => true,
28 | 'no_unreachable_default_argument_value' => true,
29 | 'no_useless_else' => true,
30 | 'no_useless_return' => true,
31 | 'phpdoc_add_missing_param_annotation' => true,
32 | 'phpdoc_order' => true,
33 | 'semicolon_after_instruction' => true,
34 | 'whitespace_after_comma_in_array' => true,
35 | 'header_comment' => ['header' => $header],
36 | 'php_unit_construct' => true,
37 | 'php_unit_dedicate_assert' => true,
38 | 'php_unit_dedicate_assert_internal_type' => true,
39 | 'php_unit_expectation' => true,
40 | 'php_unit_mock_short_will_return' => true,
41 | 'php_unit_mock' => true,
42 | 'php_unit_namespaced' => true,
43 | 'php_unit_no_expectation_annotation' => true,
44 | "phpdoc_order_by_value" => ['annotations' => ['covers']],
45 | 'php_unit_set_up_tear_down_visibility' => true,
46 | 'php_unit_test_case_static_method_calls' => ['call_type' => 'this'],
47 | 'no_whitespace_in_blank_line' => true,
48 | 'nullable_type_declaration_for_default_null_value' => true,
49 | 'array_syntax' => ['syntax' => 'short'],
50 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']],
51 | 'binary_operator_spaces' => ['default' => 'at_least_single_space'],
52 | ])
53 | ->setIndent("\t")
54 | ->setFinder(
55 | PhpCsFixer\Finder::create()
56 | ->in(__DIR__)
57 | ->append([__FILE__])
58 | )
59 | ;
60 |
61 |
62 | return $config;
63 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## master
2 |
3 | ## 1.4.8 (2018-03-22)
4 |
5 | * Now we return null for missing properties, like we do for missing keys for arrays.
6 |
7 | ## 1.4.7 (2018-02-09)
8 |
9 | * Paginate tag shall now respect request parameters.
10 | * It is now possible to set a custom query param for the paginate tag.
11 | * Page number will now never go overboard.
12 |
13 | ## 1.4.6 (2018-02-07)
14 |
15 | * TagPaginate shall not pollute the global scope, but work in own scope.
16 | * TagPaginate errors if no collection present instead of vague warning.
17 |
18 | ## 1.4.5 (2017-12-12)
19 |
20 | * Capture tag shall save a variable in the global context.
21 |
22 | ## 1.4.4 (2017-11-03)
23 |
24 | * TagUnless is an inverted TagIf: simplified implementation
25 | * Allow dashes in filenames
26 |
27 | ## 1.4.3 (2017-10-10)
28 |
29 | * `escape` and `escape_once` filters now escape everything, but arrays
30 | * New standard filter for explicit string conversion
31 |
32 | ## 1.4.2 (2017-10-09)
33 |
34 | * Better caching for non-extending templates
35 | * Simplified 'assign' tag to use rules for variables
36 | * Now supporting PHP 7.2
37 | * Different types of exception depending on the case
38 | * Filterbank will not call instance methods statically
39 | * Callback-type filters
40 |
41 | ## 1.4.1 (2017-09-28)
42 |
43 | * Unquoted template names in 'include' tag, as in Jekyll
44 | * Caching now works correctly with 'extends' tag
45 |
46 | ## 1.4.0 (2017-09-25)
47 |
48 | * Dropped support for EOL'ed versions of PHP (< 5.6)
49 | * Arrays won't be silently cast to string as 'Array' anymore
50 | * Complex objects could now be passed between templates and to filters
51 | * Additional test coverage
52 |
53 | ## 1.3.1 (2017-09-23)
54 |
55 | * Support for numeric and variable array indicies
56 | * Support loop break and continue
57 | * Allow looping over extended ranges
58 | * Math filters now work with floats
59 | * Fixed 'default' filter
60 | * Local cache with data stored in a private variable
61 | * Virtual file system to get inversion of control and DI
62 | * Lots of tests with the coverage upped to 97%
63 | * Small bug fixes and various enhancements
64 |
65 | ## 1.3.0 (2017-07-17)
66 |
67 | * Support Traversable loops and filters
68 | * Fix date filter for format with colon
69 | * Various minor improvements and bugs fixes
70 |
71 | ## 1.2.1 (2016-12-12)
72 |
73 | * Remove content injection from $_GET.
74 | * Add PHP 5.6, 7.0, 7.1 to Travis file.
75 |
76 | ## 1.2 (2016-06-11)
77 |
78 | * Added "ESCAPE_BY_DEFAULT" setting for context-aware auto-escaping.
79 | * Made "Context" work with plain objects.
80 | * "escape" now uses "htmlentities".
81 | * Fixed "escape_now".
82 |
83 | ## 1.1 (2015-06-01)
84 |
85 | * New tags: "paginate", "unless", "ifchanged" were added
86 | * Added support for "for in (range)" syntax
87 | * Added support for multiple conditions in if statements
88 | * Added support for hashes/objects in for loops
89 |
90 | ## 1.0 (2014-09-07)
91 |
92 | * Add namespaces
93 | * Add composer support
94 | * Implement new standard filters
95 | * Add 'raw' tag
96 |
97 | ## 0.9.2 (2012-08-15)
98 |
99 | * context->set allows now global vars
100 | * Allow Templatenames with Fileextension
101 | * Tag 'extends' supports now multiple inheritance
102 | * Clean up code, change all variables and methods to camelCase
103 |
104 | ## 0.9.1 (2012-05-12)
105 |
106 | * added the extends and block filter
107 | * Initial release
108 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Guz Alexander, http://guzalexander.com
2 | Copyright (c) 2011, 2012 Harald Hanek, http://www.delacap.com
3 | Copyright (c) 2006 Mateo Murphy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Liquid template engine for PHP [](https://github.com/kalimatas/php-liquid/actions/workflows/tests.yaml) [](https://coveralls.io/github/kalimatas/php-liquid?branch=master) [](https://packagist.org/packages/liquid/liquid)
2 |
3 | Liquid is a PHP port of the [Liquid template engine for Ruby](https://github.com/Shopify/liquid), which was written by Tobias Lutke. Although there are many other templating engines for PHP, including Smarty (from which Liquid was partially inspired), Liquid had some advantages that made porting worthwhile:
4 |
5 | * Readable and human friendly syntax, that is usable in any type of document, not just html, without need for escaping.
6 | * Quick and easy to use and maintain.
7 | * 100% secure, no possibility of embedding PHP code.
8 | * Clean OO design, rather than the mix of OO and procedural found in other templating engines.
9 | * Seperate compiling and rendering stages for improved performance.
10 | * Easy to extend with your own "tags and filters":https://github.com/harrydeluxe/php-liquid/wiki/Liquid-for-programmers.
11 | * 100% Markup compatibility with a Ruby templating engine, making templates usable for either.
12 | * Unit tested: Liquid is fully unit-tested. The library is stable and ready to be used in large projects.
13 |
14 | ## Why Liquid?
15 |
16 | Why another templating library?
17 |
18 | Liquid was written to meet three templating library requirements: good performance, easy to extend, and simply to use.
19 |
20 | ## Installing
21 |
22 | You can install this lib via [composer](https://getcomposer.org/):
23 |
24 | composer require liquid/liquid
25 |
26 | ## Example template
27 |
28 | {% if products %}
29 |
30 | {% for product in products %}
31 | -
32 |
{{ product.name }}
33 | Only {{ product.price | price }}
34 |
35 | {{ product.description | prettyprint | paragraph }}
36 |
37 | {{ 'it rocks!' | paragraph }}
38 |
39 |
40 | {% endfor %}
41 |
42 | {% endif %}
43 |
44 | ## How to use Liquid
45 |
46 | The main class is `Liquid::Template` class. There are two separate stages of working with Liquid templates: parsing and rendering. Here is a simple example:
47 |
48 | use Liquid\Template;
49 |
50 | $template = new Template();
51 | $template->parse("Hello, {{ name }}!");
52 | echo $template->render(array('name' => 'Alex'));
53 |
54 | // Will echo
55 | // Hello, Alex!
56 |
57 | To find more examples have a look at the `examples` directory or at the original Ruby implementation repository's [wiki page](https://github.com/Shopify/liquid/wiki).
58 |
59 | ## Advanced usage
60 |
61 | You would probably want to add a caching layer (at very least a request-wide one), enable context-aware automatic escaping, and do load includes from disk with full file names.
62 |
63 | use Liquid\Liquid;
64 | use Liquid\Template;
65 | use Liquid\Cache\Local;
66 |
67 | Liquid::set('INCLUDE_SUFFIX', '');
68 | Liquid::set('INCLUDE_PREFIX', '');
69 | Liquid::set('INCLUDE_ALLOW_EXT', true);
70 | Liquid::set('ESCAPE_BY_DEFAULT', true);
71 |
72 | $template = new Template(__DIR__.'/protected/templates/');
73 |
74 | $template->parse("Hello, {% include 'honorific.html' %}{{ plain-html | raw }} {{ comment-with-xss }}");
75 | $template->setCache(new Local());
76 |
77 | echo $template->render([
78 | 'name' => 'Alex',
79 | 'plain-html' => 'Your comment was:',
80 | 'comment-with-xss' => '',
81 | ]);
82 |
83 | Will output:
84 |
85 | Hello, Mx. Alex
86 | Your comment was: <script>alert();</script>
87 |
88 | Note that automatic escaping is not a standard Liquid feature: use with care.
89 |
90 | Similarly, the following snippet will parse and render `templates/home.liquid` while storing parsing results in a class-local cache:
91 |
92 | \Liquid\Liquid::set('INCLUDE_PREFIX', '');
93 |
94 | $template = new \Liquid\Template(__DIR__ . '/protected/templates');
95 | $template->setCache(new \Liquid\Cache\Local());
96 | echo $template->parseFile('home')->render();
97 |
98 | If you render the same template over and over for at least a dozen of times, the class-local cache will give you a slight speed up in range of some milliseconds per render depending on a complexity of your template.
99 |
100 | You should probably extend `Liquid\Template` to initialize everything you do with `Liquid::set` in one place.
101 |
102 | ### Custom filters
103 |
104 | Adding filters has never been easier.
105 |
106 | $template = new Template();
107 | $template->registerFilter('absolute_url', function ($arg) {
108 | return "https://www.example.com$arg";
109 | });
110 | $template->parse("{{ my_url | absolute_url }}");
111 | echo $template->render(array(
112 | 'my_url' => '/test'
113 | ));
114 | // expect: https://www.example.com/test
115 |
116 | ## Requirements
117 |
118 | * PHP 7.4+
119 |
120 | Some earlier versions could be used with PHP 5.3/5.4/5.5/5.6, though they're not supported anymore.
121 |
122 | ## Issues
123 |
124 | Have a bug? Please create an issue here on GitHub!
125 |
126 | [https://github.com/kalimatas/php-liquid/issues](https://github.com/kalimatas/php-liquid/issues)
127 |
128 | ## Fork notes
129 |
130 | This fork is based on [php-liquid](https://github.com/harrydeluxe/php-liquid) by Harald Hanek.
131 |
132 | It contains several improvements:
133 |
134 | * namespaces
135 | * installing via composer
136 | * new standard filters
137 | * `raw` tag added
138 |
139 | Any help is appreciated!
140 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "liquid/liquid",
3 | "type": "library",
4 | "description": "Liquid template engine for PHP",
5 | "keywords": [
6 | "liquid",
7 | "template"
8 | ],
9 | "homepage": "https://github.com/kalimatas/php-liquid",
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Guz Alexander",
14 | "email": "kalimatas@gmail.com",
15 | "homepage": "http://guzalexander.com"
16 | },
17 | {
18 | "name": "Harald Hanek"
19 | },
20 | {
21 | "name": "Mateo Murphy"
22 | },
23 | {
24 | "name": "Alexey Kopytko",
25 | "email": "alexey@kopytko.com",
26 | "homepage": "https://www.alexeykopytko.com/"
27 | }
28 | ],
29 | "require": {
30 | "php": "^7.4 || ^8.0"
31 | },
32 | "require-dev": {
33 | "ergebnis/composer-normalize": ">=2.47",
34 | "friendsofphp/php-cs-fixer": "^3.75",
35 | "infection/infection": ">=0.17.6",
36 | "php-coveralls/php-coveralls": "^2.8",
37 | "phpunit/phpunit": "^9.6.23"
38 | },
39 | "config": {
40 | "sort-packages": true,
41 | "allow-plugins": true
42 | },
43 | "extra": {
44 | "branch-alias": {
45 | "dev-master": "1.x-dev"
46 | }
47 | },
48 | "autoload": {
49 | "psr-4": {
50 | "Liquid\\": "src/Liquid"
51 | }
52 | },
53 | "autoload-dev": {
54 | "psr-4": {
55 | "Liquid\\": "tests/Liquid"
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "timeout": 2,
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 | tests/
15 |
16 |
17 |
18 |
19 |
20 | src/
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Liquid/AbstractBlock.php:
--------------------------------------------------------------------------------
1 | nodelist;
51 | }
52 |
53 | /**
54 | * Parses the given tokens
55 | *
56 | * @param array $tokens
57 | *
58 | * @throws \Liquid\LiquidException
59 | * @return void
60 | */
61 | public function parse(array &$tokens)
62 | {
63 | // Constructor is not reliably called by subclasses, so we need to ensure these are set
64 | $this->startRegexp ??= new Regexp('/^' . Liquid::get('TAG_START') . '/');
65 | $this->tagRegexp ??= new Regexp('/^' . Liquid::get('TAG_START') . Liquid::get('WHITESPACE_CONTROL') . '?\s*(\w+)\s*(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('TAG_END') . '$/s');
66 | $this->variableStartRegexp ??= new Regexp('/^' . Liquid::get('VARIABLE_START') . '/');
67 |
68 | $startRegexp = $this->startRegexp;
69 | $tagRegexp = $this->tagRegexp;
70 | $variableStartRegexp = $this->variableStartRegexp;
71 |
72 | $this->nodelist = [];
73 |
74 | $tags = Template::getTags();
75 |
76 | for ($i = 0, $n = count($tokens); $i < $n; $i++) {
77 | if ($tokens[$i] === null) {
78 | continue;
79 | }
80 | $token = $tokens[$i];
81 | $tokens[$i] = null;
82 |
83 | if ($startRegexp->match($token)) {
84 | $this->whitespaceHandler($token);
85 | if ($tagRegexp->match($token)) {
86 | // If we found the proper block delimitor just end parsing here and let the outer block proceed
87 | if ($tagRegexp->matches[1] == $this->blockDelimiter()) {
88 | $this->endTag();
89 | return;
90 | }
91 |
92 | $tagName = null;
93 | if (array_key_exists($tagRegexp->matches[1], $tags)) {
94 | $tagName = $tags[$tagRegexp->matches[1]];
95 | } else {
96 | $tagName = self::TAG_PREFIX . ucwords($tagRegexp->matches[1]);
97 | $tagName = (class_exists($tagName) === true) ? $tagName : null;
98 | }
99 |
100 | if ($tagName !== null) {
101 | $this->nodelist[] = new $tagName($tagRegexp->matches[2], $tokens, $this->fileSystem);
102 | if ($tagRegexp->matches[1] == 'extends') {
103 | return;
104 | }
105 | } else {
106 | $this->unknownTag($tagRegexp->matches[1], $tagRegexp->matches[2], $tokens);
107 | }
108 | } else {
109 | throw new ParseException("Tag $token was not properly terminated (won't match $tagRegexp)");
110 | }
111 | } elseif ($variableStartRegexp->match($token)) {
112 | $this->whitespaceHandler($token);
113 | $this->nodelist[] = $this->createVariable($token);
114 | } else {
115 | // This is neither a tag or a variable, proceed with an ltrim
116 | if (self::$trimWhitespace) {
117 | $token = ltrim($token);
118 | }
119 |
120 | self::$trimWhitespace = false;
121 | $this->nodelist[] = $token;
122 | }
123 | }
124 |
125 | $this->assertMissingDelimitation();
126 | }
127 |
128 | /**
129 | * Handle the whitespace.
130 | *
131 | * @param string $token
132 | */
133 | protected function whitespaceHandler($token)
134 | {
135 | $this->whitespaceControl ??= Liquid::get('WHITESPACE_CONTROL');
136 |
137 | /*
138 | * This assumes that TAG_START is always '{%', and a whitespace control indicator
139 | * is exactly one character long, on a third position.
140 | */
141 | if ($token[2] === $this->whitespaceControl) {
142 | $previousToken = end($this->nodelist);
143 | if (is_string($previousToken)) { // this can also be a tag or a variable
144 | $this->nodelist[key($this->nodelist)] = rtrim($previousToken);
145 | }
146 | }
147 |
148 | /*
149 | * This assumes that TAG_END is always '%}', and a whitespace control indicator
150 | * is exactly one character long, on a third position from the end.
151 | */
152 | self::$trimWhitespace = $token[-3] === $this->whitespaceControl;
153 | }
154 |
155 | /**
156 | * Render the block.
157 | *
158 | * @param Context $context
159 | *
160 | * @return string
161 | */
162 | public function render(Context $context)
163 | {
164 | return $this->renderAll($this->nodelist, $context);
165 | }
166 |
167 | /**
168 | * Renders all the given nodelist's nodes
169 | *
170 | * @param array $list
171 | * @param Context $context
172 | *
173 | * @return string
174 | */
175 | protected function renderAll(array $list, Context $context)
176 | {
177 | $result = '';
178 |
179 | foreach ($list as $token) {
180 | if (is_object($token) && method_exists($token, 'render')) {
181 | $value = $token->render($context);
182 | } else {
183 | $value = $token;
184 | }
185 |
186 | if (is_array($value)) {
187 | $value = htmlspecialchars(implode($value));
188 | }
189 |
190 | $result .= $value;
191 |
192 | if (isset($context->registers['break'])) {
193 | break;
194 | }
195 | if (isset($context->registers['continue'])) {
196 | break;
197 | }
198 |
199 | $context->tick();
200 | }
201 |
202 | return $result;
203 | }
204 |
205 | /**
206 | * An action to execute when the end tag is reached
207 | */
208 | protected function endTag()
209 | {
210 | // Do nothing by default
211 | }
212 |
213 | /**
214 | * Handler for unknown tags
215 | *
216 | * @param string $tag
217 | * @param string $params
218 | * @param array $tokens
219 | *
220 | * @throws \Liquid\Exception\ParseException
221 | */
222 | protected function unknownTag($tag, $params, array $tokens)
223 | {
224 | switch ($tag) {
225 | case 'else':
226 | throw new ParseException($this->blockName() . " does not expect else tag");
227 | case 'end':
228 | throw new ParseException("'end' is not a valid delimiter for " . $this->blockName() . " tags. Use " . $this->blockDelimiter());
229 | default:
230 | throw new ParseException("Unknown tag $tag");
231 | }
232 | }
233 |
234 | /**
235 | * This method is called at the end of parsing, and will throw an error unless
236 | * this method is subclassed, like it is for Document
237 | *
238 | * @throws \Liquid\Exception\ParseException
239 | * @return bool
240 | */
241 | protected function assertMissingDelimitation()
242 | {
243 | throw new ParseException($this->blockName() . " tag was never closed");
244 | }
245 |
246 | /**
247 | * Returns the string that delimits the end of the block
248 | *
249 | * @return string
250 | */
251 | protected function blockDelimiter()
252 | {
253 | return "end" . $this->blockName();
254 | }
255 |
256 | /**
257 | * Returns the name of the block
258 | *
259 | * @return string
260 | */
261 | private function blockName()
262 | {
263 | $reflection = new \ReflectionClass($this);
264 | return str_replace('tag', '', strtolower($reflection->getShortName()));
265 | }
266 |
267 | /**
268 | * Create a variable for the given token
269 | *
270 | * @param string $token
271 | *
272 | * @throws \Liquid\Exception\ParseException
273 | * @return Variable
274 | */
275 | private function createVariable($token)
276 | {
277 | $this->variableRegexp ??= new Regexp('/^' . Liquid::get('VARIABLE_START') . Liquid::get('WHITESPACE_CONTROL') . '?(.*?)' . Liquid::get('WHITESPACE_CONTROL') . '?' . Liquid::get('VARIABLE_END') . '$/s');
278 |
279 | if ($this->variableRegexp->match($token)) {
280 | return new Variable($this->variableRegexp->matches[1]);
281 | }
282 |
283 | throw new ParseException("Variable $token was not properly terminated");
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/src/Liquid/AbstractTag.php:
--------------------------------------------------------------------------------
1 | markup = $markup;
57 | $this->fileSystem = $fileSystem;
58 | $this->config = &Liquid::$config;
59 |
60 | $this->parse($tokens);
61 | }
62 |
63 | /**
64 | * Parse the given tokens.
65 | *
66 | * @param array $tokens
67 | */
68 | public function parse(array &$tokens)
69 | {
70 | // Do nothing by default
71 | }
72 |
73 | /**
74 | * Render the tag with the given context.
75 | *
76 | * @param Context $context
77 | *
78 | * @return string
79 | */
80 | abstract public function render(Context $context);
81 |
82 | /**
83 | * Extracts tag attributes from a markup string.
84 | *
85 | * @param string $markup
86 | */
87 | protected function extractAttributes($markup)
88 | {
89 | $this->attributes = [];
90 |
91 | $attributeRegexp = new Regexp(Liquid::get('TAG_ATTRIBUTES'));
92 |
93 | $matches = $attributeRegexp->scan($markup);
94 |
95 | foreach ($matches as $match) {
96 | $this->attributes[$match[0]] = $match[1];
97 | }
98 | }
99 |
100 | /**
101 | * Returns the name of the tag.
102 | *
103 | * @return string
104 | */
105 | protected function name()
106 | {
107 | return strtolower(get_class($this));
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Liquid/Cache.php:
--------------------------------------------------------------------------------
1 | expire = $options['cache_expire'];
33 | }
34 |
35 | if (isset($options['cache_prefix'])) {
36 | $this->prefix = $options['cache_prefix'];
37 | }
38 | }
39 |
40 | /**
41 | * Retrieves a value from cache with a specified key.
42 | *
43 | * @param string $key a unique key identifying the cached value
44 | * @param bool $unserialize
45 | *
46 | * @return mixed|boolean the value stored in cache, false if the value is not in the cache or expired.
47 | */
48 | abstract public function read($key, $unserialize = true);
49 |
50 | /**
51 | * Check if specified key exists in cache.
52 | *
53 | * @param string $key a unique key identifying the cached value
54 | *
55 | * @return boolean true if the key is in cache, false otherwise
56 | */
57 | abstract public function exists($key);
58 |
59 | /**
60 | * Stores a value identified by a key in cache.
61 | *
62 | * @param string $key the key identifying the value to be cached
63 | * @param mixed $value the value to be cached
64 | * @param bool $serialize
65 | *
66 | * @return boolean true if the value is successfully stored into cache, false otherwise
67 | */
68 | abstract public function write($key, $value, $serialize = true);
69 |
70 | /**
71 | * Deletes all values from cache.
72 | *
73 | * @param bool $expiredOnly
74 | *
75 | * @return boolean whether the flush operation was successful.
76 | */
77 | abstract public function flush($expiredOnly = false);
78 | }
79 |
--------------------------------------------------------------------------------
/src/Liquid/Cache/Apc.php:
--------------------------------------------------------------------------------
1 | prefix . $key);
48 | }
49 |
50 | /**
51 | * {@inheritdoc}
52 | */
53 | public function exists($key)
54 | {
55 | apc_fetch($this->prefix . $key, $success);
56 | return (bool) $success;
57 | }
58 |
59 | /**
60 | * {@inheritdoc}
61 | */
62 | public function write($key, $value, $serialize = true)
63 | {
64 | return apc_store($this->prefix . $key, $value, $this->expire);
65 | }
66 |
67 | /**
68 | * {@inheritdoc}
69 | */
70 | public function flush($expiredOnly = false)
71 | {
72 | return apc_clear_cache('user');
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Liquid/Cache/File.php:
--------------------------------------------------------------------------------
1 | path = realpath($options['cache_dir']) . DIRECTORY_SEPARATOR;
37 | } else {
38 | throw new NotFoundException('Cachedir not exists or not writable');
39 | }
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public function read($key, $unserialize = true)
46 | {
47 | if (!$this->exists($key)) {
48 | return false;
49 | }
50 |
51 | if ($unserialize) {
52 | return unserialize(file_get_contents($this->path . $this->prefix . $key));
53 | }
54 |
55 | return file_get_contents($this->path . $this->prefix . $key);
56 | }
57 |
58 | /**
59 | * {@inheritdoc}
60 | */
61 | public function exists($key)
62 | {
63 | $cacheFile = $this->path . $this->prefix . $key;
64 |
65 | if (!file_exists($cacheFile) || filemtime($cacheFile) + $this->expire < time()) {
66 | return false;
67 | }
68 |
69 | return true;
70 | }
71 |
72 | /**
73 | * {@inheritdoc}
74 | */
75 | public function write($key, $value, $serialize = true)
76 | {
77 | $bytes = file_put_contents($this->path . $this->prefix . $key, $serialize ? serialize($value) : $value);
78 | $this->gc();
79 |
80 | return $bytes !== false;
81 | }
82 |
83 | /**
84 | * {@inheritdoc}
85 | */
86 | public function flush($expiredOnly = false)
87 | {
88 | foreach (glob($this->path . $this->prefix . '*') as $file) {
89 | if ($expiredOnly) {
90 | if (filemtime($file) + $this->expire < time()) {
91 | unlink($file);
92 | }
93 | } else {
94 | unlink($file);
95 | }
96 | }
97 | }
98 |
99 | /**
100 | * {@inheritdoc}
101 | */
102 | protected function gc()
103 | {
104 | $this->flush(true);
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/Liquid/Cache/Local.php:
--------------------------------------------------------------------------------
1 | cache[$key])) {
29 | return $this->cache[$key];
30 | }
31 |
32 | return false;
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function exists($key)
39 | {
40 | return isset($this->cache[$key]);
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function write($key, $value, $serialize = true)
47 | {
48 | $this->cache[$key] = $value;
49 | return true;
50 | }
51 |
52 | /**
53 | * {@inheritdoc}
54 | */
55 | public function flush($expiredOnly = false)
56 | {
57 | $this->cache = [];
58 | return true;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Liquid/Context.php:
--------------------------------------------------------------------------------
1 | assigns = [$assigns];
63 | $this->registers = $registers;
64 | $this->filterbank = new Filterbank($this);
65 |
66 | // first empty array serves as source for overrides, e.g. as in TagDecrement
67 | $this->environments = [[], []];
68 |
69 | if (Liquid::get('EXPOSE_SERVER')) {
70 | $this->environments[1] = $_SERVER;
71 | } else {
72 | $this->environments[1] = array_filter(
73 | $_SERVER,
74 | function ($key) {
75 | return in_array(
76 | $key,
77 | (array)Liquid::get('SERVER_SUPERGLOBAL_WHITELIST')
78 | );
79 | },
80 | ARRAY_FILTER_USE_KEY
81 | );
82 | }
83 | }
84 |
85 | /**
86 | * Sets a tick function, this function is called sometimes while liquid is rendering a template.
87 | *
88 | * @param callable $tickFunction
89 | */
90 | public function setTickFunction(callable $tickFunction)
91 | {
92 | $this->tickFunction = $tickFunction;
93 | }
94 |
95 | /**
96 | * Add a filter to the context
97 | *
98 | * @param mixed $filter
99 | */
100 | public function addFilters($filter, ?callable $callback = null)
101 | {
102 | $this->filterbank->addFilter($filter, $callback);
103 | }
104 |
105 | /**
106 | * Invoke the filter that matches given name
107 | *
108 | * @param string $name The name of the filter
109 | * @param mixed $value The value to filter
110 | * @param array $args Additional arguments for the filter
111 | *
112 | * @return string
113 | */
114 | public function invoke($name, $value, array $args = [])
115 | {
116 | try {
117 | return $this->filterbank->invoke($name, $value, $args);
118 | } catch (\TypeError $typeError) {
119 | throw new LiquidException($typeError->getMessage(), 0, $typeError);
120 | }
121 | }
122 |
123 | /**
124 | * Merges the given assigns into the current assigns
125 | *
126 | * @param array $newAssigns
127 | */
128 | public function merge($newAssigns)
129 | {
130 | $this->assigns[0] = array_merge($this->assigns[0], $newAssigns);
131 | }
132 |
133 | /**
134 | * Push new local scope on the stack.
135 | *
136 | * @return bool
137 | */
138 | public function push()
139 | {
140 | array_unshift($this->assigns, []);
141 | return true;
142 | }
143 |
144 | /**
145 | * Pops the current scope from the stack.
146 | *
147 | * @throws LiquidException
148 | * @return bool
149 | */
150 | public function pop()
151 | {
152 | if (count($this->assigns) == 1) {
153 | throw new LiquidException('No elements to pop');
154 | }
155 |
156 | array_shift($this->assigns);
157 | }
158 |
159 | /**
160 | * Replaces []
161 | *
162 | * @param string
163 | * @param mixed $key
164 | *
165 | * @return mixed
166 | */
167 | public function get($key)
168 | {
169 | return $this->resolve($key);
170 | }
171 |
172 | /**
173 | * Replaces []=
174 | *
175 | * @param string $key
176 | * @param mixed $value
177 | * @param bool $global
178 | */
179 | public function set($key, $value, $global = false)
180 | {
181 | if ($global) {
182 | for ($i = 0; $i < count($this->assigns); $i++) {
183 | $this->assigns[$i][$key] = $value;
184 | }
185 | } else {
186 | $this->assigns[0][$key] = $value;
187 | }
188 | }
189 |
190 | /**
191 | * Returns true if the given key will properly resolve
192 | *
193 | * @param string $key
194 | *
195 | * @return bool
196 | */
197 | public function hasKey($key)
198 | {
199 | return (!is_null($this->resolve($key)));
200 | }
201 |
202 | /**
203 | * Resolve a key by either returning the appropriate literal or by looking up the appropriate variable
204 | *
205 | * Test for empty has been moved to interpret condition, in Decision
206 | *
207 | * @param string $key
208 | *
209 | * @throws LiquidException
210 | * @return mixed
211 | */
212 | private function resolve($key)
213 | {
214 | // This shouldn't happen
215 | if (is_array($key)) {
216 | throw new LiquidException("Cannot resolve arrays as key");
217 | }
218 |
219 | if (is_null($key) || $key == 'null') {
220 | return null;
221 | }
222 |
223 | if ($key == 'true') {
224 | return true;
225 | }
226 |
227 | if ($key == 'false') {
228 | return false;
229 | }
230 |
231 | if (preg_match('/^\'(.*)\'$/', $key, $matches)) {
232 | return $matches[1];
233 | }
234 |
235 | if (preg_match('/^"(.*)"$/', $key, $matches)) {
236 | return $matches[1];
237 | }
238 |
239 | if (preg_match('/^(-?\d+)$/', $key, $matches)) {
240 | return $matches[1];
241 | }
242 |
243 | if (preg_match('/^(-?\d[\d\.]+)$/', $key, $matches)) {
244 | return $matches[1];
245 | }
246 |
247 | return $this->variable($key);
248 | }
249 |
250 | /**
251 | * Fetches the current key in all the scopes
252 | *
253 | * @param string $key
254 | *
255 | * @return mixed
256 | */
257 | private function fetch($key)
258 | {
259 | // TagDecrement depends on environments being checked before assigns
260 | foreach ($this->environments as $environment) {
261 | if (array_key_exists($key, $environment)) {
262 | return $environment[$key];
263 | }
264 | }
265 |
266 | foreach ($this->assigns as $scope) {
267 | if (array_key_exists($key, $scope)) {
268 | $obj = $scope[$key];
269 |
270 | if ($obj instanceof Drop) {
271 | $obj->setContext($this);
272 | }
273 |
274 | return $obj;
275 | }
276 | }
277 |
278 | return null;
279 | }
280 |
281 | /**
282 | * Resolved the namespaced queries gracefully.
283 | *
284 | * @param string $key
285 | *
286 | * @see Decision::stringValue
287 | * @see AbstractBlock::renderAll
288 | *
289 | * @throws LiquidException
290 | * @return mixed
291 | */
292 | private function variable($key)
293 | {
294 | // Support numeric and variable array indicies
295 | if (preg_match("|\[[0-9]+\]|", $key)) {
296 | $key = preg_replace("|\[([0-9]+)\]|", ".$1", $key);
297 | } elseif (preg_match("|\[[0-9a-z._]+\]|", $key, $matches)) {
298 | $index = $this->get(str_replace(["[", "]"], "", $matches[0]));
299 | if (strlen($index)) {
300 | $key = preg_replace("|\[([0-9a-z._]+)\]|", ".$index", $key);
301 | }
302 | }
303 |
304 | $parts = explode(Liquid::get('VARIABLE_ATTRIBUTE_SEPARATOR'), $key);
305 |
306 | $object = $this->fetch(array_shift($parts));
307 |
308 | while (count($parts) > 0) {
309 | // since we still have a part to consider
310 | // and since we can't dig deeper into plain values
311 | // it can be thought as if it has a property with a null value
312 | if (!is_object($object) && !is_array($object) && !is_string($object)) {
313 | return null;
314 | }
315 |
316 | // first try to cast an object to an array or value
317 | if (is_object($object)) {
318 | if (method_exists($object, 'toLiquid')) {
319 | $object = $object->toLiquid();
320 | } elseif (method_exists($object, 'toArray')) {
321 | $object = $object->toArray();
322 | }
323 | }
324 |
325 | if (is_null($object)) {
326 | return null;
327 | }
328 |
329 | if ($object instanceof Drop) {
330 | $object->setContext($this);
331 | }
332 |
333 | $nextPartName = array_shift($parts);
334 |
335 | if (is_string($object)) {
336 | if ($nextPartName == 'size') {
337 | // if the last part of the context variable is .size we return the string length
338 | return mb_strlen($object);
339 | }
340 |
341 | // no other special properties for strings, yet
342 | return null;
343 | }
344 |
345 | if (is_array($object)) {
346 | // if the last part of the context variable is .first we return the first array element
347 | if ($nextPartName == 'first' && count($parts) == 0 && !array_key_exists('first', $object)) {
348 | return StandardFilters::first($object);
349 | }
350 |
351 | // if the last part of the context variable is .last we return the last array element
352 | if ($nextPartName == 'last' && count($parts) == 0 && !array_key_exists('last', $object)) {
353 | return StandardFilters::last($object);
354 | }
355 |
356 | // if the last part of the context variable is .size we just return the count
357 | if ($nextPartName == 'size' && count($parts) == 0 && !array_key_exists('size', $object)) {
358 | return count($object);
359 | }
360 |
361 | // no key - no value
362 | if (!array_key_exists($nextPartName, $object)) {
363 | return null;
364 | }
365 |
366 | $object = $object[$nextPartName];
367 | continue;
368 | }
369 |
370 | if (!is_object($object)) {
371 | // we got plain value, yet asked to resolve a part
372 | // think plain values have a null part with any name
373 | return null;
374 | }
375 |
376 | if ($object instanceof \Countable) {
377 | // if the last part of the context variable is .size we just return the count
378 | if ($nextPartName == 'size' && count($parts) == 0) {
379 | return count($object);
380 | }
381 | }
382 |
383 | if ($object instanceof Drop) {
384 | // if the object is a drop, make sure it supports the given method
385 | if (!$object->hasKey($nextPartName)) {
386 | return null;
387 | }
388 |
389 | $object = $object->invokeDrop($nextPartName);
390 | continue;
391 | }
392 |
393 | // if it has `get` or `field_exists` methods
394 | if (method_exists($object, Liquid::get('HAS_PROPERTY_METHOD'))) {
395 | if (!call_user_func([$object, Liquid::get('HAS_PROPERTY_METHOD')], $nextPartName)) {
396 | return null;
397 | }
398 |
399 | $object = call_user_func([$object, Liquid::get('GET_PROPERTY_METHOD')], $nextPartName);
400 | continue;
401 | }
402 |
403 | // if it's just a regular object, attempt to access a public method
404 | if (is_callable([$object, $nextPartName])) {
405 | $object = call_user_func([$object, $nextPartName]);
406 | continue;
407 | }
408 |
409 | // if a magic accessor method present...
410 | if (is_object($object) && method_exists($object, '__get')) {
411 | $object = $object->$nextPartName;
412 | continue;
413 | }
414 |
415 | // Inexistent property is a null, PHP-speak
416 | if (!property_exists($object, $nextPartName)) {
417 | return null;
418 | }
419 |
420 | // then try a property (independent of accessibility)
421 | if (property_exists($object, $nextPartName)) {
422 | $object = $object->$nextPartName;
423 | continue;
424 | }
425 |
426 | // we'll try casting this object in the next iteration
427 | }
428 |
429 | // lastly, try to get an embedded value of an object
430 | // value could be of any type, not just string, so we have to do this
431 | // conversion here, not later in AbstractBlock::renderAll
432 | if (is_object($object) && method_exists($object, 'toLiquid')) {
433 | $object = $object->toLiquid();
434 | }
435 |
436 | /*
437 | * Before here were checks for object types and object to string conversion.
438 | *
439 | * Now we just return what we have:
440 | * - Traversable objects are taken care of inside filters
441 | * - Object-to-string conversion is handled at the last moment in Decision::stringValue, and in AbstractBlock::renderAll
442 | *
443 | * This way complex objects could be passed between templates and to filters
444 | */
445 |
446 | return $object;
447 | }
448 |
449 | public function tick()
450 | {
451 | if ($this->tickFunction === null) {
452 | return;
453 | }
454 |
455 | $tickFunction = $this->tickFunction;
456 | $tickFunction($this);
457 | }
458 | }
459 |
--------------------------------------------------------------------------------
/src/Liquid/CustomFilters.php:
--------------------------------------------------------------------------------
1 | valid();
53 | }
54 |
55 | // toLiquid is handled in Context::variable
56 | $class = get_class($value);
57 | throw new RenderException("Value of type $class has no `toLiquid` nor `__toString` methods");
58 | }
59 |
60 | // Arrays simply return true
61 | if (is_array($value)) {
62 | return $value;
63 | }
64 |
65 | return $value;
66 | }
67 |
68 | /**
69 | * Check to see if to variables are equal in a given context
70 | *
71 | * @param string $left
72 | * @param string $right
73 | * @param Context $context
74 | *
75 | * @return bool
76 | */
77 | protected function equalVariables($left, $right, Context $context)
78 | {
79 | $left = $this->stringValue($context->get($left));
80 | $right = $this->stringValue($context->get($right));
81 |
82 | return ($left == $right);
83 | }
84 |
85 | /**
86 | * Interpret a comparison
87 | *
88 | * @param string $left
89 | * @param string $right
90 | * @param string $op
91 | * @param Context $context
92 | *
93 | * @throws \Liquid\Exception\RenderException
94 | * @return bool
95 | */
96 | protected function interpretCondition($left, $right, $op, Context $context)
97 | {
98 | if (is_null($op)) {
99 | $value = $this->stringValue($context->get($left));
100 | return $value;
101 | }
102 |
103 | // values of 'empty' have a special meaning in array comparisons
104 | if ($right == 'empty' && is_array($context->get($left))) {
105 | $left = count($context->get($left));
106 | $right = 0;
107 | } elseif ($left == 'empty' && is_array($context->get($right))) {
108 | $right = count($context->get($right));
109 | $left = 0;
110 | } else {
111 | $left = $context->get($left);
112 | $right = $context->get($right);
113 |
114 | $left = $this->stringValue($left);
115 | $right = $this->stringValue($right);
116 | }
117 |
118 | // special rules for null values
119 | if (is_null($left) || is_null($right)) {
120 | // null == null returns true
121 | if ($op == '==' && is_null($left) && is_null($right)) {
122 | return true;
123 | }
124 |
125 | // null != anything other than null return true
126 | if ($op == '!=' && (!is_null($left) || !is_null($right))) {
127 | return true;
128 | }
129 |
130 | // everything else, return false;
131 | return false;
132 | }
133 |
134 | // regular rules
135 | switch ($op) {
136 | case '==':
137 | return ($left == $right);
138 |
139 | case '!=':
140 | return ($left != $right);
141 |
142 | case '>':
143 | return ($left > $right);
144 |
145 | case '<':
146 | return ($left < $right);
147 |
148 | case '>=':
149 | return ($left >= $right);
150 |
151 | case '<=':
152 | return ($left <= $right);
153 |
154 | case 'contains':
155 | return is_array($left) ? in_array($right, $left) : (strpos($left, $right) !== false);
156 |
157 | default:
158 | throw new RenderException("Error in tag '" . $this->name() . "' - Unknown operator $op");
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Liquid/Document.php:
--------------------------------------------------------------------------------
1 | nodelist as $token) {
47 | if ($token instanceof TagExtends) {
48 | $seenExtends = true;
49 | } elseif ($token instanceof TagBlock) {
50 | $seenBlock = true;
51 | }
52 | }
53 |
54 | /*
55 | * We try to keep the base templates in cache (that not extend anything).
56 | *
57 | * At the same time if we re-render all other blocks we see, we avoid most
58 | * if not all related caching quirks. This may be suboptimal.
59 | */
60 | if ($seenBlock && !$seenExtends) {
61 | return true;
62 | }
63 |
64 | foreach ($this->nodelist as $token) {
65 | // check any of the tokens for includes
66 | if ($token instanceof TagInclude && $token->hasIncludes()) {
67 | return true;
68 | }
69 |
70 | if ($token instanceof TagExtends && $token->hasIncludes()) {
71 | return true;
72 | }
73 | }
74 |
75 | return false;
76 | }
77 |
78 | /**
79 | * There isn't a real delimiter
80 | *
81 | * @return string
82 | */
83 | protected function blockDelimiter()
84 | {
85 | return '';
86 | }
87 |
88 | /**
89 | * Document blocks don't need to be terminated since they are not actually opened
90 | */
91 | protected function assertMissingDelimitation()
92 | {
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Liquid/Drop.php:
--------------------------------------------------------------------------------
1 | 'sales', 'limit' => 10 ));
26 | * }
27 | * }
28 | *
29 | * tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
30 | * tmpl.render('product' => ProductDrop.new ) // will invoke topSales query.
31 | *
32 | * Your drop can either implement the methods sans any parameters or implement the beforeMethod(name) method which is a
33 | * catch all.
34 | */
35 | abstract class Drop
36 | {
37 | /**
38 | * @var Context
39 | */
40 | protected $context;
41 |
42 | /**
43 | * Catch all method that is invoked before a specific method
44 | *
45 | * @param string $method
46 | *
47 | * @return null
48 | */
49 | protected function beforeMethod($method)
50 | {
51 | return null;
52 | }
53 |
54 | /**
55 | * @param Context $context
56 | */
57 | public function setContext(Context $context)
58 | {
59 | $this->context = $context;
60 | }
61 |
62 | /**
63 | * Invoke a specific method
64 | *
65 | * @param string $method
66 | *
67 | * @return mixed
68 | */
69 | public function invokeDrop($method)
70 | {
71 | $result = $this->beforeMethod($method);
72 |
73 | if (is_null($result) && is_callable([$this, $method])) {
74 | $result = $this->$method();
75 | }
76 |
77 | return $result;
78 | }
79 |
80 | /**
81 | * Returns true if the drop supports the given method
82 | *
83 | * @param string $name
84 | *
85 | * @return bool
86 | */
87 | public function hasKey($name)
88 | {
89 | return true;
90 | }
91 |
92 | /**
93 | * @return Drop
94 | */
95 | public function toLiquid()
96 | {
97 | return $this;
98 | }
99 |
100 | /**
101 | * @return string
102 | */
103 | public function __toString()
104 | {
105 | return get_class($this);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Liquid/Exception/CacheException.php:
--------------------------------------------------------------------------------
1 | root = $root;
53 | }
54 |
55 | /**
56 | * Retrieve a template file
57 | *
58 | * @param string $templatePath
59 | *
60 | * @return string template content
61 | */
62 | public function readTemplateFile($templatePath)
63 | {
64 | return file_get_contents($this->fullPath($templatePath));
65 | }
66 |
67 | /**
68 | * Resolves a given path to a full template file path, making sure it's valid
69 | *
70 | * @param string $templatePath
71 | *
72 | * @throws \Liquid\Exception\ParseException
73 | * @throws \Liquid\Exception\NotFoundException
74 | * @return string
75 | */
76 | public function fullPath($templatePath)
77 | {
78 | if (empty($templatePath)) {
79 | throw new ParseException("Empty template name");
80 | }
81 |
82 | $nameRegex = Liquid::get('INCLUDE_ALLOW_EXT')
83 | ? new Regexp('/^[^.\/][a-zA-Z0-9_\.\/-]+$/')
84 | : new Regexp('/^[^.\/][a-zA-Z0-9_\/-]+$/');
85 |
86 | if (!$nameRegex->match($templatePath)) {
87 | throw new ParseException("Illegal template name '$templatePath'");
88 | }
89 |
90 | $templateDir = dirname($templatePath);
91 | $templateFile = basename($templatePath);
92 |
93 | if (!Liquid::get('INCLUDE_ALLOW_EXT')) {
94 | $templateFile = Liquid::get('INCLUDE_PREFIX') . $templateFile . '.' . Liquid::get('INCLUDE_SUFFIX');
95 | }
96 |
97 | $fullPath = join(DIRECTORY_SEPARATOR, [$this->root, $templateDir, $templateFile]);
98 |
99 | $realFullPath = realpath($fullPath);
100 | if ($realFullPath === false) {
101 | throw new NotFoundException("File not found: $fullPath");
102 | }
103 |
104 | if (strpos($realFullPath, $this->root) !== 0) {
105 | throw new NotFoundException("Illegal template full path: {$realFullPath} not under {$this->root}");
106 | }
107 |
108 | return $realFullPath;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/Liquid/FileSystem/Virtual.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
41 | }
42 |
43 | /**
44 | * Retrieve a template file
45 | *
46 | * @param string $templatePath
47 | *
48 | * @return string template content
49 | */
50 | public function readTemplateFile($templatePath)
51 | {
52 | return call_user_func($this->callback, $templatePath);
53 | }
54 |
55 | public function __sleep()
56 | {
57 | // we cannot serialize a closure
58 | if ($this->callback instanceof \Closure) {
59 | throw new FilesystemException("Virtual file system with a Closure as a callback cannot be used with a serializing cache");
60 | }
61 |
62 | return array_keys(get_object_vars($this));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Liquid/Filterbank.php:
--------------------------------------------------------------------------------
1 | context = $context;
51 |
52 | $this->addFilter(\Liquid\StandardFilters::class);
53 | $this->addFilter(\Liquid\CustomFilters::class);
54 | }
55 |
56 | /**
57 | * Adds a filter to the bank
58 | *
59 | * @param mixed $filter Can either be an object, the name of a class (in which case the
60 | * filters will be called statically) or the name of a function.
61 | *
62 | * @throws \Liquid\Exception\WrongArgumentException
63 | * @return bool
64 | */
65 | public function addFilter($filter, ?callable $callback = null)
66 | {
67 | // If it is a callback, save it as it is
68 | if (is_string($filter) && $callback) {
69 | $this->methodMap[$filter] = $callback;
70 | return true;
71 | }
72 |
73 | // If the filter is a class, register all its static methods
74 | if (is_string($filter) && class_exists($filter)) {
75 | $reflection = new \ReflectionClass($filter);
76 | foreach ($reflection->getMethods(\ReflectionMethod::IS_STATIC) as $method) {
77 | $this->methodMap[$method->name] = $method->class;
78 | }
79 |
80 | return true;
81 | }
82 |
83 | // If it's a global function, register it simply
84 | if (is_string($filter) && function_exists($filter)) {
85 | $this->methodMap[$filter] = false;
86 | return true;
87 | }
88 |
89 | // If it isn't an object an isn't a string either, it's a bad parameter
90 | if (!is_object($filter)) {
91 | throw new WrongArgumentException("Parameter passed to addFilter must be an object or a string");
92 | }
93 |
94 | // If the passed filter was an object, store the object for future reference.
95 | $filter->context = $this->context;
96 | $className = get_class($filter);
97 | $this->filters[$className] = $filter;
98 |
99 | // Then register all public static and not methods as filters
100 | foreach (get_class_methods($filter) as $method) {
101 | if (strtolower($method) === '__construct') {
102 | continue;
103 | }
104 | $this->methodMap[$method] = $className;
105 | }
106 |
107 | return true;
108 | }
109 |
110 | /**
111 | * Invokes the filter with the given name
112 | *
113 | * @param string $name The name of the filter
114 | * @param string $value The value to filter
115 | * @param array $args The additional arguments for the filter
116 | *
117 | * @return string
118 | */
119 | public function invoke($name, $value, array $args = [])
120 | {
121 | // workaround for a single standard filter being a reserved keyword - we can't use overloading for static calls
122 | if ($name == 'default') {
123 | $name = '_default';
124 | }
125 |
126 | array_unshift($args, $value);
127 |
128 | // Consult the mapping
129 | if (!isset($this->methodMap[$name])) {
130 | return $value;
131 | }
132 |
133 | $class = $this->methodMap[$name];
134 |
135 | // If we have a callback
136 | if (is_callable($class)) {
137 | return call_user_func_array($class, $args);
138 | }
139 |
140 | // If we have a registered object for the class, use that instead
141 | if (isset($this->filters[$class])) {
142 | $class = $this->filters[$class];
143 | }
144 |
145 | // If we're calling a function
146 | if ($class === false) {
147 | return call_user_func_array($name, $args);
148 | }
149 |
150 | // Call a class or an instance method
151 | return call_user_func_array([$class, $name], $args);
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/Liquid/Liquid.php:
--------------------------------------------------------------------------------
1 | 'field_exists',
29 |
30 | // This method is called on object when resolving variables when
31 | // a given property exists.
32 | 'GET_PROPERTY_METHOD' => 'get',
33 |
34 | // Separator between filters.
35 | 'FILTER_SEPARATOR' => '\|',
36 |
37 | // Separator for arguments.
38 | 'ARGUMENT_SEPARATOR' => ',',
39 |
40 | // Separator for argument names and values.
41 | 'FILTER_ARGUMENT_SEPARATOR' => ':',
42 |
43 | // Separator for variable attributes.
44 | 'VARIABLE_ATTRIBUTE_SEPARATOR' => '.',
45 |
46 | // Allow template names with extension in include and extends tags.
47 | 'INCLUDE_ALLOW_EXT' => false,
48 |
49 | // Suffix for include files.
50 | 'INCLUDE_SUFFIX' => 'liquid',
51 |
52 | // Prefix for include files.
53 | 'INCLUDE_PREFIX' => '_',
54 |
55 | // Whitespace control.
56 | 'WHITESPACE_CONTROL' => '-',
57 |
58 | // Tag start.
59 | 'TAG_START' => '{%',
60 |
61 | // Tag end.
62 | 'TAG_END' => '%}',
63 |
64 | // Variable start.
65 | 'VARIABLE_START' => '{{',
66 |
67 | // Variable end.
68 | 'VARIABLE_END' => '}}',
69 |
70 | // Variable name.
71 | 'VARIABLE_NAME' => '[a-zA-Z_][a-zA-Z_0-9.-]*',
72 |
73 | 'QUOTED_STRING' => '(?:"[^"]*"|\'[^\']*\')',
74 | 'QUOTED_STRING_FILTER_ARGUMENT' => '"[^"]*"|\'[^\']*\'',
75 |
76 | // Automatically escape any variables unless told otherwise by a "raw" filter
77 | 'ESCAPE_BY_DEFAULT' => false,
78 |
79 | // The name of the key to use when building pagination query strings e.g. ?page=1
80 | 'PAGINATION_REQUEST_KEY' => 'page',
81 |
82 | // The name of the context key used to denote the current page number
83 | 'PAGINATION_CONTEXT_KEY' => 'page',
84 |
85 | // Whenever variables from $_SERVER should be directly available to templates
86 | 'EXPOSE_SERVER' => false,
87 |
88 | // $_SERVER variables whitelist - exposed even when EXPOSE_SERVER is false
89 | 'SERVER_SUPERGLOBAL_WHITELIST' => [
90 | 'HTTP_ACCEPT',
91 | 'HTTP_ACCEPT_CHARSET',
92 | 'HTTP_ACCEPT_ENCODING',
93 | 'HTTP_ACCEPT_LANGUAGE',
94 | 'HTTP_CONNECTION',
95 | 'HTTP_HOST',
96 | 'HTTP_REFERER',
97 | 'HTTP_USER_AGENT',
98 | 'HTTPS',
99 | 'REQUEST_METHOD',
100 | 'REQUEST_URI',
101 | 'SERVER_NAME',
102 | ],
103 | ];
104 |
105 | /**
106 | * Get a configuration setting.
107 | *
108 | * @param string $key setting key
109 | *
110 | * @return string
111 | */
112 | public static function get($key)
113 | {
114 | // backward compatibility
115 | if ($key === 'ALLOWED_VARIABLE_CHARS') {
116 | return substr(self::$config['VARIABLE_NAME'], 0, -1);
117 | }
118 | if (array_key_exists($key, self::$config)) {
119 | return self::$config[$key];
120 | }
121 | // This case is needed for compound settings
122 | switch ($key) {
123 | case 'QUOTED_FRAGMENT':
124 | return '(?:' . self::get('QUOTED_STRING') . '|[^\s,\|\'"]+)';
125 | case 'TAG_ATTRIBUTES':
126 | return '/(\w+)\s*\:\s*(' . self::get('QUOTED_FRAGMENT') . ')/';
127 | case 'TOKENIZATION_REGEXP':
128 | return '/(' . self::$config['TAG_START'] . '.*?' . self::$config['TAG_END'] . '|' . self::$config['VARIABLE_START'] . '.*?' . self::$config['VARIABLE_END'] . ')/s';
129 | default:
130 | return null;
131 | }
132 | }
133 |
134 | /**
135 | * Changes/creates a setting.
136 | *
137 | * @param string $key
138 | * @param string $value
139 | */
140 | public static function set($key, $value)
141 | {
142 | // backward compatibility
143 | if ($key === 'ALLOWED_VARIABLE_CHARS') {
144 | $key = 'VARIABLE_NAME';
145 | $value .= '+';
146 | }
147 | self::$config[$key] = $value;
148 | }
149 |
150 | /**
151 | * Flatten a multidimensional array into a single array. Does not maintain keys.
152 | *
153 | * @param array $array
154 | *
155 | * @return array
156 | */
157 | public static function arrayFlatten($array)
158 | {
159 | $return = [];
160 |
161 | foreach ($array as $element) {
162 | if (is_array($element)) {
163 | $return = array_merge($return, self::arrayFlatten($element));
164 | } else {
165 | $return[] = $element;
166 | }
167 | }
168 | return $return;
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/Liquid/LiquidException.php:
--------------------------------------------------------------------------------
1 | pattern = (substr($pattern, 0, 1) != '/')
44 | ? '/' . $this->quote($pattern) . '/'
45 | : $pattern;
46 | }
47 |
48 | /**
49 | * Quotes regular expression characters
50 | *
51 | * @param string $string
52 | *
53 | * @return string
54 | */
55 | public function quote($string)
56 | {
57 | return preg_quote($string, '/');
58 | }
59 |
60 | /**
61 | * Returns an array of matches for the string in the same way as Ruby's scan method
62 | *
63 | * @param string $string
64 | *
65 | * @return array
66 | */
67 | public function scan($string)
68 | {
69 | preg_match_all($this->pattern, $string, $matches);
70 |
71 | if (count($matches) == 1) {
72 | return $matches[0];
73 | }
74 |
75 | array_shift($matches);
76 |
77 | $result = [];
78 |
79 | foreach ($matches as $matchKey => $subMatches) {
80 | foreach ($subMatches as $subMatchKey => $subMatch) {
81 | $result[$subMatchKey][$matchKey] = $subMatch;
82 | }
83 | }
84 |
85 | return $result;
86 | }
87 |
88 | /**
89 | * Matches the given string. Only matches once.
90 | *
91 | * @param string $string
92 | *
93 | * @return int 1 if there was a match, 0 if there wasn't
94 | */
95 | public function match($string)
96 | {
97 | return preg_match($this->pattern, $string, $this->matches);
98 | }
99 |
100 | /**
101 | * Matches the given string. Matches all.
102 | *
103 | * @param string $string
104 | *
105 | * @return int The number of matches
106 | */
107 | public function matchAll($string)
108 | {
109 | return preg_match_all($this->pattern, $string, $this->matches);
110 | }
111 |
112 | /**
113 | * Splits the given string
114 | *
115 | * @param string $string
116 | * @param int $limit Limits the amount of results returned
117 | *
118 | * @return array
119 | */
120 | public function split($string, $limit = -1)
121 | {
122 | return preg_split($this->pattern, $string, $limit);
123 | }
124 |
125 | /**
126 | * Returns the original pattern primarily for debugging purposes
127 | *
128 | * @return string
129 | */
130 | public function __toString()
131 | {
132 | return $this->pattern;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/Liquid/StandardFilters.php:
--------------------------------------------------------------------------------
1 | format($dateFormat);
93 |
94 | return $formatted;
95 | }
96 |
97 |
98 | /**
99 | * Default
100 | *
101 | * @param string $input
102 | * @param string $default_value
103 | *
104 | * @return string
105 | */
106 | public static function _default($input, $default_value)
107 | {
108 | $isBlank = $input == '' || $input === false || $input === null;
109 | return $isBlank ? $default_value : $input;
110 | }
111 |
112 |
113 | /**
114 | * division
115 | *
116 | * @param float $input
117 | * @param float $operand
118 | *
119 | * @return float
120 | */
121 | public static function divided_by($input, $operand)
122 | {
123 | return (float)$input / (float)$operand;
124 | }
125 |
126 |
127 | /**
128 | * Convert an input to lowercase
129 | *
130 | * @param string $input
131 | *
132 | * @return string
133 | */
134 | public static function downcase($input)
135 | {
136 | return is_string($input) ? mb_strtolower($input) : $input;
137 | }
138 |
139 |
140 | /**
141 | * Pseudo-filter: negates auto-added escape filter
142 | *
143 | * @param string $input
144 | *
145 | * @return string
146 | */
147 | public static function raw($input)
148 | {
149 | return $input;
150 | }
151 |
152 |
153 | /**
154 | * Converts into JSON string
155 | *
156 | * @param mixed $input
157 | *
158 | * @return string
159 | */
160 | public static function json($input)
161 | {
162 | return json_encode($input);
163 | }
164 |
165 | /**
166 | * Creates an array including only the objects with a given property value
167 | * @link https://shopify.github.io/liquid/filters/where/
168 | *
169 | * @param mixed $input
170 | * @param string ...$args
171 | *
172 | * @throws LiquidException
173 | * @return mixed
174 | */
175 | public static function where($input, string ...$args)
176 | {
177 | if (is_array($input)) {
178 | switch (count($args)) {
179 | case 1:
180 | return array_values(array_filter($input, fn ($v) => !in_array($v[$args[0]] ?? null, [null, false], true)));
181 | case 2:
182 | return array_values(array_filter($input, fn ($v) => ($v[$args[0]] ?? '') == $args[1]));
183 | default:
184 | throw new LiquidException('Wrong number of arguments to function `where`, given ' . count($args) . ', expected 1 or 2');
185 | }
186 | }
187 | return $input;
188 | }
189 |
190 | /**
191 | * Escape a string
192 | *
193 | * @param string $input
194 | *
195 | * @return string
196 | */
197 | public static function escape($input)
198 | {
199 | // Arrays are taken care down the stack with an error
200 | if (is_array($input)) {
201 | return $input;
202 | }
203 |
204 | if (is_null($input)) {
205 | return '';
206 | }
207 |
208 | return htmlentities($input, ENT_QUOTES);
209 | }
210 |
211 |
212 | /**
213 | * Escape a string once, keeping all previous HTML entities intact
214 | *
215 | * @param string $input
216 | *
217 | * @return string
218 | */
219 | public static function escape_once($input)
220 | {
221 | // Arrays are taken care down the stack with an error
222 | if (is_array($input)) {
223 | return $input;
224 | }
225 |
226 | return htmlentities($input, ENT_QUOTES, null, false);
227 | }
228 |
229 |
230 | /**
231 | * Returns the first element of an array
232 | *
233 | * @param array|\Iterator $input
234 | *
235 | * @return mixed
236 | */
237 | public static function first($input)
238 | {
239 | if ($input instanceof \Iterator) {
240 | $input->rewind();
241 | return $input->current();
242 | }
243 | return is_array($input) ? reset($input) : $input;
244 | }
245 |
246 |
247 | /**
248 | * @param mixed $input number
249 | *
250 | * @return int
251 | */
252 | public static function floor($input)
253 | {
254 | return (int) floor((float)$input);
255 | }
256 |
257 |
258 | /**
259 | * Joins elements of an array with a given character between them
260 | *
261 | * @param array|\Traversable $input
262 | * @param string $glue
263 | *
264 | * @return string
265 | */
266 | public static function join($input, $glue = ' ')
267 | {
268 | if ($input instanceof \Traversable) {
269 | $str = '';
270 | foreach ($input as $elem) {
271 | if ($str) {
272 | $str .= $glue;
273 | }
274 | $str .= $elem;
275 | }
276 | return $str;
277 | }
278 | return is_array($input) ? implode($glue, $input) : $input;
279 | }
280 |
281 |
282 | /**
283 | * Returns the last element of an array
284 | *
285 | * @param array|\Traversable $input
286 | *
287 | * @return mixed
288 | */
289 | public static function last($input)
290 | {
291 | if ($input instanceof \Traversable) {
292 | $last = null;
293 | foreach ($input as $elem) {
294 | $last = $elem;
295 | }
296 | return $last;
297 | }
298 | return is_array($input) ? end($input) : $input;
299 | }
300 |
301 |
302 | /**
303 | * @param string $input
304 | *
305 | * @return string
306 | */
307 | public static function lstrip($input)
308 | {
309 | return ltrim($input);
310 | }
311 |
312 |
313 | /**
314 | * Map/collect on a given property
315 | *
316 | * @param array|\Traversable $input
317 | * @param string $property
318 | *
319 | * @return string
320 | */
321 | public static function map($input, $property)
322 | {
323 | if ($input instanceof \Traversable) {
324 | $input = iterator_to_array($input);
325 | }
326 | if (!is_array($input)) {
327 | return $input;
328 | }
329 | return array_map(function ($elem) use ($property) {
330 | if (is_callable($elem)) {
331 | return $elem();
332 | } elseif (is_array($elem) && array_key_exists($property, $elem)) {
333 | return $elem[$property];
334 | }
335 | return null;
336 | }, $input);
337 | }
338 |
339 |
340 | /**
341 | * subtraction
342 | *
343 | * @param float $input
344 | * @param float $operand
345 | *
346 | * @return float
347 | */
348 | public static function minus($input, $operand)
349 | {
350 | return (float)$input - (float)$operand;
351 | }
352 |
353 |
354 | /**
355 | * modulo
356 | *
357 | * @param float $input
358 | * @param float $operand
359 | *
360 | * @return float
361 | */
362 | public static function modulo($input, $operand)
363 | {
364 | return fmod((float)$input, (float)$operand);
365 | }
366 |
367 |
368 | /**
369 | * Replace each newline (\n) with html break
370 | *
371 | * @param string $input
372 | *
373 | * @return string
374 | */
375 | public static function newline_to_br($input)
376 | {
377 | return is_string($input) ? str_replace("\n", "
\n", $input) : $input;
378 | }
379 |
380 |
381 | /**
382 | * addition
383 | *
384 | * @param float $input
385 | * @param float $operand
386 | *
387 | * @return float
388 | */
389 | public static function plus($input, $operand)
390 | {
391 | return (float)$input + (float)$operand;
392 | }
393 |
394 |
395 | /**
396 | * Prepend a string to another
397 | *
398 | * @param string $input
399 | * @param string $string
400 | *
401 | * @return string
402 | */
403 | public static function prepend($input, $string)
404 | {
405 | return $string . $input;
406 | }
407 |
408 |
409 | /**
410 | * Remove a substring
411 | *
412 | * @param string $input
413 | * @param string $string
414 | *
415 | * @return string
416 | */
417 | public static function remove($input, $string)
418 | {
419 | return str_replace($string, '', $input);
420 | }
421 |
422 |
423 | /**
424 | * Remove the first occurrences of a substring
425 | *
426 | * @param string $input
427 | * @param string $string
428 | *
429 | * @return string
430 | */
431 | public static function remove_first($input, $string)
432 | {
433 | if (($pos = strpos($input, $string)) !== false) {
434 | $input = substr_replace($input, '', $pos, strlen($string));
435 | }
436 |
437 | return $input;
438 | }
439 |
440 |
441 | /**
442 | * Replace occurrences of a string with another
443 | *
444 | * @param string $input
445 | * @param string $string
446 | * @param string $replacement
447 | *
448 | * @return string
449 | */
450 | public static function replace($input, $string, $replacement = '')
451 | {
452 | return str_replace($string, $replacement, $input);
453 | }
454 |
455 |
456 | /**
457 | * Replace the first occurrences of a string with another
458 | *
459 | * @param string $input
460 | * @param string $string
461 | * @param string $replacement
462 | *
463 | * @return string
464 | */
465 | public static function replace_first($input, $string, $replacement = '')
466 | {
467 | if (($pos = strpos($input, $string)) !== false) {
468 | $input = substr_replace($input, $replacement, $pos, strlen($string));
469 | }
470 |
471 | return $input;
472 | }
473 |
474 |
475 | /**
476 | * Reverse the elements of an array
477 | *
478 | * @param array|\Traversable $input
479 | *
480 | * @return array
481 | */
482 | public static function reverse($input)
483 | {
484 | if ($input instanceof \Traversable) {
485 | $input = iterator_to_array($input);
486 | }
487 | return array_reverse($input);
488 | }
489 |
490 |
491 | /**
492 | * Round a number
493 | *
494 | * @param float $input
495 | * @param int $n precision
496 | *
497 | * @return float
498 | */
499 | public static function round($input, $n = 0)
500 | {
501 | return round((float)$input, (int)$n);
502 | }
503 |
504 |
505 | /**
506 | * @param string $input
507 | *
508 | * @return string
509 | */
510 | public static function rstrip($input)
511 | {
512 | return rtrim($input);
513 | }
514 |
515 |
516 | /**
517 | * Return the size of an array or of an string
518 | *
519 | * @param mixed $input
520 | * @throws RenderException
521 | * @return int
522 | */
523 | public static function size($input)
524 | {
525 | if ($input instanceof \Iterator) {
526 | return iterator_count($input);
527 | }
528 |
529 | if (is_array($input)) {
530 | return count($input);
531 | }
532 |
533 | if (is_object($input)) {
534 | if (method_exists($input, 'size')) {
535 | return $input->size();
536 | }
537 |
538 | if (!method_exists($input, '__toString')) {
539 | $class = get_class($input);
540 | throw new RenderException("Size of $class cannot be estimated: it has no method 'size' nor can be converted to a string");
541 | }
542 | }
543 |
544 | // only plain values and stringable objects left at this point
545 | return strlen($input);
546 | }
547 |
548 |
549 | /**
550 | * @param array|\Iterator|string $input
551 | * @param int $offset
552 | * @param int $length
553 | *
554 | * @return array|\Iterator|string
555 | */
556 | public static function slice($input, $offset, $length = null)
557 | {
558 | if ($input instanceof \Iterator) {
559 | $input = iterator_to_array($input);
560 | }
561 | if (is_array($input)) {
562 | $input = array_slice($input, $offset, $length);
563 | } elseif (is_string($input)) {
564 | $input = mb_substr($input, $offset, $length);
565 | }
566 |
567 | return $input;
568 | }
569 |
570 |
571 | /**
572 | * Sort the elements of an array
573 | *
574 | * @param array|\Traversable $input
575 | * @param string $property use this property of an array element
576 | *
577 | * @return array
578 | */
579 | public static function sort($input, $property = null)
580 | {
581 | if ($input instanceof \Traversable) {
582 | $input = iterator_to_array($input);
583 | }
584 | if ($property === null) {
585 | asort($input);
586 | } else {
587 | $first = reset($input);
588 | if ($first !== false && is_array($first) && array_key_exists($property, $first)) {
589 | uasort($input, function ($a, $b) use ($property) {
590 | if (($a[$property] ?? 0) == ($b[$property] ?? 0)) {
591 | return 0;
592 | }
593 |
594 | return ($a[$property] ?? 0) < ($b[$property] ?? 0) ? -1 : 1;
595 | });
596 | }
597 | }
598 |
599 | return $input;
600 | }
601 |
602 | /**
603 | * Explicit string conversion.
604 | *
605 | * @param mixed $input
606 | *
607 | * @return string
608 | */
609 | public static function string($input)
610 | {
611 | return strval($input);
612 | }
613 |
614 | /**
615 | * Split input string into an array of substrings separated by given pattern.
616 | *
617 | * @param string $input
618 | * @param string $pattern
619 | *
620 | * @return array
621 | */
622 | public static function split($input, $pattern)
623 | {
624 | if ($input === '' || $input === null) {
625 | return [];
626 | }
627 |
628 | if ($pattern === '') {
629 | return mb_str_split($input);
630 | }
631 |
632 | return explode($pattern, $input);
633 | }
634 |
635 |
636 | /**
637 | * @param string $input
638 | *
639 | * @return string
640 | */
641 | public static function strip($input)
642 | {
643 | return trim($input);
644 | }
645 |
646 |
647 | /**
648 | * Removes html tags from text
649 | *
650 | * @param string $input
651 | *
652 | * @return string
653 | */
654 | public static function strip_html($input)
655 | {
656 | return is_string($input) ? strip_tags($input) : $input;
657 | }
658 |
659 |
660 | /**
661 | * Strip all newlines (\n, \r) from string
662 | *
663 | * @param string $input
664 | *
665 | * @return string
666 | */
667 | public static function strip_newlines($input)
668 | {
669 | return is_string($input) ? str_replace([
670 | "\n", "\r",
671 | ], '', $input) : $input;
672 | }
673 |
674 |
675 | /**
676 | * multiplication
677 | *
678 | * @param float $input
679 | * @param float $operand
680 | *
681 | * @return float
682 | */
683 | public static function times($input, $operand)
684 | {
685 | return (float)$input * (float)$operand;
686 | }
687 |
688 |
689 | /**
690 | * Truncate a string down to x characters
691 | *
692 | * @param string $input
693 | * @param int $characters
694 | * @param string $ending string to append if truncated
695 | *
696 | * @return string
697 | */
698 | public static function truncate($input, $characters = 100, $ending = '...')
699 | {
700 | if (is_string($input) || is_numeric($input)) {
701 | if (strlen($input) > $characters) {
702 | return mb_substr($input, 0, $characters) . $ending;
703 | }
704 | }
705 |
706 | return $input;
707 | }
708 |
709 |
710 | /**
711 | * Truncate string down to x words
712 | *
713 | * @param string $input
714 | * @param int $words
715 | * @param string $ending string to append if truncated
716 | *
717 | * @return string
718 | */
719 | public static function truncatewords($input, $words = 3, $ending = '...')
720 | {
721 | if (is_string($input)) {
722 | $wordlist = explode(" ", $input);
723 |
724 | if (count($wordlist) > $words) {
725 | return implode(" ", array_slice($wordlist, 0, $words)) . $ending;
726 | }
727 | }
728 |
729 | return $input;
730 | }
731 |
732 |
733 | /**
734 | * Remove duplicate elements from an array
735 | *
736 | * @param array|\Traversable $input
737 | *
738 | * @return array
739 | */
740 | public static function uniq($input)
741 | {
742 | if ($input instanceof \Traversable) {
743 | $input = iterator_to_array($input);
744 | }
745 | return array_unique($input);
746 | }
747 |
748 |
749 | /**
750 | * Convert an input to uppercase
751 | *
752 | * @param string $input
753 | *
754 | * @return string
755 | */
756 | public static function upcase($input)
757 | {
758 | return is_string($input) ? mb_strtoupper($input) : $input;
759 | }
760 |
761 |
762 | /**
763 | * URL encodes a string
764 | *
765 | * @param string $input
766 | *
767 | * @return string
768 | */
769 | public static function url_encode($input)
770 | {
771 | return urlencode($input);
772 | }
773 |
774 | /**
775 | * Decodes a URL-encoded string
776 | *
777 | * @param string $input
778 | *
779 | * @return string
780 | */
781 | public static function url_decode($input)
782 | {
783 | return urldecode($input);
784 | }
785 | }
786 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagAssign.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
58 | $this->to = $syntaxRegexp->matches[1];
59 | $this->from = new Variable($syntaxRegexp->matches[2]);
60 | } else {
61 | throw new ParseException("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]");
62 | }
63 | }
64 |
65 | /**
66 | * Renders the tag
67 | *
68 | * @param Context $context
69 | *
70 | * @return string|void
71 | */
72 | public function render(Context $context)
73 | {
74 | $output = $this->from->render($context);
75 |
76 | $context->set($this->to, $output, true);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagBlock.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
50 | $this->block = $syntaxRegexp->matches[1];
51 | parent::__construct($markup, $tokens, $fileSystem);
52 | } else {
53 | throw new ParseException("Syntax Error in 'block' - Valid syntax: block [name]");
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagBreak.php:
--------------------------------------------------------------------------------
1 | registers['break'] = true;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagCapture.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
50 | $this->to = $syntaxRegexp->matches[1];
51 | parent::__construct($markup, $tokens, $fileSystem);
52 | } else {
53 | throw new ParseException("Syntax Error in 'capture' - Valid syntax: capture [var] [value]");
54 | }
55 | }
56 |
57 | /**
58 | * Renders the block
59 | *
60 | * @param Context $context
61 | *
62 | * @return string
63 | */
64 | public function render(Context $context)
65 | {
66 | $output = parent::render($context);
67 |
68 | $context->set($this->to, $output, true);
69 | return '';
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagCase.php:
--------------------------------------------------------------------------------
1 | nodelists = [];
70 | $this->elseNodelist = [];
71 |
72 | parent::__construct($markup, $tokens, $fileSystem);
73 |
74 | $syntaxRegexp = new Regexp('/' . Liquid::get('QUOTED_FRAGMENT') . '/');
75 |
76 | if ($syntaxRegexp->match($markup)) {
77 | $this->left = $syntaxRegexp->matches[0];
78 | } else {
79 | throw new ParseException("Syntax Error in tag 'case' - Valid syntax: case [condition]"); // harry
80 | }
81 | }
82 |
83 | /**
84 | * Pushes the last nodelist onto the stack
85 | */
86 | public function endTag()
87 | {
88 | $this->pushNodelist();
89 | }
90 |
91 | /**
92 | * Unknown tag handler
93 | *
94 | * @param string $tag
95 | * @param string $params
96 | * @param array $tokens
97 | *
98 | * @throws \Liquid\Exception\ParseException
99 | */
100 | public function unknownTag($tag, $params, array $tokens)
101 | {
102 | switch ($tag) {
103 | case 'when':
104 | $whenSyntax = preg_match_all('/(?<=,|or|^)\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')/', $params, $matches);
105 | // push the current nodelist onto the stack and prepare for a new one
106 | if ($whenSyntax) {
107 | $this->pushNodelist();
108 | $this->right = $matches[1];
109 | $this->nodelist = [];
110 | } else {
111 | throw new ParseException("Syntax Error in tag 'case' - Valid when condition: when [condition]"); // harry
112 | }
113 | break;
114 |
115 | case 'else':
116 | // push the last nodelist onto the stack and prepare to receive the else nodes
117 | $this->pushNodelist();
118 | $this->right = null;
119 | $this->elseNodelist = &$this->nodelist;
120 | $this->nodelist = [];
121 | break;
122 |
123 | default:
124 | parent::unknownTag($tag, $params, $tokens);
125 | }
126 | }
127 |
128 | /**
129 | * Pushes the current right value and nodelist into the nodelist stack
130 | */
131 | public function pushNodelist()
132 | {
133 | if (!is_null($this->right)) {
134 | $this->nodelists[] = [$this->right, $this->nodelist];
135 | }
136 | }
137 |
138 | /**
139 | * Renders the node
140 | *
141 | * @param Context $context
142 | *
143 | * @return string
144 | */
145 | public function render(Context $context)
146 | {
147 | $output = ''; // array();
148 | $runElseBlock = true;
149 |
150 | foreach ($this->nodelists as $data) {
151 | list($right, $nodelist) = $data;
152 |
153 | foreach ($right as $var) {
154 | if ($this->equalVariables($this->left, $var, $context)) {
155 | $runElseBlock = false;
156 |
157 | $context->push();
158 | $output .= $this->renderAll($nodelist, $context);
159 | $context->pop();
160 |
161 | break;
162 | }
163 | }
164 | }
165 |
166 | if ($runElseBlock) {
167 | $context->push();
168 | $output .= $this->renderAll($this->elseNodelist, $context);
169 | $context->pop();
170 | }
171 |
172 | return $output;
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagComment.php:
--------------------------------------------------------------------------------
1 | registers['continue'] = true;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagCycle.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
66 | $this->variables = $this->variablesFromString($namedSyntax->matches[2]);
67 | $this->name = $namedSyntax->matches[1];
68 | } elseif ($simpleSyntax->match($markup)) {
69 | $this->variables = $this->variablesFromString($markup);
70 | $this->name = "'" . implode($this->variables) . "'";
71 | } else {
72 | throw new ParseException("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]");
73 | }
74 | }
75 |
76 | /**
77 | * Renders the tag
78 | *
79 | * @var Context $context
80 | * @return string
81 | */
82 | public function render(Context $context)
83 | {
84 | $context->push();
85 |
86 | $key = $context->get($this->name);
87 |
88 | if (isset($context->registers['cycle'][$key])) {
89 | $iteration = $context->registers['cycle'][$key];
90 | } else {
91 | $iteration = 0;
92 | }
93 |
94 | $result = $context->get($this->variables[$iteration]);
95 |
96 | $iteration += 1;
97 |
98 | if ($iteration >= count($this->variables)) {
99 | $iteration = 0;
100 | }
101 |
102 | $context->registers['cycle'][$key] = $iteration;
103 |
104 | $context->pop();
105 |
106 | return $result;
107 | }
108 |
109 | /**
110 | * Extract variables from a string of markup
111 | *
112 | * @param string $markup
113 | *
114 | * @return array;
115 | */
116 | private function variablesFromString($markup)
117 | {
118 | $regexp = new Regexp('/\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*/');
119 | $parts = explode(',', $markup);
120 | $result = [];
121 |
122 | foreach ($parts as $part) {
123 | $regexp->match($part);
124 |
125 | if (!empty($regexp->matches[1])) {
126 | $result[] = $regexp->matches[1];
127 | }
128 | }
129 |
130 | return $result;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagDecrement.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
55 | $this->toDecrement = $syntax->matches[0];
56 | } else {
57 | throw new ParseException("Syntax Error in 'decrement' - Valid syntax: decrement [var]");
58 | }
59 | }
60 |
61 | /**
62 | * Renders the tag
63 | *
64 | * @param Context $context
65 | *
66 | * @return string|void
67 | */
68 | public function render(Context $context)
69 | {
70 | // if the value is not set in the environment check to see if it
71 | // exists in the context, and if not set it to 0
72 | if (!isset($context->environments[0][$this->toDecrement])) {
73 | // check for a context value
74 | $fromContext = $context->get($this->toDecrement);
75 |
76 | // we already have a value in the context
77 | $context->environments[0][$this->toDecrement] = (null !== $fromContext) ? $fromContext : 0;
78 | }
79 |
80 | // decrement the environment value
81 | $context->environments[0][$this->toDecrement]--;
82 |
83 | return '';
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagExtends.php:
--------------------------------------------------------------------------------
1 | match($markup) && isset($regex->matches[1])) {
62 | $this->templateName = substr($regex->matches[1], 1, strlen($regex->matches[1]) - 2);
63 | } else {
64 | throw new ParseException("Error in tag 'extends' - Valid syntax: extends '[template name]'");
65 | }
66 |
67 | parent::__construct($markup, $tokens, $fileSystem);
68 | }
69 |
70 | /**
71 | * @param array $tokens
72 | *
73 | * @return array
74 | */
75 | private function findBlocks(array $tokens)
76 | {
77 | $blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/');
78 | $blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/');
79 |
80 | $b = [];
81 | $name = null;
82 |
83 | for ($i = 0, $n = count($tokens); $i < $n; $i++) {
84 | if ($tokens[$i] === null) {
85 | continue;
86 | }
87 | $token = $tokens[$i];
88 | $tokens[$i] = null;
89 |
90 | if ($blockstartRegexp->match($token)) {
91 | $name = $blockstartRegexp->matches[1];
92 | $b[$name] = [];
93 | } elseif ($blockendRegexp->match($token)) {
94 | $name = null;
95 | } else {
96 | if ($name !== null) {
97 | array_push($b[$name], $token);
98 | }
99 | }
100 | }
101 |
102 | return $b;
103 | }
104 |
105 | /**
106 | * Parses the tokens
107 | *
108 | * @param array $tokens
109 | *
110 | * @throws \Liquid\Exception\MissingFilesystemException
111 | */
112 | public function parse(array &$tokens)
113 | {
114 | if ($this->fileSystem === null) {
115 | throw new MissingFilesystemException("No file system");
116 | }
117 |
118 | // read the source of the template and create a new sub document
119 | $source = $this->fileSystem->readTemplateFile($this->templateName);
120 |
121 | // tokens in this new document
122 | $maintokens = Template::tokenize($source);
123 |
124 | $eRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*extends (.*)?' . Liquid::get('TAG_END') . '$/');
125 | foreach ($maintokens as $maintoken) {
126 | if ($eRegexp->match($maintoken)) {
127 | $m = $eRegexp->matches[1];
128 | break;
129 | }
130 | }
131 |
132 | if (isset($m)) {
133 | $rest = array_merge($maintokens, $tokens);
134 | } else {
135 | $childtokens = $this->findBlocks($tokens);
136 |
137 | $blockstartRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*block (\w+)\s*(.*)?' . Liquid::get('TAG_END') . '$/');
138 | $blockendRegexp = new Regexp('/^' . Liquid::get('TAG_START') . '\s*endblock\s*?' . Liquid::get('TAG_END') . '$/');
139 |
140 | $name = null;
141 |
142 | $rest = [];
143 | $keep = false;
144 |
145 | for ($i = 0; $i < count($maintokens); $i++) {
146 | if ($blockstartRegexp->match($maintokens[$i])) {
147 | $name = $blockstartRegexp->matches[1];
148 |
149 | if (isset($childtokens[$name])) {
150 | $keep = true;
151 | array_push($rest, $maintokens[$i]);
152 | foreach ($childtokens[$name] as $item) {
153 | array_push($rest, $item);
154 | }
155 | }
156 | }
157 | if (!$keep) {
158 | array_push($rest, $maintokens[$i]);
159 | }
160 |
161 | if ($blockendRegexp->match($maintokens[$i]) && $keep === true) {
162 | $keep = false;
163 | array_push($rest, $maintokens[$i]);
164 | }
165 | }
166 | }
167 |
168 | $cache = Template::getCache();
169 |
170 | if (!$cache) {
171 | $this->document = new Document($rest, $this->fileSystem);
172 | return;
173 | }
174 |
175 | $this->hash = md5($source);
176 |
177 | $this->document = $cache->read($this->hash);
178 |
179 | if ($this->document == false || $this->document->hasIncludes() == true) {
180 | $this->document = new Document($rest, $this->fileSystem);
181 | $cache->write($this->hash, $this->document);
182 | }
183 | }
184 |
185 | /**
186 | * Check for cached includes; if there are - do not use cache
187 | *
188 | * @see Document::hasIncludes()
189 | * @return boolean
190 | */
191 | public function hasIncludes()
192 | {
193 | if ($this->document->hasIncludes() == true) {
194 | return true;
195 | }
196 |
197 | $source = $this->fileSystem->readTemplateFile($this->templateName);
198 |
199 | if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) {
200 | return false;
201 | }
202 |
203 | return true;
204 | }
205 |
206 | /**
207 | * Renders the node
208 | *
209 | * @param Context $context
210 | *
211 | * @return string
212 | */
213 | public function render(Context $context)
214 | {
215 | $context->push();
216 | $result = $this->document->render($context);
217 | $context->pop();
218 | return $result;
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagFor.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
79 | $this->variableName = $syntaxRegexp->matches[1];
80 | $this->collectionName = $syntaxRegexp->matches[2];
81 | $this->name = $syntaxRegexp->matches[1] . '-' . $syntaxRegexp->matches[2];
82 | $this->extractAttributes($markup);
83 | } else {
84 | $syntaxRegexp = new Regexp('/(\w+)\s+in\s+\((\d+|' . Liquid::get('VARIABLE_NAME') . ')\s*\.\.\s*(\d+|' . Liquid::get('VARIABLE_NAME') . ')\)/');
85 | if ($syntaxRegexp->match($markup)) {
86 | $this->type = 'digit';
87 | $this->variableName = $syntaxRegexp->matches[1];
88 | $this->start = $syntaxRegexp->matches[2];
89 | $this->collectionName = $syntaxRegexp->matches[3];
90 | $this->name = $syntaxRegexp->matches[1].'-digit';
91 | $this->extractAttributes($markup);
92 | } else {
93 | throw new ParseException("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]");
94 | }
95 | }
96 | }
97 |
98 | /**
99 | * Renders the tag
100 | *
101 | * @param Context $context
102 | *
103 | * @return null|string
104 | */
105 | public function render(Context $context)
106 | {
107 | if (!isset($context->registers['for'])) {
108 | $context->registers['for'] = [];
109 | }
110 |
111 | if ($this->type == 'digit') {
112 | return $this->renderDigit($context);
113 | }
114 |
115 | // that's the default
116 | return $this->renderCollection($context);
117 | }
118 |
119 | private function renderCollection(Context $context)
120 | {
121 | $collection = $context->get($this->collectionName);
122 |
123 | if ($collection instanceof \Generator && !$collection->valid()) {
124 | return '';
125 | }
126 |
127 | if ($collection instanceof \Traversable) {
128 | $collection = iterator_to_array($collection);
129 | }
130 |
131 | if (is_null($collection) || !is_array($collection) || count($collection) == 0) {
132 | return '';
133 | }
134 |
135 | $range = [0, count($collection)];
136 |
137 | if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) {
138 | $offset = 0;
139 |
140 | if (isset($this->attributes['offset'])) {
141 | $offset = ($this->attributes['offset'] == 'continue') ? $context->registers['for'][$this->name] : $context->get($this->attributes['offset']);
142 | }
143 |
144 | $limit = (isset($this->attributes['limit'])) ? $context->get($this->attributes['limit']) : null;
145 | $rangeEnd = $limit ? $limit : count($collection) - $offset;
146 | $range = [$offset, $rangeEnd];
147 |
148 | $context->registers['for'][$this->name] = $rangeEnd + $offset;
149 | }
150 |
151 | $result = '';
152 | $segment = array_slice($collection, $range[0], $range[1]);
153 | if (!count($segment)) {
154 | return null;
155 | }
156 |
157 | $context->push();
158 | $length = count($segment);
159 |
160 | $index = 0;
161 | foreach ($segment as $key => $item) {
162 | $value = is_numeric($key) ? $item : [$key, $item];
163 | $context->set($this->variableName, $value);
164 | $context->set('forloop', [
165 | 'name' => $this->name,
166 | 'length' => $length,
167 | 'index' => $index + 1,
168 | 'index0' => $index,
169 | 'rindex' => $length - $index,
170 | 'rindex0' => $length - $index - 1,
171 | 'first' => (int)($index == 0),
172 | 'last' => (int)($index == $length - 1),
173 | ]);
174 |
175 | $result .= $this->renderAll($this->nodelist, $context);
176 |
177 | $index++;
178 |
179 | if (isset($context->registers['break'])) {
180 | unset($context->registers['break']);
181 | break;
182 | }
183 | if (isset($context->registers['continue'])) {
184 | unset($context->registers['continue']);
185 | }
186 | }
187 |
188 | $context->pop();
189 |
190 | return $result;
191 | }
192 |
193 | private function renderDigit(Context $context)
194 | {
195 | $start = $this->start;
196 | if (!is_integer($this->start)) {
197 | $start = $context->get($this->start);
198 | }
199 |
200 | $end = $this->collectionName;
201 | if (!is_integer($this->collectionName)) {
202 | $end = $context->get($this->collectionName);
203 | }
204 |
205 | $range = [$start, $end];
206 |
207 | $context->push();
208 | $result = '';
209 | $index = 0;
210 | $length = $range[1] - $range[0];
211 | for ($i = $range[0]; $i <= $range[1]; $i++) {
212 | $context->set($this->variableName, $i);
213 | $context->set('forloop', [
214 | 'name' => $this->name,
215 | 'length' => $length,
216 | 'index' => $index + 1,
217 | 'index0' => $index,
218 | 'rindex' => $length - $index,
219 | 'rindex0' => $length - $index - 1,
220 | 'first' => (int)($index == 0),
221 | 'last' => (int)($index == $length - 1),
222 | ]);
223 |
224 | $result .= $this->renderAll($this->nodelist, $context);
225 |
226 | $index++;
227 |
228 | if (isset($context->registers['break'])) {
229 | unset($context->registers['break']);
230 | break;
231 | }
232 | if (isset($context->registers['continue'])) {
233 | unset($context->registers['continue']);
234 | }
235 | }
236 |
237 | $context->pop();
238 |
239 | return $result;
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagIf.php:
--------------------------------------------------------------------------------
1 | nodelist = & $this->nodelistHolders[count($this->blocks)];
57 |
58 | array_push($this->blocks, ['if', $markup, &$this->nodelist]);
59 |
60 | parent::__construct($markup, $tokens, $fileSystem);
61 | }
62 |
63 | /**
64 | * Handler for unknown tags, handle else tags
65 | *
66 | * @param string $tag
67 | * @param array $params
68 | * @param array $tokens
69 | */
70 | public function unknownTag($tag, $params, array $tokens)
71 | {
72 | if ($tag == 'else' || $tag == 'elsif') {
73 | // Update reference to nodelistHolder for this block
74 | $this->nodelist = & $this->nodelistHolders[count($this->blocks) + 1];
75 | $this->nodelistHolders[count($this->blocks) + 1] = [];
76 |
77 | array_push($this->blocks, [$tag, $params, &$this->nodelist]);
78 | } else {
79 | parent::unknownTag($tag, $params, $tokens);
80 | }
81 | }
82 |
83 | /**
84 | * Render the tag
85 | *
86 | * @param Context $context
87 | *
88 | * @throws \Liquid\Exception\ParseException
89 | * @return string
90 | */
91 | public function render(Context $context)
92 | {
93 | $context->push();
94 |
95 | $logicalRegex = new Regexp('/\s+(and|or)\s+/');
96 | $conditionalRegex = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')\s*([=!<>a-z_]+)?\s*(' . Liquid::get('QUOTED_FRAGMENT') . ')?/');
97 |
98 | $result = '';
99 | foreach ($this->blocks as $block) {
100 | if ($block[0] == 'else') {
101 | $result = $this->renderAll($block[2], $context);
102 |
103 | break;
104 | }
105 |
106 | if ($block[0] == 'if' || $block[0] == 'elsif') {
107 | // Extract logical operators
108 | $logicalRegex->matchAll($block[1]);
109 |
110 | $logicalOperators = $logicalRegex->matches;
111 | $logicalOperators = $logicalOperators[1];
112 | // Extract individual conditions
113 | $temp = $logicalRegex->split($block[1]);
114 |
115 | $conditions = [];
116 |
117 | foreach ($temp as $condition) {
118 | if ($conditionalRegex->match($condition)) {
119 | $left = (isset($conditionalRegex->matches[1])) ? $conditionalRegex->matches[1] : null;
120 | $operator = (isset($conditionalRegex->matches[2])) ? $conditionalRegex->matches[2] : null;
121 | $right = (isset($conditionalRegex->matches[3])) ? $conditionalRegex->matches[3] : null;
122 |
123 | array_push($conditions, [
124 | 'left' => $left,
125 | 'operator' => $operator,
126 | 'right' => $right,
127 | ]);
128 | } else {
129 | throw new ParseException("Syntax Error in tag 'if' - Valid syntax: if [condition]");
130 | }
131 | }
132 | if (count($logicalOperators)) {
133 | // If statement contains and/or
134 | $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context);
135 | foreach ($logicalOperators as $k => $logicalOperator) {
136 | if ($logicalOperator == 'and') {
137 | $display = ($display && $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context));
138 | } else {
139 | $display = ($display || $this->interpretCondition($conditions[$k + 1]['left'], $conditions[$k + 1]['right'], $conditions[$k + 1]['operator'], $context));
140 | }
141 | }
142 | } else {
143 | // If statement is a single condition
144 | $display = $this->interpretCondition($conditions[0]['left'], $conditions[0]['right'], $conditions[0]['operator'], $context);
145 | }
146 |
147 | // hook for unless tag
148 | $display = $this->negateIfUnless($display);
149 |
150 | if ($display) {
151 | $result = $this->renderAll($block[2], $context);
152 |
153 | break;
154 | }
155 | }
156 | }
157 |
158 | $context->pop();
159 |
160 | return $result;
161 | }
162 |
163 | protected function negateIfUnless($display)
164 | {
165 | // no need to negate a condition in a regular `if` tag (will do that in `unless` tag)
166 | return $display;
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagIfchanged.php:
--------------------------------------------------------------------------------
1 | lastValue == $output) {
56 | return '';
57 | }
58 | $this->lastValue = $output;
59 | return $this->lastValue;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagInclude.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
84 | throw new ParseException("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]");
85 | }
86 |
87 | $unquoted = (strpos($regex->matches[1], '"') === false && strpos($regex->matches[1], "'") === false);
88 |
89 | $start = 1;
90 | $len = strlen($regex->matches[1]) - 2;
91 |
92 | if ($unquoted) {
93 | $start = 0;
94 | $len = strlen($regex->matches[1]);
95 | }
96 |
97 | $this->templateName = substr($regex->matches[1], $start, $len);
98 |
99 | if (isset($regex->matches[1])) {
100 | $this->collection = (isset($regex->matches[3])) ? ($regex->matches[3] == "for") : null;
101 | $this->variable = (isset($regex->matches[4])) ? $regex->matches[4] : null;
102 | }
103 |
104 | $this->extractAttributes($markup);
105 |
106 | parent::__construct($markup, $tokens, $fileSystem);
107 | }
108 |
109 | /**
110 | * Parses the tokens
111 | *
112 | * @param array $tokens
113 | *
114 | * @throws \Liquid\Exception\MissingFilesystemException
115 | */
116 | public function parse(array &$tokens)
117 | {
118 | if ($this->fileSystem === null) {
119 | throw new MissingFilesystemException("No file system");
120 | }
121 |
122 | // read the source of the template and create a new sub document
123 | $source = $this->fileSystem->readTemplateFile($this->templateName);
124 |
125 | $cache = Template::getCache();
126 |
127 | if (!$cache) {
128 | // tokens in this new document
129 | $templateTokens = Template::tokenize($source);
130 | $this->document = new Document($templateTokens, $this->fileSystem);
131 | return;
132 | }
133 |
134 | $this->hash = md5($source);
135 | $this->document = $cache->read($this->hash);
136 |
137 | if ($this->document == false || $this->document->hasIncludes() == true) {
138 | $templateTokens = Template::tokenize($source);
139 | $this->document = new Document($templateTokens, $this->fileSystem);
140 | $cache->write($this->hash, $this->document);
141 | }
142 | }
143 |
144 | /**
145 | * Check for cached includes; if there are - do not use cache
146 | *
147 | * @see Document::hasIncludes()
148 | * @return boolean
149 | */
150 | public function hasIncludes()
151 | {
152 | if ($this->document->hasIncludes() == true) {
153 | return true;
154 | }
155 |
156 | $source = $this->fileSystem->readTemplateFile($this->templateName);
157 |
158 | if (Template::getCache()->exists(md5($source)) && $this->hash === md5($source)) {
159 | return false;
160 | }
161 |
162 | return true;
163 | }
164 |
165 | /**
166 | * Renders the node
167 | *
168 | * @param Context $context
169 | *
170 | * @return string
171 | */
172 | public function render(Context $context)
173 | {
174 | $result = '';
175 | $variable = $context->get($this->variable);
176 |
177 | $context->push();
178 |
179 | foreach ($this->attributes as $key => $value) {
180 | $context->set($key, $context->get($value));
181 | }
182 |
183 | if ($this->collection) {
184 | foreach ($variable as $item) {
185 | $context->set($this->templateName, $item);
186 | $result .= $this->document->render($context);
187 | }
188 | } else {
189 | if (!is_null($this->variable)) {
190 | $context->set($this->templateName, $variable);
191 | }
192 |
193 | $result .= $this->document->render($context);
194 | }
195 |
196 | $context->pop();
197 |
198 | return $result;
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagIncrement.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
55 | $this->toIncrement = $syntax->matches[0];
56 | } else {
57 | throw new ParseException("Syntax Error in 'increment' - Valid syntax: increment [var]");
58 | }
59 | }
60 |
61 | /**
62 | * Renders the tag
63 | *
64 | * @param Context $context
65 | *
66 | * @return string|void
67 | */
68 | public function render(Context $context)
69 | {
70 | // If the value is not set in the environment check to see if it
71 | // exists in the context, and if not set it to -1
72 | if (!isset($context->environments[0][$this->toIncrement])) {
73 | // check for a context value
74 | $from_context = $context->get($this->toIncrement);
75 |
76 | // we already have a value in the context
77 | $context->environments[0][$this->toIncrement] = (null !== $from_context) ? $from_context : -1;
78 | }
79 |
80 | // Increment the value
81 | $context->environments[0][$this->toIncrement]++;
82 |
83 | return '';
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagPaginate.php:
--------------------------------------------------------------------------------
1 |
30 | * {% endfor %}
31 | * {% endpaginate %}
32 | *
33 | */
34 |
35 | class TagPaginate extends AbstractBlock
36 | {
37 | /**
38 | * @var array The collection to paginate
39 | */
40 | private $collectionName;
41 |
42 | /**
43 | * @var array The collection object
44 | */
45 | private $collection;
46 |
47 | /**
48 | * @var int The size of the collection
49 | */
50 | private $collectionSize;
51 |
52 | /**
53 | * @var int The number of items to paginate by
54 | */
55 | private $numberItems;
56 |
57 | /**
58 | * @var int The current page
59 | */
60 | private $currentPage;
61 |
62 | /**
63 | * @var int The current offset (no of pages times no of items)
64 | */
65 | private $currentOffset;
66 |
67 | /**
68 | * @var int Total pages
69 | */
70 | private $totalPages;
71 |
72 |
73 | /**
74 | * Constructor
75 | *
76 | * @param string $markup
77 | * @param array $tokens
78 | * @param FileSystem|null $fileSystem
79 | *
80 | * @throws \Liquid\Exception\ParseException
81 | *
82 | */
83 | public function __construct($markup, array &$tokens, ?FileSystem $fileSystem = null)
84 | {
85 | parent::__construct($markup, $tokens, $fileSystem);
86 |
87 | $syntax = new Regexp('/(' . Liquid::get('VARIABLE_NAME') . ')\s+by\s+(\w+)/');
88 |
89 | if ($syntax->match($markup)) {
90 | $this->collectionName = $syntax->matches[1];
91 | $this->numberItems = $syntax->matches[2];
92 | $this->extractAttributes($markup);
93 | } else {
94 | throw new ParseException("Syntax Error - Valid syntax: paginate [collection] by [items]");
95 | }
96 | }
97 |
98 | /**
99 | * Renders the tag
100 | *
101 | * @param Context $context
102 | *
103 | * @return string
104 | *
105 | */
106 | public function render(Context $context)
107 | {
108 | $this->collection = $context->get($this->collectionName);
109 |
110 | if ($this->collection instanceof \Traversable) {
111 | $this->collection = iterator_to_array($this->collection);
112 | }
113 |
114 | if (!is_array($this->collection)) {
115 | // TODO do not throw up if error mode allows, see #83
116 | throw new RenderException("Missing collection with name '{$this->collectionName}'");
117 | }
118 |
119 | // How many pages are there?
120 | $this->collectionSize = count($this->collection);
121 | $this->totalPages = ceil($this->collectionSize / $this->numberItems);
122 |
123 | // Whatever there is in the context, we need a number
124 | $this->currentPage = intval($context->get(Liquid::get('PAGINATION_CONTEXT_KEY')));
125 |
126 | // Page number can only be between 1 and a number of pages
127 | $this->currentPage = max(1, min($this->currentPage, $this->totalPages));
128 |
129 | // Find the offset and select that part
130 | $this->currentOffset = ($this->currentPage - 1) * $this->numberItems;
131 | $paginatedCollection = array_slice($this->collection, $this->currentOffset, $this->numberItems);
132 |
133 | // We must work in a new scope so we won't pollute a global scope
134 | $context->push();
135 |
136 | // Sets the collection if it's a key of another collection (ie search.results, collection.products, blog.articles)
137 | $segments = explode('.', $this->collectionName);
138 | if (count($segments) == 2) {
139 | $context->set($segments[0], [$segments[1] => $paginatedCollection]);
140 | } else {
141 | $context->set($this->collectionName, $paginatedCollection);
142 | }
143 |
144 | $paginate = [
145 | 'page_size' => $this->numberItems,
146 | 'current_page' => $this->currentPage,
147 | 'current_offset' => $this->currentOffset,
148 | 'pages' => $this->totalPages,
149 | 'items' => $this->collectionSize,
150 | ];
151 |
152 | // Get the name of the request field to use in URLs
153 | $pageRequestKey = Liquid::get('PAGINATION_REQUEST_KEY');
154 |
155 | if ($this->currentPage > 1) {
156 | $paginate['previous']['title'] = 'Previous';
157 | $paginate['previous']['url'] = $this->currentUrl($context, [
158 | $pageRequestKey => $this->currentPage - 1,
159 | ]);
160 | }
161 |
162 | if ($this->currentPage < $this->totalPages) {
163 | $paginate['next']['title'] = 'Next';
164 | $paginate['next']['url'] = $this->currentUrl($context, [
165 | $pageRequestKey => $this->currentPage + 1,
166 | ]);
167 | }
168 |
169 | $context->set('paginate', $paginate);
170 |
171 | $result = parent::render($context);
172 |
173 | $context->pop();
174 |
175 | return $result;
176 | }
177 |
178 | /**
179 | * Returns the current page URL
180 | *
181 | * @param Context $context
182 | * @param array $queryPart
183 | *
184 | * @return string
185 | *
186 | */
187 | public function currentUrl($context, $queryPart = [])
188 | {
189 | // From here we have $url->path and $url->query
190 | $url = (object) parse_url($context->get('REQUEST_URI') ?: '');
191 |
192 | // Let's merge the query part
193 | if (isset($url->query)) {
194 | parse_str($url->query, $url->query);
195 | $url->query = array_merge($url->query, $queryPart);
196 | } else {
197 | $url->query = $queryPart;
198 | }
199 |
200 | $url->query = http_build_query($url->query);
201 |
202 | $scheme = $context->get('HTTPS') == 'on' ? 'https' : 'http';
203 |
204 | return "$scheme://{$context->get('HTTP_HOST')}{$url->path}?{$url->query}";
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagRaw.php:
--------------------------------------------------------------------------------
1 | nodelist = [];
38 |
39 | for ($i = 0, $n = count($tokens); $i < $n; $i++) {
40 | if ($tokens[$i] === null) {
41 | continue;
42 | }
43 | $token = $tokens[$i];
44 | $tokens[$i] = null;
45 |
46 | if ($tagRegexp->match($token)) {
47 | // If we found the proper block delimiter just end parsing here and let the outer block proceed
48 | if ($tagRegexp->matches[1] == $this->blockDelimiter()) {
49 | break;
50 | }
51 | }
52 |
53 | $this->nodelist[] = $token;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagTablerow.php:
--------------------------------------------------------------------------------
1 | match($markup)) {
64 | $this->variableName = $syntax->matches[1];
65 | $this->collectionName = $syntax->matches[2];
66 |
67 | $this->extractAttributes($markup);
68 | } else {
69 | throw new ParseException("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols:3");
70 | }
71 | }
72 |
73 | /**
74 | * Renders the current node
75 | *
76 | * @param Context $context
77 | * @throws \Liquid\Exception\RenderException
78 | * @return string
79 | */
80 | public function render(Context $context)
81 | {
82 | $collection = $context->get($this->collectionName);
83 |
84 | if ($collection instanceof \Traversable) {
85 | $collection = iterator_to_array($collection);
86 | }
87 |
88 | if (!is_array($collection)) {
89 | throw new RenderException("Not an array");
90 | }
91 |
92 | // discard keys
93 | $collection = array_values($collection);
94 |
95 | if (isset($this->attributes['limit']) || isset($this->attributes['offset'])) {
96 | $limit = $context->get($this->attributes['limit']);
97 | $offset = $context->get($this->attributes['offset']);
98 | $collection = array_slice($collection, $offset, $limit);
99 | }
100 |
101 | $length = count($collection);
102 |
103 | $cols = isset($this->attributes['cols']) ? $context->get($this->attributes['cols']) : PHP_INT_MAX;
104 |
105 | $row = 1;
106 | $col = 0;
107 |
108 | $result = "\n";
109 |
110 | $context->push();
111 |
112 | foreach ($collection as $index => $item) {
113 | $context->set($this->variableName, $item);
114 | $context->set('tablerowloop', [
115 | 'length' => $length,
116 | 'index' => $index + 1,
117 | 'index0' => $index,
118 | 'rindex' => $length - $index,
119 | 'rindex0' => $length - $index - 1,
120 | 'first' => (int)($index == 0),
121 | 'last' => (int)($index == $length - 1),
122 | ]);
123 |
124 | $text = $this->renderAll($this->nodelist, $context);
125 | $break = isset($context->registers['break']);
126 | $continue = isset($context->registers['continue']);
127 |
128 | if ((!$break && !$continue) || strlen(trim($text)) > 0) {
129 | $result .= "$text | ";
130 | }
131 |
132 | if ($col == $cols && !($index == $length - 1)) {
133 | $col = 0;
134 | $result .= "
\n\n";
135 | }
136 |
137 | if ($break) {
138 | unset($context->registers['break']);
139 | break;
140 | }
141 | if ($continue) {
142 | unset($context->registers['continue']);
143 | }
144 | }
145 |
146 | $context->pop();
147 |
148 | $result .= "
\n";
149 |
150 | return $result;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/Liquid/Tag/TagUnless.php:
--------------------------------------------------------------------------------
1 | parse(template_source);
24 | * $tpl->render(array('foo'=>1, 'bar'=>2);
25 | */
26 | class Template
27 | {
28 | const CLASS_PREFIX = '\Liquid\Cache\\';
29 |
30 | /**
31 | * @var Document The root of the node tree
32 | */
33 | private $root;
34 |
35 | /**
36 | * @var FileSystem The file system to use for includes
37 | */
38 | private $fileSystem;
39 |
40 | /**
41 | * @var array Globally included filters
42 | */
43 | private $filters = [];
44 |
45 | /**
46 | * @var callable|null Called "sometimes" while rendering. For example to abort the execution of a rendering.
47 | */
48 | private $tickFunction = null;
49 |
50 | /**
51 | * @var array Custom tags
52 | */
53 | private static $tags = [];
54 |
55 | /**
56 | * @var Cache
57 | */
58 | private static $cache;
59 |
60 | /**
61 | * Constructor.
62 | *
63 | * @param string $path
64 | * @param array|Cache $cache
65 | *
66 | * @return Template
67 | */
68 | public function __construct($path = null, $cache = null)
69 | {
70 | $this->fileSystem = $path !== null
71 | ? new LocalFileSystem($path)
72 | : null;
73 |
74 | $this->setCache($cache);
75 | }
76 |
77 | /**
78 | * @param FileSystem $fileSystem
79 | */
80 | public function setFileSystem(FileSystem $fileSystem)
81 | {
82 | $this->fileSystem = $fileSystem;
83 | }
84 |
85 | /**
86 | * @param array|Cache|null $cache
87 | *
88 | * @throws \Liquid\Exception\CacheException
89 | */
90 | public static function setCache($cache)
91 | {
92 | if (is_array($cache)) {
93 | if (isset($cache['cache']) && class_exists($classname = self::CLASS_PREFIX . ucwords($cache['cache']))) {
94 | self::$cache = new $classname($cache);
95 | } else {
96 | throw new CacheException('Invalid cache options!');
97 | }
98 | }
99 |
100 | if ($cache instanceof Cache) {
101 | self::$cache = $cache;
102 | }
103 |
104 | if (is_null($cache)) {
105 | self::$cache = null;
106 | }
107 | }
108 |
109 | /**
110 | * @return Cache
111 | */
112 | public static function getCache()
113 | {
114 | return self::$cache;
115 | }
116 |
117 | /**
118 | * @return Document
119 | */
120 | public function getRoot()
121 | {
122 | return $this->root;
123 | }
124 |
125 | /**
126 | * Register custom Tags
127 | *
128 | * @param string $name
129 | * @param string $class
130 | */
131 | public static function registerTag($name, $class)
132 | {
133 | self::$tags[$name] = $class;
134 | }
135 |
136 | /**
137 | * @return array
138 | */
139 | public static function getTags()
140 | {
141 | return self::$tags;
142 | }
143 |
144 | /**
145 | * Register the filter
146 | *
147 | * @param string $filter
148 | */
149 | public function registerFilter($filter, ?callable $callback = null)
150 | {
151 | // Store callback for later use
152 | if ($callback) {
153 | $this->filters[] = [$filter, $callback];
154 | } else {
155 | $this->filters[] = $filter;
156 | }
157 | }
158 |
159 | public function setTickFunction(callable $tickFunction)
160 | {
161 | $this->tickFunction = $tickFunction;
162 | }
163 |
164 | /**
165 | * Tokenizes the given source string
166 | *
167 | * @param string $source
168 | *
169 | * @return array
170 | */
171 | public static function tokenize($source)
172 | {
173 | return empty($source)
174 | ? []
175 | : preg_split(Liquid::get('TOKENIZATION_REGEXP'), $source, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
176 | }
177 |
178 | /**
179 | * Parses the given source string
180 | *
181 | * @param string $source
182 | *
183 | * @return Template
184 | */
185 | public function parse($source)
186 | {
187 | if (!self::$cache) {
188 | return $this->parseAlways($source);
189 | }
190 |
191 | $hash = md5($source);
192 | $this->root = self::$cache->read($hash);
193 |
194 | // if no cached version exists, or if it checks for includes
195 | if ($this->root == false || $this->root->hasIncludes() == true) {
196 | $this->parseAlways($source);
197 | self::$cache->write($hash, $this->root);
198 | }
199 |
200 | return $this;
201 | }
202 |
203 | /**
204 | * Parses the given source string regardless of caching
205 | *
206 | * @param string $source
207 | *
208 | * @return Template
209 | */
210 | private function parseAlways($source)
211 | {
212 | $tokens = Template::tokenize($source);
213 | $this->root = new Document($tokens, $this->fileSystem);
214 |
215 | return $this;
216 | }
217 |
218 | /**
219 | * Parses the given template file
220 | *
221 | * @param string $templatePath
222 | * @throws \Liquid\Exception\MissingFilesystemException
223 | * @return Template
224 | */
225 | public function parseFile($templatePath)
226 | {
227 | if (!$this->fileSystem) {
228 | throw new MissingFilesystemException("Could not load a template without an initialized file system");
229 | }
230 |
231 | return $this->parse($this->fileSystem->readTemplateFile($templatePath));
232 | }
233 |
234 | /**
235 | * Renders the current template
236 | *
237 | * @param array $assigns an array of values for the template
238 | * @param array $filters additional filters for the template
239 | * @param array $registers additional registers for the template
240 | *
241 | * @return string
242 | */
243 | public function render(array $assigns = [], $filters = null, array $registers = [])
244 | {
245 | $context = new Context($assigns, $registers);
246 |
247 | if ($this->tickFunction) {
248 | $context->setTickFunction($this->tickFunction);
249 | }
250 |
251 | if (!is_null($filters)) {
252 | if (is_array($filters)) {
253 | $this->filters = array_merge($this->filters, $filters);
254 | } else {
255 | $this->filters[] = $filters;
256 | }
257 | }
258 |
259 | foreach ($this->filters as $filter) {
260 | if (is_array($filter)) {
261 | // Unpack a callback saved as second argument
262 | $context->addFilters(...$filter);
263 | } else {
264 | $context->addFilters($filter);
265 | }
266 | }
267 |
268 | return $this->root->render($context);
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/Liquid/Variable.php:
--------------------------------------------------------------------------------
1 | markup = $markup;
42 |
43 | $filterSep = new Regexp('/' . Liquid::get('FILTER_SEPARATOR') . '\s*(.*)/m');
44 | $syntaxParser = new Regexp('/(' . Liquid::get('QUOTED_FRAGMENT') . ')(.*)/ms');
45 | $filterParser = new Regexp('/(?:\s+|' . Liquid::get('QUOTED_FRAGMENT') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')+/');
46 | $filterArgsRegex = new Regexp('/(?:' . Liquid::get('FILTER_ARGUMENT_SEPARATOR') . '|' . Liquid::get('ARGUMENT_SEPARATOR') . ')\s*((?:\w+\s*\:\s*)?' . Liquid::get('QUOTED_FRAGMENT') . ')/');
47 |
48 | $this->filters = [];
49 | if ($syntaxParser->match($markup)) {
50 | $nameMarkup = $syntaxParser->matches[1];
51 | $this->name = $nameMarkup;
52 | $filterMarkup = $syntaxParser->matches[2];
53 |
54 | if ($filterSep->match($filterMarkup)) {
55 | $filterParser->matchAll($filterSep->matches[1]);
56 |
57 | foreach ($filterParser->matches[0] as $filter) {
58 | $filter = trim($filter);
59 | if (preg_match('/\w+/', $filter, $matches)) {
60 | $filterName = $matches[0];
61 | $filterArgsRegex->matchAll($filter);
62 | $matches = Liquid::arrayFlatten($filterArgsRegex->matches[1]);
63 | $this->filters[] = $this->parseFilterExpressions($filterName, $matches);
64 | }
65 | }
66 | }
67 | }
68 |
69 | if (Liquid::get('ESCAPE_BY_DEFAULT')) {
70 | // if auto_escape is enabled, and
71 | // - there's no raw filter, and
72 | // - no escape filter
73 | // - no other standard html-adding filter
74 | // then
75 | // - add a mandatory escape filter
76 |
77 | $addEscapeFilter = true;
78 |
79 | foreach ($this->filters as $filter) {
80 | // with empty filters set we would just move along
81 | if (in_array($filter[0], ['escape', 'escape_once', 'raw', 'newline_to_br'])) {
82 | // if we have any raw-like filter, stop
83 | $addEscapeFilter = false;
84 | break;
85 | }
86 | }
87 |
88 | if ($addEscapeFilter) {
89 | $this->filters[] = ['escape', []];
90 | }
91 | }
92 | }
93 |
94 | /**
95 | * @param string $filterName
96 | * @param array $unparsedArgs
97 | * @return array
98 | */
99 | private static function parseFilterExpressions($filterName, array $unparsedArgs)
100 | {
101 | $filterArgs = [];
102 | $keywordArgs = [];
103 |
104 | $justTagAttributes = new Regexp('/\A' . trim(Liquid::get('TAG_ATTRIBUTES'), '/') . '\z/');
105 |
106 | foreach ($unparsedArgs as $a) {
107 | if ($justTagAttributes->match($a)) {
108 | $keywordArgs[$justTagAttributes->matches[1]] = $justTagAttributes->matches[2];
109 | } else {
110 | $filterArgs[] = $a;
111 | }
112 | }
113 |
114 | if (count($keywordArgs)) {
115 | $filterArgs[] = $keywordArgs;
116 | }
117 |
118 | return [$filterName, $filterArgs];
119 | }
120 |
121 | /**
122 | * Gets the variable name
123 | *
124 | * @return string The name of the variable
125 | */
126 | public function getName()
127 | {
128 | return $this->name;
129 | }
130 |
131 | /**
132 | * Gets all Filters
133 | *
134 | * @return array
135 | */
136 | public function getFilters()
137 | {
138 | return $this->filters;
139 | }
140 |
141 | /**
142 | * Renders the variable with the data in the context
143 | *
144 | * @param Context $context
145 | *
146 | * @return mixed|string
147 | */
148 | public function render(Context $context)
149 | {
150 | $output = $context->get($this->name);
151 | foreach ($this->filters as $filter) {
152 | list($filtername, $filterArgKeys) = $filter;
153 |
154 | $filterArgValues = [];
155 | $keywordArgValues = [];
156 |
157 | foreach ($filterArgKeys as $arg_key) {
158 | if (is_array($arg_key)) {
159 | foreach ($arg_key as $keywordArgName => $keywordArgKey) {
160 | $keywordArgValues[$keywordArgName] = $context->get($keywordArgKey);
161 | }
162 |
163 | $filterArgValues[] = $keywordArgValues;
164 | } else {
165 | $filterArgValues[] = $context->get($arg_key);
166 | }
167 | }
168 |
169 | $output = $context->invoke($filtername, $output, $filterArgValues);
170 | }
171 | return $output;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------