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

timber / timber / 5690057835

pending completion
5690057835

push

github

nlemoine
Merge branch '2.x' of github.com:timber/timber into 2.x-refactor-file-models

# Conflicts:
#	src/Attachment.php
#	src/ExternalImage.php
#	src/FileSize.php
#	src/URLHelper.php

1134 of 1134 new or added lines in 55 files covered. (100.0%)

3923 of 4430 relevant lines covered (88.56%)

59.08 hits per line

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

97.74
/src/PostExcerpt.php
1
<?php
2

3
namespace Timber;
4

5
/**
6
 * The PostExcerpt class lets a user modify a post preview/excerpt to their liking.
7
 *
8
 * It’s designed to be used through the `Timber\Post::excerpt()` method. The public methods of this
9
 * class all return the object itself, which means that this is a **chainable object**. This means
10
 * that you could change the output of the excerpt by **adding more methods**. But you can also pass
11
 * in your arguments to the object constructor or to `Timber\Post::excerpt()`.
12
 *
13
 * By default, the excerpt will
14
 *
15
 * - have a length of 50 words, which will be forced, even if a longer excerpt is set on the post.
16
 * - be stripped of all HTML tags.
17
 * - have an ellipsis (…) as the end of the text.
18
 * - have a "Read More" link appended, if there’s more to read in the post content.
19
 *
20
 * One thing to note: If the excerpt already contains all of the text that can also be found in the
21
 * post’s content, then the read more link as well as the string to use as the end will not be
22
 * added.
23
 *
24
 * This class will also handle cases where you use the `<!-- more -->` tag inside your post content.
25
 * You can also change the text used for the read more link by adding your desired text to the
26
 * `<!-- more -->` tag. Here’s an example: `<!-- more Start your journey -->`.
27
 *
28
 * You can change the defaults that are used for excerpts through the
29
 * [`timber/post/excerpt/defaults`](https://timber.github.io/docs/v2/hooks/filters/#timber/post/excerpts/defaults)
30
 * filter.
31
 *
32
 * @api
33
 * @since 1.0.4
34
 * @see \Timber\Post::excerpt()
35
 * @example
36
 * ```twig
37
 * {# Use default excerpt #}
38
 * <p>{{ post.excerpt }}</p>
39
 *
40
 * {# Preferred method: Use hash notation to pass arguments. #}
41
 * <div>{{ post.excerpt({ words: 100, read_more: 'Keep reading' }) }}</div>
42
 *
43
 * {# Change the post excerpt text only #}
44
 * <p>{{ post.excerpt.read_more('Continue Reading') }}</p>
45
 *
46
 * {# Additionally restrict the length to 50 words #}
47
 * <p>{{ post.excerpt.length(50).read_more('Continue Reading') }}</p>
48
 * ```
49
 */
