diff --git a/src/TwigConverter.php b/src/TwigConverter.php index 90dd3a6..75eb5b4 100644 --- a/src/TwigConverter.php +++ b/src/TwigConverter.php @@ -13,7 +13,7 @@ class TwigConverter public function __construct() { $loader = new Twiggy\Loader\ArrayLoader; - $twiggy = new Twiggy\Environment($loader, ['cache' => false]); + $twiggy = new Twiggy\Environment($loader, ['cache' => false, 'optimizations'=>2]); $twiggy->addExtension(new Twiggy\Extra\Cache\CacheExtension); $twiggy->addExtension(new Twiggy\Extra\Html\HtmlExtension); $twiggy->addExtension(new Twiggy\Extension\DebugExtension); @@ -37,7 +37,8 @@ public function convert(string $code): string private function postProcess(string $code): string { - $code = preg_replace('~\bclass=(["\']){html_classes\((.*)\)}~i', 'n:class=$1$2', $code); + $code = \preg_replace('~\bclass=(["\']){html_classes\((.*)\)}~i', 'n:class=$1$2', $code); + $code = \str_replace(['{$_self}', '{block _'], ['{$this->getName()}', '{block b_'], $code); return $code; } } diff --git a/src/Twiggy/Compiler.php b/src/Twiggy/Compiler.php index 3fe7b60..3028d6e 100644 --- a/src/Twiggy/Compiler.php +++ b/src/Twiggy/Compiler.php @@ -1,5 +1,4 @@ + */ class Compiler { private $lastLine; @@ -26,7 +28,14 @@ class Compiler private $sourceLine; private $varNameSalt = 0; - + private function addQuote(string $value): string + { + $escaped = addcslashes($value, "\x00..\x1F"); + return $value === $escaped + ? "'" . $value . "'" + : '"' . $escaped . '"'; + } + public function __construct(Environment $env) { $this->env = $env; @@ -125,10 +134,22 @@ public function write(...$strings) public function string(string $value) { if (!$this->isSymbol($value)) { - $escaped = addcslashes($value, "\x00..\x1F"); - $value = $value === $escaped - ? "'" . $value . "'" - : '"' . $escaped . '"'; + $value = $this->addQuote($value); + } + $this->source .= $value; + return $this; + } + + /** + * Adds a value to the compiled code, quoted if it is a string. + * + * @param mixed $value + * @return $this + */ + public function quote($value) + { + if (is_string($value)) { + $value = $this->addQuote($value); } $this->source .= $value; return $this; @@ -172,7 +193,6 @@ public function repr($value) } $this->repr($v); } - $this->raw(']'); } else { $this->string($value); diff --git a/src/Twiggy/Extension/CoreExtension.php b/src/Twiggy/Extension/CoreExtension.php index d2388c2..672a3dc 100644 --- a/src/Twiggy/Extension/CoreExtension.php +++ b/src/Twiggy/Extension/CoreExtension.php @@ -1,5 +1,4 @@ getCharset(); - if ($charset !== 'UTF-8') { - $values = twig_convert_encoding($values, 'UTF-8', $charset); - } + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @return mixed A random value from the given sequence + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + */ + function twig_random(Environment $env, $values = null, $max = null) { + if ($values === null) { + return $max === null ? mt_rand() : mt_rand(0, $max); + } - // unicode version of str_split() - // split at all positions, but not after the start and not before the end - $values = preg_split('/(? $value) { - $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); + return mt_rand($min, $max); } - } - } - if (!twig_test_iterable($values)) { - return $values; - } + if (\is_string($values)) { + if ($values === '') { + return ''; + } - $values = twig_to_array($values); + $charset = $env->getCharset(); - if (\count($values) === 0) { - throw new RuntimeError('The random function cannot pick from an empty array.'); - } + if ($charset !== 'UTF-8') { + $values = twig_convert_encoding($values, 'UTF-8', $charset); + } - return $values[array_rand($values, 1)]; -} + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8'); + } + } + } -/** - * Converts a date to the given format. - * - * {{ post.published_at|date("m/d/Y") }} - * - * @param \DateTimeInterface|\DateInterval|string $date A date - * @param string|null $format The target format, null to use the default - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return string The formatted date - */ -function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) -{ - if ($format === null) { - $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); - $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; - } + if (!twig_test_iterable($values)) { + return $values; + } - if ($date instanceof \DateInterval) { - return $date->format($format); - } + $values = twig_to_array($values); - return twig_date_converter($env, $date, $timezone)->format($format); -} + if (\count($values) === 0) { + throw new RuntimeError('The random function cannot pick from an empty array.'); + } + return $values[array_rand($values, 1)]; + } -/** - * Returns a new date object modified. - * - * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} - * - * @param \DateTimeInterface|string $date A date - * @param string $modifier A modifier string - * - * @return \DateTimeInterface - */ -function twig_date_modify_filter(Environment $env, $date, $modifier) -{ - $date = twig_date_converter($env, $date, false); - return $date->modify($modifier); -} + /** + * Converts a date to the given format. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string $date A date + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return string The formatted date + */ + function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) { + if ($format === null) { + $formats = $env->getExtension(CoreExtension::class)->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + if ($date instanceof \DateInterval) { + return $date->format($format); + } -/** - * Converts an input to a \DateTime instance. - * - * {% if date(user.created_at) < date('+2days') %} - * {# do something #} - * {% endif %} - * - * @param \DateTimeInterface|string|null $date A date or null to use the current time - * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged - * - * @return \DateTimeInterface - */ -function twig_date_converter(Environment $env, $date = null, $timezone = null) -{ - // determine the timezone - if ($timezone !== false) { - if ($timezone === null) { - $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); - } elseif (!$timezone instanceof \DateTimeZone) { - $timezone = new DateTimeZone($timezone); + return twig_date_converter($env, $date, $timezone)->format($format); } - } - // immutable dates - if ($date instanceof \DateTimeImmutable) { - return $timezone !== false ? $date->setTimezone($timezone) : $date; - } - if ($date instanceof \DateTimeInterface) { - $date = clone $date; - if ($timezone !== false) { - $date->setTimezone($timezone); + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string $date A date + * @param string $modifier A modifier string + * + * @return \DateTimeInterface + */ + function twig_date_modify_filter(Environment $env, $date, $modifier) { + $date = twig_date_converter($env, $date, false); + + return $date->modify($modifier); } - return $date; - } - if ($date === null || $date === 'now') { - return new DateTime($date, $timezone !== false ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); - } + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|null $date A date or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTimeInterface + */ + function twig_date_converter(Environment $env, $date = null, $timezone = null) { + // determine the timezone + if ($timezone !== false) { + if ($timezone === null) { + $timezone = $env->getExtension(CoreExtension::class)->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } - $asString = (string) $date; - if (ctype_digit($asString) || (!empty($asString) && $asString[0] === '-' && ctype_digit(substr($asString, 1)))) { - $date = new DateTime('@' . $date); - } else { - $date = new DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); - } + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return $timezone !== false ? $date->setTimezone($timezone) : $date; + } - if ($timezone !== false) { - $date->setTimezone($timezone); - } + if ($date instanceof \DateTimeInterface) { + $date = clone $date; + if ($timezone !== false) { + $date->setTimezone($timezone); + } - return $date; -} + return $date; + } + if ($date === null || $date === 'now') { + return new \DateTime($date, $timezone !== false ? $timezone : $env->getExtension(CoreExtension::class)->getTimezone()); + } -/** - * Replaces strings within a string. - * - * @param string $str String to replace in - * @param array|\Traversable $from Replace values - * - * @return string - */ -function twig_replace_filter($str, $from) -{ - if (!twig_test_iterable($from)) { - throw new RuntimeError(sprintf('The "replace" filter expects an array or "Traversable" as replace values, got "%s".', \is_object($from) ? $from::class : \gettype($from))); - } + $asString = (string)$date; + if (ctype_digit($asString) || (!empty($asString) && $asString[0] === '-' && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@' . $date); + } else { + $date = new \DateTime($date, $env->getExtension(CoreExtension::class)->getTimezone()); + } - return strtr($str, twig_to_array($from)); -} + if ($timezone !== false) { + $date->setTimezone($timezone); + } + return $date; + } -/** - * Rounds a number. - * - * @param int|float $value The value to round - * @param int|float $precision The rounding precision - * @param string $method The method to use for rounding - * - * @return int|float The rounded number - */ -function twig_round($value, $precision = 0, $method = 'common') -{ - if ($method === 'common') { - return round($value, $precision); - } - if ($method !== 'ceil' && $method !== 'floor') { - throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); - } + /** + * Replaces strings within a string. + * + * @param string $str String to replace in + * @param array|\Traversable $from Replace values + * + * @return string + */ + function twig_replace_filter($str, $from) { + if (!twig_test_iterable($from)) { + 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))); + } - return $method($value * 10 ** $precision) / 10 ** $precision; -} + return strtr($str, twig_to_array($from)); + } -/** - * Number format filter. - * - * All of the formatting options can be left null, in that case the defaults will - * be used. Supplying any of the parameters will override the defaults set in the - * environment object. - * - * @param mixed $number A float/int/string of the number to format - * @param int $decimal the number of decimal points to display - * @param string $decimalPoint the character(s) to use for the decimal point - * @param string $thousandSep the character(s) to use for the thousands separator - * - * @return string The formatted number - */ -function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) -{ - $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); - if ($decimal === null) { - $decimal = $defaults[0]; - } + /** + * Rounds a number. + * + * @param int|float $value The value to round + * @param int|float $precision The rounding precision + * @param string $method The method to use for rounding + * + * @return int|float The rounded number + */ + function twig_round($value, $precision = 0, $method = 'common') { + if ($method === 'common') { + return round($value, $precision); + } - if ($decimalPoint === null) { - $decimalPoint = $defaults[1]; - } + if ($method !== 'ceil' && $method !== 'floor') { + throw new RuntimeError('The round filter only supports the "common", "ceil", and "floor" methods.'); + } - if ($thousandSep === null) { - $thousandSep = $defaults[2]; - } + return $method($value * 10 ** $precision) / 10 ** $precision; + } - return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); -} + /** + * Number format filter. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int $decimal the number of decimal points to display + * @param string $decimalPoint the character(s) to use for the decimal point + * @param string $thousandSep the character(s) to use for the thousands separator + * + * @return string The formatted number + */ + function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) { + $defaults = $env->getExtension(CoreExtension::class)->getNumberFormat(); + if ($decimal === null) { + $decimal = $defaults[0]; + } -/** - * URL encodes (RFC 3986) a string as a path segment or an array as a query string. - * - * @param string|array $url A URL or an array of query parameters - * - * @return string The URL encoded value - */ -function twig_urlencode_filter($url) -{ - if (\is_array($url)) { - return http_build_query($url, '', '&', PHP_QUERY_RFC3986); - } + if ($decimalPoint === null) { + $decimalPoint = $defaults[1]; + } - return rawurlencode($url); -} + if ($thousandSep === null) { + $thousandSep = $defaults[2]; + } + return number_format((float)$number, $decimal, $decimalPoint, $thousandSep); + } -/** - * Merges an array with another one. - * - * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} - * - * {% set items = items|merge({ 'peugeot': 'car' }) %} - * - * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #} - * - * @param array|\Traversable $arr1 An array - * @param array|\Traversable $arr2 An array - * - * @return array The merged array - */ -function twig_array_merge($arr1, $arr2) -{ - if (!twig_test_iterable($arr1)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1))); - } - if (!twig_test_iterable($arr2)) { - throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2))); - } + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array $url A URL or an array of query parameters + * + * @return string The URL encoded value + */ + function twig_urlencode_filter($url) { + if (\is_array($url)) { + return http_build_query($url, '', '&', PHP_QUERY_RFC3986); + } - return array_merge(twig_to_array($arr1), twig_to_array($arr2)); -} + return rawurlencode($url); + } -/** - * Slices a variable. - * - * @param mixed $item A variable - * @param int $start Start of the slice - * @param int $length Size of the slice - * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) - * - * @return mixed The sliced variable - */ -function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - while ($item instanceof \IteratorAggregate) { - $item = $item->getIterator(); - } + /** + * Merges an array with another one. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #} + * + * @param array|\Traversable $arr1 An array + * @param array|\Traversable $arr2 An array + * + * @return array The merged array + */ + function twig_array_merge($arr1, $arr2) { + if (!twig_test_iterable($arr1)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($arr1))); + } - if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { - try { - return iterator_to_array(new LimitIterator($item, $start, $length ?? -1), $preserveKeys); - } catch (OutOfBoundsException $e) { - return []; + if (!twig_test_iterable($arr2)) { + throw new RuntimeError(sprintf('The merge filter only works with arrays or "Traversable", got "%s" as second argument.', \gettype($arr2))); } - } - $item = iterator_to_array($item, $preserveKeys); - } + return array_merge(twig_to_array($arr1), twig_to_array($arr2)); + } - if (\is_array($item)) { - return \array_slice($item, $start, $length, $preserveKeys); - } - $item = (string) $item; + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + */ + function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } - return (string) mb_substr($item, $start, $length, $env->getCharset()); -} + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } + $item = iterator_to_array($item, $preserveKeys); + } -/** - * Returns the first element of the item. - * - * @param mixed $item A variable - * - * @return mixed The first element of the item - */ -function twig_first(Environment $env, $item) -{ - $elements = twig_slice($env, $item, 0, 1, false); + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); + } - return \is_string($elements) ? $elements : current($elements); -} + $item = (string)$item; + return (string)mb_substr($item, $start, $length, $env->getCharset()); + } -/** - * Returns the last element of the item. - * - * @param mixed $item A variable - * - * @return mixed The last element of the item - */ -function twig_last(Environment $env, $item) -{ - $elements = twig_slice($env, $item, -1, 1, false); - return \is_string($elements) ? $elements : current($elements); -} + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + */ + function twig_first(Environment $env, $item) { + $elements = twig_slice($env, $item, 0, 1, false); + return \is_string($elements) ? $elements : current($elements); + } -/** - * Joins the values to a string. - * - * The separators between elements are empty strings per default, you can define them with the optional parameters. - * - * {{ [1, 2, 3]|join(', ', ' and ') }} - * {# returns 1, 2 and 3 #} - * - * {{ [1, 2, 3]|join('|') }} - * {# returns 1|2|3 #} - * - * {{ [1, 2, 3]|join }} - * {# returns 123 #} - * - * @param array $value An array - * @param string $glue The separator - * @param string|null $and The separator for the last pair - * - * @return string The concatenated string - */ -function twig_join_filter($value, $glue = '', $and = null) -{ - if (!twig_test_iterable($value)) { - $value = (array) $value; - } - $value = twig_to_array($value, false); + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + */ + function twig_last(Environment $env, $item) { + $elements = twig_slice($env, $item, -1, 1, false); - if (\count($value) === 0) { - return ''; - } + return \is_string($elements) ? $elements : current($elements); + } - if ($and === null || $and === $glue) { - return implode($glue, $value); - } - if (\count($value) === 1) { - return $value[0]; - } + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param array $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @return string The concatenated string + */ + function twig_join_filter($value, $glue = '', $and = null) { + if (!twig_test_iterable($value)) { + $value = (array)$value; + } - return implode($glue, \array_slice($value, 0, -1)) . $and . $value[\count($value) - 1]; -} + $value = twig_to_array($value, false); + if (\count($value) === 0) { + return ''; + } -/** - * Splits the string into an array. - * - * {{ "one,two,three"|split(',') }} - * {# returns [one, two, three] #} - * - * {{ "one,two,three,four,five"|split(',', 3) }} - * {# returns [one, two, "three,four,five"] #} - * - * {{ "123"|split('') }} - * {# returns [1, 2, 3] #} - * - * {{ "aabbcc"|split('', 2) }} - * {# returns [aa, bb, cc] #} - * - * @param string $value A string - * @param string $delimiter The delimiter - * @param int $limit The limit - * - * @return array The split string as an array - */ -function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) -{ - if (\strlen($delimiter) > 0) { - return $limit === null - ? explode($delimiter, $value) - : explode($delimiter, $value, $limit); - } + if ($and === null || $and === $glue) { + return implode($glue, $value); + } - if ($limit <= 1) { - return preg_split('/(?getCharset()); - if ($length < $limit) { - return [$value]; - } + return implode($glue, \array_slice($value, 0, -1)) . $and . $value[\count($value) - 1]; + } - $r = []; - for ($i = 0; $i < $length; $i += $limit) { - $r[] = mb_substr($value, $i, $limit, $env->getCharset()); - } - return $r; -} + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string $value A string + * @param string $delimiter The delimiter + * @param int $limit The limit + * + * @return array The split string as an array + */ + function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) { + if (\strlen($delimiter) > 0) { + return $limit === null + ? explode($delimiter, $value) + : explode($delimiter, $value, $limit); + } + if ($limit <= 1) { + return preg_split('/(?getCharset()); + if ($length < $limit) { + return [$value]; + } + $r = []; + for ($i = 0; $i < $length; $i += $limit) { + $r[] = mb_substr($value, $i, $limit, $env->getCharset()); + } -/** - * @internal - */ -function _twig_default_filter($value, $default = '') -{ - if (twig_test_empty($value)) { - return $default; - } + return $r; + } - return $value; -} +// The '_default' filter is used internally to avoid using the ternary operator +// which costs a lot for big contexts (before PHP 5.4). So, on average, +// a function call is cheaper. -/** - * Returns the keys for the given array. - * - * It is useful when you want to iterate over the keys of an array: - * - * {% for key in array|keys %} - * {# ... #} - * {% endfor %} - * - * @param array $array An array - * - * @return array The keys - */ -function twig_get_array_keys_filter($array) -{ - if ($array instanceof \Traversable) { - while ($array instanceof \IteratorAggregate) { - $array = $array->getIterator(); - } - if ($array instanceof \Iterator) { - $keys = []; - $array->rewind(); - while ($array->valid()) { - $keys[] = $array->key(); - $array->next(); + /** + * @internal + */ + function _twig_default_filter($value, $default = '') { + if (twig_test_empty($value)) { + return $default; } - return $keys; + return $value; } - $keys = []; - foreach ($array as $key => $item) { - $keys[] = $key; - } - return $keys; - } + /** + * Returns the keys for the given array. + * + * It is useful when you want to iterate over the keys of an array: + * + * {% for key in array|keys %} + * {# ... #} + * {% endfor %} + * + * @param array $array An array + * + * @return array The keys + */ + function twig_get_array_keys_filter($array) { + if ($array instanceof \Traversable) { + while ($array instanceof \IteratorAggregate) { + $array = $array->getIterator(); + } - if (!\is_array($array)) { - return []; - } + if ($array instanceof \Iterator) { + $keys = []; + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } - return array_keys($array); -} + return $keys; + } + $keys = []; + foreach ($array as $key => $item) { + $keys[] = $key; + } -/** - * Reverses a variable. - * - * @param array|\Traversable|string $item An array, a \Traversable instance, or a string - * @param bool $preserveKeys Whether to preserve key or not - * - * @return mixed The reversed input - */ -function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) -{ - if ($item instanceof \Traversable) { - return array_reverse(iterator_to_array($item), $preserveKeys); - } + return $keys; + } - if (\is_array($item)) { - return array_reverse($item, $preserveKeys); - } + if (!\is_array($array)) { + return []; + } + + return array_keys($array); + } - $string = (string) $item; - $charset = $env->getCharset(); + /** + * Reverses a variable. + * + * @param array|\Traversable|string $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + */ + function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); + } - if ($charset !== 'UTF-8') { - $item = twig_convert_encoding($string, 'UTF-8', $charset); - } + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } - preg_match_all('/./us', $item, $matches); + $string = (string)$item; - $string = implode('', array_reverse($matches[0])); + $charset = $env->getCharset(); - if ($charset !== 'UTF-8') { - $string = twig_convert_encoding($string, $charset, 'UTF-8'); - } + if ($charset !== 'UTF-8') { + $item = twig_convert_encoding($string, 'UTF-8', $charset); + } - return $string; -} + preg_match_all('/./us', $item, $matches); + $string = implode('', array_reverse($matches[0])); -/** - * Sorts an array. - * - * @param array|\Traversable $array - * - * @return array - */ -function twig_sort_filter($array, $arrow = null) -{ - if ($array instanceof \Traversable) { - $array = iterator_to_array($array); - } elseif (!\is_array($array)) { - throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); - } + if ($charset !== 'UTF-8') { + $string = twig_convert_encoding($string, $charset, 'UTF-8'); + } - if ($arrow !== null) { - uasort($array, $arrow); - } else { - asort($array); - } + return $string; + } - return $array; -} + /** + * Sorts an array. + * + * @param array|\Traversable $array + * + * @return array + */ + function twig_sort_filter($array, $arrow = null) { + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } elseif (!\is_array($array)) { + throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array))); + } -/** - * @internal - */ -function twig_in_filter($value, $compare) -{ - if ($value instanceof Markup) { - $value = (string) $value; - } - if ($compare instanceof Markup) { - $compare = (string) $compare; - } + if ($arrow !== null) { + uasort($array, $arrow); + } else { + asort($array); + } - if (\is_string($compare)) { - if (\is_string($value) || \is_int($value) || \is_float($value)) { - return $value === '' || str_contains($compare, (string) $value); + return $array; } - return false; - } - if (!is_iterable($compare)) { - return false; - } + /** + * @internal + */ + function twig_in_filter($value, $compare) { + if ($value instanceof Markup) { + $value = (string)$value; + } + if ($compare instanceof Markup) { + $compare = (string)$compare; + } - if (\is_object($value) || \is_resource($value)) { - if (!\is_array($compare)) { - foreach ($compare as $item) { - if ($item === $value) { - return true; + if (\is_string($compare)) { + if (\is_string($value) || \is_int($value) || \is_float($value)) { + return $value === '' || strpos($compare, (string)$value) !== false; } + + return false; } - return false; - } + if (!is_iterable($compare)) { + return false; + } - return \in_array($value, $compare, true); - } + if (\is_object($value) || \is_resource($value)) { + if (!\is_array($compare)) { + foreach ($compare as $item) { + if ($item === $value) { + return true; + } + } - foreach ($compare as $item) { - if (twig_compare($value, $item) === 0) { - return true; - } - } + return false; + } - return false; -} + return \in_array($value, $compare, true); + } + foreach ($compare as $item) { + if (twig_compare($value, $item) === 0) { + return true; + } + } -/** - * Compares two values using a more strict version of the PHP non-strict comparison operator. - * - * @see https://wiki.php.net/rfc/string_to_number_comparison - * @see https://wiki.php.net/rfc/trailing_whitespace_numerics - * - * @internal - */ -function twig_compare($a, $b) -{ - // int <=> string - if (\is_int($a) && \is_string($b)) { - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - if ((int) $bTrim == $bTrim) { - return $a <=> (int) $bTrim; - } else { - return (float) $a <=> (float) $bTrim; - } - } - if (\is_string($a) && \is_int($b)) { - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; - } - if ((int) $aTrim == $aTrim) { - return (int) $aTrim <=> $b; - } else { - return (float) $aTrim <=> (float) $b; + return false; } - } - // float <=> string - if (\is_float($a) && \is_string($b)) { - if (is_nan($a)) { - return 1; - } - $bTrim = trim($b, " \t\n\r\v\f"); - if (!is_numeric($bTrim)) { - return (string) $a <=> $b; - } - return $a <=> (float) $bTrim; - } - if (\is_string($a) && \is_float($b)) { - if (is_nan($b)) { - return 1; - } - $aTrim = trim($a, " \t\n\r\v\f"); - if (!is_numeric($aTrim)) { - return $a <=> (string) $b; - } + /** + * Compares two values using a more strict version of the PHP non-strict comparison operator. + * + * @see https://wiki.php.net/rfc/string_to_number_comparison + * @see https://wiki.php.net/rfc/trailing_whitespace_numerics + * + * @internal + */ + function twig_compare($a, $b) { + // int <=> string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string)$a <=> $b; + } + if ((int)$bTrim == $bTrim) { + return $a <=> (int)$bTrim; + } else { + return (float)$a <=> (float)$bTrim; + } + } + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string)$b; + } + if ((int)$aTrim == $aTrim) { + return (int)$aTrim <=> $b; + } else { + return (float)$aTrim <=> (float)$b; + } + } - return (float) $aTrim <=> $b; - } + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string)$a <=> $b; + } - // fallback to <=> - return $a <=> $b; -} + return $a <=> (float)$bTrim; + } + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string)$b; + } + return (float)$aTrim <=> $b; + } -/** - * Returns a trimmed string. - * - * @return string - * - * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') - */ -function twig_trim_filter($string, $characterMask = null, $side = 'both') -{ - if ($characterMask === null) { - $characterMask = " \t\n\r\0\x0B"; - } + // fallback to <=> + return $a <=> $b; + } - switch ($side) { - case 'both': - return trim($string, $characterMask); - case 'left': - return ltrim($string, $characterMask); - case 'right': - return rtrim($string, $characterMask); - default: - throw new RuntimeError('Trimming side must be "left", "right" or "both".'); - } -} + /** + * Returns a trimmed string. + * + * @return string + * + * @throws RuntimeError When an invalid trimming side is used (not a string or not 'left', 'right', or 'both') + */ + function twig_trim_filter($string, $characterMask = null, $side = 'both') { + if ($characterMask === null) { + $characterMask = " \t\n\r\0\x0B"; + } -/** - * Removes whitespaces between HTML tags. - * - * @return string - */ -function twig_spaceless($content) -{ - return trim(preg_replace('/>\s+', '><', $content)); -} + switch ($side) { + case 'both': + return trim($string, $characterMask); + case 'left': + return ltrim($string, $characterMask); + case 'right': + return rtrim($string, $characterMask); + default: + throw new RuntimeError('Trimming side must be "left", "right" or "both".'); + } + } -function twig_convert_encoding($string, $to, $from) -{ - if (!\function_exists('iconv')) { - throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); - } + /** + * Removes whitespaces between HTML tags. + * + * @return string + */ + function twig_spaceless($content) { + return trim(preg_replace('/>\s+', '><', $content)); + } - return iconv($from, $to, $string); -} + function twig_convert_encoding($string, $to, $from) { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } -/** - * Returns the length of a variable. - * - * @param mixed $thing A variable - * - * @return int The length of the value - */ -function twig_length_filter(Environment $env, $thing) -{ - if ($thing === null) { - return 0; - } + return iconv($from, $to, $string); + } - if (is_scalar($thing)) { - return mb_strlen($thing, $env->getCharset()); - } - if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { - return \count($thing); - } + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @return int The length of the value + */ + function twig_length_filter(Environment $env, $thing) { + if ($thing === null) { + return 0; + } - if ($thing instanceof \Traversable) { - return iterator_count($thing); - } + if (is_scalar($thing)) { + return mb_strlen($thing, $env->getCharset()); + } - if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { - return mb_strlen((string) $thing, $env->getCharset()); - } + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } - return 1; -} + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } + if (method_exists($thing, '__toString') && !$thing instanceof \Countable) { + return mb_strlen((string)$thing, $env->getCharset()); + } -/** - * Converts a string to uppercase. - * - * @param string $string A string - * - * @return string The uppercased string - */ -function twig_upper_filter(Environment $env, $string) -{ - return mb_strtoupper($string, $env->getCharset()); -} + return 1; + } -/** - * Converts a string to lowercase. - * - * @param string $string A string - * - * @return string The lowercased string - */ -function twig_lower_filter(Environment $env, $string) -{ - return mb_strtolower($string, $env->getCharset()); -} + /** + * Converts a string to uppercase. + * + * @param string $string A string + * + * @return string The uppercased string + */ + function twig_upper_filter(Environment $env, $string) { + return mb_strtoupper($string, $env->getCharset()); + } -/** - * Returns a titlecased string. - * - * @param string $string A string - * - * @return string The titlecased string - */ -function twig_title_string_filter(Environment $env, $string) -{ - if (null !== $charset = $env->getCharset()) { - return mb_convert_case($string, MB_CASE_TITLE, $charset); - } + /** + * Converts a string to lowercase. + * + * @param string $string A string + * + * @return string The lowercased string + */ + function twig_lower_filter(Environment $env, $string) { + return mb_strtolower($string, $env->getCharset()); + } - return ucwords(strtolower($string)); -} + /** + * Returns a titlecased string. + * + * @param string $string A string + * + * @return string The titlecased string + */ + function twig_title_string_filter(Environment $env, $string) { + if (null !== $charset = $env->getCharset()) { + return mb_convert_case($string, MB_CASE_TITLE, $charset); + } -/** - * Returns a capitalized string. - * - * @param string $string A string - * - * @return string The capitalized string - */ -function twig_capitalize_string_filter(Environment $env, $string) -{ - $charset = $env->getCharset(); + return ucwords(strtolower($string)); + } - return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset) . mb_strtolower(mb_substr($string, 1, null, $charset), $charset); -} + /** + * Returns a capitalized string. + * + * @param string $string A string + * + * @return string The capitalized string + */ + function twig_capitalize_string_filter(Environment $env, $string) { + $charset = $env->getCharset(); -/** - * @internal - */ -function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) -{ - if (!method_exists($template, $method)) { - $parent = $template; - while ($parent = $parent->getParent($context)) { - if (method_exists($parent, $method)) { - return $parent->$method(...$args); - } + return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset) . mb_strtolower(mb_substr($string, 1, null, $charset), $charset); } - throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); - } - return $template->$method(...$args); -} + /** + * @internal + */ + function twig_call_macro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); + } + } + throw new RuntimeError(sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + } -/** - * @internal - */ -function twig_ensure_traversable($seq) -{ - if ($seq instanceof \Traversable || \is_array($seq)) { - return $seq; - } + return $template->$method(...$args); + } - return []; -} + /** + * @internal + */ + function twig_ensure_traversable($seq) { + if ($seq instanceof \Traversable || \is_array($seq)) { + return $seq; + } -/** - * @internal - */ -function twig_to_array($seq, $preserveKeys = true) -{ - if ($seq instanceof \Traversable) { - return iterator_to_array($seq, $preserveKeys); - } + return []; + } - if (!\is_array($seq)) { - return $seq; - } - return $preserveKeys ? $seq : array_values($seq); -} + /** + * @internal + */ + function twig_to_array($seq, $preserveKeys = true) { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } + if (!\is_array($seq)) { + return $seq; + } -/** - * Checks if a variable is empty. - * - * {# evaluates to true if the foo variable is null, false, or the empty string #} - * {% if foo is empty %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is empty, false otherwise - */ -function twig_test_empty($value) -{ - if ($value instanceof \Countable) { - return \count($value) === 0; - } + return $preserveKeys ? $seq : array_values($seq); + } - if ($value instanceof \Traversable) { - return !iterator_count($value); - } - if (\is_object($value) && method_exists($value, '__toString')) { - return (string) $value === ''; - } + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is empty, false otherwise + */ + function twig_test_empty($value) { + if ($value instanceof \Countable) { + return \count($value) === 0; + } - return $value === '' || $value === false || $value === null || $value === []; -} + if ($value instanceof \Traversable) { + return !iterator_count($value); + } + if (\is_object($value) && method_exists($value, '__toString')) { + return (string)$value === ''; + } -/** - * Checks if a variable is traversable. - * - * {# evaluates to true if the foo variable is an array or a traversable object #} - * {% if foo is iterable %} - * {# ... #} - * {% endif %} - * - * @param mixed $value A variable - * - * @return bool true if the value is traversable - */ -function twig_test_iterable($value) -{ - return $value instanceof \Traversable || \is_array($value); -} + return $value === '' || $value === false || $value === null || $value === []; + } -/** - * Renders a template. - * - * @param array $context - * @param string|array $template The template to render or an array of templates to try consecutively - * @param array $variables The variables to pass to the template - * @param bool $withContext - * @param bool $ignoreMissing Whether to ignore missing templates or not - * @param bool $sandboxed Whether to sandbox the template or not - * - * @return string The rendered template - */ -function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) -{ - $alreadySandboxed = false; - $sandbox = null; - if ($withContext) { - $variables = array_merge($context, $variables); - } - - if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { - $sandbox = $env->getExtension(SandboxExtension::class); - if (!$alreadySandboxed = $sandbox->isSandboxed()) { - $sandbox->enableSandbox(); + /** + * Checks if a variable is traversable. + * + * {# evaluates to true if the foo variable is an array or a traversable object #} + * {% if foo is iterable %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @return bool true if the value is traversable + */ + function twig_test_iterable($value) { + return $value instanceof \Traversable || \is_array($value); } - foreach ((\is_array($template) ? $template : [$template]) as $name) { - // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security - if ($name instanceof TemplateWrapper || $name instanceof Template) { - $name->unwrap()->checkSecurity(); - } - } - } - try { - $loaded = null; - try { - $loaded = $env->resolveTemplate($template); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; + /** + * Renders a template. + * + * @param array $context + * @param string|array $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @return string The rendered template + */ + function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); } - } - return $loaded ? $loaded->render($variables) : ''; - } finally { - if ($isSandboxed && !$alreadySandboxed) { - $sandbox->disableSandbox(); - } - } -} + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } + foreach ((\is_array($template) ? $template : [$template]) as $name) { + // if a Template instance is passed, it might have been instantiated outside of a sandbox, check security + if ($name instanceof TemplateWrapper || $name instanceof Template) { + $name->unwrap()->checkSecurity(); + } + } + } -/** - * Returns a template content without rendering it. - * - * @param string $name The template name - * @param bool $ignoreMissing Whether to ignore missing templates or not - * - * @return string The template source - */ -function twig_source(Environment $env, $name, $ignoreMissing = false) -{ - $loader = $env->getLoader(); - try { - return $loader->getSourceContext($name)->getCode(); - } catch (LoaderError $e) { - if (!$ignoreMissing) { - throw $e; - } - } -} + try { + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + } + return $loaded ? $loaded->render($variables) : ''; + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } + } + } -/** - * Provides the ability to get constants from instances as well as class/global constants. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return string - */ -function twig_constant($constant, $object = null) -{ - if ($object !== null) { - $constant = $object::class . '::' . $constant; - } - return \constant($constant); -} + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @return string The template source + */ + function twig_source(Environment $env, $name, $ignoreMissing = false) { + $loader = $env->getLoader(); + try { + return $loader->getSourceContext($name)->getCode(); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + } + } -/** - * Checks if a constant exists. - * - * @param string $constant The name of the constant - * @param object|null $object The object to get the constant from - * - * @return bool - */ -function twig_constant_is_defined($constant, $object = null) -{ - if ($object !== null) { - $constant = $object::class . '::' . $constant; - } + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return string + */ + function twig_constant($constant, $object = null) { + if ($object !== null) { + $constant = \get_class($object) . '::' . $constant; + } - return \defined($constant); -} + return \constant($constant); + } -/** - * Batches item. - * - * @param array $items An array of items - * @param int $size The size of the batch - * @param mixed $fill A value used to fill missing items - * - * @return array - */ -function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) -{ - if (!twig_test_iterable($items)) { - throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? $items::class : \gettype($items))); - } + /** + * Checks if a constant exists. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * + * @return bool + */ + function twig_constant_is_defined($constant, $object = null) { + if ($object !== null) { + $constant = \get_class($object) . '::' . $constant; + } - $size = ceil($size); + return \defined($constant); + } - $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); - if ($fill !== null && $result) { - $last = \count($result) - 1; - if ($fillCount = $size - \count($result[$last])) { - for ($i = 0; $i < $fillCount; ++$i) { - $result[$last][] = $fill; + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @return array + */ + function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) { + if (!twig_test_iterable($items)) { + throw new RuntimeError(sprintf('The "batch" filter expects an array or "Traversable", got "%s".', \is_object($items) ? \get_class($items) : \gettype($items))); } - } - } - - return $result; -} + $size = ceil($size); -/** - * Returns the attribute value for a given array/object. - * - * @param mixed $object The object or array from where to get the item - * @param mixed $item The item to get from the array or object - * @param array $arguments An array of arguments to pass if the item is an object method - * @param string $type The type of attribute (@see \LatteTools\Twiggy\Template constants) - * @param bool $isDefinedTest Whether this is only a defined check - * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not - * @param int $lineno The template line where the attribute was called - * - * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true - * - * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false - * - * @internal - */ -function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) -{ - // array - if ($type !== Template::METHOD_CALL) { - $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + $result = array_chunk(twig_to_array($items, $preserveKeys), $size, $preserveKeys); - if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) - || ($object instanceof ArrayAccess && isset($object[$arrayItem])) - ) { - if ($isDefinedTest) { - return true; + if ($fill !== null && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } + } } - return $object[$arrayItem]; + return $result; } - if ($type === Template::ARRAY_CALL || !\is_object($object)) { - if ($isDefinedTest) { - return false; - } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \LatteTools\Twiggy\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + function twig_get_attribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) { + // array + if ($type !== Template::METHOD_CALL) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int)$item : $item; + + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array)$object))) + || ($object instanceof ArrayAccess && isset($object[$arrayItem])) + ) { + if ($isDefinedTest) { + return true; + } + + return $object[$arrayItem]; + } + + if ($type === Template::ARRAY_CALL || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof ArrayAccess) { + $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, \get_class($object)); + } elseif (\is_object($object)) { + $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, \get_class($object)); + } elseif (\is_array($object)) { + if (empty($object)) { + $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); + } else { + $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif ($type === Template::ARRAY_CALL) { + if ($object === null) { + $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + } elseif ($object === null) { + $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } } - if ($object instanceof ArrayAccess) { - $message = sprintf('Key "%s" in object with ArrayAccess of class "%s" does not exist.', $arrayItem, $object::class); - } elseif (\is_object($object)) { - $message = sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, $object::class); - } elseif (\is_array($object)) { - if (empty($object)) { - $message = sprintf('Key "%s" does not exist as the array is empty.', $arrayItem); - } else { - $message = sprintf('Key "%s" for array with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + if (!\is_object($object)) { + if ($isDefinedTest) { + return false; } - } elseif ($type === Template::ARRAY_CALL) { + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + if ($object === null) { - $message = sprintf('Impossible to access a key ("%s") on a null variable.', $item); + $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); + } elseif (\is_array($object)) { + $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); } else { - $message = sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); } - } elseif ($object === null) { - $message = sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); - } else { - $message = sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); + + throw new RuntimeError($message, $lineno, $source); } - throw new RuntimeError($message, $lineno, $source); - } - } + if ($object instanceof Template) { + throw new RuntimeError('Accessing \LatteTools\Twiggy\Template attributes is forbidden.', $lineno, $source); + } - if (!\is_object($object)) { - if ($isDefinedTest) { - return false; - } + // object property + if ($type !== Template::METHOD_CALL) { + if (isset($object->$item) || \array_key_exists((string)$item, (array)$object)) { + if ($isDefinedTest) { + return true; + } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; - } + if ($sandboxed) { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } - if ($object === null) { - $message = sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); - } elseif (\is_array($object)) { - $message = sprintf('Impossible to invoke a method ("%s") on an array.', $item); - } else { - $message = sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, \gettype($object), $object); - } + return $object->$item; + } + } - throw new RuntimeError($message, $lineno, $source); - } + static $cache = []; + + $class = \get_class($object); + + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + sort($methods); + $lcMethods = array_map(fn($value) => strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; + + if ($lcName[0] === 'g' && strpos($lcName, 'get') === 0) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + } elseif ($lcName[0] === 'i' && strpos($lcName, 'is') === 0) { + $name = substr($method, 2); + $lcName = substr($lcName, 2); + } elseif ($lcName[0] === 'h' && strpos($lcName, 'has') === 0) { + $name = substr($method, 3); + $lcName = substr($lcName, 3); + if (\in_array('is' . $lcName, $lcMethods, true)) { + continue; + } + } else { + continue; + } + + // skip get() and is() methods (in which case, $name is empty) + if ($name) { + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } + + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } + } + } + $cache[$class] = $classCache; + } - if ($object instanceof Template) { - throw new RuntimeError('Accessing \LatteTools\Twiggy\Template attributes is forbidden.', $lineno, $source); - } + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); + } - // object property - if ($type !== Template::METHOD_CALL) { - if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { if ($isDefinedTest) { return true; } if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } - - return $object->$item; - } - } - - static $cache = []; - - $class = $object::class; - - // object method - // precedence: getXxx() > isXxx() > hasXxx() - if (!isset($cache[$class])) { - $methods = get_class_methods($object); - sort($methods); - $lcMethods = array_map(fn($value) => strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), $methods); - $classCache = []; - foreach ($methods as $i => $method) { - $classCache[$method] = $method; - $classCache[$lcName = $lcMethods[$i]] = $method; - - if ($lcName[0] === 'g' && str_starts_with($lcName, 'get')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - } elseif ($lcName[0] === 'i' && str_starts_with($lcName, 'is')) { - $name = substr($method, 2); - $lcName = substr($lcName, 2); - } elseif ($lcName[0] === 'h' && str_starts_with($lcName, 'has')) { - $name = substr($method, 3); - $lcName = substr($lcName, 3); - if (\in_array('is' . $lcName, $lcMethods, true)) { - continue; - } - } else { - continue; + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); } - // skip get() and is() methods (in which case, $name is empty) - if ($name) { - if (!isset($classCache[$name])) { - $classCache[$name] = $method; - } - - if (!isset($classCache[$lcName])) { - $classCache[$lcName] = $method; + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; } + throw $e; } - } - $cache[$class] = $classCache; - } - - $call = false; - if (isset($cache[$class][$item])) { - $method = $cache[$class][$item]; - } elseif (isset($cache[$class][$lcItem = strtr($item, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')])) { - $method = $cache[$class][$lcItem]; - } elseif (isset($cache[$class]['__call'])) { - $method = $item; - $call = true; - } else { - if ($isDefinedTest) { - return false; + return $ret; } - if ($ignoreStrictCheck || !$env->isStrictVariables()) { - return; - } - throw new RuntimeError(sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); - } - - if ($isDefinedTest) { - return true; - } - - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); - } + /** + * Returns the values from a single column in the input array. + * + *
+ * {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+ *
+ * {% set fruits = items|column('fruit') %}
+ *
+ * {# fruits now contains ['apple', 'orange'] #}
+ *
+ *
+ * @param array|Traversable $array An array
+ * @param mixed $name The column name
+ * @param mixed $index The column to use as the index/keys for the returned array
+ *
+ * @return array The array of values
+ */
+ function twig_array_column($array, $name, $index = null): array {
+ if ($array instanceof Traversable) {
+ $array = iterator_to_array($array);
+ } elseif (!\is_array($array)) {
+ throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
+ }
- // Some objects throw exceptions when they have __call, and the method we try
- // to call is not supported. If ignoreStrictCheck is true, we should return null.
- try {
- $ret = $object->$method(...$arguments);
- } catch (BadMethodCallException $e) {
- if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) {
- return;
+ return array_column($array, $name, $index);
}
- throw $e;
- }
- return $ret;
-}
+ function twig_array_filter(Environment $env, $array, $arrow) {
+ if (!twig_test_iterable($array)) {
+ throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
+ }
-/**
- * Returns the values from a single column in the input array.
- *
- *
- * {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
- *
- * {% set fruits = items|column('fruit') %}
- *
- * {# fruits now contains ['apple', 'orange'] #}
- *
- *
- * @param array|Traversable $array An array
- * @param mixed $name The column name
- * @param mixed $index The column to use as the index/keys for the returned array
- *
- * @return array The array of values
- */
-function twig_array_column($array, $name, $index = null): array
-{
- if ($array instanceof Traversable) {
- $array = iterator_to_array($array);
- } elseif (!\is_array($array)) {
- throw new RuntimeError(sprintf('The column filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
- }
-
- return array_column($array, $name, $index);
-}
-
+ if (
+ !$arrow instanceof Closure
+ && $env->hasExtension('\LatteTools\Twiggy\Extension\SandboxExtension')
+ && $env->getExtension('\LatteTools\Twiggy\Extension\SandboxExtension')->isSandboxed()
+ ) {
+ throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.');
+ }
-function twig_array_filter(Environment $env, $array, $arrow)
-{
- if (!twig_test_iterable($array)) {
- throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? $array::class : \gettype($array)));
- }
+ if (\is_array($array)) {
+ return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
+ }
- if (
- !$arrow instanceof Closure
- && $env->hasExtension('\LatteTools\Twiggy\Extension\SandboxExtension')
- && $env->getExtension('\LatteTools\Twiggy\Extension\SandboxExtension')->isSandboxed()
- ) {
- throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.');
- }
+ // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
+ return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow);
+ }
- if (\is_array($array)) {
- return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
- }
- // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator
- return new CallbackFilterIterator(new IteratorIterator($array), $arrow);
-}
+ function twig_array_map(Environment $env, $array, $arrow) {
+ if (
+ !$arrow instanceof Closure
+ && $env->hasExtension('\LatteTools\Twiggy\Extension\SandboxExtension')
+ && $env->getExtension('\LatteTools\Twiggy\Extension\SandboxExtension')->isSandboxed()
+ ) {
+ throw new RuntimeError('The callable passed to the "map" filter must be a Closure in sandbox mode.');
+ }
+ $r = [];
+ foreach ($array as $k => $v) {
+ $r[$k] = $arrow($v, $k);
+ }
-function twig_array_map(Environment $env, $array, $arrow)
-{
- if (
- !$arrow instanceof Closure
- && $env->hasExtension('\LatteTools\Twiggy\Extension\SandboxExtension')
- && $env->getExtension('\LatteTools\Twiggy\Extension\SandboxExtension')->isSandboxed()
- ) {
- throw new RuntimeError('The callable passed to the "map" filter must be a Closure in sandbox mode.');
- }
+ return $r;
+ }
- $r = [];
- foreach ($array as $k => $v) {
- $r[$k] = $arrow($v, $k);
- }
- return $r;
-}
+ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) {
+ if (
+ !$arrow instanceof Closure
+ && $env->hasExtension('\LatteTools\Twiggy\Extension\SandboxExtension')
+ && $env->getExtension('\LatteTools\Twiggy\Extension\SandboxExtension')->isSandboxed()
+ ) {
+ throw new RuntimeError('The callable passed to the "reduce" filter must be a Closure in sandbox mode.');
+ }
+ if (!\is_array($array)) {
+ if (!$array instanceof \Traversable) {
+ throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
+ }
-function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
-{
- if (
- !$arrow instanceof Closure
- && $env->hasExtension('\LatteTools\Twiggy\Extension\SandboxExtension')
- && $env->getExtension('\LatteTools\Twiggy\Extension\SandboxExtension')->isSandboxed()
- ) {
- throw new RuntimeError('The callable passed to the "reduce" filter must be a Closure in sandbox mode.');
- }
+ $array = iterator_to_array($array);
+ }
- if (!\is_array($array)) {
- if (!$array instanceof \Traversable) {
- throw new RuntimeError(sprintf('The "reduce" filter only works with arrays or "Traversable", got "%s" as first argument.', \gettype($array)));
+ return array_reduce($array, $arrow, $initial);
}
-
- $array = iterator_to_array($array);
}
-
- return array_reduce($array, $arrow, $initial);
-}
}
diff --git a/src/Twiggy/Extension/DebugExtension.php b/src/Twiggy/Extension/DebugExtension.php
index f1e4a5f..2a595ed 100644
--- a/src/Twiggy/Extension/DebugExtension.php
+++ b/src/Twiggy/Extension/DebugExtension.php
@@ -38,25 +38,25 @@ public function getFunctions(): array
use LatteTools\Twiggy\Environment;
use LatteTools\Twiggy\Template;
use LatteTools\Twiggy\TemplateWrapper;
+if (!\function_exists('twig_var_dump')) {
+ function twig_var_dump(Environment $env, $context, ...$vars) {
+ if (!$env->isDebug()) {
+ return;
+ }
-function twig_var_dump(Environment $env, $context, ...$vars)
-{
- if (!$env->isDebug()) {
- return;
- }
-
- ob_start();
+ ob_start();
- if (!$vars) {
- $vars = [];
- foreach ($context as $key => $value) {
- if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
- $vars[$key] = $value;
+ if (!$vars) {
+ $vars = [];
+ foreach ($context as $key => $value) {
+ if (!$value instanceof Template && !$value instanceof TemplateWrapper) {
+ $vars[$key] = $value;
+ }
}
+ } else {
}
- } else {
- }
- return ob_get_clean();
+ return ob_get_clean();
+ }
}
}
diff --git a/src/Twiggy/Extension/EscaperExtension.php b/src/Twiggy/Extension/EscaperExtension.php
index 1ba81db..f04eb16 100644
--- a/src/Twiggy/Extension/EscaperExtension.php
+++ b/src/Twiggy/Extension/EscaperExtension.php
@@ -1,5 +1,4 @@
getExtension(EscaperExtension::class);
- if (!isset($ext->safeClasses[$c])) {
- $ext->safeClasses[$c] = [];
- foreach (class_parents($string) + class_implements($string) as $class) {
- if (isset($ext->safeClasses[$class])) {
- $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
- foreach ($ext->safeClasses[$class] as $s) {
- $ext->safeLookup[$s][$c] = true;
+
+ /**
+ * Escapes a string.
+ *
+ * @param mixed $string The value to be escaped
+ * @param string $strategy The escaping strategy
+ * @param string $charset The charset
+ * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
+ *
+ * @return string
+ */
+ function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false) {
+ if ($autoescape && $string instanceof Markup) {
+ return $string;
+ }
+
+ if (!\is_string($string)) {
+ if (\is_object($string) && method_exists($string, '__toString')) {
+ if ($autoescape) {
+ $c = \get_class($string);
+ $ext = $env->getExtension(EscaperExtension::class);
+ if (!isset($ext->safeClasses[$c])) {
+ $ext->safeClasses[$c] = [];
+ foreach (class_parents($string) + class_implements($string) as $class) {
+ if (isset($ext->safeClasses[$class])) {
+ $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
+ foreach ($ext->safeClasses[$class] as $s) {
+ $ext->safeLookup[$s][$c] = true;
+ }
}
}
}
+ if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
+ return (string)$string;
+ }
}
- if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
- return (string) $string;
- }
- }
- $string = (string) $string;
- } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) {
- return $string;
+ $string = (string)$string;
+ } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'], true)) {
+ return $string;
+ }
}
- }
-
- if ($string === '') {
- return '';
- }
- if ($charset === null) {
- $charset = $env->getCharset();
- }
+ if ($string === '') {
+ return '';
+ }
- switch ($strategy) {
- case 'html':
- // see https://secure.php.net/htmlspecialchars
-
- // Using a static variable to avoid initializing the array
- // each time the function is called. Moving the declaration on the
- // top of the function slow downs other escaping strategies.
- static $htmlspecialcharsCharsets = [
- 'ISO-8859-1' => true, 'ISO8859-1' => true,
- 'ISO-8859-15' => true, 'ISO8859-15' => true,
- 'utf-8' => true, 'UTF-8' => true,
- 'CP866' => true, 'IBM866' => true, '866' => true,
- 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
- '1251' => true,
- 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
- 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
- 'BIG5' => true, '950' => true,
- 'GB2312' => true, '936' => true,
- 'BIG5-HKSCS' => true,
- 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
- 'EUC-JP' => true, 'EUCJP' => true,
- 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
- ];
-
- if (isset($htmlspecialcharsCharsets[$charset])) {
- return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
- }
+ if ($charset === null) {
+ $charset = $env->getCharset();
+ }
- if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
- // cache the lowercase variant for future iterations
- $htmlspecialcharsCharsets[$charset] = true;
+ switch ($strategy) {
+ case 'html':
+ // see https://secure.php.net/htmlspecialchars
+
+ // Using a static variable to avoid initializing the array
+ // each time the function is called. Moving the declaration on the
+ // top of the function slow downs other escaping strategies.
+ static $htmlspecialcharsCharsets = [
+ 'ISO-8859-1' => true, 'ISO8859-1' => true,
+ 'ISO-8859-15' => true, 'ISO8859-15' => true,
+ 'utf-8' => true, 'UTF-8' => true,
+ 'CP866' => true, 'IBM866' => true, '866' => true,
+ 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
+ '1251' => true,
+ 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
+ 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
+ 'BIG5' => true, '950' => true,
+ 'GB2312' => true, '936' => true,
+ 'BIG5-HKSCS' => true,
+ 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
+ 'EUC-JP' => true, 'EUCJP' => true,
+ 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
+ ];
- return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
- }
+ if (isset($htmlspecialcharsCharsets[$charset])) {
+ return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
+ }
- $string = twig_convert_encoding($string, 'UTF-8', $charset);
- $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+ if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
+ // cache the lowercase variant for future iterations
+ $htmlspecialcharsCharsets[$charset] = true;
- return iconv('UTF-8', $charset, $string);
+ return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
+ }
- case 'js':
- // escape all non-alphanumeric characters
- // into their \x or \uHHHH representations
- if ($charset !== 'UTF-8') {
$string = twig_convert_encoding($string, 'UTF-8', $charset);
- }
-
- if (!preg_match('//u', $string)) {
- throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
- }
+ $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
- $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
- $char = $matches[0];
-
- /*
- * A few characters have short escape sequences in JSON and JavaScript.
- * Escape sequences supported only by JavaScript, not JSON, are omitted.
- * \" is also supported but omitted, because the resulting string is not HTML safe.
- */
- static $shortMap = [
- '\\' => '\\\\',
- '/' => '\\/',
- "\x08" => '\b',
- "\x0C" => '\f',
- "\x0A" => '\n',
- "\x0D" => '\r',
- "\x09" => '\t',
- ];
+ return iconv('UTF-8', $charset, $string);
- if (isset($shortMap[$char])) {
- return $shortMap[$char];
+ case 'js':
+ // escape all non-alphanumeric characters
+ // into their \x or \uHHHH representations
+ if ($charset !== 'UTF-8') {
+ $string = twig_convert_encoding($string, 'UTF-8', $charset);
}
- $codepoint = mb_ord($char);
- if (0x10000 > $codepoint) {
- return sprintf('\u%04X', $codepoint);
+ if (!preg_match('//u', $string)) {
+ throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
}
- // Split characters outside the BMP into surrogate pairs
- // https://tools.ietf.org/html/rfc2781.html#section-2.1
- $u = $codepoint - 0x10000;
- $high = 0xD800 | ($u >> 10);
- $low = 0xDC00 | ($u & 0x3FF);
+ $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
+ $char = $matches[0];
- return sprintf('\u%04X\u%04X', $high, $low);
- }, $string);
+ /*
+ * A few characters have short escape sequences in JSON and JavaScript.
+ * Escape sequences supported only by JavaScript, not JSON, are omitted.
+ * \" is also supported but omitted, because the resulting string is not HTML safe.
+ */
+ static $shortMap = [
+ '\\' => '\\\\',
+ '/' => '\\/',
+ "\x08" => '\b',
+ "\x0C" => '\f',
+ "\x0A" => '\n',
+ "\x0D" => '\r',
+ "\x09" => '\t',
+ ];
- if ($charset !== 'UTF-8') {
- $string = iconv('UTF-8', $charset, $string);
- }
+ if (isset($shortMap[$char])) {
+ return $shortMap[$char];
+ }
- return $string;
+ $codepoint = mb_ord($char);
+ if (0x10000 > $codepoint) {
+ return sprintf('\u%04X', $codepoint);
+ }
- case 'css':
- if ($charset !== 'UTF-8') {
- $string = twig_convert_encoding($string, 'UTF-8', $charset);
- }
+ // Split characters outside the BMP into surrogate pairs
+ // https://tools.ietf.org/html/rfc2781.html#section-2.1
+ $u = $codepoint - 0x10000;
+ $high = 0xD800 | ($u >> 10);
+ $low = 0xDC00 | ($u & 0x3FF);
- if (!preg_match('//u', $string)) {
- throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
- }
+ return sprintf('\u%04X\u%04X', $high, $low);
+ }, $string);
- $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
- $char = $matches[0];
+ if ($charset !== 'UTF-8') {
+ $string = iconv('UTF-8', $charset, $string);
+ }
- return sprintf('\\%X ', \strlen($char) === 1 ? \ord($char) : mb_ord($char, 'UTF-8'));
- }, $string);
+ return $string;
- if ($charset !== 'UTF-8') {
- $string = iconv('UTF-8', $charset, $string);
- }
+ case 'css':
+ if ($charset !== 'UTF-8') {
+ $string = twig_convert_encoding($string, 'UTF-8', $charset);
+ }
- return $string;
+ if (!preg_match('//u', $string)) {
+ throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
+ }
- case 'html_attr':
- if ($charset !== 'UTF-8') {
- $string = twig_convert_encoding($string, 'UTF-8', $charset);
- }
+ $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
+ $char = $matches[0];
- if (!preg_match('//u', $string)) {
- throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
- }
+ return sprintf('\\%X ', \strlen($char) === 1 ? \ord($char) : mb_ord($char, 'UTF-8'));
+ }, $string);
- $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
- /**
- * This function is adapted from code coming from Zend Framework.
- */
- $chr = $matches[0];
- $ord = \ord($chr);
-
- /*
- * The following replaces characters undefined in HTML with the
- * hex entity for the Unicode replacement character.
- */
- if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) {
- return '�';
+ if ($charset !== 'UTF-8') {
+ $string = iconv('UTF-8', $charset, $string);
}
- /*
- * Check if the current character to escape has a name entity we should
- * replace it with while grabbing the hex value of the character.
- */
- if (\strlen($chr) === 1) {
- /*
- * While HTML supports far more named entities, the lowest common denominator
- * has become HTML5's XML Serialisation which is restricted to the those named
- * entities that XML supports. Using HTML entities would result in this error:
- * XML Parsing Error: undefined entity
+ return $string;
+
+ case 'html_attr':
+ if ($charset !== 'UTF-8') {
+ $string = twig_convert_encoding($string, 'UTF-8', $charset);
+ }
+
+ if (!preg_match('//u', $string)) {
+ throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
+ }
+
+ $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
+ /**
+ * This function is adapted from code coming from Zend Framework.
+ *
+ * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
+ * @license https://framework.zend.com/license/new-bsd New BSD License
*/
- static $entityMap = [
- 34 => '"', /* quotation mark */
- 38 => '&', /* ampersand */
- 60 => '<', /* less-than sign */
- 62 => '>', /* greater-than sign */
- ];
+ $chr = $matches[0];
+ $ord = \ord($chr);
- if (isset($entityMap[$ord])) {
- return $entityMap[$ord];
+ /*
+ * The following replaces characters undefined in HTML with the
+ * hex entity for the Unicode replacement character.
+ */
+ if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r") || ($ord >= 0x7f && $ord <= 0x9f)) {
+ return '�';
}
- return sprintf('%02X;', $ord);
- }
+ /*
+ * Check if the current character to escape has a name entity we should
+ * replace it with while grabbing the hex value of the character.
+ */
+ if (\strlen($chr) === 1) {
+ /*
+ * While HTML supports far more named entities, the lowest common denominator
+ * has become HTML5's XML Serialisation which is restricted to the those named
+ * entities that XML supports. Using HTML entities would result in this error:
+ * XML Parsing Error: undefined entity
+ */
+ static $entityMap = [
+ 34 => '"', /* quotation mark */
+ 38 => '&', /* ampersand */
+ 60 => '<', /* less-than sign */
+ 62 => '>', /* greater-than sign */
+ ];
+
+ if (isset($entityMap[$ord])) {
+ return $entityMap[$ord];
+ }
- /*
- * Per OWASP recommendations, we'll use hex entities for any other
- * characters where a named entity does not exist.
- */
- return sprintf('%04X;', mb_ord($chr, 'UTF-8'));
- }, $string);
+ return sprintf('%02X;', $ord);
+ }
- if ($charset !== 'UTF-8') {
- $string = iconv('UTF-8', $charset, $string);
- }
+ /*
+ * Per OWASP recommendations, we'll use hex entities for any other
+ * characters where a named entity does not exist.
+ */
+ return sprintf('%04X;', mb_ord($chr, 'UTF-8'));
+ }, $string);
- return $string;
+ if ($charset !== 'UTF-8') {
+ $string = iconv('UTF-8', $charset, $string);
+ }
- case 'url':
- return rawurlencode($string);
+ return $string;
- default:
- static $escapers;
+ case 'url':
+ return rawurlencode($string);
- if ($escapers === null) {
- $escapers = $env->getExtension(EscaperExtension::class)->getEscapers();
- }
+ default:
+ static $escapers;
- if (isset($escapers[$strategy])) {
- return $escapers[$strategy]($env, $string, $charset);
- }
+ if ($escapers === null) {
+ $escapers = $env->getExtension(EscaperExtension::class)->getEscapers();
+ }
- $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
+ if (isset($escapers[$strategy])) {
+ return $escapers[$strategy]($env, $string, $charset);
+ }
- throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
+ $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
+
+ throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
+ }
}
-}
-/**
- * @internal
- */
-function twig_escape_filter_is_safe(Node $filterArgs)
-{
- foreach ($filterArgs as $arg) {
- if ($arg instanceof ConstantExpression) {
- return [$arg->getAttribute('value')];
+ /**
+ * @internal
+ */
+ function twig_escape_filter_is_safe(Node $filterArgs) {
+ foreach ($filterArgs as $arg) {
+ if ($arg instanceof ConstantExpression) {
+ return [$arg->getAttribute('value')];
+ }
+
+ return [];
}
- return [];
+ return ['html'];
}
-
- return ['html'];
}
}
diff --git a/src/Twiggy/Node/Expression/GetAttrExpression.php b/src/Twiggy/Node/Expression/GetAttrExpression.php
index 5e87665..6983537 100644
--- a/src/Twiggy/Node/Expression/GetAttrExpression.php
+++ b/src/Twiggy/Node/Expression/GetAttrExpression.php
@@ -1,5 +1,4 @@
raw('isset(');
}
- $property = !$attr instanceof ConstantExpression || !is_int($attr->getAttribute('value'));
+ $property = !$attr instanceof ConstantExpression || ($type !== Template::ARRAY_CALL && !is_int($attr->getAttribute('value')));
$compiler->subcompile($this->getNode('node'))
->raw($property ? '->' : '[');
if ($attr instanceof ConstantExpression) {
- $compiler->raw($attr->getAttribute('value'));
+ $value = $attr->getAttribute('value');
+ if ($property) {
+ $compiler->raw($value);
+ } else {
+ $compiler->quote($value);
+ }
} else {
$compiler
->raw('{')
->subcompile($this->getNode('attribute'))
- ->raw('}');
+ ->raw('}')
+ ;
}
if (!$property) {
diff --git a/tests/fixtures-twig/expressions/_self.latte b/tests/fixtures-twig/expressions/_self.latte
index 8a5a4fd..388b377 100644
--- a/tests/fixtures-twig/expressions/_self.latte
+++ b/tests/fixtures-twig/expressions/_self.latte
@@ -1,3 +1,3 @@
Test: _self returns the template name
-{$_self}
+{$this->getName()}
diff --git a/tests/fixtures-twig/expressions/array.latte b/tests/fixtures-twig/expressions/array.latte
index 06130e0..1851a0d 100644
--- a/tests/fixtures-twig/expressions/array.latte
+++ b/tests/fixtures-twig/expressions/array.latte
@@ -14,7 +14,7 @@ Test: Twig supports array notation
{* nested arrays *}
{var $a = [1, 2, [1, 2], [foo => [foo => bar]]]}
{$a[2]|join:','}
-{$a[3]->foo|join:','}
+{$a[3]['foo']|join:','}
{* works even if [] is used inside the array *}
{=[$foo->{$bar}]|join:','}
@@ -33,9 +33,9 @@ Test: Twig supports array notation
{$ary|join:','}
{* ArrayAccess *}
-{$array_access->a}
+{$array_access['a']}
{* array that does not exist *}
{$does_not_exist[0] ?? ok}
{$does_not_exist[0]->does_not_exist_either ?? ok}
-{$does_not_exist[0]->does_not_exist_either ?? ok}
+{$does_not_exist[0]['does_not_exist_either'] ?? ok}
diff --git a/tests/fixtures-twig/expressions/array_call.latte b/tests/fixtures-twig/expressions/array_call.latte
index 2e06641..38c9ccb 100644
--- a/tests/fixtures-twig/expressions/array_call.latte
+++ b/tests/fixtures-twig/expressions/array_call.latte
@@ -1,6 +1,6 @@
Test: Twig supports method calls
{$items->foo}
-{$items->foo}
+{$items['foo']}
{$items->{$foo}}
{$items->{$items->{$foo}}}
diff --git a/tests/fixtures-twig/expressions/method_call.latte b/tests/fixtures-twig/expressions/method_call.latte
index d5ede6c..3799da2 100644
--- a/tests/fixtures-twig/expressions/method_call.latte
+++ b/tests/fixtures-twig/expressions/method_call.latte
@@ -3,7 +3,7 @@ Test: Twig supports method calls
{$items->foo->foo}
{$items->foo->getFoo()}
{$items->foo->bar}
-{$items->foo->bar}
+{$items->foo['bar']}
{$items->foo->bar(a, 43)}
{$items->foo->bar($foo)}
{$items->foo->self->foo()}
diff --git a/tests/fixtures-twig/filters/default.latte b/tests/fixtures-twig/filters/default.latte
index 6ee03b2..4808983 100644
--- a/tests/fixtures-twig/filters/default.latte
+++ b/tests/fixtures-twig/filters/default.latte
@@ -8,13 +8,13 @@ Variable:
{=($undefinedVar ?? 'default') === 'default' ? ok : ko}
Array access:
{=($nested->definedVar ?? 'default') === 'default' ? ko : ok}
-{=($nested->definedVar ?? 'default') === 'default' ? ko : ok}
+{=($nested['definedVar'] ?? 'default') === 'default' ? ko : ok}
{=($nested->zeroVar ?? 'default') === 'default' ? ko : ok}
{=($nested->emptyVar ?? 'default') === 'default' ? ok : ko}
{=($nested->nullVar ?? 'default') === 'default' ? ok : ko}
{=($nested->undefinedVar ?? 'default') === 'default' ? ok : ko}
-{=($nested->undefinedVar ?? 'default') === 'default' ? ok : ko}
-{=($undefined->undefined ?? 'default') === 'default' ? ok : ko}
+{=($nested['undefinedVar'] ?? 'default') === 'default' ? ok : ko}
+{=($undefined['undefined'] ?? 'default') === 'default' ? ok : ko}
{=($undefinedVar->foo ?? 'default') === 'default' ? ok : ko}
Plain values:
{=((defined|default:'default')) === 'default' ? ko : ok}
@@ -34,9 +34,9 @@ Object methods:
Deep nested:
{=($nested->undefinedVar->foo->bar ?? 'default') === 'default' ? ok : ko}
{=($nested->definedArray[0] ?? 'default') === 'default' ? ko : ok}
-{=($nested->definedArray[0] ?? 'default') === 'default' ? ko : ok}
-{=($nested->undefinedVar[0] ?? 'default') === 'default' ? ok : ko}
-{=($undefined->undefined[0] ?? 'default') === 'default' ? ok : ko}
+{=($nested['definedArray'][0] ?? 'default') === 'default' ? ko : ok}
+{=($nested['undefinedVar'][0] ?? 'default') === 'default' ? ok : ko}
+{=($undefined['undefined'][0] ?? 'default') === 'default' ? ok : ko}
{=($object->self->foo ?? 'default') === 'default' ? ko : ok}
{=($object->self->undefinedMethod ?? 'default') === 'default' ? ok : ko}
{=($object->undefinedMethod->self ?? 'default') === 'default' ? ok : ko}
diff --git a/tests/fixtures-twig/filters/json_encode.latte b/tests/fixtures-twig/filters/json_encode.latte
index 1da421e..fb4faa7 100644
--- a/tests/fixtures-twig/filters/json_encode.latte
+++ b/tests/fixtures-twig/filters/json_encode.latte
@@ -1,5 +1,5 @@
Test: "json_encode" filter
-{json_encode(foo)}
-{json_encode($foo)}
-{json_encode([$foo, foo])}
+{json_encode(foo)|noescape}
+{json_encode($foo)|noescape}
+{json_encode([$foo, foo])|noescape}
diff --git a/tests/fixtures-twig/filters/urlencode.latte b/tests/fixtures-twig/filters/urlencode.latte
index 9e84136..ada2d24 100644
--- a/tests/fixtures-twig/filters/urlencode.latte
+++ b/tests/fixtures-twig/filters/urlencode.latte
@@ -1,6 +1,6 @@
Test: "url_encode" filter
{=[foo => bar, number => 3, 'spéßi%l' => 'e%c0d@d', 'spa ce' => '']|query}
-{=[foo => bar, number => 3, 'spéßi%l' => 'e%c0d@d', 'spa ce' => '']|query}
+{=[foo => bar, number => 3, 'spéßi%l' => 'e%c0d@d', 'spa ce' => '']|query|noescape}
{=(([]|query)|default:'default')}
{='spéßi%le%c0d@dspa ce'|query}
diff --git a/tests/fixtures-twig/tags/autoescape/functions.latte b/tests/fixtures-twig/tags/autoescape/functions.latte
index 196ec31..c5760f8 100644
--- a/tests/fixtures-twig/tags/autoescape/functions.latte
+++ b/tests/fixtures-twig/tags/autoescape/functions.latte
@@ -22,13 +22,13 @@ unsafe_br
{unsafe_br()}
unsafe_br()|raw
-{unsafe_br()}
+{unsafe_br()|noescape}
safe_br()|escape
{safe_br()}
safe_br()|raw
-{safe_br()}
+{safe_br()|noescape}
unsafe_br()|escape
{unsafe_br()}
@@ -39,6 +39,4 @@ autoescape js
safe_br
-{safe_br()}
-
-
+{safe_br()}
\ No newline at end of file
diff --git a/tests/fixtures-twig/tags/autoescape/literal.latte b/tests/fixtures-twig/tags/autoescape/literal.latte
index 842d00c..d813cae 100644
--- a/tests/fixtures-twig/tags/autoescape/literal.latte
+++ b/tests/fixtures-twig/tags/autoescape/literal.latte
@@ -26,12 +26,12 @@ Test: "autoescape" tag does not apply escaping on literals
{=false ? "\n" : false ? $someVar : '