Passed
Push — master ( 16aeb6...6afa06 )
by Arnaud
05:29
created

Core::html()   D

Complexity

Conditions 18
Paths 93

Size

Total Lines 86
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 26.9152

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 18
eloc 50
c 1
b 1
f 0
nc 93
nop 4
dl 0
loc 86
ccs 37
cts 53
cp 0.6981
crap 26.9152
rs 4.8666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Renderer\Extension;
15
16
use Cecil\Assets\Asset;
17
use Cecil\Assets\Cache;
18
use Cecil\Assets\Image;
19
use Cecil\Assets\Url;
20
use Cecil\Builder;
21
use Cecil\Collection\CollectionInterface;
22
use Cecil\Collection\Page\Collection as PagesCollection;
23
use Cecil\Collection\Page\Page;
24
use Cecil\Collection\Page\Type;
25
use Cecil\Config;
26
use Cecil\Converter\Parsedown;
27
use Cecil\Exception\ConfigException;
28
use Cecil\Exception\RuntimeException;
29
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
30
use Cocur\Slugify\Slugify;
31
use MatthiasMullie\Minify;
32
use ScssPhp\ScssPhp\Compiler;
33
use Symfony\Component\VarDumper\Cloner\VarCloner;
34
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
35
use Symfony\Component\Yaml\Exception\ParseException;
36
use Symfony\Component\Yaml\Yaml;
37
38
/**
39
 * Class Renderer\Extension\Core.
40
 */
41
class Core extends SlugifyExtension
42
{
43
    /** @var Builder */
44
    protected $builder;
45
46
    /** @var Config */
47
    protected $config;
48
49
    /** @var Slugify */
50
    private static $slugifier;
51
52 1
    public function __construct(Builder $builder)
53
    {
54 1
        if (!self::$slugifier instanceof Slugify) {
55 1
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
56
        }
57
58 1
        parent::__construct(self::$slugifier);
59
60 1
        $this->builder = $builder;
61 1
        $this->config = $builder->getConfig();
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function getName(): string
68
    {
69
        return 'CoreExtension';
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 1
    public function getFunctions()
76
    {
77 1
        return [
78 1
            new \Twig\TwigFunction('url', [$this, 'url'], ['needs_context' => true]),
79
            // assets
80 1
            new \Twig\TwigFunction('asset', [$this, 'asset']),
81 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
82 1
            new \Twig\TwigFunction('image_srcset', [$this, 'imageSrcset']),
83 1
            new \Twig\TwigFunction('image_sizes', [$this, 'imageSizes']),
84
            // content
85 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
86
            // others
87 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
88 1
            new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]),
89
            // deprecated
90 1
            new \Twig\TwigFunction(
91 1
                'hash',
92 1
                [$this, 'integrity'],
93 1
                ['deprecated' => true, 'alternative' => 'integrity']
94 1
            ),
95 1
            new \Twig\TwigFunction(
96 1
                'minify',
97 1
                [$this, 'minify'],
98 1
                ['deprecated' => true, 'alternative' => 'minify filter']
99 1
            ),
100 1
            new \Twig\TwigFunction(
101 1
                'toCSS',
102 1
                [$this, 'toCss'],
103 1
                ['deprecated' => true, 'alternative' => 'to_css filter']
104 1
            ),
105 1
        ];
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111 1
    public function getFilters(): array
112
    {
113 1
        return [
114 1
            new \Twig\TwigFilter('url', [$this, 'url'], ['needs_context' => true]),
115
            // collections
116 1
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
117 1
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
118 1
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
119 1
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
120
            // assets
121 1
            new \Twig\TwigFilter('html', [$this, 'html'], ['needs_context' => true]),
122 1
            new \Twig\TwigFilter('inline', [$this, 'inline']),
123 1
            new \Twig\TwigFilter('fingerprint', [$this, 'fingerprint']),
124 1
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
125 1
            new \Twig\TwigFilter('minify', [$this, 'minify']),
126 1
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
127 1
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
128 1
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
129 1
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
130 1
            new \Twig\TwigFilter('resize', [$this, 'resize']),
131 1
            new \Twig\TwigFilter('dataurl', [$this, 'dataurl']),
132 1
            new \Twig\TwigFilter('dominant_color', [$this, 'dominantColor']),
133 1
            new \Twig\TwigFilter('lqip', [$this, 'lqip']),
134 1
            new \Twig\TwigFilter('webp', [$this, 'webp']),
135
            // content
136 1
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
137 1
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
138 1
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
139 1
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
140 1
            new \Twig\TwigFilter('toc', [$this, 'markdownToToc']),
141 1
            new \Twig\TwigFilter('json_decode', [$this, 'jsonDecode']),
142 1
            new \Twig\TwigFilter('yaml_parse', [$this, 'yamlParse']),
143 1
            new \Twig\TwigFilter('preg_split', [$this, 'pregSplit']),
144 1
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
145 1
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
146 1
            new \Twig\TwigFilter('splitline', [$this, 'splitLine']),
147 1
        ];
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 1
    public function getTests()
154
    {
155 1
        return [
156 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
157 1
        ];
158
    }
159
160
    /**
161
     * Filters by Section.
162
     */
163
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
164
    {
165
        return $this->filterBy($pages, 'section', $section);
166
    }
167
168
    /**
169
     * Filters a pages collection by variable's name/value.
170
     */
171 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
172
    {
173 1
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
174
            // is a dedicated getter exists?
175 1
            $method = 'get' . ucfirst($variable);
176 1
            if (method_exists($page, $method) && $page->$method() == $value) {
177
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
178
            }
179
            // or a classic variable
180 1
            if ($page->getVariable($variable) == $value) {
181 1
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
182
            }
183 1
        });
184
185 1
        return $filteredPages;
186
    }
187
188
    /**
189
     * Sorts a collection by title.
190
     */
191 1
    public function sortByTitle(\Traversable $collection): array
192
    {
193 1
        $sort = \SORT_ASC;
194
195 1
        $collection = iterator_to_array($collection);
196 1
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), $sort, \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
0 ignored issues
show
Bug introduced by
array_keys($collection) cannot be passed to array_multisort() as the parameter $array expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
        array_multisort(/** @scrutinizer ignore-type */ array_keys(/** @scrutinizer ignore-type */ $collection), $sort, \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
Loading history...
Bug introduced by
SORT_NATURAL | SORT_FLAG_CASE cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), $sort, /** @scrutinizer ignore-type */ \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
Loading history...
197
198 1
        return $collection;
199
    }
