3 if (!defined('ENT_SUBSTITUTE')) {
4 define('ENT_SUBSTITUTE', 8);
8 * This file is part of Twig.
10 * (c) Fabien Potencier
12 * For the full copyright and license information, please view the LICENSE
13 * file that was distributed with this source code.
19 class Twig_Extension_Core extends Twig_Extension
21 protected $dateFormats = array('F j, Y H:i', '%d days');
22 protected $numberFormat = array(0, '.', ',');
23 protected $timezone = null;
24 protected $escapers = array();
27 * Defines a new escaper to be used via the escape filter.
29 * @param string $strategy The strategy name that should be used as a strategy in the escape call
30 * @param callable $callable A valid PHP callable
32 public function setEscaper($strategy, $callable)
34 $this->escapers[$strategy] = $callable;
38 * Gets all defined escapers.
40 * @return array An array of escapers
42 public function getEscapers()
44 return $this->escapers;
48 * Sets the default format to be used by the date filter.
50 * @param string $format The default date format string
51 * @param string $dateIntervalFormat The default date interval format string
53 public function setDateFormat($format = null, $dateIntervalFormat = null)
55 if (null !== $format) {
56 $this->dateFormats[0] = $format;
59 if (null !== $dateIntervalFormat) {
60 $this->dateFormats[1] = $dateIntervalFormat;
65 * Gets the default format to be used by the date filter.
67 * @return array The default date format string and the default date interval format string
69 public function getDateFormat()
71 return $this->dateFormats;
75 * Sets the default timezone to be used by the date filter.
77 * @param DateTimeZone|string $timezone The default timezone string or a DateTimeZone object
79 public function setTimezone($timezone)
81 $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
85 * Gets the default timezone to be used by the date filter.
87 * @return DateTimeZone The default timezone currently in use
89 public function getTimezone()
91 if (null === $this->timezone) {
92 $this->timezone = new DateTimeZone(date_default_timezone_get());
95 return $this->timezone;
99 * Sets the default format to be used by the number_format filter.
101 * @param int $decimal the number of decimal places to use
102 * @param string $decimalPoint the character(s) to use for the decimal point
103 * @param string $thousandSep the character(s) to use for the thousands separator
105 public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
107 $this->numberFormat = array($decimal, $decimalPoint, $thousandSep);
111 * Get the default format used by the number_format filter.
113 * @return array The arguments for number_format()
115 public function getNumberFormat()
117 return $this->numberFormat;
120 public function getTokenParsers()
123 new Twig_TokenParser_For(),
124 new Twig_TokenParser_If(),
125 new Twig_TokenParser_Extends(),
126 new Twig_TokenParser_Include(),
127 new Twig_TokenParser_Block(),
128 new Twig_TokenParser_Use(),
129 new Twig_TokenParser_Filter(),
130 new Twig_TokenParser_Macro(),
131 new Twig_TokenParser_Import(),
132 new Twig_TokenParser_From(),
133 new Twig_TokenParser_Set(),
134 new Twig_TokenParser_Spaceless(),
135 new Twig_TokenParser_Flush(),
136 new Twig_TokenParser_Do(),
137 new Twig_TokenParser_Embed(),
138 new Twig_TokenParser_With(),
142 public function getFilters()
145 // formatting filters
146 new Twig_SimpleFilter('date', 'twig_date_format_filter', array('needs_environment' => true)),
147 new Twig_SimpleFilter('date_modify', 'twig_date_modify_filter', array('needs_environment' => true)),
148 new Twig_SimpleFilter('format', 'sprintf'),
149 new Twig_SimpleFilter('replace', 'twig_replace_filter'),
150 new Twig_SimpleFilter('number_format', 'twig_number_format_filter', array('needs_environment' => true)),
151 new Twig_SimpleFilter('abs', 'abs'),
152 new Twig_SimpleFilter('round', 'twig_round'),
155 new Twig_SimpleFilter('url_encode', 'twig_urlencode_filter'),
156 new Twig_SimpleFilter('json_encode', 'twig_jsonencode_filter'),
157 new Twig_SimpleFilter('convert_encoding', 'twig_convert_encoding'),
160 new Twig_SimpleFilter('title', 'twig_title_string_filter', array('needs_environment' => true)),
161 new Twig_SimpleFilter('capitalize', 'twig_capitalize_string_filter', array('needs_environment' => true)),
162 new Twig_SimpleFilter('upper', 'strtoupper'),
163 new Twig_SimpleFilter('lower', 'strtolower'),
164 new Twig_SimpleFilter('striptags', 'strip_tags'),
165 new Twig_SimpleFilter('trim', 'twig_trim_filter'),
166 new Twig_SimpleFilter('nl2br', 'nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
169 new Twig_SimpleFilter('join', 'twig_join_filter'),
170 new Twig_SimpleFilter('split', 'twig_split_filter', array('needs_environment' => true)),
171 new Twig_SimpleFilter('sort', 'twig_sort_filter'),
172 new Twig_SimpleFilter('merge', 'twig_array_merge'),
173 new Twig_SimpleFilter('batch', 'twig_array_batch'),
175 // string/array filters
176 new Twig_SimpleFilter('reverse', 'twig_reverse_filter', array('needs_environment' => true)),
177 new Twig_SimpleFilter('length', 'twig_length_filter', array('needs_environment' => true)),
178 new Twig_SimpleFilter('slice', 'twig_slice', array('needs_environment' => true)),
179 new Twig_SimpleFilter('first', 'twig_first', array('needs_environment' => true)),
180 new Twig_SimpleFilter('last', 'twig_last', array('needs_environment' => true)),
182 // iteration and runtime
183 new Twig_SimpleFilter('default', '_twig_default_filter', array('node_class' => 'Twig_Node_Expression_Filter_Default')),
184 new Twig_SimpleFilter('keys', 'twig_get_array_keys_filter'),
187 new Twig_SimpleFilter('escape', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
188 new Twig_SimpleFilter('e', 'twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
191 if (function_exists('mb_get_info')) {
192 $filters[] = new Twig_SimpleFilter('upper', 'twig_upper_filter', array('needs_environment' => true));
193 $filters[] = new Twig_SimpleFilter('lower', 'twig_lower_filter', array('needs_environment' => true));
199 public function getFunctions()
202 new Twig_SimpleFunction('max', 'max'),
203 new Twig_SimpleFunction('min', 'min'),
204 new Twig_SimpleFunction('range', 'range'),
205 new Twig_SimpleFunction('constant', 'twig_constant'),
206 new Twig_SimpleFunction('cycle', 'twig_cycle'),
207 new Twig_SimpleFunction('random', 'twig_random', array('needs_environment' => true)),
208 new Twig_SimpleFunction('date', 'twig_date_converter', array('needs_environment' => true)),
209 new Twig_SimpleFunction('include', 'twig_include', array('needs_environment' => true, 'needs_context' => true, 'is_safe' => array('all'))),
210 new Twig_SimpleFunction('source', 'twig_source', array('needs_environment' => true, 'is_safe' => array('all'))),
214 public function getTests()
217 new Twig_SimpleTest('even', null, array('node_class' => 'Twig_Node_Expression_Test_Even')),
218 new Twig_SimpleTest('odd', null, array('node_class' => 'Twig_Node_Expression_Test_Odd')),
219 new Twig_SimpleTest('defined', null, array('node_class' => 'Twig_Node_Expression_Test_Defined')),
220 new Twig_SimpleTest('sameas', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas', 'deprecated' => '1.21', 'alternative' => 'same as')),
221 new Twig_SimpleTest('same as', null, array('node_class' => 'Twig_Node_Expression_Test_Sameas')),
222 new Twig_SimpleTest('none', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
223 new Twig_SimpleTest('null', null, array('node_class' => 'Twig_Node_Expression_Test_Null')),
224 new Twig_SimpleTest('divisibleby', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby', 'deprecated' => '1.21', 'alternative' => 'divisible by')),
225 new Twig_SimpleTest('divisible by', null, array('node_class' => 'Twig_Node_Expression_Test_Divisibleby')),
226 new Twig_SimpleTest('constant', null, array('node_class' => 'Twig_Node_Expression_Test_Constant')),
227 new Twig_SimpleTest('empty', 'twig_test_empty'),
228 new Twig_SimpleTest('iterable', 'twig_test_iterable'),
232 public function getOperators()
236 'not' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
237 '-' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Neg'),
238 '+' => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'),
241 'or' => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
242 'and' => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
243 'b-or' => array('precedence' => 16, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
244 'b-xor' => array('precedence' => 17, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
245 'b-and' => array('precedence' => 18, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
246 '==' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
247 '!=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
248 '<' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
249 '>' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Greater', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
250 '>=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_GreaterEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
251 '<=' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
252 'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
253 'in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
254 'matches' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Matches', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
255 'starts with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_StartsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
256 'ends with' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_EndsWith', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
257 '..' => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
258 '+' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
259 '-' => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
260 '~' => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
261 '*' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mul', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
262 '/' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Div', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
263 '//' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_FloorDiv', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
264 '%' => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
265 'is' => array('precedence' => 100, 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
266 'is not' => array('precedence' => 100, 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
267 '**' => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT),
268 '??' => array('precedence' => 300, 'class' => 'Twig_Node_Expression_NullCoalesce', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT),
273 public function getName()
280 * Cycles over a value.
282 * @param ArrayAccess|array $values
283 * @param int $position The cycle position
285 * @return string The next value in the cycle
287 function twig_cycle($values, $position)
289 if (!is_array($values) && !$values instanceof ArrayAccess) {
293 return $values[$position % count($values)];
297 * Returns a random value depending on the supplied parameter type:
298 * - a random item from a Traversable or array
299 * - a random character from a string
300 * - a random integer between 0 and the integer parameter.
302 * @param Twig_Environment $env
303 * @param Traversable|array|int|float|string $values The values to pick a random item from
305 * @throws Twig_Error_Runtime when $values is an empty array (does not apply to an empty string which is returned as is)
307 * @return mixed A random value from the given sequence
309 function twig_random(Twig_Environment $env, $values = null)
311 if (null === $values) {
315 if (is_int($values) || is_float($values)) {
316 return $values < 0 ? mt_rand($values, 0) : mt_rand(0, $values);
319 if ($values instanceof Traversable) {
320 $values = iterator_to_array($values);
321 } elseif (is_string($values)) {
322 if ('' === $values) {
325 if (null !== $charset = $env->getCharset()) {
326 if ('UTF-8' !== $charset) {
327 $values = twig_convert_encoding($values, 'UTF-8', $charset);
330 // unicode version of str_split()
331 // split at all positions, but not after the start and not before the end
332 $values = preg_split('/(?<!^)(?!$)/u', $values);
334 if ('UTF-8' !== $charset) {
335 foreach ($values as $i => $value) {
336 $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
340 return $values[mt_rand(0, strlen($values) - 1)];
344 if (!is_array($values)) {
348 if (0 === count($values)) {
349 throw new Twig_Error_Runtime('The random function cannot pick from an empty array.');
352 return $values[array_rand($values, 1)];
356 * Converts a date to the given format.
359 * {{ post.published_at|date("m/d/Y") }}
362 * @param Twig_Environment $env
363 * @param DateTime|DateTimeInterface|DateInterval|string $date A date
364 * @param string|null $format The target format, null to use the default
365 * @param DateTimeZone|string|null|false $timezone The target timezone, null to use the default, false to leave unchanged
367 * @return string The formatted date
369 function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null)
371 if (null === $format) {
372 $formats = $env->getExtension('Twig_Extension_Core')->getDateFormat();
373 $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
376 if ($date instanceof DateInterval) {
377 return $date->format($format);
380 return twig_date_converter($env, $date, $timezone)->format($format);
384 * Returns a new date object modified.
387 * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }}
390 * @param Twig_Environment $env
391 * @param DateTime|string $date A date
392 * @param string $modifier A modifier string
394 * @return DateTime A new date object
396 function twig_date_modify_filter(Twig_Environment $env, $date, $modifier)
398 $date = twig_date_converter($env, $date, false);
399 $resultDate = $date->modify($modifier);
401 // This is a hack to ensure PHP 5.2 support and support for DateTimeImmutable
402 // DateTime::modify does not return the modified DateTime object < 5.3.0
403 // and DateTimeImmutable does not modify $date.
404 return null === $resultDate ? $date : $resultDate;
408 * Converts an input to a DateTime instance.
411 * {% if date(user.created_at) < date('+2days') %}
416 * @param Twig_Environment $env
417 * @param DateTime|DateTimeInterface|string|null $date A date
418 * @param DateTimeZone|string|null|false $timezone The target timezone, null to use the default, false to leave unchanged
420 * @return DateTime A DateTime instance
422 function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null)
424 // determine the timezone
425 if (false !== $timezone) {
426 if (null === $timezone) {
427 $timezone = $env->getExtension('Twig_Extension_Core')->getTimezone();
428 } elseif (!$timezone instanceof DateTimeZone) {
429 $timezone = new DateTimeZone($timezone);
434 if ($date instanceof DateTimeImmutable) {
435 return false !== $timezone ? $date->setTimezone($timezone) : $date;
438 if ($date instanceof DateTime || $date instanceof DateTimeInterface) {
440 if (false !== $timezone) {
441 $date->setTimezone($timezone);
447 if (null === $date || 'now' === $date) {
448 return new DateTime($date, false !== $timezone ? $timezone : $env->getExtension('Twig_Extension_Core')->getTimezone());
451 $asString = (string) $date;
452 if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
453 $date = new DateTime('@'.$date);
455 $date = new DateTime($date, $env->getExtension('Twig_Extension_Core')->getTimezone());
458 if (false !== $timezone) {
459 $date->setTimezone($timezone);
466 * Replaces strings within a string.
468 * @param string $str String to replace in
469 * @param array|Traversable $from Replace values
470 * @param string|null $to Replace to, deprecated (@see https://secure.php.net/manual/en/function.strtr.php)
474 function twig_replace_filter($str, $from, $to = null)
476 if ($from instanceof Traversable) {
477 $from = iterator_to_array($from);
478 } elseif (is_string($from) && is_string($to)) {
479 @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);
481 return strtr($str, $from, $to);
482 } elseif (!is_array($from)) {
483 throw new Twig_Error_Runtime(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', is_object($from) ? get_class($from) : gettype($from)));
486 return strtr($str, $from);
492 * @param int|float $value The value to round
493 * @param int|float $precision The rounding precision
494 * @param string $method The method to use for rounding
496 * @return int|float The rounded number
498 function twig_round($value, $precision = 0, $method = 'common')
500 if ('common' == $method) {
501 return round($value, $precision);
504 if ('ceil' != $method && 'floor' != $method) {
505 throw new Twig_Error_Runtime('The round filter only supports the "common", "ceil", and "floor" methods.');
508 return $method($value * pow(10, $precision)) / pow(10, $precision);
512 * Number format filter.
514 * All of the formatting options can be left null, in that case the defaults will
515 * be used. Supplying any of the parameters will override the defaults set in the
516 * environment object.
518 * @param Twig_Environment $env
519 * @param mixed $number A float/int/string of the number to format
520 * @param int $decimal the number of decimal points to display
521 * @param string $decimalPoint the character(s) to use for the decimal point
522 * @param string $thousandSep the character(s) to use for the thousands separator
524 * @return string The formatted number
526 function twig_number_format_filter(Twig_Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
528 $defaults = $env->getExtension('Twig_Extension_Core')->getNumberFormat();
529 if (null === $decimal) {
530 $decimal = $defaults[0];
533 if (null === $decimalPoint) {
534 $decimalPoint = $defaults[1];
537 if (null === $thousandSep) {
538 $thousandSep = $defaults[2];
541 return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
545 * URL encodes (RFC 3986) a string as a path segment or an array as a query string.
547 * @param string|array $url A URL or an array of query parameters
549 * @return string The URL encoded value
551 function twig_urlencode_filter($url)
553 if (is_array($url)) {
554 if (defined('PHP_QUERY_RFC3986')) {
555 return http_build_query($url, '', '&', PHP_QUERY_RFC3986);
558 return http_build_query($url, '', '&');
561 return rawurlencode($url);
564 if (PHP_VERSION_ID < 50300) {
566 * JSON encodes a variable.
568 * @param mixed $value the value to encode
569 * @param int $options Not used on PHP 5.2.x
571 * @return mixed The JSON encoded value
573 function twig_jsonencode_filter($value, $options = 0)
575 if ($value instanceof Twig_Markup) {
576 $value = (string) $value;
577 } elseif (is_array($value)) {
578 array_walk_recursive($value, '_twig_markup2string');
581 return json_encode($value);
585 * JSON encodes a variable.
587 * @param mixed $value the value to encode
588 * @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
590 * @return mixed The JSON encoded value
592 function twig_jsonencode_filter($value, $options = 0)
594 if ($value instanceof Twig_Markup) {
595 $value = (string) $value;
596 } elseif (is_array($value)) {
597 array_walk_recursive($value, '_twig_markup2string');
600 return json_encode($value, $options);
604 function _twig_markup2string(&$value)
606 if ($value instanceof Twig_Markup) {
607 $value = (string) $value;
612 * Merges an array with another one.
615 * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
617 * {% set items = items|merge({ 'peugeot': 'car' }) %}
619 * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
622 * @param array|Traversable $arr1 An array
623 * @param array|Traversable $arr2 An array
625 * @return array The merged array
627 function twig_array_merge($arr1, $arr2)
629 if ($arr1 instanceof Traversable) {
630 $arr1 = iterator_to_array($arr1);
631 } elseif (!is_array($arr1)) {
632 throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', gettype($arr1)));
635 if ($arr2 instanceof Traversable) {
636 $arr2 = iterator_to_array($arr2);
637 } elseif (!is_array($arr2)) {
638 throw new Twig_Error_Runtime(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', gettype($arr2)));
641 return array_merge($arr1, $arr2);
647 * @param Twig_Environment $env
648 * @param mixed $item A variable
649 * @param int $start Start of the slice
650 * @param int $length Size of the slice
651 * @param bool $preserveKeys Whether to preserve key or not (when the input is an array)
653 * @return mixed The sliced variable
655 function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false)
657 if ($item instanceof Traversable) {
658 while ($item instanceof IteratorAggregate) {
659 $item = $item->getIterator();
662 if ($start >= 0 && $length >= 0 && $item instanceof Iterator) {
664 return iterator_to_array(new LimitIterator($item, $start, null === $length ? -1 : $length), $preserveKeys);
665 } catch (OutOfBoundsException $exception) {
670 $item = iterator_to_array($item, $preserveKeys);
673 if (is_array($item)) {
674 return array_slice($item, $start, $length, $preserveKeys);
677 $item = (string) $item;
679 if (function_exists('mb_get_info') && null !== $charset = $env->getCharset()) {
680 return (string) mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset);
683 return (string) (null === $length ? substr($item, $start) : substr($item, $start, $length));
687 * Returns the first element of the item.
689 * @param Twig_Environment $env
690 * @param mixed $item A variable
692 * @return mixed The first element of the item
694 function twig_first(Twig_Environment $env, $item)
696 $elements = twig_slice($env, $item, 0, 1, false);
698 return is_string($elements) ? $elements : current($elements);
702 * Returns the last element of the item.
704 * @param Twig_Environment $env
705 * @param mixed $item A variable
707 * @return mixed The last element of the item
709 function twig_last(Twig_Environment $env, $item)
711 $elements = twig_slice($env, $item, -1, 1, false);
713 return is_string($elements) ? $elements : current($elements);
717 * Joins the values to a string.
719 * The separator between elements is an empty string per default, you can define it with the optional parameter.
722 * {{ [1, 2, 3]|join('|') }}
723 * {# returns 1|2|3 #}
725 * {{ [1, 2, 3]|join }}
729 * @param array $value An array
730 * @param string $glue The separator
732 * @return string The concatenated string
734 function twig_join_filter($value, $glue = '')
736 if ($value instanceof Traversable) {
737 $value = iterator_to_array($value, false);
740 return implode($glue, (array) $value);
744 * Splits the string into an array.
747 * {{ "one,two,three"|split(',') }}
748 * {# returns [one, two, three] #}
750 * {{ "one,two,three,four,five"|split(',', 3) }}
751 * {# returns [one, two, "three,four,five"] #}
753 * {{ "123"|split('') }}
754 * {# returns [1, 2, 3] #}
756 * {{ "aabbcc"|split('', 2) }}
757 * {# returns [aa, bb, cc] #}
760 * @param Twig_Environment $env
761 * @param string $value A string
762 * @param string $delimiter The delimiter
763 * @param int $limit The limit
765 * @return array The split string as an array
767 function twig_split_filter(Twig_Environment $env, $value, $delimiter, $limit = null)
769 if (!empty($delimiter)) {
770 return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit);
773 if (!function_exists('mb_get_info') || null === $charset = $env->getCharset()) {
774 return str_split($value, null === $limit ? 1 : $limit);
778 return preg_split('/(?<!^)(?!$)/u', $value);
781 $length = mb_strlen($value, $charset);
782 if ($length < $limit) {
783 return array($value);
787 for ($i = 0; $i < $length; $i += $limit) {
788 $r[] = mb_substr($value, $i, $limit, $charset);
794 // The '_default' filter is used internally to avoid using the ternary operator
795 // which costs a lot for big contexts (before PHP 5.4). So, on average,
796 // a function call is cheaper.
800 function _twig_default_filter($value, $default = '')
802 if (twig_test_empty($value)) {
810 * Returns the keys for the given array.
812 * It is useful when you want to iterate over the keys of an array:
815 * {% for key in array|keys %}
820 * @param array $array An array
822 * @return array The keys
824 function twig_get_array_keys_filter($array)
826 if ($array instanceof Traversable) {
827 while ($array instanceof IteratorAggregate) {
828 $array = $array->getIterator();
831 if ($array instanceof Iterator) {
834 while ($array->valid()) {
835 $keys[] = $array->key();
843 foreach ($array as $key => $item) {
850 if (!is_array($array)) {
854 return array_keys($array);
858 * Reverses a variable.
860 * @param Twig_Environment $env
861 * @param array|Traversable|string $item An array, a Traversable instance, or a string
862 * @param bool $preserveKeys Whether to preserve key or not
864 * @return mixed The reversed input
866 function twig_reverse_filter(Twig_Environment $env, $item, $preserveKeys = false)
868 if ($item instanceof Traversable) {
869 return array_reverse(iterator_to_array($item), $preserveKeys);
872 if (is_array($item)) {
873 return array_reverse($item, $preserveKeys);
876 if (null !== $charset = $env->getCharset()) {
877 $string = (string) $item;
879 if ('UTF-8' !== $charset) {
880 $item = twig_convert_encoding($string, 'UTF-8', $charset);
883 preg_match_all('/./us', $item, $matches);
885 $string = implode('', array_reverse($matches[0]));
887 if ('UTF-8' !== $charset) {
888 $string = twig_convert_encoding($string, $charset, 'UTF-8');
894 return strrev((string) $item);
900 * @param array|Traversable $array
904 function twig_sort_filter($array)
906 if ($array instanceof Traversable) {
907 $array = iterator_to_array($array);
908 } elseif (!is_array($array)) {
909 throw new Twig_Error_Runtime(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', gettype($array)));
920 function twig_in_filter($value, $compare)
922 if (is_array($compare)) {
923 return in_array($value, $compare, is_object($value) || is_resource($value));
924 } elseif (is_string($compare) && (is_string($value) || is_int($value) || is_float($value))) {
925 return '' === $value || false !== strpos($compare, (string) $value);
926 } elseif ($compare instanceof Traversable) {
927 if (is_object($value) || is_resource($value)) {
928 foreach ($compare as $item) {
929 if ($item === $value) {
934 foreach ($compare as $item) {
935 if ($item == $value) {
948 * Returns a trimmed string.
952 * @throws Twig_Error_Runtime When an invalid trimming side is used (not a string or not 'left', 'right', or 'both')
954 function twig_trim_filter($string, $characterMask = null, $side = 'both')
956 if (null === $characterMask) {
957 $characterMask = " \t\n\r\0\x0B";
962 return trim($string, $characterMask);
964 return ltrim($string, $characterMask);
966 return rtrim($string, $characterMask);
968 throw new Twig_Error_Runtime('Trimming side must be "left", "right" or "both".');
975 * @param Twig_Environment $env
976 * @param mixed $string The value to be escaped
977 * @param string $strategy The escaping strategy
978 * @param string $charset The charset
979 * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
983 function twig_escape_filter(Twig_Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
985 if ($autoescape && $string instanceof Twig_Markup) {
989 if (!is_string($string)) {
990 if (is_object($string) && method_exists($string, '__toString')) {
991 $string = (string) $string;
992 } elseif (in_array($strategy, array('html', 'js', 'css', 'html_attr', 'url'))) {
997 if (null === $charset) {
998 $charset = $env->getCharset();
1001 switch ($strategy) {
1003 // see https://secure.php.net/htmlspecialchars
1005 // Using a static variable to avoid initializing the array
1006 // each time the function is called. Moving the declaration on the
1007 // top of the function slow downs other escaping strategies.
1008 static $htmlspecialcharsCharsets = array(
1009 'ISO-8859-1' => true, 'ISO8859-1' => true,
1010 'ISO-8859-15' => true, 'ISO8859-15' => true,
1011 'utf-8' => true, 'UTF-8' => true,
1012 'CP866' => true, 'IBM866' => true, '866' => true,
1013 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
1015 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
1016 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
1017 'BIG5' => true, '950' => true,
1018 'GB2312' => true, '936' => true,
1019 'BIG5-HKSCS' => true,
1020 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
1021 'EUC-JP' => true, 'EUCJP' => true,
1022 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
1025 if (isset($htmlspecialcharsCharsets[$charset])) {
1026 return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1029 if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
1030 // cache the lowercase variant for future iterations
1031 $htmlspecialcharsCharsets[$charset] = true;
1033 return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1036 $string = twig_convert_encoding($string, 'UTF-8', $charset);
1037 $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1039 return twig_convert_encoding($string, $charset, 'UTF-8');
1042 // escape all non-alphanumeric characters
1043 // into their \x or \uHHHH representations
1044 if ('UTF-8' !== $charset) {
1045 $string = twig_convert_encoding($string, 'UTF-8', $charset);
1048 if (0 == strlen($string) ? false : 1 !== preg_match('/^./su', $string)) {
1049 throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
1052 $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', '_twig_escape_js_callback', $string);
1054 if ('UTF-8' !== $charset) {
1055 $string = twig_convert_encoding($string, $charset, 'UTF-8');
1061 if ('UTF-8' !== $charset) {
1062 $string = twig_convert_encoding($string, 'UTF-8', $charset);
1065 if (0 == strlen($string) ? false : 1 !== preg_match('/^./su', $string)) {
1066 throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
1069 $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', '_twig_escape_css_callback', $string);
1071 if ('UTF-8' !== $charset) {
1072 $string = twig_convert_encoding($string, $charset, 'UTF-8');
1078 if ('UTF-8' !== $charset) {
1079 $string = twig_convert_encoding($string, 'UTF-8', $charset);
1082 if (0 == strlen($string) ? false : 1 !== preg_match('/^./su', $string)) {
1083 throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
1086 $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', '_twig_escape_html_attr_callback', $string);
1088 if ('UTF-8' !== $charset) {
1089 $string = twig_convert_encoding($string, $charset, 'UTF-8');
1095 if (PHP_VERSION_ID < 50300) {
1096 return str_replace('%7E', '~', rawurlencode($string));
1099 return rawurlencode($string);
1104 if (null === $escapers) {
1105 $escapers = $env->getExtension('Twig_Extension_Core')->getEscapers();
1108 if (isset($escapers[$strategy])) {
1109 return call_user_func($escapers[$strategy], $env, $string, $charset);
1112 $validStrategies = implode(', ', array_merge(array('html', 'js', 'url', 'css', 'html_attr'), array_keys($escapers)));
1114 throw new Twig_Error_Runtime(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
1121 function twig_escape_filter_is_safe(Twig_Node $filterArgs)
1123 foreach ($filterArgs as $arg) {
1124 if ($arg instanceof Twig_Node_Expression_Constant) {
1125 return array($arg->getAttribute('value'));
1131 return array('html');
1134 if (function_exists('mb_convert_encoding')) {
1135 function twig_convert_encoding($string, $to, $from)
1137 return mb_convert_encoding($string, $to, $from);
1139 } elseif (function_exists('iconv')) {
1140 function twig_convert_encoding($string, $to, $from)
1142 return iconv($from, $to, $string);
1145 function twig_convert_encoding($string, $to, $from)
1147 throw new Twig_Error_Runtime('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
1151 function _twig_escape_js_callback($matches)
1153 $char = $matches[0];
1156 * A few characters have short escape sequences in JSON and JavaScript.
1157 * Escape sequences supported only by JavaScript, not JSON, are ommitted.
1158 * \" is also supported but omitted, because the resulting string is not HTML safe.
1160 static $shortMap = array(
1170 if (isset($shortMap[$char])) {
1171 return $shortMap[$char];
1175 $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
1176 $char = strtoupper(bin2hex($char));
1178 if (4 >= strlen($char)) {
1179 return sprintf('\u%04s', $char);
1182 return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
1185 function _twig_escape_css_callback($matches)
1187 $char = $matches[0];
1190 if (!isset($char[1])) {
1191 $hex = ltrim(strtoupper(bin2hex($char)), '0');
1192 if (0 === strlen($hex)) {
1196 return '\\'.$hex.' ';
1200 $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
1202 return '\\'.ltrim(strtoupper(bin2hex($char)), '0').' ';
1206 * This function is adapted from code coming from Zend Framework.
1208 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
1209 * @license https://framework.zend.com/license/new-bsd New BSD License
1211 function _twig_escape_html_attr_callback($matches)
1214 * While HTML supports far more named entities, the lowest common denominator
1215 * has become HTML5's XML Serialisation which is restricted to the those named
1216 * entities that XML supports. Using HTML entities would result in this error:
1217 * XML Parsing Error: undefined entity
1219 static $entityMap = array(
1220 34 => 'quot', /* quotation mark */
1221 38 => 'amp', /* ampersand */
1222 60 => 'lt', /* less-than sign */
1223 62 => 'gt', /* greater-than sign */
1230 * The following replaces characters undefined in HTML with the
1231 * hex entity for the Unicode replacement character.
1233 if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
1238 * Check if the current character to escape has a name entity we should
1239 * replace it with while grabbing the hex value of the character.
1241 if (1 == strlen($chr)) {
1242 $hex = strtoupper(substr('00'.bin2hex($chr), -2));
1244 $chr = twig_convert_encoding($chr, 'UTF-16BE', 'UTF-8');
1245 $hex = strtoupper(substr('0000'.bin2hex($chr), -4));
1248 $int = hexdec($hex);
1249 if (array_key_exists($int, $entityMap)) {
1250 return sprintf('&%s;', $entityMap[$int]);
1254 * Per OWASP recommendations, we'll use hex entities for any other
1255 * characters where a named entity does not exist.
1257 return sprintf('&#x%s;', $hex);
1260 // add multibyte extensions if possible
1261 if (function_exists('mb_get_info')) {
1263 * Returns the length of a variable.
1265 * @param Twig_Environment $env
1266 * @param mixed $thing A variable
1268 * @return int The length of the value
1270 function twig_length_filter(Twig_Environment $env, $thing)
1272 if (null === $thing) {
1276 if (is_scalar($thing)) {
1277 return mb_strlen($thing, $env->getCharset());
1280 if ($thing instanceof \SimpleXMLElement) {
1281 return count($thing);
1284 if (is_object($thing) && method_exists($thing, '__toString') && !$thing instanceof \Countable) {
1285 return mb_strlen((string) $thing, $env->getCharset());
1288 if ($thing instanceof \Countable || is_array($thing)) {
1289 return count($thing);
1292 if ($thing instanceof \IteratorAggregate) {
1293 return iterator_count($thing);
1300 * Converts a string to uppercase.
1302 * @param Twig_Environment $env
1303 * @param string $string A string
1305 * @return string The uppercased string
1307 function twig_upper_filter(Twig_Environment $env, $string)
1309 if (null !== $charset = $env->getCharset()) {
1310 return mb_strtoupper($string, $charset);
1313 return strtoupper($string);
1317 * Converts a string to lowercase.
1319 * @param Twig_Environment $env
1320 * @param string $string A string
1322 * @return string The lowercased string
1324 function twig_lower_filter(Twig_Environment $env, $string)
1326 if (null !== $charset = $env->getCharset()) {
1327 return mb_strtolower($string, $charset);
1330 return strtolower($string);
1334 * Returns a titlecased string.
1336 * @param Twig_Environment $env
1337 * @param string $string A string
1339 * @return string The titlecased string
1341 function twig_title_string_filter(Twig_Environment $env, $string)
1343 if (null !== $charset = $env->getCharset()) {
1344 return mb_convert_case($string, MB_CASE_TITLE, $charset);
1347 return ucwords(strtolower($string));
1351 * Returns a capitalized string.
1353 * @param Twig_Environment $env
1354 * @param string $string A string
1356 * @return string The capitalized string
1358 function twig_capitalize_string_filter(Twig_Environment $env, $string)
1360 if (null !== $charset = $env->getCharset()) {
1361 return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).mb_strtolower(mb_substr($string, 1, mb_strlen($string, $charset), $charset), $charset);
1364 return ucfirst(strtolower($string));
1367 // and byte fallback
1370 * Returns the length of a variable.
1372 * @param Twig_Environment $env
1373 * @param mixed $thing A variable
1375 * @return int The length of the value
1377 function twig_length_filter(Twig_Environment $env, $thing)
1379 if (null === $thing) {
1383 if (is_scalar($thing)) {
1384 return strlen($thing);
1387 if ($thing instanceof \SimpleXMLElement) {
1388 return count($thing);
1391 if (is_object($thing) && method_exists($thing, '__toString') && !$thing instanceof \Countable) {
1392 return strlen((string) $thing);
1395 if ($thing instanceof \Countable || is_array($thing)) {
1396 return count($thing);
1399 if ($thing instanceof \IteratorAggregate) {
1400 return iterator_count($thing);
1407 * Returns a titlecased string.
1409 * @param Twig_Environment $env
1410 * @param string $string A string
1412 * @return string The titlecased string
1414 function twig_title_string_filter(Twig_Environment $env, $string)
1416 return ucwords(strtolower($string));
1420 * Returns a capitalized string.
1422 * @param Twig_Environment $env
1423 * @param string $string A string
1425 * @return string The capitalized string
1427 function twig_capitalize_string_filter(Twig_Environment $env, $string)
1429 return ucfirst(strtolower($string));
1436 function twig_ensure_traversable($seq)
1438 if ($seq instanceof Traversable || is_array($seq)) {
1446 * Checks if a variable is empty.
1449 * {# evaluates to true if the foo variable is null, false, or the empty string #}
1450 * {% if foo is empty %}
1455 * @param mixed $value A variable
1457 * @return bool true if the value is empty, false otherwise
1459 function twig_test_empty($value)
1461 if ($value instanceof Countable) {
1462 return 0 == count($value);
1465 if (is_object($value) && method_exists($value, '__toString')) {
1466 return '' === (string) $value;
1469 return '' === $value || false === $value || null === $value || array() === $value;
1473 * Checks if a variable is traversable.
1476 * {# evaluates to true if the foo variable is an array or a traversable object #}
1477 * {% if foo is iterable %}
1482 * @param mixed $value A variable
1484 * @return bool true if the value is traversable
1486 function twig_test_iterable($value)
1488 return $value instanceof Traversable || is_array($value);
1492 * Renders a template.
1494 * @param Twig_Environment $env
1495 * @param array $context
1496 * @param string|array $template The template to render or an array of templates to try consecutively
1497 * @param array $variables The variables to pass to the template
1498 * @param bool $withContext
1499 * @param bool $ignoreMissing Whether to ignore missing templates or not
1500 * @param bool $sandboxed Whether to sandbox the template or not
1502 * @return string The rendered template
1504 function twig_include(Twig_Environment $env, $context, $template, $variables = array(), $withContext = true, $ignoreMissing = false, $sandboxed = false)
1506 $alreadySandboxed = false;
1509 $variables = array_merge($context, $variables);
1512 if ($isSandboxed = $sandboxed && $env->hasExtension('Twig_Extension_Sandbox')) {
1513 $sandbox = $env->getExtension('Twig_Extension_Sandbox');
1514 if (!$alreadySandboxed = $sandbox->isSandboxed()) {
1515 $sandbox->enableSandbox();
1521 $result = $env->resolveTemplate($template)->render($variables);
1522 } catch (Twig_Error_Loader $e) {
1523 if (!$ignoreMissing) {
1524 if ($isSandboxed && !$alreadySandboxed) {
1525 $sandbox->disableSandbox();
1530 } catch (Throwable $e) {
1531 if ($isSandboxed && !$alreadySandboxed) {
1532 $sandbox->disableSandbox();
1536 } catch (Exception $e) {
1537 if ($isSandboxed && !$alreadySandboxed) {
1538 $sandbox->disableSandbox();
1544 if ($isSandboxed && !$alreadySandboxed) {
1545 $sandbox->disableSandbox();
1552 * Returns a template content without rendering it.
1554 * @param Twig_Environment $env
1555 * @param string $name The template name
1556 * @param bool $ignoreMissing Whether to ignore missing templates or not
1558 * @return string The template source
1560 function twig_source(Twig_Environment $env, $name, $ignoreMissing = false)
1562 $loader = $env->getLoader();
1564 if (!$loader instanceof Twig_SourceContextLoaderInterface) {
1565 return $loader->getSource($name);
1567 return $loader->getSourceContext($name)->getCode();
1569 } catch (Twig_Error_Loader $e) {
1570 if (!$ignoreMissing) {
1577 * Provides the ability to get constants from instances as well as class/global constants.
1579 * @param string $constant The name of the constant
1580 * @param null|object $object The object to get the constant from
1584 function twig_constant($constant, $object = null)
1586 if (null !== $object) {
1587 $constant = get_class($object).'::'.$constant;
1590 return constant($constant);
1594 * Checks if a constant exists.
1596 * @param string $constant The name of the constant
1597 * @param null|object $object The object to get the constant from
1601 function twig_constant_is_defined($constant, $object = null)
1603 if (null !== $object) {
1604 $constant = get_class($object).'::'.$constant;
1607 return defined($constant);
1613 * @param array $items An array of items
1614 * @param int $size The size of the batch
1615 * @param mixed $fill A value used to fill missing items
1619 function twig_array_batch($items, $size, $fill = null)
1621 if ($items instanceof Traversable) {
1622 $items = iterator_to_array($items, false);
1625 $size = ceil($size);
1627 $result = array_chunk($items, $size, true);
1629 if (null !== $fill && !empty($result)) {
1630 $last = count($result) - 1;
1631 if ($fillCount = $size - count($result[$last])) {
1632 $result[$last] = array_merge(
1634 array_fill(0, $fillCount, $fill)
1642 class_alias('Twig_Extension_Core', 'Twig\Extension\CoreExtension', false);