1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Twig\Extension {
13use Twig\ExpressionParser;
14use Twig\TokenParser\BlockTokenParser;
15use Twig\TokenParser\DeprecatedTokenParser;
16use Twig\TokenParser\DoTokenParser;
17use Twig\TokenParser\EmbedTokenParser;
18use Twig\TokenParser\ExtendsTokenParser;
19use Twig\TokenParser\FilterTokenParser;
20use Twig\TokenParser\FlushTokenParser;
21use Twig\TokenParser\ForTokenParser;
22use Twig\TokenParser\FromTokenParser;
23use Twig\TokenParser\IfTokenParser;
24use Twig\TokenParser\ImportTokenParser;
25use Twig\TokenParser\IncludeTokenParser;
26use Twig\TokenParser\MacroTokenParser;
27use Twig\TokenParser\SetTokenParser;
28use Twig\TokenParser\SpacelessTokenParser;
29use Twig\TokenParser\UseTokenParser;
30use Twig\TokenParser\WithTokenParser;
31use Twig\TwigFilter;
32use Twig\TwigFunction;
33use Twig\TwigTest;
34
35/**
36 * @final
37 */
38class CoreExtension extends AbstractExtension
39{
40    protected $dateFormats = ['F j, Y H:i', '%d days'];
41    protected $numberFormat = [0, '.', ','];
42    protected $timezone = null;
43    protected $escapers = [];
44
45    /**
46     * Defines a new escaper to be used via the escape filter.
47     *
48     * @param string   $strategy The strategy name that should be used as a strategy in the escape call
49     * @param callable $callable A valid PHP callable
50     */
51    public function setEscaper($strategy, $callable)
52    {
53        $this->escapers[$strategy] = $callable;
54    }
55
56    /**
57     * Gets all defined escapers.
58     *
59     * @return array An array of escapers
60     */
61    public function getEscapers()
62    {
63        return $this->escapers;
64    }
65
66    /**
67     * Sets the default format to be used by the date filter.
68     *
69     * @param string $format             The default date format string
70     * @param string $dateIntervalFormat The default date interval format string
71     */
72    public function setDateFormat($format = null, $dateIntervalFormat = null)
73    {
74        if (null !== $format) {
75            $this->dateFormats[0] = $format;
76        }
77
78        if (null !== $dateIntervalFormat) {
79            $this->dateFormats[1] = $dateIntervalFormat;
80        }
81    }
82
83    /**
84     * Gets the default format to be used by the date filter.
85     *
86     * @return array The default date format string and the default date interval format string
87     */
88    public function getDateFormat()
89    {
90        return $this->dateFormats;
91    }
92
93    /**
94     * Sets the default timezone to be used by the date filter.
95     *
96     * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object
97     */
98    public function setTimezone($timezone)
99    {
100        $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone);
101    }
102
103    /**
104     * Gets the default timezone to be used by the date filter.
105     *
106     * @return \DateTimeZone The default timezone currently in use
107     */
108    public function getTimezone()
109    {
110        if (null === $this->timezone) {
111            $this->timezone = new \DateTimeZone(date_default_timezone_get());
112        }
113
114        return $this->timezone;
115    }
116
117    /**
118     * Sets the default format to be used by the number_format filter.
119     *
120     * @param int    $decimal      the number of decimal places to use
121     * @param string $decimalPoint the character(s) to use for the decimal point
122     * @param string $thousandSep  the character(s) to use for the thousands separator
123     */
124    public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
125    {
126        $this->numberFormat = [$decimal, $decimalPoint, $thousandSep];
127    }
128
129    /**
130     * Get the default format used by the number_format filter.
131     *
132     * @return array The arguments for number_format()
133     */
134    public function getNumberFormat()
135    {
136        return $this->numberFormat;
137    }
138
139    public function getTokenParsers()
140    {
141        return [
142            new ForTokenParser(),
143            new IfTokenParser(),
144            new ExtendsTokenParser(),
145            new IncludeTokenParser(),
146            new BlockTokenParser(),
147            new UseTokenParser(),
148            new FilterTokenParser(),
149            new MacroTokenParser(),
150            new ImportTokenParser(),
151            new FromTokenParser(),
152            new SetTokenParser(),
153            new SpacelessTokenParser(),
154            new FlushTokenParser(),
155            new DoTokenParser(),
156            new EmbedTokenParser(),
157            new WithTokenParser(),
158            new DeprecatedTokenParser(),
159        ];
160    }
161
162    public function getFilters()
163    {
164        $filters = [
165            // formatting filters
166            new TwigFilter('date', 'twig_date_format_filter', ['needs_environment' => true]),
167            new TwigFilter('date_modify', 'twig_date_modify_filter', ['needs_environment' => true]),
168            new TwigFilter('format', 'sprintf'),
169            new TwigFilter('replace', 'twig_replace_filter'),
170            new TwigFilter('number_format', 'twig_number_format_filter', ['needs_environment' => true]),
171            new TwigFilter('abs', 'abs'),
172            new TwigFilter('round', 'twig_round'),
173
174            // encoding
175            new TwigFilter('url_encode', 'twig_urlencode_filter'),
176            new TwigFilter('json_encode', 'twig_jsonencode_filter'),
177            new TwigFilter('convert_encoding', 'twig_convert_encoding'),
178
179            // string filters
180            new TwigFilter('title', 'twig_title_string_filter', ['needs_environment' => true]),
181            new TwigFilter('capitalize', 'twig_capitalize_string_filter', ['needs_environment' => true]),
182            new TwigFilter('upper', 'strtoupper'),
183            new TwigFilter('lower', 'strtolower'),
184            new TwigFilter('striptags', 'strip_tags'),
185            new TwigFilter('trim', 'twig_trim_filter'),
186            new TwigFilter('nl2br', 'nl2br', ['pre_escape' => 'html', 'is_safe' => ['html']]),
187            new TwigFilter('spaceless', 'twig_spaceless', ['is_safe' => ['html']]),
188
189            // array helpers
190            new TwigFilter('join', 'twig_join_filter'),
191            new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]),
192            new TwigFilter('sort', 'twig_sort_filter'),
193            new TwigFilter('merge', 'twig_array_merge'),
194            new TwigFilter('batch', 'twig_array_batch'),
195
196            // string/array filters
197            new TwigFilter('reverse', 'twig_reverse_filter', ['needs_environment' => true]),
198            new TwigFilter('length', 'twig_length_filter', ['needs_environment' => true]),
199            new TwigFilter('slice', 'twig_slice', ['needs_environment' => true]),
200            new TwigFilter('first', 'twig_first', ['needs_environment' => true]),
201            new TwigFilter('last', 'twig_last', ['needs_environment' => true]),
202
203            // iteration and runtime
204            new TwigFilter('default', '_twig_default_filter', ['node_class' => '\Twig\Node\Expression\Filter\DefaultFilter']),
205            new TwigFilter('keys', 'twig_get_array_keys_filter'),
206
207            // escaping
208            new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
209            new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
210        ];
211
212        if (\function_exists('mb_get_info')) {
213            $filters[] = new TwigFilter('upper', 'twig_upper_filter', ['needs_environment' => true]);
214            $filters[] = new TwigFilter('lower', 'twig_lower_filter', ['needs_environment' => true]);
215        }
216
217        return $filters;
218    }
219
220    public function getFunctions()
221    {
222        return [
223            new TwigFunction('max', 'max'),
224            new TwigFunction('min', 'min'),
225            new TwigFunction('range', 'range'),
226            new TwigFunction('constant', 'twig_constant'),
227            new TwigFunction('cycle', 'twig_cycle'),
228            new TwigFunction('random', 'twig_random', ['needs_environment' => true]),
229            new TwigFunction('date', 'twig_date_converter', ['needs_environment' => true]),
230            new TwigFunction('include', 'twig_include', ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
231            new TwigFunction('source', 'twig_source', ['needs_environment' => true, 'is_safe' => ['all']]),
232        ];
233    }
234
235    public function getTests()
236    {
237        return [
238            new TwigTest('even', null, ['node_class' => '\Twig\Node\Expression\Test\EvenTest']),
239            new TwigTest('odd', null, ['node_class' => '\Twig\Node\Expression\Test\OddTest']),
240            new TwigTest('defined', null, ['node_class' => '\Twig\Node\Expression\Test\DefinedTest']),
241            new TwigTest('sameas', null, ['node_class' => '\Twig\Node\Expression\Test\SameasTest', 'deprecated' => '1.21', 'alternative' => 'same as']),
242            new TwigTest('same as', null, ['node_class' => '\Twig\Node\Expression\Test\SameasTest']),
243            new TwigTest('none', null, ['node_class' => '\Twig\Node\Expression\Test\NullTest']),
244            new TwigTest('null', null, ['node_class' => '\Twig\Node\Expression\Test\NullTest']),
245            new TwigTest('divisibleby', null, ['node_class' => '\Twig\Node\Expression\Test\DivisiblebyTest', 'deprecated' => '1.21', 'alternative' => 'divisible by']),
246            new TwigTest('divisible by', null, ['node_class' => '\Twig\Node\Expression\Test\DivisiblebyTest']),
247            new TwigTest('constant', null, ['node_class' => '\Twig\Node\Expression\Test\ConstantTest']),
248            new TwigTest('empty', 'twig_test_empty'),
249            new TwigTest('iterable', 'twig_test_iterable'),
250        ];
251    }
252
253    public function getOperators()
254    {
255        return [
256            [
257                'not' => ['precedence' => 50, 'class' => '\Twig\Node\Expression\Unary\NotUnary'],
258                '-' => ['precedence' => 500, 'class' => '\Twig\Node\Expression\Unary\NegUnary'],
259                '+' => ['precedence' => 500, 'class' => '\Twig\Node\Expression\Unary\PosUnary'],
260            ],
261            [
262                'or' => ['precedence' => 10, 'class' => '\Twig\Node\Expression\Binary\OrBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
263                'and' => ['precedence' => 15, 'class' => '\Twig\Node\Expression\Binary\AndBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
264                'b-or' => ['precedence' => 16, 'class' => '\Twig\Node\Expression\Binary\BitwiseOrBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
265                'b-xor' => ['precedence' => 17, 'class' => '\Twig\Node\Expression\Binary\BitwiseXorBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
266                'b-and' => ['precedence' => 18, 'class' => '\Twig\Node\Expression\Binary\BitwiseAndBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
267                '==' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\EqualBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
268                '!=' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\NotEqualBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
269                '<' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\LessBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
270                '>' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\GreaterBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
271                '>=' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\GreaterEqualBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
272                '<=' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\LessEqualBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
273                'not in' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\NotInBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
274                'in' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\InBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
275                'matches' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\MatchesBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
276                'starts with' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\StartsWithBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
277                'ends with' => ['precedence' => 20, 'class' => '\Twig\Node\Expression\Binary\EndsWithBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
278                '..' => ['precedence' => 25, 'class' => '\Twig\Node\Expression\Binary\RangeBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
279                '+' => ['precedence' => 30, 'class' => '\Twig\Node\Expression\Binary\AddBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
280                '-' => ['precedence' => 30, 'class' => '\Twig\Node\Expression\Binary\SubBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
281                '~' => ['precedence' => 40, 'class' => '\Twig\Node\Expression\Binary\ConcatBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
282                '*' => ['precedence' => 60, 'class' => '\Twig\Node\Expression\Binary\MulBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
283                '/' => ['precedence' => 60, 'class' => '\Twig\Node\Expression\Binary\DivBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
284                '//' => ['precedence' => 60, 'class' => '\Twig\Node\Expression\Binary\FloorDivBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
285                '%' => ['precedence' => 60, 'class' => '\Twig\Node\Expression\Binary\ModBinary', 'associativity' => ExpressionParser::OPERATOR_LEFT],
286                'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
287                'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT],
288                '**' => ['precedence' => 200, 'class' => '\Twig\Node\Expression\Binary\PowerBinary', 'associativity' => ExpressionParser::OPERATOR_RIGHT],
289                '??' => ['precedence' => 300, 'class' => '\Twig\Node\Expression\NullCoalesceExpression', 'associativity' => ExpressionParser::OPERATOR_RIGHT],
290            ],
291        ];
292    }
293
294    public function getName()
295    {
296        return 'core';
297    }
298}
299
300class_alias('Twig\Extension\CoreExtension', 'Twig_Extension_Core');
301}
302
303namespace {
304use Twig\Environment;
305use Twig\Error\LoaderError;
306use Twig\Error\RuntimeError;
307use Twig\Loader\SourceContextLoaderInterface;
308use Twig\Markup;
309use Twig\Node\Expression\ConstantExpression;
310use Twig\Node\Node;
311
312/**
313 * Cycles over a value.
314 *
315 * @param \ArrayAccess|array $values
316 * @param int                $position The cycle position
317 *
318 * @return string The next value in the cycle
319 */
320function twig_cycle($values, $position)
321{
322    if (!\is_array($values) && !$values instanceof \ArrayAccess) {
323        return $values;
324    }
325
326    return $values[$position % \count($values)];
327}
328
329/**
330 * Returns a random value depending on the supplied parameter type:
331 * - a random item from a \Traversable or array
332 * - a random character from a string
333 * - a random integer between 0 and the integer parameter.
334 *
335 * @param \Traversable|array|int|float|string $values The values to pick a random item from
336 * @param int|null                            $max    Maximum value used when $values is an int
337 *
338 * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is)
339 *
340 * @return mixed A random value from the given sequence
341 */
342function twig_random(Environment $env, $values = null, $max = null)
343{
344    if (null === $values) {
345        return null === $max ? mt_rand() : mt_rand(0, $max);
346    }
347
348    if (\is_int($values) || \is_float($values)) {
349        if (null === $max) {
350            if ($values < 0) {
351                $max = 0;
352                $min = $values;
353            } else {
354                $max = $values;
355                $min = 0;
356            }
357        } else {
358            $min = $values;
359            $max = $max;
360        }
361
362        return mt_rand($min, $max);
363    }
364
365    if (\is_string($values)) {
366        if ('' === $values) {
367            return '';
368        }
369        if (null !== $charset = $env->getCharset()) {
370            if ('UTF-8' !== $charset) {
371                $values = twig_convert_encoding($values, 'UTF-8', $charset);
372            }
373
374            // unicode version of str_split()
375            // split at all positions, but not after the start and not before the end
376            $values = preg_split('/(?<!^)(?!$)/u', $values);
377
378            if ('UTF-8' !== $charset) {
379                foreach ($values as $i => $value) {
380                    $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
381                }
382            }
383        } else {
384            return $values[mt_rand(0, \strlen($values) - 1)];
385        }
386    }
387
388    if (!twig_test_iterable($values)) {
389        return $values;
390    }
391
392    $values = twig_to_array($values);
393
394    if (0 === \count($values)) {
395        throw new RuntimeError('The random function cannot pick from an empty array.');
396    }
397
398    return $values[array_rand($values, 1)];
399}
400
401/**
402 * Converts a date to the given format.
403 *
404 *   {{ post.published_at|date("m/d/Y") }}
405 *
406 * @param \DateTime|\DateTimeInterface|\DateInterval|string $date     A date
407 * @param string|null                                       $format   The target format, null to use the default
408 * @param \DateTimeZone|string|false|null                   $timezone The target timezone, null to use the default, false to leave unchanged
409 *
410 * @return string The formatted date
411 */
412function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null)
413{
414    if (null === $format) {
415        $formats = $env->getExtension('\Twig\Extension\CoreExtension')->getDateFormat();
416        $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
417    }
418
419    if ($date instanceof \DateInterval) {
420        return $date->format($format);
421    }
422
423    return twig_date_converter($env, $date, $timezone)->format($format);
424}
425
426/**
427 * Returns a new date object modified.
428 *
429 *   {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
430 *
431 * @param \DateTime|string $date     A date
432 * @param string           $modifier A modifier string
433 *
434 * @return \DateTime
435 */
436function twig_date_modify_filter(Environment $env, $date, $modifier)
437{
438    $date = twig_date_converter($env, $date, false);
439    $resultDate = $date->modify($modifier);
440
441    // This is a hack to ensure PHP 5.2 support and support for \DateTimeImmutable
442    // \DateTime::modify does not return the modified \DateTime object < 5.3.0
443    // and \DateTimeImmutable does not modify $date.
444    return null === $resultDate ? $date : $resultDate;
445}
446
447/**
448 * Converts an input to a \DateTime instance.
449 *
450 *    {% if date(user.created_at) < date('+2days') %}
451 *      {# do something #}
452 *    {% endif %}
453 *
454 * @param \DateTime|\DateTimeInterface|string|null $date     A date
455 * @param \DateTimeZone|string|false|null          $timezone The target timezone, null to use the default, false to leave unchanged
456 *
457 * @return \DateTime
458 */
459function twig_date_converter(Environment $env, $date = null, $timezone = null)
460{
461    // determine the timezone
462    if (false !== $timezone) {
463        if (null === $timezone) {
464            $timezone = $env->getExtension('\Twig\Extension\CoreExtension')->getTimezone();
465        } elseif (!$timezone instanceof \DateTimeZone) {
466            $timezone = new \DateTimeZone($timezone);
467        }
468    }
469
470    // immutable dates
471    if ($date instanceof \DateTimeImmutable) {
472        return false !== $timezone ? $date->setTimezone($timezone) : $date;
473    }
474
475    if ($date instanceof \DateTime || $date instanceof \DateTimeInterface) {
476        $date = clone $date;
477        if (false !== $timezone) {
478            $date->setTimezone($timezone);
479        }
480
481        return $date;
482    }
483
484    if (null === $date || 'now' === $date) {
485        return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension('\Twig\Extension\CoreExtension')->getTimezone());
486    }
487
488    $asString = (string) $date;
489    if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
490        $date = new \DateTime('@'.$date);
491    } else {
492        $date = new \DateTime($date, $env->getExtension('\Twig\Extension\CoreExtension')->getTimezone());
493    }
494
495    if (false !== $timezone) {
496        $date->setTimezone($timezone);
497    }
498
499    return $date;
500}
501
502/**
503 * Replaces strings within a string.
504 *
505 * @param string             $str  String to replace in
506 * @param array|\Traversable $from Replace values
507 * @param string|null        $to   Replace to, deprecated (@see https://secure.php.net/manual/en/function.strtr.php)
508 *
509 * @return string
510 */
511function twig_replace_filter($str, $from, $to = null)
512{
513    if (\is_string($from) && \is_string($to)) {
514        @trigger_error('Using "replace" with character by character replacement is deprecated since version 1.22 and will be removed in Twig 2.0', E_USER_DEPRECATED);
515
516        return strtr($str, $from, $to);
517    } elseif (!twig_test_iterable($from)) {
518        throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? \get_class($from) : \gettype($from)));
519    }
520
521    return strtr($str, twig_to_array($from));
522}
523
524/**
525 * Rounds a number.
526 *
527 * @param int|float $value     The value to round
528 * @param int|float $precision The rounding precision
529 * @param string    $method    The method to use for rounding
530 *
531 * @return int|float The rounded number
532 */
533function twig_round($value, $precision = 0, $method = 'common')
534{
535    if ('common' == $method) {
536        return round($value, $precision);
537    }
538
539    if ('ceil' != $method && 'floor' != $method) {
540        throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.');
541    }
542
543    return $method($value * pow(10, $precision)) / pow(10, $precision);
544}
545
546/**
547 * Number format filter.
548 *
549 * All of the formatting options can be left null, in that case the defaults will
550 * be used.  Supplying any of the parameters will override the defaults set in the
551 * environment object.
552 *
553 * @param mixed  $number       A float/int/string of the number to format
554 * @param int    $decimal      the number of decimal points to display
555 * @param string $decimalPoint the character(s) to use for the decimal point
556 * @param string $thousandSep  the character(s) to use for the thousands separator
557 *
558 * @return string The formatted number
559 */
560function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
561{
562    $defaults = $env->getExtension('\Twig\Extension\CoreExtension')->getNumberFormat();
563    if (null === $decimal) {
564        $decimal = $defaults[0];
565    }
566
567    if (null === $decimalPoint) {
568        $decimalPoint = $defaults[1];
569    }
570
571    if (null === $thousandSep) {
572        $thousandSep = $defaults[2];
573    }
574
575    return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
576}
577
578/**
579 * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
580 *
581 * @param string|array $url A URL or an array of query parameters
582 *
583 * @return string The URL encoded value
584 */
585function twig_urlencode_filter($url)
586{
587    if (\is_array($url)) {
588        if (\defined('PHP_QUERY_RFC3986')) {
589            return http_build_query($url, '', '&', PHP_QUERY_RFC3986);
590        }
591
592        return http_build_query($url, '', '&');
593    }
594
595    return rawurlencode($url);
596}
597
598/**
599 * JSON encodes a variable.
600 *
601 * @param mixed $value   the value to encode
602 * @param int   $options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT
603 *
604 * @return mixed The JSON encoded value
605 */
606function twig_jsonencode_filter($value, $options = 0)
607{
608    if ($value instanceof Markup) {
609        $value = (string) $value;
610    } elseif (\is_array($value)) {
611        array_walk_recursive($value, '_twig_markup2string');
612    }
613
614    return json_encode($value, $options);
615}
616
617function _twig_markup2string(&$value)
618{
619    if ($value instanceof Markup) {
620        $value = (string) $value;
621    }
622}
623
624/**
625 * Merges an array with another one.
626 *
627 *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
628 *
629 *  {% set items = items|merge({ 'peugeot': 'car' }) %}
630 *
631 *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
632 *
633 * @param array|\Traversable $arr1 An array
634 * @param array|\Traversable $arr2 An array
635 *
636 * @return array The merged array
637 */
638function twig_array_merge($arr1, $arr2)
639{
640    if (!twig_test_iterable($arr1)) {
641        throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1)));
642    }
643
644    if (!twig_test_iterable($arr2)) {
645        throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2)));
646    }
647
648    return array_merge(twig_to_array($arr1), twig_to_array($arr2));
649}
650
651/**
652 * Slices a variable.
653 *
654 * @param mixed $item         A variable
655 * @param int   $start        Start of the slice
656 * @param int   $length       Size of the slice
657 * @param bool  $preserveKeys Whether to preserve key or not (when the input is an array)
658 *
659 * @return mixed The sliced variable
660 */
661function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false)
662{
663    if ($item instanceof \Traversable) {
664        while ($item instanceof \IteratorAggregate) {
665            $item = $item->getIterator();
666        }
667
668        if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) {
669            try {
670                return iterator_to_array(new \LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys);
671            } catch (\OutOfBoundsException $exception) {
672                return [];
673            }
674        }
675
676        $item = iterator_to_array($item, $preserveKeys);
677    }
678
679    if (\is_array($item)) {
680        return \array_slice($item, $start, $length, $preserveKeys);
681    }
682
683    $item = (string) $item;
684
685    if (\function_exists('mb_get_info') && null !== $charset = $env->getCharset()) {
686        return (string) mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset);
687    }
688
689    return (string) (null === $length ? substr($item, $start) : substr($item, $start, $length));
690}
691
692/**
693 * Returns the first element of the item.
694 *
695 * @param mixed $item A variable
696 *
697 * @return mixed The first element of the item
698 */
699function twig_first(Environment $env, $item)
700{
701    $elements = twig_slice($env, $item, 0, 1, false);
702
703    return \is_string($elements) ? $elements : current($elements);
704}
705
706/**
707 * Returns the last element of the item.
708 *
709 * @param mixed $item A variable
710 *
711 * @return mixed The last element of the item
712 */
713function twig_last(Environment $env, $item)
714{
715    $elements = twig_slice($env, $item, -1, 1, false);
716
717    return \is_string($elements) ? $elements : current($elements);
718}
719
720/**
721 * Joins the values to a string.
722 *
723 * The separators between elements are empty strings per default, you can define them with the optional parameters.
724 *
725 *  {{ [1, 2, 3]|join(', ', ' and ') }}
726 *  {# returns 1, 2 and 3 #}
727 *
728 *  {{ [1, 2, 3]|join('|') }}
729 *  {# returns 1|2|3 #}
730 *
731 *  {{ [1, 2, 3]|join }}
732 *  {# returns 123 #}
733 *
734 * @param array       $value An array
735 * @param string      $glue  The separator
736 * @param string|null $and   The separator for the last pair
737 *
738 * @return string The concatenated string
739 */
740function twig_join_filter($value, $glue = '', $and = null)
741{
742    $value = twig_to_array($value, false);
743
744    if (!\is_array($value) || 0 === \count($value)) {
745        return '';
746    }
747
748    if (null === $and || $and === $glue) {
749        return implode($glue, $value);
750    }
751
752    if (1 === \count($value)) {
753        return $value[0];
754    }
755
756    return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1];
757}
758
759/**
760 * Splits the string into an array.
761 *
762 *  {{ "one,two,three"|split(',') }}
763 *  {# returns [one, two, three] #}
764 *
765 *  {{ "one,two,three,four,five"|split(',', 3) }}
766 *  {# returns [one, two, "three,four,five"] #}
767 *
768 *  {{ "123"|split('') }}
769 *  {# returns [1, 2, 3] #}
770 *
771 *  {{ "aabbcc"|split('', 2) }}
772 *  {# returns [aa, bb, cc] #}
773 *
774 * @param string $value     A string
775 * @param string $delimiter The delimiter
776 * @param int    $limit     The limit
777 *
778 * @return array The split string as an array
779 */
780function twig_split_filter(Environment $env, $value, $delimiter, $limit = null)
781{
782    if (!empty($delimiter)) {
783        return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
784    }
785
786    if (!\function_exists('mb_get_info') || null === $charset = $env->getCharset()) {
787        return str_split($value, null === $limit ? 1 : $limit);
788    }
789
790    if ($limit <= 1) {
791        return preg_split('/(?<!^)(?!$)/u', $value);
792    }
793
794    $length = mb_strlen($value, $charset);
795    if ($length < $limit) {
796        return [$value];
797    }
798
799    $r = [];
800    for ($i = 0; $i < $length; $i += $limit) {
801        $r[] = mb_substr($value, $i, $limit, $charset);
802    }
803
804    return $r;
805}
806
807// The '_default' filter is used internally to avoid using the ternary operator
808// which costs a lot for big contexts (before PHP 5.4). So, on average,
809// a function call is cheaper.
810/**
811 * @internal
812 */
813function _twig_default_filter($value, $default = '')
814{
815    if (twig_test_empty($value)) {
816        return $default;
817    }
818
819    return $value;
820}
821
822/**
823 * Returns the keys for the given array.
824 *
825 * It is useful when you want to iterate over the keys of an array:
826 *
827 *  {% for key in array|keys %}
828 *      {# ... #}
829 *  {% endfor %}
830 *
831 * @param array $array An array
832 *
833 * @return array The keys
834 */
835function twig_get_array_keys_filter($array)
836{
837    if ($array instanceof \Traversable) {
838        while ($array instanceof \IteratorAggregate) {
839            $array = $array->getIterator();
840        }
841
842        if ($array instanceof \Iterator) {
843            $keys = [];
844            $array->rewind();
845            while ($array->valid()) {
846                $keys[] = $array->key();
847                $array->next();
848            }
849
850            return $keys;
851        }
852
853        $keys = [];
854        foreach ($array as $key => $item) {
855            $keys[] = $key;
856        }
857
858        return $keys;
859    }
860
861    if (!\is_array($array)) {
862        return [];
863    }
864
865    return array_keys($array);
866}
867
868/**
869 * Reverses a variable.
870 *
871 * @param array|\Traversable|string $item         An array, a \Traversable instance, or a string
872 * @param bool                      $preserveKeys Whether to preserve key or not
873 *
874 * @return mixed The reversed input
875 */
876function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
877{
878    if ($item instanceof \Traversable) {
879        return array_reverse(iterator_to_array($item), $preserveKeys);
880    }
881
882    if (\is_array($item)) {
883        return array_reverse($item, $preserveKeys);
884    }
885
886    if (null !== $charset = $env->getCharset()) {
887        $string = (string) $item;
888
889        if ('UTF-8' !== $charset) {
890            $item = twig_convert_encoding($string, 'UTF-8', $charset);
891        }
892
893        preg_match_all('/./us', $item, $matches);
894
895        $string = implode('', array_reverse($matches[0]));
896
897        if ('UTF-8' !== $charset) {
898            $string = twig_convert_encoding($string, $charset, 'UTF-8');
899        }
900
901        return $string;
902    }
903
904    return strrev((string) $item);
905}
906
907/**
908 * Sorts an array.
909 *
910 * @param array|\Traversable $array
911 *
912 * @return array
913 */
914function twig_sort_filter($array)
915{
916    if ($array instanceof \Traversable) {
917        $array = iterator_to_array($array);
918    } elseif (!\is_array($array)) {
919        throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));
920    }
921
922    asort($array);
923
924    return $array;
925}
926
927/**
928 * @internal
929 */
930function twig_in_filter($value, $compare)
931{
932    if (\is_array($compare)) {
933        return \in_array($value, $compare, \is_object($value) || \is_resource($value));
934    } elseif (\is_string($compare) && (\is_string($value) || \is_int($value) || \is_float($value))) {
935        return '' === $value || false !== strpos($compare, (string) $value);
936    } elseif ($compare instanceof \Traversable) {
937        if (\is_object($value) || \is_resource($value)) {
938            foreach ($compare as $item) {
939                if ($item === $value) {
940                    return true;
941                }
942            }
943        } else {
944            foreach ($compare as $item) {
945                if ($item == $value) {
946                    return true;
947                }
948            }
949        }
950
951        return false;
952    }
953
954    return false;
955}
956
957/**
958 * Returns a trimmed string.
959 *
960 * @return string
961 *
962 * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
963 */
964function twig_trim_filter($string, $characterMask = null, $side = 'both')
965{
966    if (null === $characterMask) {
967        $characterMask = " \t\n\r\0\x0B";
968    }
969
970    switch ($side) {
971        case 'both':
972            return trim($string, $characterMask);
973        case 'left':
974            return ltrim($string, $characterMask);
975        case 'right':
976            return rtrim($string, $characterMask);
977        default:
978            throw new RuntimeError('Trimming side must be "left", "right" or "both".');
979    }
980}
981
982/**
983 * Removes whitespaces between HTML tags.
984 *
985 * @return string
986 */
987function twig_spaceless($content)
988{
989    return trim(preg_replace('/>\s+</', '><', $content));
990}
991
992/**
993 * Escapes a string.
994 *
995 * @param mixed  $string     The value to be escaped
996 * @param string $strategy   The escaping strategy
997 * @param string $charset    The charset
998 * @param bool   $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
999 *
1000 * @return string
1001 */
1002function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
1003{
1004    if ($autoescape && $string instanceof Markup) {
1005        return $string;
1006    }
1007
1008    if (!\is_string($string)) {
1009        if (\is_object($string) && method_exists($string, '__toString')) {
1010            $string = (string) $string;
1011        } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
1012            return $string;
1013        }
1014    }
1015
1016    if ('' === $string) {
1017        return '';
1018    }
1019
1020    if (null === $charset) {
1021        $charset = $env->getCharset();
1022    }
1023
1024    switch ($strategy) {
1025        case 'html':
1026            // see https://secure.php.net/htmlspecialchars
1027
1028            // Using a static variable to avoid initializing the array
1029            // each time the function is called. Moving the declaration on the
1030            // top of the function slow downs other escaping strategies.
1031            static $htmlspecialcharsCharsets = [
1032                'ISO-8859-1' => true, 'ISO8859-1' => true,
1033                'ISO-8859-15' => true, 'ISO8859-15' => true,
1034                'utf-8' => true, 'UTF-8' => true,
1035                'CP866' => true, 'IBM866' => true, '866' => true,
1036                'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
1037                '1251' => true,
1038                'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
1039                'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
1040                'BIG5' => true, '950' => true,
1041                'GB2312' => true, '936' => true,
1042                'BIG5-HKSCS' => true,
1043                'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
1044                'EUC-JP' => true, 'EUCJP' => true,
1045                'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
1046            ];
1047
1048            if (isset($htmlspecialcharsCharsets[$charset])) {
1049                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1050            }
1051
1052            if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
1053                // cache the lowercase variant for future iterations
1054                $htmlspecialcharsCharsets[$charset] = true;
1055
1056                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1057            }
1058
1059            $string = twig_convert_encoding($string, 'UTF-8', $charset);
1060            $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1061
1062            return twig_convert_encoding($string, $charset, 'UTF-8');
1063
1064        case 'js':
1065            // escape all non-alphanumeric characters
1066            // into their \x or \uHHHH representations
1067            if ('UTF-8' !== $charset) {
1068                $string = twig_convert_encoding($string, 'UTF-8', $charset);
1069            }
1070
1071            if (!preg_match('//u', $string)) {
1072                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
1073            }
1074
1075            $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string);
1076
1077            if ('UTF-8' !== $charset) {
1078                $string = twig_convert_encoding($string, $charset, 'UTF-8');
1079            }
1080
1081            return $string;
1082
1083        case 'css':
1084            if ('UTF-8' !== $charset) {
1085                $string = twig_convert_encoding($string, 'UTF-8', $charset);
1086            }
1087
1088            if (!preg_match('//u', $string)) {
1089                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
1090            }
1091
1092            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string);
1093
1094            if ('UTF-8' !== $charset) {
1095                $string = twig_convert_encoding($string, $charset, 'UTF-8');
1096            }
1097
1098            return $string;
1099
1100        case 'html_attr':
1101            if ('UTF-8' !== $charset) {
1102                $string = twig_convert_encoding($string, 'UTF-8', $charset);
1103            }
1104
1105            if (!preg_match('//u', $string)) {
1106                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
1107            }
1108
1109            $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string);
1110
1111            if ('UTF-8' !== $charset) {
1112                $string = twig_convert_encoding($string, $charset, 'UTF-8');
1113            }
1114
1115            return $string;
1116
1117        case 'url':
1118            return rawurlencode($string);
1119
1120        default:
1121            static $escapers;
1122
1123            if (null === $escapers) {
1124                $escapers = $env->getExtension('\Twig\Extension\CoreExtension')->getEscapers();
1125            }
1126
1127            if (isset($escapers[$strategy])) {
1128                return \call_user_func($escapers[$strategy], $env, $string, $charset);
1129            }
1130
1131            $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
1132
1133            throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
1134    }
1135}
1136
1137/**
1138 * @internal
1139 */
1140function twig_escape_filter_is_safe(Node $filterArgs)
1141{
1142    foreach ($filterArgs as $arg) {
1143        if ($arg instanceof ConstantExpression) {
1144            return [$arg->getAttribute('value')];
1145        }
1146
1147        return [];
1148    }
1149
1150    return ['html'];
1151}
1152
1153if (\function_exists('mb_convert_encoding')) {
1154    function twig_convert_encoding($string, $to, $from)
1155    {
1156        return mb_convert_encoding($string, $to, $from);
1157    }
1158} elseif (\function_exists('iconv')) {
1159    function twig_convert_encoding($string, $to, $from)
1160    {
1161        return iconv($from, $to, $string);
1162    }
1163} else {
1164    function twig_convert_encoding($string, $to, $from)
1165    {
1166        throw new RuntimeError('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
1167    }
1168}
1169
1170if (\function_exists('mb_ord')) {
1171    function twig_ord($string)
1172    {
1173        return mb_ord($string, 'UTF-8');
1174    }
1175} else {
1176    function twig_ord($string)
1177    {
1178        $code = ($string = unpack('C*', substr($string, 0, 4))) ? $string[1] : 0;
1179        if (0xF0 <= $code) {
1180            return (($code - 0xF0) << 18) + (($string[2] - 0x80) << 12) + (($string[3] - 0x80) << 6) + $string[4] - 0x80;
1181        }
1182        if (0xE0 <= $code) {
1183            return (($code - 0xE0) << 12) + (($string[2] - 0x80) << 6) + $string[3] - 0x80;
1184        }
1185        if (0xC0 <= $code) {
1186            return (($code - 0xC0) << 6) + $string[2] - 0x80;
1187        }
1188
1189        return $code;
1190    }
1191}
1192
1193function _twig_escape_js_callback($matches)
1194{
1195    $char = $matches[0];
1196
1197    /*
1198     * A few characters have short escape sequences in JSON and JavaScript.
1199     * Escape sequences supported only by JavaScript, not JSON, are ommitted.
1200     * \" is also supported but omitted, because the resulting string is not HTML safe.
1201     */
1202    static $shortMap = [
1203        '\\' => '\\\\',
1204        '/' => '\\/',
1205        "\x08" => '\b',
1206        "\x0C" => '\f',
1207        "\x0A" => '\n',
1208        "\x0D" => '\r',
1209        "\x09" => '\t',
1210    ];
1211
1212    if (isset($shortMap[$char])) {
1213        return $shortMap[$char];
1214    }
1215
1216    // \uHHHH
1217    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
1218    $char = strtoupper(bin2hex($char));
1219
1220    if (4 >= \strlen($char)) {
1221        return sprintf('\u%04s', $char);
1222    }
1223
1224    return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
1225}
1226
1227function _twig_escape_css_callback($matches)
1228{
1229    $char = $matches[0];
1230
1231    return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : twig_ord($char));
1232}
1233
1234/**
1235 * This function is adapted from code coming from Zend Framework.
1236 *
1237 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
1238 * @license   https://framework.zend.com/license/new-bsd New BSD License
1239 */
1240function _twig_escape_html_attr_callback($matches)
1241{
1242    $chr = $matches[0];
1243    $ord = \ord($chr);
1244
1245    /*
1246     * The following replaces characters undefined in HTML with the
1247     * hex entity for the Unicode replacement character.
1248     */
1249    if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
1250        return '&#xFFFD;';
1251    }
1252
1253    /*
1254     * Check if the current character to escape has a name entity we should
1255     * replace it with while grabbing the hex value of the character.
1256     */
1257    if (1 == \strlen($chr)) {
1258        /*
1259         * While HTML supports far more named entities, the lowest common denominator
1260         * has become HTML5's XML Serialisation which is restricted to the those named
1261         * entities that XML supports. Using HTML entities would result in this error:
1262         *     XML Parsing Error: undefined entity
1263         */
1264        static $entityMap = [
1265            34 => '&quot;', /* quotation mark */
1266            38 => '&amp;',  /* ampersand */
1267            60 => '&lt;',   /* less-than sign */
1268            62 => '&gt;',   /* greater-than sign */
1269        ];
1270
1271        if (isset($entityMap[$ord])) {
1272            return $entityMap[$ord];
1273        }
1274
1275        return sprintf('&#x%02X;', $ord);
1276    }
1277
1278    /*
1279     * Per OWASP recommendations, we'll use hex entities for any other
1280     * characters where a named entity does not exist.
1281     */
1282    return sprintf('&#x%04X;', twig_ord($chr));
1283}
1284
1285// add multibyte extensions if possible
1286if (\function_exists('mb_get_info')) {
1287    /**
1288     * Returns the length of a variable.
1289     *
1290     * @param mixed $thing A variable
1291     *
1292     * @return int The length of the value
1293     */
1294    function twig_length_filter(Environment $env, $thing)
1295    {
1296        if (null === $thing) {
1297            return 0;
1298        }
1299
1300        if (is_scalar($thing)) {
1301            return mb_strlen($thing, $env->getCharset());
1302        }
1303
1304        if ($thing instanceof \SimpleXMLElement) {
1305            return \count($thing);
1306        }
1307
1308        if (\is_object($thing) && method_exists($thing, '__toString') && !$thing instanceof \Countable) {
1309            return mb_strlen((string) $thing, $env->getCharset());
1310        }
1311
1312        if ($thing instanceof \Countable || \is_array($thing)) {
1313            return \count($thing);
1314        }
1315
1316        if ($thing instanceof \IteratorAggregate) {
1317            return iterator_count($thing);
1318        }
1319
1320        return 1;
1321    }
1322
1323    /**
1324     * Converts a string to uppercase.
1325     *
1326     * @param string $string A string
1327     *
1328     * @return string The uppercased string
1329     */
1330    function twig_upper_filter(Environment $env, $string)
1331    {
1332        if (null !== $charset = $env->getCharset()) {
1333            return mb_strtoupper($string, $charset);
1334        }
1335
1336        return strtoupper($string);
1337    }
1338
1339    /**
1340     * Converts a string to lowercase.
1341     *
1342     * @param string $string A string
1343     *
1344     * @return string The lowercased string
1345     */
1346    function twig_lower_filter(Environment $env, $string)
1347    {
1348        if (null !== $charset = $env->getCharset()) {
1349            return mb_strtolower($string, $charset);
1350        }
1351
1352        return strtolower($string);
1353    }
1354
1355    /**
1356     * Returns a titlecased string.
1357     *
1358     * @param string $string A string
1359     *
1360     * @return string The titlecased string
1361     */
1362    function twig_title_string_filter(Environment $env, $string)
1363    {
1364        if (null !== $charset = $env->getCharset()) {
1365            return mb_convert_case($string, MB_CASE_TITLE, $charset);
1366        }
1367
1368        return ucwords(strtolower($string));
1369    }
1370
1371    /**
1372     * Returns a capitalized string.
1373     *
1374     * @param string $string A string
1375     *
1376     * @return string The capitalized string
1377     */
1378    function twig_capitalize_string_filter(Environment $env, $string)
1379    {
1380        if (null !== $charset = $env->getCharset()) {
1381            return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).mb_strtolower(mb_substr($string, 1, mb_strlen($string, $charset), $charset), $charset);
1382        }
1383
1384        return ucfirst(strtolower($string));
1385    }
1386}
1387// and byte fallback
1388else {
1389    /**
1390     * Returns the length of a variable.
1391     *
1392     * @param mixed $thing A variable
1393     *
1394     * @return int The length of the value
1395     */
1396    function twig_length_filter(Environment $env, $thing)
1397    {
1398        if (null === $thing) {
1399            return 0;
1400        }
1401
1402        if (is_scalar($thing)) {
1403            return \strlen($thing);
1404        }
1405
1406        if ($thing instanceof \SimpleXMLElement) {
1407            return \count($thing);
1408        }
1409
1410        if (\is_object($thing) && method_exists($thing, '__toString') && !$thing instanceof \Countable) {
1411            return \strlen((string) $thing);
1412        }
1413
1414        if ($thing instanceof \Countable || \is_array($thing)) {
1415            return \count($thing);
1416        }
1417
1418        if ($thing instanceof \IteratorAggregate) {
1419            return iterator_count($thing);
1420        }
1421
1422        return 1;
1423    }
1424
1425    /**
1426     * Returns a titlecased string.
1427     *
1428     * @param string $string A string
1429     *
1430     * @return string The titlecased string
1431     */
1432    function twig_title_string_filter(Environment $env, $string)
1433    {
1434        return ucwords(strtolower($string));
1435    }
1436
1437    /**
1438     * Returns a capitalized string.
1439     *
1440     * @param string $string A string
1441     *
1442     * @return string The capitalized string
1443     */
1444    function twig_capitalize_string_filter(Environment $env, $string)
1445    {
1446        return ucfirst(strtolower($string));
1447    }
1448}
1449
1450/**
1451 * @internal
1452 */
1453function twig_ensure_traversable($seq)
1454{
1455    if ($seq instanceof \Traversable || \is_array($seq)) {
1456        return $seq;
1457    }
1458
1459    return [];
1460}
1461
1462/**
1463 * @internal
1464 */
1465function twig_to_array($seq, $preserveKeys = true)
1466{
1467    if ($seq instanceof \Traversable) {
1468        return iterator_to_array($seq, $preserveKeys);
1469    }
1470
1471    if (!is_array($seq)) {
1472        return (array) $seq;
1473    }
1474
1475    if(!$preserveKeys) {
1476        return array_values($seq);
1477    }
1478
1479    return $seq;
1480}
1481
1482/**
1483 * Checks if a variable is empty.
1484 *
1485 *    {# evaluates to true if the foo variable is null, false, or the empty string #}
1486 *    {% if foo is empty %}
1487 *        {# ... #}
1488 *    {% endif %}
1489 *
1490 * @param mixed $value A variable
1491 *
1492 * @return bool true if the value is empty, false otherwise
1493 */
1494function twig_test_empty($value)
1495{
1496    if ($value instanceof \Countable) {
1497        return 0 == \count($value);
1498    }
1499
1500    if (\is_object($value) && method_exists($value, '__toString')) {
1501        return '' === (string) $value;
1502    }
1503
1504    return '' === $value || false === $value || null === $value || [] === $value;
1505}
1506
1507/**
1508 * Checks if a variable is traversable.
1509 *
1510 *    {# evaluates to true if the foo variable is an array or a traversable object #}
1511 *    {% if foo is iterable %}
1512 *        {# ... #}
1513 *    {% endif %}
1514 *
1515 * @param mixed $value A variable
1516 *
1517 * @return bool true if the value is traversable
1518 */
1519function twig_test_iterable($value)
1520{
1521    return $value instanceof \Traversable || \is_array($value);
1522}
1523
1524/**
1525 * Renders a template.
1526 *
1527 * @param array        $context
1528 * @param string|array $template      The template to render or an array of templates to try consecutively
1529 * @param array        $variables     The variables to pass to the template
1530 * @param bool         $withContext
1531 * @param bool         $ignoreMissing Whether to ignore missing templates or not
1532 * @param bool         $sandboxed     Whether to sandbox the template or not
1533 *
1534 * @return string The rendered template
1535 */
1536function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false)
1537{
1538    $alreadySandboxed = false;
1539    $sandbox = null;
1540    if ($withContext) {
1541        $variables = array_merge($context, $variables);
1542    }
1543
1544    if ($isSandboxed = $sandboxed && $env->hasExtension('\Twig\Extension\SandboxExtension')) {
1545        $sandbox = $env->getExtension('\Twig\Extension\SandboxExtension');
1546        if (!$alreadySandboxed = $sandbox->isSandboxed()) {
1547            $sandbox->enableSandbox();
1548        }
1549    }
1550
1551    $result = '';
1552    try {
1553        $result = $env->resolveTemplate($template)->render($variables);
1554    } catch (LoaderError $e) {
1555        if (!$ignoreMissing) {
1556            if ($isSandboxed && !$alreadySandboxed) {
1557                $sandbox->disableSandbox();
1558            }
1559
1560            throw $e;
1561        }
1562    } catch (\Throwable $e) {
1563        if ($isSandboxed && !$alreadySandboxed) {
1564            $sandbox->disableSandbox();
1565        }
1566
1567        throw $e;
1568    } catch (\Exception $e) {
1569        if ($isSandboxed && !$alreadySandboxed) {
1570            $sandbox->disableSandbox();
1571        }
1572
1573        throw $e;
1574    }
1575
1576    if ($isSandboxed && !$alreadySandboxed) {
1577        $sandbox->disableSandbox();
1578    }
1579
1580    return $result;
1581}
1582
1583/**
1584 * Returns a template content without rendering it.
1585 *
1586 * @param string $name          The template name
1587 * @param bool   $ignoreMissing Whether to ignore missing templates or not
1588 *
1589 * @return string The template source
1590 */
1591function twig_source(Environment $env, $name, $ignoreMissing = false)
1592{
1593    $loader = $env->getLoader();
1594    try {
1595        if (!$loader instanceof SourceContextLoaderInterface) {
1596            return $loader->getSource($name);
1597        } else {
1598            return $loader->getSourceContext($name)->getCode();
1599        }
1600    } catch (LoaderError $e) {
1601        if (!$ignoreMissing) {
1602            throw $e;
1603        }
1604    }
1605}
1606
1607/**
1608 * Provides the ability to get constants from instances as well as class/global constants.
1609 *
1610 * @param string      $constant The name of the constant
1611 * @param object|null $object   The object to get the constant from
1612 *
1613 * @return string
1614 */
1615function twig_constant($constant, $object = null)
1616{
1617    if (null !== $object) {
1618        $constant = \get_class($object).'::'.$constant;
1619    }
1620
1621    return \constant($constant);
1622}
1623
1624/**
1625 * Checks if a constant exists.
1626 *
1627 * @param string      $constant The name of the constant
1628 * @param object|null $object   The object to get the constant from
1629 *
1630 * @return bool
1631 */
1632function twig_constant_is_defined($constant, $object = null)
1633{
1634    if (null !== $object) {
1635        $constant = \get_class($object).'::'.$constant;
1636    }
1637
1638    return \defined($constant);
1639}
1640
1641/**
1642 * Batches item.
1643 *
1644 * @param array $items An array of items
1645 * @param int   $size  The size of the batch
1646 * @param mixed $fill  A value used to fill missing items
1647 *
1648 * @return array
1649 */
1650function twig_array_batch($items, $size, $fill = null, $preserveKeys = true)
1651{
1652    $size = ceil($size);
1653
1654    $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys);
1655
1656    if (null !== $fill && $result) {
1657        $last = \count($result) - 1;
1658        if ($fillCount = $size - \count($result[$last])) {
1659            for ($i = 0; $i < $fillCount; $i++) {
1660                $result[$last][] = $fill;
1661            }
1662        }
1663    }
1664
1665    return $result;
1666}
1667}
1668