50
class PostExcerpt
51
{
52
    /**
53
     * Post.
54
     *
55
     * @var \Timber\Post
56
     */
57
    protected $post;
58

59
    /**
60
     * Excerpt end.
61
     *
62
     * @var string
63
     */
64
    protected $end = '&hellip;';
65

66
    /**
67
     * Force length.
68
     *
69
     * @var bool
70
     */
71
    protected $force = false;
72

73
    /**
74
     * Length in words.
75
     *
76
     * @var int
77
     */
78
    protected $length = 50;
79

80
    /**
81
     * Length in characters.
82
     *
83
     * @var int|bool
84
     */
85
    protected $char_length = false;
86

87
    /**
88
     * Read more text.
89
     *
90
     * @var string
91
     */
92
    protected $read_more = 'Read More';
93

94
    /**
95
     * HTML tag stripping behavior.
96
     *
97
     * @var string|bool
98
     */
99
    protected $strip = true;
100

101
    /**
102
     * Whether a read more link should be added even if the excerpt isn’t trimmed (when the excerpt
103
     * isn’t shorter than the post’s content).
104
     *
105
     * @since 2.0.0
106
     * @var bool
107
     */
108
    protected $always_add_read_more = false;
109

110
    /**
111
     * Whether the end string should be added even if the excerpt isn’t trimmed (when the excerpt
112
     * isn’t shorter than the post’s content).
113
     *
114
     * @since 2.0.0
115
     * @var bool
116
     */
117
    protected $always_add_end = false;
118

119
    /**
120
     * Destroy tags.
121
     *
122
     * @var array List of tags that should always be destroyed.
123
     */
124
    protected $destroy_tags = ['script', 'style'];
125

126
    /**
127
     * PostExcerpt constructor.
128
     *
129
     * @api
130
     *
131
     * @param \Timber\Post $post The post to pull the excerpt from.
132
     * @param array        $options {
133
     *     An array of configuration options for generating the excerpt. Default empty.
134
     *
135
     *     @type int      $words     Number of words in the excerpt. Default `50`.
136
     *     @type int|bool $chars     Number of characters in the excerpt. Default `false` (no
137
     *                               character limit).
138
     *     @type string   $end       String to append to the end of the excerpt. Default '&hellip;'
139
     *                               (HTML ellipsis character).
140
     *     @type bool     $force     Whether to shorten the excerpt to the length/word count
141
     *                               specified, even if an editor wrote a manual excerpt longer
142
     *                               than the set length. Default `false`.
143
     *     @type bool     $strip     Whether to strip HTML tags. Default `true`.
144
     *     @type string   $read_more String for what the "Read More" text should be. Default
145
     *                               'Read More'.
146
     *     @type bool     $always_add_read_more Whether a read more link should be added even if the
147
     *                                          excerpt isn’t trimmed (when the excerpt isn’t
148
     *                                          shorter than the post’s content). Default `false`.
149
     *     @type bool     $always_add_end       Whether the end string should be added even if the
150
     *                                          excerpt isn’t trimmed (when the excerpt isn’t
151
     *                                          shorter than the post’s content). Default `false`.
152
     * }
153
     */
154
    public function __construct($post, array $options = [])
155
    {
156
        $this->post = $post;
43✔
157

158
        $defaults = [
43✔
159
            'words' => 50,
43✔
160
            'chars' => false,
43✔
161
            'end' => '&hellip;',
43✔
162
            'force' => false,
43✔
163
            'strip' => true,
43✔
164
            'read_more' => 'Read More',
43✔
165
            'always_add_read_more' => false,
43✔
166
            'always_add_end' => false,
43✔
167
        ];
43✔
168

169
        /**
170
         * Filters the default options used for post excerpts.
171
         *
172
         * @since 2.0.0
173
         * @example
174
         * ```php
175
         * add_filter( 'timber/post/excerpt/defaults', function( $defaults ) {
176
         *     // Only add a read more link if the post content isn’t longer than the excerpt.
177
         *     $defaults['always_add_read_more'] = false;
178
         *
179
         *     // Set a default character limit.
180
         *     $defaults['words'] = 240;
181
         *
182
         *     return $defaults;
183
         * } );
184
         * ```
185
         *
186
         * @param array $defaults An array of default options. You can see which options you can use
187
         *                         when you look at the `$options` parameter for
188
         *                        [PostExcerpt::__construct()](https://timber.github.io/docs/v2/reference/timber-postexcerpt/#__construct).
189
         */
190
        $defaults = \apply_filters('timber/post/excerpt/defaults', $defaults);
43✔
191

192
        // Set up excerpt defaults.
193
        $options = \wp_parse_args($options, $defaults);
43✔
194

195
        // Set excerpt properties
196
        $this->length = $options['words'];
43✔
197
        $this->char_length = $options['chars'];
43✔
198
        $this->end = $options['end'];
43✔
199
        $this->force = $options['force'];
43✔
200
        $this->strip = $options['strip'];
43✔
201
        $this->read_more = $options['read_more'];
43✔
202
        $this->always_add_read_more = $options['always_add_read_more'];
43✔
203
        $this->always_add_end = $options['always_add_end'];
43✔
204
    }
205

206
    /**
207
     * Returns the resulting excerpt.
208
     *
209
     * @api
210
     * @return string
211
     */
212
    public function __toString()
213
    {
214
        return $this->run();
43✔
215
    }
216

217
    /**
218
     * Restricts the length of the excerpt to a certain amount of words.
219
     *
220
     * @api
221
     * @example
222
     * ```twig
223
     * <p>{{ post.excerpt.length(50) }}</p>
224
     * ```
225
     * @param int $length The maximum amount of words (not letters) for the excerpt. Default `50`.
226
     * @return \Timber\PostExcerpt
227
     */
228
    public function length($length = 50)
229
    {
230
        $this->length = $length;
8✔
231
        return $this;
8✔
232
    }
233

234
    /**
235
     * Restricts the length of the excerpt to a certain amount of characters.
236
     *
237
     * @api
238
     * @example
239
     * ```twig
240
     * <p>{{ post.excerpt.chars(180) }}</p>
241
     * ```
242
     * @param int|bool $char_length The maximum amount of characters for the excerpt. Default
243
     *                              `false`.
244
     * @return \Timber\PostExcerpt
245
     */
246
    public function chars($char_length = false)
247
    {
248
        $this->char_length = $char_length;
3✔
249
        return $this;
3✔
250
    }
251

252
    /**
253
     * Defines the text to end the excerpt with.
254
     *
255
     * @api
256
     * @example
257
     * ```twig
258
     * <p>{{ post.excerpt.end('… and much more!') }}</p>
259
     * ```
260
     * @param string $end The text for the end of the excerpt. Default `…`.
261
     * @return \Timber\PostExcerpt
262
     */
263
    public function end($end = '&hellip;')
264
    {
265
        $this->end = $end;
1✔
266
        return $this;
1✔
267
    }
268

269
    /**
270
     * Forces excerpt lengths.
271
     *
272
     * What happens if your custom post excerpt is longer than the length requested? By default, it
273
     * will use the full `post_excerpt`. However, you can set this to `true` to *force* your excerpt
274
     * to be of the desired length.
275
     *
276
     * @api
277
     * @example
278
     * ```twig
279
     * <p>{{ post.excerpt.length(20).force }}</p>
280
     * ```
281
     * @param bool $force Whether the length of the excerpt should be forced to the requested
282
     *                    length, even if an editor wrote a manual excerpt that is longer than the
283
     *                    set length. Default `true`.
284
     * @return \Timber\PostExcerpt
285
     */
286
    public function force($force = true)
287
    {
288
        $this->force = $force;
4✔
289
        return $this;
4✔
290
    }
291

292
    /**
293
     * Defines the text to be used for the "Read More" link.
294
     *
295
     * Set this to `false` to not add a "Read More" link.
296
     *
297
     * @api
298
     * ```twig
299
     * <p>{{ post.excerpt.read_more('Learn more') }}</p>
300
     * ```
301
     *
302
     * @param string $text Text for the link. Default 'Read More'.
303
     *
304
     * @return \Timber\PostExcerpt
305
     */
306
    public function read_more($text = 'Read More')
307
    {
308
        $this->read_more = $text;
4✔
309
        return $this;
4✔
310
    }
311

312
    /**
313
     * Defines how HTML tags should be stripped from the excerpt.
314
     *
315
     * @api
316
     * ```twig
317
     * {# Strips all HTML tags, except for bold or emphasized text #}
318
     * <p>{{ post.excerpt.length('50').strip('<strong><em>') }}</p>
319
     * ```
320
     * @param bool|string $strip Whether or how HTML tags in the excerpt should be stripped. Use
321
     *                           `true` to strip all tags, `false` for no stripping, or a string for
322
     *                           a list of allowed tags (e.g. '<p><a>'). Default `true`.
323
     * @return \Timber\PostExcerpt
324
     */
325
    public function strip($strip = true)
326
    {
327
        $this->strip = $strip;
5✔
328
        return $this;
5✔
329
    }
330

331
    /**
332
     * Assembles excerpt.
333
     *
334
     * @internal
335
     *
336
     * @param string $text The text to use for the excerpt.
337
     * @param array  $args An array of arguments for the assembly.
338
     */
339
    protected function assemble($text, $args = [])
340
    {
341
        $text = \trim($text);
42✔
342
        $last = $text[\strlen($text) - 1];
42✔
343
        $last_p_tag = null;
42✔
344
        if ($last != '.' && ($this->always_add_end || $args['add_end'])) {
42✔
345
            $text .= $this->end;
24✔
346
        }
347
        if (!$this->strip) {
42✔
348
            $last_p_tag = \strrpos($text, '</p>');
5✔
349
            if ($last_p_tag !== false) {
5✔
350
                $text = \substr($text, 0, $last_p_tag);
2✔
351
            }
352
            if ($last != '.' && ($this->always_add_end || $args['add_end'])) {
5✔
353
                $text .= $this->end . ' ';
1✔
354
            }
355
        }
356

357
        // Maybe add read more link.
358
        if ($this->read_more && ($this->always_add_read_more || $args['add_read_more'])) {
42✔
359
            /**
360
             * Filters the CSS class used for excerpt links.
361
             *
362
             * @since 2.0.0
363
             * @example
364
             * ```php
365
             * // Change the CSS class for excerpt links.
366
             * add_filter( 'timber/post/excerpt/read_more_class', function( $class ) {
367
             *     return 'read-more__link';
368
             * } );
369
             * ```
370
             *
371
             * @param string $class The CSS class to use for the excerpt link. Default `read-more`.
372
             */
373
            $read_more_class = \apply_filters('timber/post/excerpt/read_more_class', 'read-more');
28✔
374

375
            /**
376
             * Filters the CSS class used for excerpt links.
377
             *
378
             * @deprecated 2.0.0
379
             * @since 1.0.4
380
             */
381
            $read_more_class = \apply_filters_deprecated(
28✔
382
                'timber/post/preview/read_more_class',
28✔
383
                [$read_more_class],
28✔
384
                '2.0.0',
28✔
385
                'timber/post/excerpt/read_more_class'
28✔
386
            );
28✔
387

388
            $linktext = \trim($this->read_more);
28✔
389

390
            $link = \sprintf(
28✔
391
                ' <a href="%1$s" class="%2$s">%3$s</a>',
28✔
392
                $this->post->link(),
28✔
393
                $read_more_class,
28✔
394
                $linktext
28✔
395
            );
28✔
396

397
            /**
398
             * Filters the link used for a read more text in an excerpt.
399
             *
400
             * @since 2.0.0
401
             * @param string       $link            The HTML link.
402
             * @param \Timber\Post $post            Post instance.
403
             * @param string       $linktext        The link text.
404
             * @param string       $read_more_class The CSS class name.
405
             */
406
            $link = \apply_filters(
28✔
407
                'timber/post/excerpt/read_more_link',
28✔
408
                $link,
28✔
409
                $this->post,
28✔
410
                $linktext,
28✔
411
                $read_more_class
28✔
412
            );
28✔
413

414
            /**
415
             * Filters the link used for a read more text in an excerpt.
416
             *
417
             * @deprecated 2.0.0
418
             * @since 1.1.3
419
             * @ticket #1142
420
             */
421
            $link = \apply_filters_deprecated(
28✔
422
                'timber/post/get_preview/read_more_link',
28✔
423
                [$link],
28✔
424
                '2.0.0',
28✔
425
                'timber/post/excerpt/read_more_link'
28✔
426
            );
28✔
427

428
            $text .= $link;
28✔
429
        }
430

431
        if (!$this->strip && $last_p_tag && (\strpos($text, '<p>') > -1 || \strpos($text, '<p '))) {
42✔
432
            $text .= '</p>';
2✔
433
        }
434
        return \trim($text);
42✔
435
    }
436

437
    protected function run()
438
    {
439
        $allowable_tags = ($this->strip && \is_string($this->strip)) ? $this->strip : false;
43✔
440
        $readmore_matches = [];
43✔
441
        $text = '';
43✔
442
        $add_read_more = false;
43✔
443
        $add_end = false;
43✔
444

445
        // A user-specified excerpt is authoritative, so check that first.
446
        if (isset($this->post->post_excerpt) && \strlen($this->post->post_excerpt)) {
43✔
447
            $text = $this->post->post_excerpt;
21✔
448
            if ($this->force) {
21✔
449
                if ($allowable_tags) {
11✔
450
                    $text = TextHelper::trim_words($text, $this->length, false, \strtr($allowable_tags, '<>', '  '));
2✔
451
                } else {
452
                    $text = TextHelper::trim_words($text, $this->length, false);
9✔
453
                }
454
                if ($this->char_length !== false) {
11✔
455
                    $text = TextHelper::trim_characters($text, $this->char_length, false);
4✔
456
                }
457

458
                $add_end = true;
11✔
459
            }
460

461
            $add_read_more = true;
21✔
462
        }
463

464
        // Check for <!-- more --> tag in post content.
465
        if (empty($text) && \preg_match('/<!--\s?more(.*?)?-->/', $this->post->post_content, $readmore_matches)) {
43✔
466
            $pieces = \explode($readmore_matches[0], $this->post->post_content);
8✔
467
            $text = $pieces[0];
8✔
468

469
            $add_read_more = true;
8✔
470

471
            /**
472
             * Custom read more text.
473
             *
474
             * The following post content example will result in the read more text to become "But
475
             * what is Elaina?": Eric is a polar bear <!-- more But what is Elaina? --> Lauren is
476
             * not a duck.
477
             */
478
            if (!empty($readmore_matches[1])) {
8✔
479
                $this->read_more = \trim($readmore_matches[1]);
2✔
480
            }
481

482
            if ($this->force) {
8✔
483
                if ($allowable_tags) {
2✔
484
                    $text = TextHelper::trim_words($text, $this->length, false, \strtr($allowable_tags, '<>', '  '));
×
485
                } else {
486
                    $text = TextHelper::trim_words($text, $this->length, false);
2✔
487
                }
488
                if ($this->char_length !== false) {
2✔
489
                    $text = TextHelper::trim_characters($text, $this->char_length, false);
×
490
                }
491

492
                $add_end = true;
2✔
493
            }
494

495
            $text = \do_shortcode($text);
8✔
496
        }
497

498
        // Build an excerpt text from the post’s content.
499
        if (empty($text)) {
43✔
500
            $text = $this->post->content();
16✔
501
            $text = TextHelper::remove_tags($text, $this->destroy_tags);
16✔
502
            $text_before_trim = \trim($text);
16✔
503
            $text_before_char_trim = '';
16✔
504

505
            if ($allowable_tags) {
16✔
506
                $text = TextHelper::trim_words($text, $this->length, false, \strtr($allowable_tags, '<>', '  '));
2✔
507
            } else {
508
                $text = TextHelper::trim_words($text, $this->length, false);
14✔
509
            }
510

511
            if ($this->char_length !== false) {
16✔
512
                $text_before_char_trim = \trim($text);
3✔
513
                $text = TextHelper::trim_characters($text, $this->char_length, false);
3✔
514
            }
515

516
            $has_trimmed_words = \strlen($text) < \strlen($text_before_trim);
16✔
517
            $has_trimmed_chars = !empty($text_before_char_trim)
16✔
518
                && \strlen($text) < \strlen($text_before_char_trim);
16✔
519

520
            if ($has_trimmed_words || $has_trimmed_chars) {
16✔
521
                $add_end = true;
9✔
522
                $add_read_more = true;
9✔
523
            }
524
        }
525
        if (empty(\trim($text))) {
43✔
526
            return \trim($text);
1✔
527
        }
528
        if ($this->strip) {
42✔
529
            $text = \trim(\strip_tags($text, $allowable_tags));
37✔
530
        }
531
        if (!empty($text)) {
42✔
532
            return $this->assemble($text, [
42✔
533
                'add_end' => $add_end,
42✔
534
                'add_read_more' => $add_read_more,
42✔
535
            ]);
42✔
536
        }
537

538
        return \trim($text);
×
539
    }
540
}
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

© 2025 Coveralls, Inc