200
201
    /**
202
     * Sorts a collection by weight.
203
     *
204
     * @param \Traversable|array $collection
205
     */
206 1
    public function sortByWeight($collection): array
207
    {
208 1
        $callback = function ($a, $b) {
209 1
            if (!isset($a['weight'])) {
210 1
                $a['weight'] = 0;
211
            }
212 1
            if (!isset($b['weight'])) {
213
                $a['weight'] = 0;
214
            }
215 1
            if ($a['weight'] == $b['weight']) {
216 1
                return 0;
217
            }
218
219 1
            return $a['weight'] < $b['weight'] ? -1 : 1;
220 1
        };
221
222 1
        if (!\is_array($collection)) {
223 1
            $collection = iterator_to_array($collection);
224
        }
225 1
        usort(/** @scrutinizer ignore-type */ $collection, $callback);
226
227 1
        return $collection;
228
    }
229
230
    /**
231
     * Sorts by creation date (or 'updated' date): the most recent first.
232
     */
233 1
    public function sortByDate(\Traversable $collection, string $variable = 'date', bool $descTitle = false): array
234
    {
235 1
        $callback = function ($a, $b) use ($variable, $descTitle) {
236 1
            if ($a[$variable] == $b[$variable]) {
237
                // if dates are equal and "descTitle" is true
238 1
                if ($descTitle && (isset($a['title']) && isset($b['title']))) {
239
                    return strnatcmp($b['title'], $a['title']);
240
                }
241
242 1
                return 0;
243
            }
244
245 1
            return $a[$variable] > $b[$variable] ? -1 : 1;
246 1
        };
247
248 1
        $collection = iterator_to_array($collection);
249 1
        usort(/** @scrutinizer ignore-type */ $collection, $callback);
250
251 1
        return $collection;
252
    }
253
254
    /**
255
     * Creates an URL.
256
     *
257
     * $options[
258
     *     'canonical' => false,
259
     *     'format'    => 'html',
260
     *     'language'  => null,
261
     * ];
262
     *
263
     * @param array                  $context
264
     * @param Page|Asset|string|null $value
265
     * @param array|null             $options
266
     */
267 1
    public function url(array $context, $value = null, array $options = null): string
268
    {
269 1
        $optionsLang = [];
270 1
        $optionsLang['language'] = (string) $context['site']['language'];
271 1
        $options = array_merge($optionsLang, $options ?? []);
272
273 1
        return (new Url($this->builder, $value, $options))->getUrl();
274
    }
275
276
    /**
277
     * Creates an Asset (CSS, JS, images, etc.) from a path or an array of paths.
278
     *
279
     * @param string|array $path    File path or array of files path (relative from `assets/` or `static/` dir).
280
     * @param array|null   $options
281
     *
282
     * @return Asset
283
     */
284 1
    public function asset($path, array|null $options = null): Asset
285
    {
286 1
        if (!\is_string($path) && !\is_array($path)) {
287
            throw new RuntimeException(sprintf('Argument of "%s()" must a string or an array.', \Cecil\Util::formatMethodName(__METHOD__)));
288
        }
289
290 1
        return new Asset($this->builder, $path, $options);
291
    }
292
293
    /**
294
     * Compiles a SCSS asset.
295
     *
296
     * @param string|Asset $asset
297
     *
298
     * @return Asset
299
     */
300 1
    public function toCss($asset): Asset
301
    {
302 1
        if (!$asset instanceof Asset) {
303
            $asset = new Asset($this->builder, $asset);
304
        }
305
306 1
        return $asset->compile();
307
    }
308
309
    /**
310
     * Minifying an asset (CSS or JS).
311
     *
312
     * @param string|Asset $asset
313
     *
314
     * @return Asset
315
     */
316 1
    public function minify($asset): Asset
317
    {
318 1
        if (!$asset instanceof Asset) {
319
            $asset = new Asset($this->builder, $asset);
320
        }
321
322 1
        return $asset->minify();
323
    }
324
325
    /**
326
     * Fingerprinting an asset.
327
     *
328
     * @param string|Asset $asset
329
     *
330
     * @return Asset
331
     */
332 1
    public function fingerprint($asset): Asset
333
    {
334 1
        if (!$asset instanceof Asset) {
335
            $asset = new Asset($this->builder, $asset);
336
        }
337
338 1
        return $asset->fingerprint();
339
    }
340
341
    /**
342
     * Resizes an image.
343
     *
344
     * @param string|Asset $asset
345
     *
346
     * @return Asset
347
     */
348 1
    public function resize($asset, int $size): Asset
349
    {
350 1
        if (!$asset instanceof Asset) {
351
            $asset = new Asset($this->builder, $asset);
352
        }
353
354 1
        return $asset->resize($size);
355
    }
356
357
    /**
358
     * Returns the data URL of an image.
359
     *
360
     * @param string|Asset $asset
361
     *
362
     * @return string
363
     */
364 1
    public function dataurl($asset): string
365
    {
366 1
        if (!$asset instanceof Asset) {
367
            $asset = new Asset($this->builder, $asset);
368
        }
369
370 1
        return $asset->dataurl();
371
    }
372
373
    /**
374
     * Hashing an asset with algo (sha384 by default).
375
     *
376
     * @param string|Asset $asset
377
     * @param string       $algo
378
     *
379
     * @return string
380
     */
381 1
    public function integrity($asset, string $algo = 'sha384'): string
382
    {
383 1
        if (!$asset instanceof Asset) {
384 1
            $asset = new Asset($this->builder, $asset);
385
        }
386
387 1
        return $asset->getIntegrity($algo);
388
    }
389
390
    /**
391
     * Minifying a CSS string.
392
     */
393 1
    public function minifyCss(?string $value): string
394
    {
395 1
        $value = $value ?? '';
396
397 1
        if ($this->builder->isDebug()) {
398 1
            return $value;
399
        }
400
401
        $cache = new Cache($this->builder);
402
        $cacheKey = $cache->createKeyFromString($value);
403
        if (!$cache->has($cacheKey)) {
404
            $minifier = new Minify\CSS($value);
405
            $value = $minifier->minify();
406
            $cache->set($cacheKey, $value);
407
        }
408
409
        return $cache->get($cacheKey, $value);
410
    }
411
412
    /**
413
     * Minifying a JavaScript string.
414
     */
415 1
    public function minifyJs(?string $value): string
