• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

timber / timber / 5690593717

pending completion
5690593717

Pull #1617

github

web-flow
Merge f587ceffa into b563d274e
Pull Request #1617: 2.x

4433 of 4433 new or added lines in 57 files covered. (100.0%)

3931 of 4433 relevant lines covered (88.68%)

58.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.24
/src/Twig.php
1
<?php
2

3
namespace Timber;
4

5
use DateInterval;
6
use DateTime;
7
use DateTimeInterface;
8
use Exception;
9

10
use Timber\Factory\PostFactory;
11
use Timber\Factory\TermFactory;
12
use Twig\Environment;
13
use Twig\Extension\CoreExtension;
14
use Twig\TwigFilter;
15
use Twig\TwigFunction;
16

17
/**
18
 * Class Twig
19
 */
20
class Twig
21
{
22
    public static $dir_name;
23

24
    /**
25
     * @codeCoverageIgnore
26
     */
27
    public static function init()
28
    {
29
        $self = new self();
30

31
        \add_filter('timber/twig', [$self, 'add_timber_functions']);
32
        \add_filter('timber/twig', [$self, 'add_timber_filters']);
33
        \add_filter('timber/twig', [$self, 'add_timber_escapers']);
34

35
        \add_filter('timber/loader/twig', [$self, 'set_defaults']);
36
    }
37

38
    /**
39
     * Get Timber default functions
40
     *
41
     * @return array Default Timber functions
42
     */
43
    public function get_timber_functions()
44
    {
45
        $post_factory = new PostFactory();
406✔
46
        $termFactory = new TermFactory();
406✔
47

48
        $functions = [
406✔
49
            'action' => [
406✔
50
                'callable' => function ($action_name, ...$args) {
406✔
51
                    \do_action_ref_array($action_name, $args);
1✔
52
                },
406✔
53
            ],
406✔
54
            'function' => [
406✔
55
                'callable' => [$this, 'exec_function'],
406✔
56
            ],
406✔
57
            'fn' => [
406✔
58
                'callable' => [$this, 'exec_function'],
406✔
59
            ],
406✔
60
            'get_post' => [
406✔
61
                'callable' => [Timber::class, 'get_post'],
406✔
62
            ],
406✔
63
            'get_image' => [
406✔
64
                'callable' => [Timber::class, 'get_image'],
406✔
65
            ],
406✔
66
            'get_external_image' => [
406✔
67
                'callable' => [Timber::class, 'get_external_image'],
406✔
68
            ],
406✔
69
            'get_attachment' => [
406✔
70
                'callable' => [Timber::class, 'get_attachment'],
406✔
71
            ],
406✔
72
            'get_posts' => [
406✔
73
                'callable' => [Timber::class, 'get_posts'],
406✔
74
            ],
406✔
75
            'get_attachment_by' => [
406✔
76
                'callable' => [Timber::class, 'get_attachment_by'],
406✔
77
            ],
406✔
78
            'get_term' => [
406✔
79
                'callable' => [Timber::class, 'get_term'],
406✔
80
            ],
406✔
81
            'get_terms' => [
406✔
82
                'callable' => [Timber::class, 'get_terms'],
406✔
83
            ],
406✔
84
            'get_user' => [
406✔
85
                'callable' => [Timber::class, 'get_user'],
406✔
86
            ],
406✔
87
            'get_users' => [
406✔
88
                'callable' => [Timber::class, 'get_users'],
406✔
89
            ],
406✔
90
            'get_comment' => [
406✔
91
                'callable' => [Timber::class, 'get_comment'],
406✔
92
            ],
406✔
93
            'get_comments' => [
406✔
94
                'callable' => [Timber::class, 'get_comments'],
406✔
95
            ],
406✔
96
            'Post' => [
406✔
97
                'callable' => function ($post_id) use ($post_factory) {
406✔
98
                    Helper::deprecated('{{ Post() }}', '{{ get_post() }} or {{ get_posts() }}', '2.0.0');
2✔
99
                    return $post_factory->from($post_id);
2✔
100
                },
406✔
101
                'options' => [
406✔
102
                    'deprecated' => true,
406✔
103
                ],
406✔
104
            ],
406✔
105
            'TimberPost' => [
406✔
106
                'callable' => function ($post_id) use ($post_factory) {
406✔
107
                    Helper::deprecated('{{ TimberPost() }}', '{{ get_post() }} or {{ get_posts() }}', '2.0.0');
2✔
108
                    return $post_factory->from($post_id);
2✔
109
                },
406✔
110
                'options' => [
406✔
111
                    'deprecated' => true,
406✔
112
                ],
406✔
113
            ],
406✔
114
            'Image' => [
406✔
115
                'callable' => function ($post_id) use ($post_factory) {
406✔
116
                    Helper::deprecated('{{ Image() }}', '{{ get_image() }}, {{ get_post() }}, {{ get_posts() }}, {{ get_attachment() }} or {{ get_attachment_by() }}', '2.0.0');
2✔
117
                    return $post_factory->from($post_id);
2✔
118
                },
406✔
119
                'options' => [
406✔
120
                    'deprecated' => true,
406✔
121
                ],
406✔
122
            ],
406✔
123
            'TimberImage' => [
406✔
124
                'callable' => function ($post_id) use ($post_factory) {
406✔
125
                    Helper::deprecated('{{ TimberImage() }}', '{{ get_image() }}, {{ get_post() }}, {{ get_posts() }}, {{ get_attachment() }} or {{ get_attachment_by() }}', '2.0.0');
2✔
126
                    return $post_factory->from($post_id);
2✔
127
                },
406✔
128
                'options' => [
406✔
129
                    'deprecated' => true,
406✔
130
                ],
406✔
131
            ],
406✔
132
            'Term' => [
406✔
133
                'callable' => function ($term_id) use ($termFactory) {
406✔
134
                    Helper::deprecated('{{ Term() }}', '{{ get_term() }} or {{ get_terms() }}', '2.0.0');
2✔
135
                    return $termFactory->from($term_id);
2✔
136
                },
406✔
137
                'options' => [
406✔
138
                    'deprecated' => true,
406✔
139
                ],
406✔
140
            ],
406✔
141
            'TimberTerm' => [
406✔
142
                'callable' => function ($term_id) use ($termFactory) {
406✔
143
                    Helper::deprecated('{{ TimberTerm() }}', '{{ get_term() }} or {{ get_terms() }}', '2.0.0');
2✔
144
                    return $termFactory->from($term_id);
2✔
145
                },
406✔
146
                'options' => [
406✔
147
                    'deprecated' => true,
406✔
148
                ],
406✔
149
            ],
406✔
150
            'User' => [
406✔
151
                'callable' => function ($user_id) {
406✔
152
                    Helper::deprecated('{{ User() }}', '{{ get_user() }} or {{ get_users() }}', '2.0.0');
2✔
153
                    return Timber::get_user($user_id);
2✔
154
                },
406✔
155
                'options' => [
406✔
156
                    'deprecated' => true,
406✔
157
                ],
406✔
158
            ],
406✔
159
            'TimberUser' => [
406✔
160
                'callable' => function ($user_id) {
406✔
161
                    Helper::deprecated('{{ TimberUser() }}', '{{ get_user() }} or {{ get_users() }}', '2.0.0');
2✔
162
                    return Timber::get_user($user_id);
2✔
163
                },
406✔
164
                'options' => [
406✔
165
                    'deprecated' => true,
406✔
166
                ],
406✔
167
            ],
406✔
168
            'shortcode' => [
406✔
169
                'callable' => 'do_shortcode',
406✔
170
            ],
406✔
171
            'bloginfo' => [
406✔
172
                'callable' => 'bloginfo',
406✔
173
            ],
406✔
174

175
            // Translation functions.
176
            '__' => [
406✔
177
                'callable' => '__',
406✔
178
            ],
406✔
179
            'translate' => [
406✔
180
                'callable' => 'translate',
406✔
181
            ],
406✔
182
            '_e' => [
406✔
183
                'callable' => '_e',
406✔
184
            ],
406✔
185
            '_n' => [
406✔
186
                'callable' => '_n',
406✔
187
            ],
406✔
188
            '_x' => [
406✔
189
                'callable' => '_x',
406✔
190
            ],
406✔
191
            '_ex' => [
406✔
192
                'callable' => '_ex',
406✔
193
            ],
406✔
194
            '_nx' => [
406✔
195
                'callable' => '_nx',
406✔
196
            ],
406✔
197
            '_n_noop' => [
406✔
198
                'callable' => '_n_noop',
406✔
199
            ],
406✔
200
            '_nx_noop' => [
406✔
201
                'callable' => '_nx_noop',
406✔
202
            ],
406✔
203
            'translate_nooped_plural' => [
406✔
204
                'callable' => 'translate_nooped_plural',
406✔
205
            ],
406✔
206
        ];
406✔
207

208
        /**
209
         * Filters the functions that are added to Twig.
210
         *
211
         * The `$functions` array is an associative array with the filter name as a key and an
212
         * arguments array as the value. In the arguments array, you pass the function to call with
213
         * a `callable` entry.
214
         *
215
         * This is an alternative filter that you can use instead of adding your function in the
216
         * `timber/twig` filter.
217
         *
218
         * @api
219
         * @since 2.0.0
220
         * @example
221
         * ```php
222
         * add_filter( 'timber/twig/functions', function( $functions ) {
223
         *     // Add your own function.
224
         *     $functions['url_to_domain'] = [
225
         *         'callable' => 'url_to_domain',
226
         *     ];
227
         *
228
         *     // Replace a function.
229
         *     $functions['get_image'] = [
230
         *         'callable' => 'custom_image_get',
231
         *     ];
232
         *
233
         *     // Remove a function.
234
         *     unset( $functions['bloginfo'] );
235
         *
236
         *     return $functions;
237
         * } );
238
         * ```
239
         *
240
         * @param array $functions
241
         */
242
        $functions = \apply_filters('timber/twig/functions', $functions);
406✔
243

244
        return $functions;
406✔
245
    }
246

247
    /**
248
     * Adds Timber-specific functions to Twig.
249
     *
250
     * @param \Twig\Environment $twig The Twig Environment.
251
     *
252
     * @return \Twig\Environment
253
     */
254
    public function add_timber_functions($twig)
255
    {
256
        foreach ($this->get_timber_functions() as $name => $function) {
406✔
257
            $twig->addFunction(
406✔
258
                new TwigFunction(
406✔
259
                    $name,
406✔
260
                    $function['callable'],
406✔
261
                    $function['options'] ?? []
406✔
262
                )
406✔
263
            );
406✔
264
        }
265

266
        return $twig;
406✔
267
    }
268

269
    /**
270
     * Get Timber default filters
271
     *
272
     * @return array Default Timber filters
273
     */
274
    public function get_timber_filters()
275
    {
276
        $filters = [
406✔
277
            /* image filters */
278
            'resize' => [
406✔
279
                'callable' => ['Timber\ImageHelper', 'resize'],
406✔
280
            ],
406✔
281
            'retina' => [
406✔
282
                'callable' => ['Timber\ImageHelper', 'retina_resize'],
406✔
283
            ],
406✔
284
            'letterbox' => [
406✔
285
                'callable' => ['Timber\ImageHelper', 'letterbox'],
406✔
286
            ],
406✔
287
            'tojpg' => [
406✔
288
                'callable' => ['Timber\ImageHelper', 'img_to_jpg'],
406✔
289
            ],
406✔
290
            'towebp' => [
406✔
291
                'callable' => ['Timber\ImageHelper', 'img_to_webp'],
406✔
292
            ],
406✔
293

294
            // Debugging filters.
295
            'get_class' => [
406✔
296
                'callable' => function ($obj) {
406✔
297
                    Helper::deprecated('{{ my_object | get_class }}', "{{ function('get_class', my_object) }}", '2.0.0');
1✔
298
                    return \get_class($obj);
1✔
299
                },
406✔
300
                'options' => [
406✔
301
                    'deprecated' => true,
406✔
302
                ],
406✔
303
            ],
406✔
304
            'print_r' => [
406✔
305
                'callable' => function ($arr) {
406✔
306
                    Helper::deprecated('{{ my_object | print_r }}', '{{ dump(my_object) }}', '2.0.0');
×
307
                    return \print_r($arr, true);
×
308
                },
406✔
309
                'options' => [
406✔
310
                    'deprecated' => true,
406✔
311
                ],
406✔
312
            ],
406✔
313

314
            // Other filters.
315
            'stripshortcodes' => [
406✔
316
                'callable' => 'strip_shortcodes',
406✔
317
            ],
406✔
318
            'array' => [
406✔
319
                'callable' => [$this, 'to_array'],
406✔
320
            ],
406✔
321
            'excerpt' => [
406✔
322
                'callable' => 'wp_trim_words',
406✔
323
            ],
406✔
324
            'excerpt_chars' => [
406✔
325
                'callable' => ['Timber\TextHelper', 'trim_characters'],
406✔
326
            ],
406✔
327
            'function' => [
406✔
328
                'callable' => [$this, 'exec_function'],
406✔
329
            ],
406✔
330
            'pretags' => [
406✔
331
                'callable' => [$this, 'twig_pretags'],
406✔
332
            ],
406✔
333
            'sanitize' => [
406✔
334
                'callable' => 'sanitize_title',
406✔
335
            ],
406✔
336
            'shortcodes' => [
406✔
337
                'callable' => 'do_shortcode',
406✔
338
            ],
406✔
339
            'wpautop' => [
406✔
340
                'callable' => 'wpautop',
406✔
341
            ],
406✔
342
            'list' => [
406✔
343
                'callable' => [$this, 'add_list_separators'],
406✔
344
            ],
406✔
345
            'pluck' => [
406✔
346
                'callable' => ['Timber\Helper', 'pluck'],
406✔
347
            ],
406✔
348
            'wp_list_filter' => [
406✔
349
                'callable' => ['Timber\Helper', 'wp_list_filter'],
406✔
350
            ],
406✔
351

352
            'relative' => [
406✔
353
                'callable' => function ($link) {
406✔
354
                    return URLHelper::get_rel_url($link, true);
1✔
355
                },
406✔
356
            ],
406✔
357

358
            /**
359
             * Date and Time filters.
360
             */
361
            'date' => [
406✔
362
                'callable' => [$this, 'twig_date_format_filter'],
406✔
363
                'options' => [
406✔
364
                    'needs_environment' => true,
406✔
365
                ],
406✔
366
            ],
406✔
367
            'time_ago' => [
406✔
368
                'callable' => ['Timber\DateTimeHelper', 'time_ago'],
406✔
369
            ],
406✔
370
            'truncate' => [
406✔
371
                'callable' => function ($text, $len) {
406✔
372
                    return TextHelper::trim_words($text, $len);
3✔
373
                },
406✔
374
            ],
406✔
375

376
            // Actions and filters.
377
            'apply_filters' => [
406✔
378
                'callable' => function () {
406✔
379
                    $args = \func_get_args();
1✔
380
                    $tag = \current(\array_splice($args, 1, 1));
1✔
381

382
                    return \apply_filters_ref_array($tag, $args);
1✔
383
                },
406✔
384
            ],
406✔
385
        ];
406✔
386

387
        /**
388
         * Filters the filters that are added to Twig.
389
         *
390
         * The `$filters` array is an associative array with the filter name as a key and an
391
         * arguments array as the value. In the arguments array, you pass the function to call with
392
         * a `callable` entry.
393
         *
394
         * This is an alternative filter that you can use instead of adding your filter in the
395
         * `timber/twig` filter.
396
         *
397
         * @api
398
         * @since 2.0.0
399
         * @example
400
         * ```php
401
         * add_filter( 'timber/twig/default_filters', function( $filters ) {
402
         *     // Add your own filter.
403
         *     $filters['price'] = [
404
         *         'callable' => 'format_price',
405
         *     ];
406
         *
407
         *     // Replace a filter.
408
         *     $filters['list'] = [
409
         *         'callable' => 'custom_list_filter',
410
         *     ];
411
         *
412
         *     // Remove a filter.
413
         *     unset( $filters['list'] );
414
         *
415
         *     return $filters;
416
         * } );
417
         * ```
418
         *
419
         * @param array $filters
420
         */
421
        $filters = \apply_filters('timber/twig/filters', $filters);
406✔
422

423
        return $filters;
406✔
424
    }
425

426
    /**
427
     * Adds filters to Twig.
428
     *
429
     * @param \Twig\Environment $twig The Twig Environment.
430
     *
431
     * @return \Twig\Environment
432
     */
433
    public function add_timber_filters($twig)
434
    {
435
        foreach ($this->get_timber_filters() as $name => $function) {
406✔
436
            $twig->addFilter(
406✔
437
                new TwigFilter(
406✔
438
                    $name,
406✔
439
                    $function['callable'],
406✔
440
                    $function['options'] ?? []
406✔
441
                )
406✔
442
            );
406✔
443
        }
444

445
        return $twig;
406✔
446
    }
447

448
    /**
449
     * Adds escapers.
450
     *
451
     * @param \Twig\Environment $twig The Twig Environment.
452
     * @return \Twig\Environment
453
     */
454
    public function add_timber_escapers($twig)
455
    {
456
        $esc_url = function (Environment $env, $string) {
406✔
457
            return \esc_url($string);
2✔
458
        };
406✔
459

460
        $wp_kses_post = function (Environment $env, $string) {
406✔
461
            return \wp_kses_post($string);
1✔
462
        };
406✔
463

464
        $esc_html = function (Environment $env, $string) {
406✔
465
            return \esc_html($string);
2✔
466
        };
406✔
467

468
        $esc_js = function (Environment $env, $string) {
406✔
469
            return \esc_js($string);
1✔
470
        };
406✔
471

472
        if (\class_exists('Twig\Extension\EscaperExtension')) {
406✔
473
            $escaper_extension = $twig->getExtension('Twig\Extension\EscaperExtension');
406✔
474
            $escaper_extension->setEscaper('esc_url', $esc_url);
406✔
475
            $escaper_extension->setEscaper('wp_kses_post', $wp_kses_post);
406✔
476
            $escaper_extension->setEscaper('esc_html', $esc_html);
406✔
477
            $escaper_extension->setEscaper('esc_js', $esc_js);
406✔
478
        }
479
        return $twig;
406✔
480
    }
481

482
    /**
483
     * Overwrite Twig defaults.
484
     *
485
     * Makes Twig compatible with how WordPress handles dates, timezones, numbers and perhaps other items in
486
     * the future
487
     *
488
     * @since 2.0.0
489
     *
490
     * @throws \Twig\Error\RuntimeError
491
     * @param \Twig\Environment $twig Twig Environment.
492
     *
493
     * @return \Twig\Environment
494
     */
495
    public function set_defaults(Environment $twig)
496
    {
497
        $twig->getExtension(CoreExtension::class)->setDateFormat(\get_option('date_format'), '%d days');
406✔
498
        $twig->getExtension(CoreExtension::class)->setTimezone(\wp_timezone_string());
406✔
499

500
        /** @see https://developer.wordpress.org/reference/functions/number_format_i18n/ */
501
        global $wp_locale;
502
        if (isset($wp_locale)) {
406✔
503
            $twig->getExtension(CoreExtension::class)->setNumberFormat(0, $wp_locale->number_format['decimal_point'], $wp_locale->number_format['thousands_sep']);
406✔
504
        }
505

506
        return $twig;
406✔
507
    }
508

509
    /**
510
     * Converts a date to the given format.
511
     *
512
     * @internal
513
     * @since 2.0.0
514
     * @see  twig_date_format_filter()
515
     * @link https://twig.symfony.com/doc/2.x/filters/date.html
516
     *
517
     * @throws Exception
518
     *
519
     * @param \Twig\Environment         $env      Twig Environment.
520
     * @param null|string|int|DateTime $date     A date.
521
     * @param null|string               $format   Optional. PHP date format. Will return the
522
     *                                            current date as a DateTimeImmutable object by
523
     *                                            default.
524
     * @param null                      $timezone Optional. The target timezone. Use `null` to use
525
     *                                            the default or
526
     *                                            `false` to leave the timezone unchanged.
527
     *
528
     * @return false|string A formatted date.
529
     */
530
    public function twig_date_format_filter(Environment $env, $date = null, $format = null, $timezone = null)
531
    {
532
        // Support for DateInterval.
533
        if ($date instanceof DateInterval) {
68✔
534
            if (null === $format) {
5✔
535
                $format = $env->getExtension(CoreExtension::class)->getDateFormat()[1];
2✔
536
            }
537

538
            return $date->format($format);
5✔
539
        }
540

541
        if (null === $date || 'now' === $date) {
63✔
542
            return DateTimeHelper::wp_date($format, null);
2✔
543
        }
544

545
        /**
546
         * If a string is given and it’s not a timestamp (e.g. "2010-01-28T15:00:00+04:00", try creating a DateTime
547
         * object and read the timezone from that string.
548
         */
549
        if (\is_string($date) && !\ctype_digit($date)) {
61✔
550
            $date_obj = \date_create($date);
15✔
551

552
            if ($date_obj) {
15✔
553
                $date = $date_obj;
15✔
554
            }
555
        }
556

557
        /**
558
         * Check for `false` parameter in |date filter in Twig
559
         *
560
         * @link https://twig.symfony.com/doc/2.x/filters/date.html#timezone
561
         */
562
        if (false === $timezone && $date instanceof DateTimeInterface) {
61✔
563
            $timezone = $date->getTimezone();
7✔
564
        }
565

566
        return DateTimeHelper::wp_date($format, $date, $timezone);
61✔
567
    }
568

569
    /**
570
     *
571
     *
572
     * @param mixed   $arr
573
     * @return array
574
     */
575
    public function to_array($arr)
576
    {
577
        if (\is_array($arr)) {
2✔
578
            return $arr;
1✔
579
        }
580
        $arr = [$arr];
1✔
581
        return $arr;
1✔
582
    }
583

584
    /**
585
     *
586
     *
587
     * @param string  $function_name
588
     * @return mixed
589
     */
590
    public function exec_function($function_name)
591
    {
592
        $args = \func_get_args();
8✔
593
        \array_shift($args);
8✔
594
        if (\is_string($function_name)) {
8✔
595
            $function_name = \trim($function_name);
8✔
596
        }
597
        return \call_user_func_array($function_name, ($args));
8✔
598
    }
599

600
    /**
601
     *
602
     *
603
     * @param string  $content
604
     * @return string
605
     */
606
    public function twig_pretags($content)
607
    {
608
        return \preg_replace_callback('|<pre.*>(.*)</pre|isU', [&$this, 'convert_pre_entities'], $content);
1✔
609
    }
610

611
    /**
612
     *
613
     *
614
     * @param array   $matches
615
     * @return string
616
     */
617
    public function convert_pre_entities($matches)
618
    {
619
        return \str_replace($matches[1], \htmlentities($matches[1]), $matches[0]);
1✔
620
    }
621

622
    /**
623
     * Formats a date.
624
     *
625
     * @deprecated 2.0.0
626
     *
627
     * @param null|string|false    $format Optional. PHP date format. Will use the `date_format`
628
     *                                     option as a default.
629
     * @param string|int|DateTime $date   A date.
630
     *
631
     * @return string
632
     */
633
    public function intl_date($date, $format = null)
634
    {
635
        Helper::deprecated('intl_date', 'DateTimeHelper::wp_date', '2.0.0');
×
636

637
        return DateTimeHelper::wp_date($format, $date);
×
638
    }
639

640
    /**
641
     *
642
     * @deprecated 2.0.0
643
     *
644
     * Returns the difference between two times in a human readable format.
645
     *
646
     * Differentiates between past and future dates.
647
     *
648
     * @see \human_time_diff()
649
     *
650
     * @param int|string $from          Base date as a timestamp or a date string.
651
     * @param int|string $to            Optional. Date to calculate difference to as a timestamp or
652
     *                                  a date string. Default to current time.
653
     * @param string     $format_past   Optional. String to use for past dates. To be used with
654
     *                                  `sprintf()`. Default `%s ago`.
655
     * @param string     $format_future Optional. String to use for future dates. To be used with
656
     *                                  `sprintf()`. Default `%s from now`.
657
     *
658
     * @return string
659
     */
660
    public static function time_ago($from, $to = null, $format_past = null, $format_future = null)
661
    {
662
        Helper::deprecated('time_ago', 'DateTimeHelper::time_ago', '2.0.0');
×
663

664
        return DateTimeHelper::time_ago($from, $to, $format_past, $format_future);
×
665
    }
666

667
    /**
668
     * @param array $arr
669
     * @param string $first_delimiter
670
     * @param string $second_delimiter
671
     * @return string
672
     */
673
    public function add_list_separators($arr, $first_delimiter = ',', $second_delimiter = ' and')
674
    {
675
        $length = \count($arr);
2✔
676
        $list = '';
2✔
677
        foreach ($arr as $index => $item) {
2✔
678
            if ($index < $length - 2) {
2✔
679
                $delimiter = $first_delimiter . ' ';
2✔
680
            } elseif ($index == $length - 2) {
2✔
681
                $delimiter = $second_delimiter . ' ';
2✔
682
            } else {
683
                $delimiter = '';
2✔
684
            }
685
            $list = $list . $item . $delimiter;
2✔
686
        }
687
        return $list;
2✔
688
    }
689
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc