Project

General

Profile

1
<?php
2

    
3
if (!defined('ENT_SUBSTITUTE')) {
4
    define('ENT_SUBSTITUTE', 8);
5
}
6

    
7
/*
8
 * This file is part of Twig.
9
 *
10
 * (c) 2009 Fabien Potencier
11
 *
12
 * For the full copyright and license information, please view the LICENSE
13
 * file that was distributed with this source code.
14
 */
15
class Twig_Extension_Core extends Twig_Extension
16
{
17
    protected $dateFormats = array('F j, Y H:i', '%d days');
18
    protected $numberFormat = array(0, '.', ',');
19
    protected $timezone = null;
20

    
21
    /**
22
     * Sets the default format to be used by the date filter.
23
     *
24
     * @param string $format             The default date format string
25
     * @param string $dateIntervalFormat The default date interval format string
26
     */
27
    public function setDateFormat($format = null, $dateIntervalFormat = null)
28
    {
29
        if (null !== $format) {
30
            $this->dateFormats[0] = $format;
31
        }
32

    
33
        if (null !== $dateIntervalFormat) {
34
            $this->dateFormats[1] = $dateIntervalFormat;
35
        }
36
    }
37

    
38
    /**
39
     * Gets the default format to be used by the date filter.
40
     *
41
     * @return array The default date format string and the default date interval format string
42
     */
43
    public function getDateFormat()
44
    {
45
        return $this->dateFormats;
46
    }
47

    
48
    /**
49
     * Sets the default timezone to be used by the date filter.
50
     *
51
     * @param DateTimeZone|string $timezone  The default timezone string or a DateTimeZone object
52
     */
53
    public function setTimezone($timezone)
54
    {
55
        $this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
56
    }
57

    
58
    /**
59
     * Gets the default timezone to be used by the date filter.
60
     *
61
     * @return DateTimeZone The default timezone currently in use
62
     */
63
    public function getTimezone()
64
    {
65
        return $this->timezone;
66
    }
67

    
68
    /**
69
     * Sets the default format to be used by the number_format filter.
70
     *
71
     * @param integer $decimal The number of decimal places to use.
72
     * @param string $decimalPoint The character(s) to use for the decimal point.
73
     * @param string $thousandSep The character(s) to use for the thousands separator.
74
     */
75
    public function setNumberFormat($decimal, $decimalPoint, $thousandSep)
76
    {
77
        $this->numberFormat = array($decimal, $decimalPoint, $thousandSep);
78
    }
79

    
80
    /**
81
     * Get the default format used by the number_format filter.
82
     *
83
     * @return array The arguments for number_format()
84
     */
85
    public function getNumberFormat()
86
    {
87
        return $this->numberFormat;
88
    }
89

    
90
    /**
91
     * Returns the token parser instance to add to the existing list.
92
     *
93
     * @return array An array of Twig_TokenParser instances
94
     */
95
    public function getTokenParsers()
96
    {
97
        return array(
98
            new Twig_TokenParser_For(),
99
            new Twig_TokenParser_If(),
100
            new Twig_TokenParser_Extends(),
101
            new Twig_TokenParser_Include(),
102
            new Twig_TokenParser_Block(),
103
            new Twig_TokenParser_Use(),
104
            new Twig_TokenParser_Filter(),
105
            new Twig_TokenParser_Macro(),
106
            new Twig_TokenParser_Import(),
107
            new Twig_TokenParser_From(),
108
            new Twig_TokenParser_Set(),
109
            new Twig_TokenParser_Spaceless(),
110
            new Twig_TokenParser_Flush(),
111
            new Twig_TokenParser_Do(),
112
        );
113
    }
114

    
115
    /**
116
     * Returns a list of filters to add to the existing list.
117
     *
118
     * @return array An array of filters
119
     */
120
    public function getFilters()
121
    {
122
        $filters = array(
123
            // formatting filters
124
            'date'          => new Twig_Filter_Function('twig_date_format_filter', array('needs_environment' => true)),
125
            'format'        => new Twig_Filter_Function('sprintf'),
126
            'replace'       => new Twig_Filter_Function('strtr'),
127
            'number_format' => new Twig_Filter_Function('twig_number_format_filter', array('needs_environment' => true)),
128

    
129
            // encoding
130
            'url_encode'       => new Twig_Filter_Function('twig_urlencode_filter'),
131
            'json_encode'      => new Twig_Filter_Function('twig_jsonencode_filter'),
132
            'convert_encoding' => new Twig_Filter_Function('twig_convert_encoding'),
133

    
134
            // string filters
135
            'title'      => new Twig_Filter_Function('twig_title_string_filter', array('needs_environment' => true)),
136
            'capitalize' => new Twig_Filter_Function('twig_capitalize_string_filter', array('needs_environment' => true)),
137
            'upper'      => new Twig_Filter_Function('strtoupper'),
138
            'lower'      => new Twig_Filter_Function('strtolower'),
139
            'striptags'  => new Twig_Filter_Function('strip_tags'),
140
            'trim'       => new Twig_Filter_Function('trim'),
141
            'nl2br'      => new Twig_Filter_Function('nl2br', array('pre_escape' => 'html', 'is_safe' => array('html'))),
142

    
143
            // array helpers
144
            'join'    => new Twig_Filter_Function('twig_join_filter'),
145
            'sort'    => new Twig_Filter_Function('twig_sort_filter'),
146
            'merge'   => new Twig_Filter_Function('twig_array_merge'),
147

    
148
            // string/array filters
149
            'reverse' => new Twig_Filter_Function('twig_reverse_filter', array('needs_environment' => true)),
150
            'length'  => new Twig_Filter_Function('twig_length_filter', array('needs_environment' => true)),
151
            'slice'   => new Twig_Filter_Function('twig_slice', array('needs_environment' => true)),
152

    
153
            // iteration and runtime
154
            'default' => new Twig_Filter_Node('Twig_Node_Expression_Filter_Default'),
155
            '_default' => new Twig_Filter_Function('_twig_default_filter'),
156

    
157
            'keys'    => new Twig_Filter_Function('twig_get_array_keys_filter'),
158

    
159
            // escaping
160
            'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
161
            'e'      => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
162
        );
163

    
164
        if (function_exists('mb_get_info')) {
165
            $filters['upper'] = new Twig_Filter_Function('twig_upper_filter', array('needs_environment' => true));
166
            $filters['lower'] = new Twig_Filter_Function('twig_lower_filter', array('needs_environment' => true));
167
        }
168

    
169
        return $filters;
170
    }
171

    
172
    /**
173
     * Returns a list of global functions to add to the existing list.
174
     *
175
     * @return array An array of global functions
176
     */
177
    public function getFunctions()
178
    {
179
        return array(
180
            'range'    => new Twig_Function_Function('range'),
181
            'constant' => new Twig_Function_Function('constant'),
182
            'cycle'    => new Twig_Function_Function('twig_cycle'),
183
            'random'   => new Twig_Function_Function('twig_random', array('needs_environment' => true)),
184
            'date'     => new Twig_Function_Function('twig_date_converter', array('needs_environment' => true)),
185
        );
186
    }
187

    
188
    /**
189
     * Returns a list of tests to add to the existing list.
190
     *
191
     * @return array An array of tests
192
     */
193
    public function getTests()
194
    {
195
        return array(
196
            'even'        => new Twig_Test_Node('Twig_Node_Expression_Test_Even'),
197
            'odd'         => new Twig_Test_Node('Twig_Node_Expression_Test_Odd'),
198
            'defined'     => new Twig_Test_Node('Twig_Node_Expression_Test_Defined'),
199
            'sameas'      => new Twig_Test_Node('Twig_Node_Expression_Test_Sameas'),
200
            'none'        => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
201
            'null'        => new Twig_Test_Node('Twig_Node_Expression_Test_Null'),
202
            'divisibleby' => new Twig_Test_Node('Twig_Node_Expression_Test_Divisibleby'),
203
            'constant'    => new Twig_Test_Node('Twig_Node_Expression_Test_Constant'),
204
            'empty'       => new Twig_Test_Function('twig_test_empty'),
205
            'iterable'    => new Twig_Test_Function('twig_test_iterable'),
206
        );
207
    }
208

    
209
    /**
210
     * Returns a list of operators to add to the existing list.
211
     *
212
     * @return array An array of operators
213
     */
214
    public function getOperators()
215
    {
216
        return array(
217
            array(
218
                'not' => array('precedence' => 50, 'class' => 'Twig_Node_Expression_Unary_Not'),
219
                '-'   => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Neg'),
220
                '+'   => array('precedence' => 500, 'class' => 'Twig_Node_Expression_Unary_Pos'),
221
            ),
222
            array(
223
                'b-and'  => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseAnd', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
224
                'b-xor'  => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseXor', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
225
                'b-or'   => array('precedence' => 5, 'class' => 'Twig_Node_Expression_Binary_BitwiseOr', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
226
                'or'     => array('precedence' => 10, 'class' => 'Twig_Node_Expression_Binary_Or', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
227
                'and'    => array('precedence' => 15, 'class' => 'Twig_Node_Expression_Binary_And', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
228
                '=='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Equal', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
229
                '!='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
230
                '<'      => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Less', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
231
                '>'      => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_Greater', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
232
                '>='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_GreaterEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
233
                '<='     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_LessEqual', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
234
                'not in' => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_NotIn', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
235
                'in'     => array('precedence' => 20, 'class' => 'Twig_Node_Expression_Binary_In', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
236
                '..'     => array('precedence' => 25, 'class' => 'Twig_Node_Expression_Binary_Range', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
237
                '+'      => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Add', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
238
                '-'      => array('precedence' => 30, 'class' => 'Twig_Node_Expression_Binary_Sub', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
239
                '~'      => array('precedence' => 40, 'class' => 'Twig_Node_Expression_Binary_Concat', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
240
                '*'      => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mul', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
241
                '/'      => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Div', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
242
                '//'     => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_FloorDiv', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
243
                '%'      => array('precedence' => 60, 'class' => 'Twig_Node_Expression_Binary_Mod', 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
244
                'is'     => array('precedence' => 100, 'callable' => array($this, 'parseTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
245
                'is not' => array('precedence' => 100, 'callable' => array($this, 'parseNotTestExpression'), 'associativity' => Twig_ExpressionParser::OPERATOR_LEFT),
246
                '**'     => array('precedence' => 200, 'class' => 'Twig_Node_Expression_Binary_Power', 'associativity' => Twig_ExpressionParser::OPERATOR_RIGHT),
247
            ),
248
        );
249
    }
250

    
251
    public function parseNotTestExpression(Twig_Parser $parser, $node)
252
    {
253
        return new Twig_Node_Expression_Unary_Not($this->parseTestExpression($parser, $node), $parser->getCurrentToken()->getLine());
254
    }
255

    
256
    public function parseTestExpression(Twig_Parser $parser, $node)
257
    {
258
        $stream = $parser->getStream();
259
        $name = $stream->expect(Twig_Token::NAME_TYPE)->getValue();
260
        $arguments = null;
261
        if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
262
            $arguments = $parser->getExpressionParser()->parseArguments();
263
        }
264

    
265
        $class = $this->getTestNodeClass($parser->getEnvironment(), $name);
266

    
267
        return new $class($node, $name, $arguments, $parser->getCurrentToken()->getLine());
268
    }
269

    
270
    protected function getTestNodeClass(Twig_Environment $env, $name)
271
    {
272
        $testMap = $env->getTests();
273
        if (isset($testMap[$name]) && $testMap[$name] instanceof Twig_Test_Node) {
274
            return $testMap[$name]->getClass();
275
        }
276

    
277
        return 'Twig_Node_Expression_Test';
278
    }
279

    
280
    /**
281
     * Returns the name of the extension.
282
     *
283
     * @return string The extension name
284
     */
285
    public function getName()
286
    {
287
        return 'core';
288
    }
289
}
290

    
291
/**
292
 * Cycles over a value.
293
 *
294
 * @param ArrayAccess|array $values An array or an ArrayAccess instance
295
 * @param integer           $i      The cycle value
296
 *
297
 * @return string The next value in the cycle
298
 */
299
function twig_cycle($values, $i)
300
{
301
    if (!is_array($values) && !$values instanceof ArrayAccess) {
302
        return $values;
303
    }
304

    
305
    return $values[$i % count($values)];
306
}
307

    
308
/**
309
 * Returns a random value depending on the supplied parameter type:
310
 * - a random item from a Traversable or array
311
 * - a random character from a string
312
 * - a random integer between 0 and the integer parameter
313
 *
314
 * @param Twig_Environment             $env    A Twig_Environment instance
315
 * @param Traversable|array|int|string $values The values to pick a random item from
316
 *
317
 * @throws Twig_Error_Runtime When $values is an empty array (does not apply to an empty string which is returned as is).
318
 *
319
 * @return mixed A random value from the given sequence
320
 */
321
function twig_random(Twig_Environment $env, $values = null)
322
{
323
    if (null === $values) {
324
        return mt_rand();
325
    }
326

    
327
    if (is_int($values) || is_float($values)) {
328
        return $values < 0 ? mt_rand($values, 0) : mt_rand(0, $values);
329
    }
330

    
331
    if ($values instanceof Traversable) {
332
        $values = iterator_to_array($values);
333
    } elseif (is_string($values)) {
334
        if ('' === $values) {
335
            return '';
336
        }
337
        if (null !== $charset = $env->getCharset()) {
338
            if ('UTF-8' != $charset) {
339
                $values = twig_convert_encoding($values, 'UTF-8', $charset);
340
            }
341

    
342
            // unicode version of str_split()
343
            // split at all positions, but not after the start and not before the end
344
            $values = preg_split('/(?<!^)(?!$)/u', $values);
345

    
346
            if ('UTF-8' != $charset) {
347
                foreach ($values as $i => $value) {
348
                    $values[$i] = twig_convert_encoding($value, $charset, 'UTF-8');
349
                }
350
            }
351
        } else {
352
            return $values[mt_rand(0, strlen($values) - 1)];
353
        }
354
    }
355

    
356
    if (!is_array($values)) {
357
        return $values;
358
    }
359

    
360
    if (0 === count($values)) {
361
        throw new Twig_Error_Runtime('The random function cannot pick from an empty array.');
362
    }
363

    
364
    return $values[array_rand($values, 1)];
365
}
366

    
367
/**
368
 * Converts a date to the given format.
369
 *
370
 * <pre>
371
 *   {{ post.published_at|date("m/d/Y") }}
372
 * </pre>
373
 *
374
 * @param Twig_Environment             $env      A Twig_Environment instance
375
 * @param DateTime|DateInterval|string $date     A date
376
 * @param string                       $format   A format
377
 * @param DateTimeZone|string          $timezone A timezone
378
 *
379
 * @return string The formatter date
380
 */
381
function twig_date_format_filter(Twig_Environment $env, $date, $format = null, $timezone = null)
382
{
383
    if (null === $format) {
384
        $formats = $env->getExtension('core')->getDateFormat();
385
        $format = $date instanceof DateInterval ? $formats[1] : $formats[0];
386
    }
387

    
388
    if ($date instanceof DateInterval || $date instanceof DateTime) {
389
        if (null !== $timezone) {
390
            $date->setTimezone($timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone));
391
        }
392

    
393
        return $date->format($format);
394
    }
395

    
396
    return twig_date_converter($env, $date, $timezone)->format($format);
397
}
398

    
399
/**
400
 * Converts an input to a DateTime instance.
401
 *
402
 * <pre>
403
 *    {% if date(user.created_at) < date('+2days') %}
404
 *      {# do something #}
405
 *    {% endif %}
406
 * </pre>
407
 *
408
 * @param Twig_Environment    $env      A Twig_Environment instance
409
 * @param DateTime|string     $date     A date
410
 * @param DateTimeZone|string $timezone A timezone
411
 *
412
 * @return DateTime A DateTime instance
413
 */
414
function twig_date_converter(Twig_Environment $env, $date = null, $timezone = null)
415
{
416
    if ($date instanceof DateTime) {
417
        return $date;
418
    }
419

    
420
    $asString = (string) $date;
421

    
422
    if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
423
        $date = new DateTime('@'.$date);
424
        $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
425
    } else {
426
        $date = new DateTime($date);
427
    }
428

    
429
    // set Timezone
430
    if (null !== $timezone) {
431
        if (!$timezone instanceof DateTimeZone) {
432
            $timezone = new DateTimeZone($timezone);
433
        }
434

    
435
        $date->setTimezone($timezone);
436
    } elseif (($timezone = $env->getExtension('core')->getTimezone()) instanceof DateTimeZone) {
437
        $date->setTimezone($timezone);
438
    }
439

    
440
    return $date;
441
}
442

    
443
/**
444
 * Number format filter.
445
 *
446
 * All of the formatting options can be left null, in that case the defaults will
447
 * be used.  Supplying any of the parameters will override the defaults set in the
448
 * environment object.
449
 *
450
 * @param Twig_Environment    $env          A Twig_Environment instance
451
 * @param mixed               $number       A float/int/string of the number to format
452
 * @param int                 $decimal      The number of decimal points to display.
453
 * @param string              $decimalPoint The character(s) to use for the decimal point.
454
 * @param string              $thousandSep  The character(s) to use for the thousands separator.
455
 *
456
 * @return string The formatted number
457
 */
458
function twig_number_format_filter(Twig_Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null)
459
{
460
    $defaults = $env->getExtension('core')->getNumberFormat();
461
    if (null === $decimal) {
462
        $decimal = $defaults[0];
463
    }
464

    
465
    if (null === $decimalPoint) {
466
        $decimalPoint = $defaults[1];
467
    }
468

    
469
    if (null === $thousandSep) {
470
        $thousandSep = $defaults[2];
471
    }
472

    
473
    return number_format((float) $number, $decimal, $decimalPoint, $thousandSep);
474
}
475

    
476
/**
477
 * URL encodes a string.
478
 *
479
 * @param string $url A URL
480
 * @param bool   $raw true to use rawurlencode() instead of urlencode
481
 *
482
 * @return string The URL encoded value
483
 */
484
function twig_urlencode_filter($url, $raw = false)
485
{
486
    if ($raw) {
487
        return rawurlencode($url);
488
    }
489

    
490
    return urlencode($url);
491
}
492

    
493
if (version_compare(PHP_VERSION, '5.3.0', '<')) {
494
    /**
495
     * JSON encodes a variable.
496
     *
497
     * @param mixed   $value   The value to encode.
498
     * @param integer $options Not used on PHP 5.2.x
499
     *
500
     * @return mixed The JSON encoded value
501
     */
502
    function twig_jsonencode_filter($value, $options = 0)
503
    {
504
        if ($value instanceof Twig_Markup) {
505
            $value = (string) $value;
506
        } elseif (is_array($value)) {
507
            array_walk_recursive($value, '_twig_markup2string');
508
        }
509

    
510
        return json_encode($value);
511
    }
512
} else {
513
    /**
514
     * JSON encodes a variable.
515
     *
516
     * @param mixed   $value   The value to encode.
517
     * @param integer $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
518
     *
519
     * @return mixed The JSON encoded value
520
     */
521
    function twig_jsonencode_filter($value, $options = 0)
522
    {
523
        if ($value instanceof Twig_Markup) {
524
            $value = (string) $value;
525
        } elseif (is_array($value)) {
526
            array_walk_recursive($value, '_twig_markup2string');
527
        }
528

    
529
        return json_encode($value, $options);
530
    }
531
}
532

    
533
function _twig_markup2string(&$value)
534
{
535
    if ($value instanceof Twig_Markup) {
536
        $value = (string) $value;
537
    }
538
}
539

    
540
/**
541
 * Merges an array with another one.
542
 *
543
 * <pre>
544
 *  {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %}
545
 *
546
 *  {% set items = items|merge({ 'peugeot': 'car' }) %}
547
 *
548
 *  {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car' } #}
549
 * </pre>
550
 *
551
 * @param array $arr1 An array
552
 * @param array $arr2 An array
553
 *
554
 * @return array The merged array
555
 */
556
function twig_array_merge($arr1, $arr2)
557
{
558
    if (!is_array($arr1) || !is_array($arr2)) {
559
        throw new Twig_Error_Runtime('The merge filter only works with arrays or hashes.');
560
    }
561

    
562
    return array_merge($arr1, $arr2);
563
}
564

    
565
/**
566
 * Slices a variable.
567
 *
568
 * @param Twig_Environment $env          A Twig_Environment instance
569
 * @param mixed            $item         A variable
570
 * @param integer          $start        Start of the slice
571
 * @param integer          $length       Size of the slice
572
 * @param Boolean          $preserveKeys Whether to preserve key or not (when the input is an array)
573
 *
574
 * @return mixed The sliced variable
575
 */
576
function twig_slice(Twig_Environment $env, $item, $start, $length = null, $preserveKeys = false)
577
{
578
    if ($item instanceof Traversable) {
579
        $item = iterator_to_array($item, false);
580
    }
581

    
582
    if (is_array($item)) {
583
        return array_slice($item, $start, $length, $preserveKeys);
584
    }
585

    
586
    $item = (string) $item;
587

    
588
    if (function_exists('mb_get_info') && null !== $charset = $env->getCharset()) {
589
        return mb_substr($item, $start, null === $length ? mb_strlen($item, $charset) - $start : $length, $charset);
590
    }
591

    
592
    return null === $length ? substr($item, $start) : substr($item, $start, $length);
593
}
594

    
595
/**
596
 * Joins the values to a string.
597
 *
598
 * The separator between elements is an empty string per default, you can define it with the optional parameter.
599
 *
600
 * <pre>
601
 *  {{ [1, 2, 3]|join('|') }}
602
 *  {# returns 1|2|3 #}
603
 *
604
 *  {{ [1, 2, 3]|join }}
605
 *  {# returns 123 #}
606
 * </pre>
607
 *
608
 * @param array  $value An array
609
 * @param string $glue  The separator
610
 *
611
 * @return string The concatenated string
612
 */
613
function twig_join_filter($value, $glue = '')
614
{
615
    if ($value instanceof Traversable) {
616
        $value = iterator_to_array($value, false);
617
    }
618

    
619
    return implode($glue, (array) $value);
620
}
621

    
622
// The '_default' filter is used internally to avoid using the ternary operator
623
// which costs a lot for big contexts (before PHP 5.4). So, on average,
624
// a function call is cheaper.
625
function _twig_default_filter($value, $default = '')
626
{
627
    if (twig_test_empty($value)) {
628
        return $default;
629
    }
630

    
631
    return $value;
632
}
633

    
634
/**
635
 * Returns the keys for the given array.
636
 *
637
 * It is useful when you want to iterate over the keys of an array:
638
 *
639
 * <pre>
640
 *  {% for key in array|keys %}
641
 *      {# ... #}
642
 *  {% endfor %}
643
 * </pre>
644
 *
645
 * @param array $array An array
646
 *
647
 * @return array The keys
648
 */
649
function twig_get_array_keys_filter($array)
650
{
651
    if (is_object($array) && $array instanceof Traversable) {
652
        return array_keys(iterator_to_array($array));
653
    }
654

    
655
    if (!is_array($array)) {
656
        return array();
657
    }
658

    
659
    return array_keys($array);
660
}
661

    
662
/**
663
 * Reverses a variable.
664
 *
665
 * @param Twig_Environment         $env          A Twig_Environment instance
666
 * @param array|Traversable|string $item         An array, a Traversable instance, or a string
667
 * @param Boolean                  $preserveKeys Whether to preserve key or not
668
 *
669
 * @return mixed The reversed input
670
 */
671
function twig_reverse_filter(Twig_Environment $env, $item, $preserveKeys = false)
672
{
673
    if (is_object($item) && $item instanceof Traversable) {
674
        return array_reverse(iterator_to_array($item), $preserveKeys);
675
    }
676

    
677
    if (is_array($item)) {
678
        return array_reverse($item, $preserveKeys);
679
    }
680

    
681
    if (null !== $charset = $env->getCharset()) {
682
        $string = (string) $item;
683

    
684
        if ('UTF-8' != $charset) {
685
            $item = twig_convert_encoding($string, 'UTF-8', $charset);
686
        }
687

    
688
        preg_match_all('/./us', $item, $matches);
689

    
690
        $string = implode('', array_reverse($matches[0]));
691

    
692
        if ('UTF-8' != $charset) {
693
            $string = twig_convert_encoding($string, $charset, 'UTF-8');
694
        }
695

    
696
        return $string;
697
    }
698

    
699
    return strrev((string) $item);
700
}
701

    
702
/**
703
 * Sorts an array.
704
 *
705
 * @param array $array An array
706
 */
707
function twig_sort_filter($array)
708
{
709
    asort($array);
710

    
711
    return $array;
712
}
713

    
714
/* used internally */
715
function twig_in_filter($value, $compare)
716
{
717
    if (is_array($compare)) {
718
        return in_array($value, $compare);
719
    } elseif (is_string($compare)) {
720
        if (!strlen((string) $value)) {
721
            return empty($compare);
722
        }
723

    
724
        return false !== strpos($compare, (string) $value);
725
    } elseif (is_object($compare) && $compare instanceof Traversable) {
726
        return in_array($value, iterator_to_array($compare, false));
727
    }
728

    
729
    return false;
730
}
731

    
732
/**
733
 * Escapes a string.
734
 *
735
 * @param Twig_Environment $env        A Twig_Environment instance
736
 * @param string           $string     The value to be escaped
737
 * @param string           $type       The escaping strategy
738
 * @param string           $charset    The charset
739
 * @param Boolean          $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
740
 */
741
function twig_escape_filter(Twig_Environment $env, $string, $type = 'html', $charset = null, $autoescape = false)
742
{
743
    if ($autoescape && is_object($string) && $string instanceof Twig_Markup) {
744
        return $string;
745
    }
746

    
747
    if (!is_string($string) && !(is_object($string) && method_exists($string, '__toString'))) {
748
        return $string;
749
    }
750

    
751
    if (null === $charset) {
752
        $charset = $env->getCharset();
753
    }
754

    
755
    $string = (string) $string;
756

    
757
    switch ($type) {
758
        case 'js':
759
            // escape all non-alphanumeric characters
760
            // into their \xHH or \uHHHH representations
761
            if ('UTF-8' != $charset) {
762
                $string = twig_convert_encoding($string, 'UTF-8', $charset);
763
            }
764

    
765
            if (null === $string = preg_replace_callback('#[^\p{L}\p{N} ]#u', '_twig_escape_js_callback', $string)) {
766
                throw new Twig_Error_Runtime('The string to escape is not a valid UTF-8 string.');
767
            }
768

    
769
            if ('UTF-8' != $charset) {
770
                $string = twig_convert_encoding($string, $charset, 'UTF-8');
771
            }
772

    
773
            return $string;
774

    
775
        case 'html':
776
            // see http://php.net/htmlspecialchars
777

    
778
            // Using a static variable to avoid initializing the array
779
            // each time the function is called. Moving the declaration on the
780
            // top of the function slow downs other escaping types.
781
            static $htmlspecialcharsCharsets = array(
782
                'iso-8859-1' => true, 'iso8859-1' => true,
783
                'iso-8859-15' => true, 'iso8859-15' => true,
784
                'utf-8' => true,
785
                'cp866' => true, 'ibm866' => true, '866' => true,
786
                'cp1251' => true, 'windows-1251' => true, 'win-1251' => true,
787
                '1251' => true,
788
                'cp1252' => true, 'windows-1252' => true, '1252' => true,
789
                'koi8-r' => true, 'koi8-ru' => true, 'koi8r' => true,
790
                'big5' => true, '950' => true,
791
                'gb2312' => true, '936' => true,
792
                'big5-hkscs' => true,
793
                'shift_jis' => true, 'sjis' => true, '932' => true,
794
                'euc-jp' => true, 'eucjp' => true,
795
                'iso8859-5' => true, 'iso-8859-5' => true, 'macroman' => true,
796
            );
797

    
798
            if (isset($htmlspecialcharsCharsets[strtolower($charset)])) {
799
                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
800
            }
801

    
802
            $string = twig_convert_encoding($string, 'UTF-8', $charset);
803
            $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
804

    
805
            return twig_convert_encoding($string, $charset, 'UTF-8');
806

    
807
        default:
808
            throw new Twig_Error_Runtime(sprintf('Invalid escape type "%s".', $type));
809
    }
810
}
811

    
812
/* used internally */
813
function twig_escape_filter_is_safe(Twig_Node $filterArgs)
814
{
815
    foreach ($filterArgs as $arg) {
816
        if ($arg instanceof Twig_Node_Expression_Constant) {
817
            return array($arg->getAttribute('value'));
818
        }
819

    
820
        return array();
821
    }
822

    
823
    return array('html');
824
}
825

    
826
if (function_exists('iconv')) {
827
    function twig_convert_encoding($string, $to, $from)
828
    {
829
        return iconv($from, $to, $string);
830
    }
831
} elseif (function_exists('mb_convert_encoding')) {
832
    function twig_convert_encoding($string, $to, $from)
833
    {
834
        return mb_convert_encoding($string, $to, $from);
835
    }
836
} else {
837
    function twig_convert_encoding($string, $to, $from)
838
    {
839
        throw new Twig_Error_Runtime('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
840
    }
841
}
842

    
843
function _twig_escape_js_callback($matches)
844
{
845
    $char = $matches[0];
846

    
847
    // \xHH
848
    if (!isset($char[1])) {
849
        return '\\x'.substr('00'.bin2hex($char), -2);
850
    }
851

    
852
    // \uHHHH
853
    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
854

    
855
    return '\\u'.substr('0000'.bin2hex($char), -4);
856
}
857

    
858
// add multibyte extensions if possible
859
if (function_exists('mb_get_info')) {
860
    /**
861
     * Returns the length of a variable.
862
     *
863
     * @param Twig_Environment $env   A Twig_Environment instance
864
     * @param mixed            $thing A variable
865
     *
866
     * @return integer The length of the value
867
     */
868
    function twig_length_filter(Twig_Environment $env, $thing)
869
    {
870
        return is_scalar($thing) ? mb_strlen($thing, $env->getCharset()) : count($thing);
871
    }
872

    
873
    /**
874
     * Converts a string to uppercase.
875
     *
876
     * @param Twig_Environment $env    A Twig_Environment instance
877
     * @param string           $string A string
878
     *
879
     * @return string The uppercased string
880
     */
881
    function twig_upper_filter(Twig_Environment $env, $string)
882
    {
883
        if (null !== ($charset = $env->getCharset())) {
884
            return mb_strtoupper($string, $charset);
885
        }
886

    
887
        return strtoupper($string);
888
    }
889

    
890
    /**
891
     * Converts a string to lowercase.
892
     *
893
     * @param Twig_Environment $env    A Twig_Environment instance
894
     * @param string           $string A string
895
     *
896
     * @return string The lowercased string
897
     */
898
    function twig_lower_filter(Twig_Environment $env, $string)
899
    {
900
        if (null !== ($charset = $env->getCharset())) {
901
            return mb_strtolower($string, $charset);
902
        }
903

    
904
        return strtolower($string);
905
    }
906

    
907
    /**
908
     * Returns a titlecased string.
909
     *
910
     * @param Twig_Environment $env    A Twig_Environment instance
911
     * @param string           $string A string
912
     *
913
     * @return string The titlecased string
914
     */
915
    function twig_title_string_filter(Twig_Environment $env, $string)
916
    {
917
        if (null !== ($charset = $env->getCharset())) {
918
            return mb_convert_case($string, MB_CASE_TITLE, $charset);
919
        }
920

    
921
        return ucwords(strtolower($string));
922
    }
923

    
924
    /**
925
     * Returns a capitalized string.
926
     *
927
     * @param Twig_Environment $env    A Twig_Environment instance
928
     * @param string           $string A string
929
     *
930
     * @return string The capitalized string
931
     */
932
    function twig_capitalize_string_filter(Twig_Environment $env, $string)
933
    {
934
        if (null !== ($charset = $env->getCharset())) {
935
            return mb_strtoupper(mb_substr($string, 0, 1, $charset), $charset).
936
                         mb_strtolower(mb_substr($string, 1, mb_strlen($string, $charset), $charset), $charset);
937
        }
938

    
939
        return ucfirst(strtolower($string));
940
    }
941
}
942
// and byte fallback
943
else
944
{
945
    /**
946
     * Returns the length of a variable.
947
     *
948
     * @param Twig_Environment $env   A Twig_Environment instance
949
     * @param mixed            $thing A variable
950
     *
951
     * @return integer The length of the value
952
     */
953
    function twig_length_filter(Twig_Environment $env, $thing)
954
    {
955
        return is_scalar($thing) ? strlen($thing) : count($thing);
956
    }
957

    
958
    /**
959
     * Returns a titlecased string.
960
     *
961
     * @param Twig_Environment $env    A Twig_Environment instance
962
     * @param string           $string A string
963
     *
964
     * @return string The titlecased string
965
     */
966
    function twig_title_string_filter(Twig_Environment $env, $string)
967
    {
968
        return ucwords(strtolower($string));
969
    }
970

    
971
    /**
972
     * Returns a capitalized string.
973
     *
974
     * @param Twig_Environment $env    A Twig_Environment instance
975
     * @param string           $string A string
976
     *
977
     * @return string The capitalized string
978
     */
979
    function twig_capitalize_string_filter(Twig_Environment $env, $string)
980
    {
981
        return ucfirst(strtolower($string));
982
    }
983
}
984

    
985
/* used internally */
986
function twig_ensure_traversable($seq)
987
{
988
    if ($seq instanceof Traversable || is_array($seq)) {
989
        return $seq;
990
    }
991

    
992
    return array();
993
}
994

    
995
/**
996
 * Checks if a variable is empty.
997
 *
998
 * <pre>
999
 * {# evaluates to true if the foo variable is null, false, or the empty string #}
1000
 * {% if foo is empty %}
1001
 *     {# ... #}
1002
 * {% endif %}
1003
 * </pre>
1004
 *
1005
 * @param mixed $value A variable
1006
 *
1007
 * @return Boolean true if the value is empty, false otherwise
1008
 */
1009
function twig_test_empty($value)
1010
{
1011
    if ($value instanceof Countable) {
1012
        return 0 == count($value);
1013
    }
1014

    
1015
    return false === $value || (empty($value) && '0' != $value);
1016
}
1017

    
1018
/**
1019
 * Checks if a variable is traversable.
1020
 *
1021
 * <pre>
1022
 * {# evaluates to true if the foo variable is an array or a traversable object #}
1023
 * {% if foo is traversable %}
1024
 *     {# ... #}
1025
 * {% endif %}
1026
 * </pre>
1027
 *
1028
 * @param mixed $value A variable
1029
 *
1030
 * @return Boolean true if the value is traversable
1031
 */
1032
function twig_test_iterable($value)
1033
{
1034
    return $value instanceof Traversable || is_array($value);
1035
}
(1-1/5)