416
    {
417 1
        $value = $value ?? '';
418
419 1
        if ($this->builder->isDebug()) {
420 1
            return $value;
421
        }
422
423
        $cache = new Cache($this->builder);
424
        $cacheKey = $cache->createKeyFromString($value);
425
        if (!$cache->has($cacheKey)) {
426
            $minifier = new Minify\JS($value);
427
            $value = $minifier->minify();
428
            $cache->set($cacheKey, $value);
429
        }
430
431
        return $cache->get($cacheKey, $value);
432
    }
433
434
    /**
435
     * Compiles a SCSS string.
436
     *
437
     * @throws RuntimeException
438
     */
439 1
    public function scssToCss(?string $value): string
440
    {
441 1
        $value = $value ?? '';
442
443 1
        $cache = new Cache($this->builder);
444 1
        $cacheKey = $cache->createKeyFromString($value);
445 1
        if (!$cache->has($cacheKey)) {
446 1
            $scssPhp = new Compiler();
447 1
            $outputStyles = ['expanded', 'compressed'];
448 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
449 1
            if (!\in_array($outputStyle, $outputStyles)) {
450
                throw new ConfigException(sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
451
            }
452 1
            $scssPhp->setOutputStyle($outputStyle);
453 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
454 1
            if (!empty($variables)) {
455 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
456 1
                $scssPhp->replaceVariables($variables);
457
            }
458 1
            $value = $scssPhp->compileString($value)->getCss();
459 1
            $cache->set($cacheKey, $value);
460
        }
461
462 1
        return $cache->get($cacheKey, $value);
463
    }
464
465
    /**
466
     * Creates the HTML element of an asset.
467
     *
468
     * $options[
469
     *     'preload'    => false,
470
     *     'responsive' => false,
471
     *     'webp'       => false,
472
     * ];
473
     *
474
     * @throws RuntimeException
475
     */
476 1
    public function html(array $context, Asset $asset, array $attributes = [], array $options = []): string
477
    {
478 1
        $htmlAttributes = '';
479 1
        $preload = false;
480 1
        $responsive = (bool) $this->config->get('assets.images.responsive.enabled') ?? false;
481 1
        $webp = (bool) $this->config->get('assets.images.webp.enabled') ?? false;
482 1
        extract($options, EXTR_IF_EXISTS);
483
484
        // builds HTML attributes
485 1
        foreach ($attributes as $name => $value) {
486 1
            $attribute = sprintf(' %s="%s"', $name, $value);
487 1
            if (!isset($value)) {
488
                $attribute = sprintf(' %s', $name);
489
            }
490 1
            $htmlAttributes .= $attribute;
491
        }
492
493
        // be sure Asset file is saved
494 1
        $asset->save();
495
496
        // CSS or JavaScript
497 1
        switch ($asset['ext']) {
498 1
            case 'css':
499 1
                if ($preload) {
500
                    return sprintf(
501
                        '<link href="%s" rel="preload" as="style" onload="this.onload=null;this.rel=\'stylesheet\'"%s><noscript><link rel="stylesheet" href="%1$s"%2$s></noscript>',
502
                        $this->url($context, $asset, $options),
503
                        $htmlAttributes
504
                    );
505
                }
506
507 1
                return sprintf('<link rel="stylesheet" href="%s"%s>', $this->url($context, $asset, $options), $htmlAttributes);
508 1
            case 'js':
509 1
                return sprintf('<script src="%s"%s></script>', $this->url($context, $asset, $options), $htmlAttributes);
510
        }
511
        // image
512 1
        if ($asset['type'] == 'image') {
513
            // responsive
514 1
            $sizes = '';
515
            if (
516 1
                $responsive && $srcset = Image::buildSrcset(
517 1
                    $asset,
518 1
                    $this->config->getAssetsImagesWidths()
519 1
                )
520
            ) {
521 1
                $htmlAttributes .= sprintf(' srcset="%s"', $srcset);
522 1
                $sizes = Image::getSizes($attributes['class'] ?? '', $this->config->getAssetsImagesSizes());
523 1
                $htmlAttributes .= sprintf(' sizes="%s"', $sizes);
524 1
                if ($asset['width'] > max($this->config->getAssetsImagesSizes())) {
525
                    $asset->resize(max($this->config->getAssetsImagesSizes()));
526
                }
527
            }
528
529
            // <img> element
530 1
            $img = sprintf(
531 1
                '<img src="%s" width="' . ($asset['width'] ?: '') . '" height="' . ($asset['height'] ?: '') . '"%s>',
532 1
                $this->url($context, $asset, $options),
533 1
                $htmlAttributes
534 1
            );
535
536
            // WebP conversion?
537 1
            if ($webp && $asset['subtype'] != 'image/webp' && !Image::isAnimatedGif($asset)) {
538
                try {
539 1
                    $assetWebp = $asset->webp();
540
                    // <source> element
541
                    $source = sprintf('<source type="image/webp" srcset="%s">', $assetWebp);
542
                    // responsive
543
                    if ($responsive && $srcset = Image::buildSrcset($assetWebp, $this->config->getAssetsImagesWidths())) {
544
                        // <source> element
545
                        $source = sprintf(
546
                            '<source type="image/webp" srcset="%s" sizes="%s">',
547
                            $srcset,
548
                            $sizes
549
                        );
550
                    }
551
552
                    return sprintf("<picture>\n  %s\n  %s\n</picture>", $source, $img);
553 1
                } catch (\Exception $e) {
554 1
                    $this->builder->getLogger()->debug($e->getMessage());
555
                }
556
            }
557
558 1
            return $img;
559
        }
560
561
        throw new RuntimeException(sprintf('%s is available for CSS, JavaScript and images files only.', '"html" filter'));
562
    }
563
564
    /**
565
     * Builds the HTML img `srcset` (responsive) attribute of an image Asset.
566
     *
567
     * @throws RuntimeException
568
     */
569 1
    public function imageSrcset(Asset $asset): string
570
    {
571 1
        return Image::buildSrcset($asset, $this->config->getAssetsImagesWidths());
572
    }
573
574
    /**
575
     * Returns the HTML img `sizes` attribute based on a CSS class name.
576
     */
577 1
    public function imageSizes(string $class): string
578
    {
579 1
        return Image::getSizes($class, $this->config->getAssetsImagesSizes());
580
    }
581
582
    /**
583
     * Converts an image Asset to WebP format.
584
     *
585
     * @throws RuntimeException
586
     */
587
    public function webp(Asset $asset, ?int $quality = null): Asset
588
    {
589
        if ($asset['subtype'] == 'image/webp') {
590
            return $asset;
591
        }
592
        if (Image::isAnimatedGif($asset)) {
593
            throw new RuntimeException(sprintf('Can\'t convert the animated GIF "%s" to WebP.', $asset['path']));
594
        }
595
596
        try {
597
            return $asset->webp($quality);
598
        } catch (\Exception $e) {
599
            throw new RuntimeException(sprintf('Can\'t convert "%s" to WebP (%s).', $asset['path'], $e->getMessage()));
600
        }
601
    }
602
603
    /**
604
     * Returns the content of an asset.
605
     */
606 1
    public function inline(Asset $asset): string
607
    {
608 1
        return $asset['content'];
609
    }
610
611
    /**
612
     * Reads $length first characters of a string and adds a suffix.
613
     */
614 1
    public function excerpt(?string $string, int $length = 450, string $suffix = ' …'): string
615
    {
616 1
        $string = $string ?? '';
617
618 1
        $string = str_replace('</p>', '<br /><br />', $string);
619 1
        $string = trim(strip_tags($string, '<br>'), '<br />');
620 1
        if (mb_strlen($string) > $length) {
621 1
            $string = mb_substr($string, 0, $length);
622 1
            $string .= $suffix;
623
        }
624
625 1
        return $string;
626
    }
627
628
    /**
629
     * Reads characters before or after '<!-- separator -->'.
630
     * Options:
631
     *  - separator: string to use as separator (`excerpt|break` by default)
632
     *  - capture: part to capture, `before` or `after` the separator (`before` by default).
633
     */
634 1
    public function excerptHtml(?string $string, array $options = []): string
635
    {
636 1
        $string = $string ?? '';
637
638 1
        $separator = (string) $this->config->get('pages.body.excerpt.separator');
639 1
        $capture = (string) $this->config->get('pages.body.excerpt.capture');
640 1
        extract($options, EXTR_IF_EXISTS);
641
642
        // https://regex101.com/r/n9TWHF/1
643 1
        $pattern = '(.*)<!--[[:blank:]]?(' . $separator . ')[[:blank:]]?-->(.*)';
644 1
        preg_match('/' . $pattern . '/is', $string, $matches);
645
646 1
        if (empty($matches)) {
647
            return $string;
648
        }
649 1
        $result = trim($matches[1]);
650 1
        if ($capture == 'after') {
651 1
            $result = trim($matches[3]);
652
        }
653
        // removes footnotes and returns result
654 1
        return preg_replace('/<sup[^>]*>[^u]*<\/sup>/', '', $result);
655
    }
656
657
    /**
658
     * Converts a Markdown string to HTML.
659
     *
660
     * @throws RuntimeException
661
     */
662 1
    public function markdownToHtml(?string $markdown): ?string
663
    {
664 1
        $markdown = $markdown ?? '';
665
666
        try {
667 1
            $parsedown = new Parsedown($this->builder);
668 1
            $html = $parsedown->text($markdown);
669
        } catch (\Exception) {
670
            throw new RuntimeException('"markdown_to_html" filter can not convert supplied Markdown.');
671
        }
672
673 1
        return $html;
674
    }
675
676
    /**
677
     * Extract table of content of a Markdown string,
678
     * in the given format ("html" or "json", "html" by default).
679
     *
680
     * @throws RuntimeException
681
     */
682 1
    public function markdownToToc(?string $markdown, $format = 'html', $url = ''): ?string
683
    {
684 1
        $markdown = $markdown ?? '';
685
686
        try {
687 1
            $parsedown = new Parsedown($this->builder, ['selectors' => ['h2'], 'url' => $url]);
688 1
            $parsedown->body($markdown);
689 1
            $return = $parsedown->contentsList($format);
690
        } catch (\Exception) {
691
            throw new RuntimeException('"toc" filter can not convert supplied Markdown.');
692
        }
693
694 1
        return $return;
695
    }
696
697
    /**
698
     * Converts a JSON string to an array.
699
     *
700
     * @throws RuntimeException
701
     */
702 1
    public function jsonDecode(?string $json): ?array
703
    {
704 1
        $json = $json ?? '';
705
706
        try {
707 1
            $array = json_decode($json, true);
708 1
            if ($array === null && json_last_error() !== JSON_ERROR_NONE) {
709 1
                throw new \Exception('JSON error.');
710
            }
711
        } catch (\Exception) {
712
            throw new RuntimeException('"json_decode" filter can not parse supplied JSON.');
713
        }
714
715 1
        return $array;
716
    }
717
718
    /**
719
     * Converts a YAML string to an array.
720
     *
721
     * @throws RuntimeException
722
     */
723 1
    public function yamlParse(?string $yaml): ?array
724
    {
725 1
        $yaml = $yaml ?? '';
726
727
        try {
728 1
            $array = Yaml::parse($yaml, Yaml::PARSE_DATETIME);
729 1
            if (!\is_array($array)) {
730 1
                throw new ParseException('YAML error.');
731
            }
732
        } catch (ParseException $e) {
733
            throw new RuntimeException(sprintf('"yaml_parse" filter can not parse supplied YAML: %s', $e->getMessage()));
734
        }
735
736 1
        return $array;
737
    }
738
739
    /**
740
     * Split a string into an array using a regular expression.
741
     *
742
     * @throws RuntimeException
743
     */
744
    public function pregSplit(?string $value, string $pattern, int $limit = 0): ?array
745
    {
746
        $value = $value ?? '';
747
748
        try {
749
            $array = preg_split($pattern, $value, $limit);
750
            if ($array === false) {
751
                throw new RuntimeException('PREG split error.');
752
            }
753
        } catch (\Exception) {
754
            throw new RuntimeException('"preg_split" filter can not split supplied string.');
755
        }
756
757
        return $array;
758
    }
759
760
    /**
761
     * Perform a regular expression match and return the group for all matches.
762
     *
763
     * @throws RuntimeException
764
     */
765
    public function pregMatchAll(?string $value, string $pattern, int $group = 0): ?array
766
    {
767
        $value = $value ?? '';
768
769
        try {
770
            $array = preg_match_all($pattern, $value, $matches, PREG_PATTERN_ORDER);
771
            if ($array === false) {
772
                throw new RuntimeException('PREG match all error.');
773
            }
774
        } catch (\Exception) {
775
            throw new RuntimeException('"preg_match_all" filter can not match in supplied string.');
776
        }
777
778
        return $matches[$group];
779
    }
780
781
    /**
782
     * Calculates estimated time to read a text.
783
     */
784 1
    public function readtime(?string $text): string
785
    {
786 1
        $text = $text ?? '';
787
788 1
        $words = str_word_count(strip_tags($text));
789 1
        $min = floor($words / 200);
790 1
        if ($min === 0) {
791
            return '1';
792
        }
793
794 1
        return (string) $min;
795
    }
796
797
    /**
798
     * Gets the value of an environment variable.
799
     */
800 1
    public function getEnv(?string $var): ?string
801
    {
802 1
        $var = $var ?? '';
803
804 1
        return getenv($var) ?: null;
805
    }
806
807
    /**
808
     * Dump variable (or Twig context).
809
     */
810 1
    public function varDump(\Twig\Environment $env, array $context, $var = null, ?array $options = null): void
811
    {
812 1
        if (!$env->isDebug()) {
813
            return;
814
        }
815
816 1
        if ($var === null) {
817
            $var = array();
818
            foreach ($context as $key => $value) {
819
                if (!$value instanceof \Twig\Template && !$value instanceof \Twig\TemplateWrapper) {
820
                    $var[$key] = $value;
821
                }
822
            }
823
        }
824
825 1
        $cloner = new VarCloner();
826 1
        $cloner->setMinDepth(3);
827 1
        $dumper = new HtmlDumper();
828 1
        $dumper->setTheme($options['theme'] ?? 'light');
829
830 1
        $data = $cloner->cloneVar($var)->withMaxDepth(3);
831 1
        $dumper->dump($data, null, ['maxDepth' => 3]);
832
    }
833
834
    /**
835
     * Tests if a variable is an Asset.
836
     */
837
    public function isAsset($variable): bool
838
    {
839
        return $variable instanceof Asset;
840
    }
841
842
    /**
843
     * Returns the dominant hex color of an image asset.
844
     *
845
     * @param string|Asset $asset
846
     *
847
     * @return string
848
     */
849 1
    public function dominantColor($asset): string
850
    {
851 1
        if (!$asset instanceof Asset) {
852
            $asset = new Asset($this->builder, $asset);
853
        }
854
855 1
        return Image::getDominantColor($asset);
856
    }
857
858
    /**
859
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
860
     *
861
     * @param string|Asset $asset
862
     *
863
     * @return string
864
     */
865 1
    public function lqip($asset): string
866
    {
867 1
        if (!$asset instanceof Asset) {
868
            $asset = new Asset($this->builder, $asset);
869
        }
870
871 1
        return Image::getLqip($asset);
872
    }
873
874
    /**
875
     * Converts an hexadecimal color to RGB.
876
     *
877
     * @throws RuntimeException
878
     */
879 1
    public function hexToRgb(?string $variable): array
880
    {
881 1
        $variable = $variable ?? '';
882
883 1
        if (!self::isHex($variable)) {
884
            throw new RuntimeException(sprintf('"%s" is not a valid hexadecimal value.', $variable));
885
        }
886 1
        $hex = ltrim($variable, '#');
887 1
        if (\strlen($hex) == 3) {
888
            $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
889
        }
890 1
        $c = hexdec($hex);
891
892 1
        return [
893 1
            'red'   => $c >> 16 & 0xFF,
894 1
            'green' => $c >> 8 & 0xFF,
895 1
            'blue'  => $c & 0xFF,
896 1
        ];
897
    }
898
899
    /**
900
     * Split a string in multiple lines.
901
     */
902 1
    public function splitLine(?string $variable, int $max = 18): array
903
    {
904 1
        $variable = $variable ?? '';
905
906 1
        return preg_split("/.{0,{$max}}\K(\s+|$)/", $variable, 0, PREG_SPLIT_NO_EMPTY);
907
    }
908
909
    /**
910
     * Is a hexadecimal color is valid?
911
     */
912 1
    private static function isHex(string $hex): bool
913
    {
914 1
        $valid = \is_string($hex);
915 1
        $hex = ltrim($hex, '#');
916 1
        $length = \strlen($hex);
917 1
        $valid = $valid && ($length === 3 || $length === 6);
918 1
        $valid = $valid && ctype_xdigit($hex);
919
920 1
        return $valid;
921
    }
922
}
923