Passed
Push — master ( 7085d8...2aef20 )
by Arnaud
06:34
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 2
Bugs 2 Features 0
Metric Value
cc 18
eloc 50
c 2
b 2
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->getAssetsImagesWidths())) {
525
                    $asset = $asset->resize(max($this->config->getAssetsImagesWidths()));
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', ?array $selectors = null, string $url = ''): ?string
683
    {
684 1
        $markdown = $markdown ?? '';
685 1
        $selectors = $selectors ?? (array) $this->config->get('pages.body.toc');
686
687
        try {
688 1
            $parsedown = new Parsedown($this->builder, ['selectors' => $selectors, 'url' => $url]);
689 1
            $parsedown->body($markdown);
690 1
            $return = $parsedown->contentsList($format);
691
        } catch (\Exception) {
692
            throw new RuntimeException('"toc" filter can not convert supplied Markdown.');
693
        }
694
695 1
        return $return;
696
    }
697
698
    /**
699
     * Converts a JSON string to an array.
700
     *
701
     * @throws RuntimeException
702
     */
703 1
    public function jsonDecode(?string $json): ?array
704
    {
705 1
        $json = $json ?? '';
706
707
        try {
708 1
            $array = json_decode($json, true);
709 1
            if ($array === null && json_last_error() !== JSON_ERROR_NONE) {
710 1
                throw new \Exception('JSON error.');
711
            }
712
        } catch (\Exception) {
713
            throw new RuntimeException('"json_decode" filter can not parse supplied JSON.');
714
        }
715
716 1
        return $array;
717
    }
718
719
    /**
720
     * Converts a YAML string to an array.
721
     *
722
     * @throws RuntimeException
723
     */
724 1
    public function yamlParse(?string $yaml): ?array
725
    {
726 1
        $yaml = $yaml ?? '';
727
728
        try {
729 1
            $array = Yaml::parse($yaml, Yaml::PARSE_DATETIME);
730 1
            if (!\is_array($array)) {
731 1
                throw new ParseException('YAML error.');
732
            }
733
        } catch (ParseException $e) {
734
            throw new RuntimeException(sprintf('"yaml_parse" filter can not parse supplied YAML: %s', $e->getMessage()));
735
        }
736
737 1
        return $array;
738
    }
739
740
    /**
741
     * Split a string into an array using a regular expression.
742
     *
743
     * @throws RuntimeException
744
     */
745
    public function pregSplit(?string $value, string $pattern, int $limit = 0): ?array
746
    {
747
        $value = $value ?? '';
748
749
        try {
750
            $array = preg_split($pattern, $value, $limit);
751
            if ($array === false) {
752
                throw new RuntimeException('PREG split error.');
753
            }
754
        } catch (\Exception) {
755
            throw new RuntimeException('"preg_split" filter can not split supplied string.');
756
        }
757
758
        return $array;
759
    }
760
761
    /**
762
     * Perform a regular expression match and return the group for all matches.
763
     *
764
     * @throws RuntimeException
765
     */
766
    public function pregMatchAll(?string $value, string $pattern, int $group = 0): ?array
767
    {
768
        $value = $value ?? '';
769
770
        try {
771
            $array = preg_match_all($pattern, $value, $matches, PREG_PATTERN_ORDER);
772
            if ($array === false) {
773
                throw new RuntimeException('PREG match all error.');
774
            }
775
        } catch (\Exception) {
776
            throw new RuntimeException('"preg_match_all" filter can not match in supplied string.');
777
        }
778
779
        return $matches[$group];
780
    }
781
782
    /**
783
     * Calculates estimated time to read a text.
784
     */
785 1
    public function readtime(?string $text): string
786
    {
787 1
        $text = $text ?? '';
788
789 1
        $words = str_word_count(strip_tags($text));
790 1
        $min = floor($words / 200);
791 1
        if ($min === 0) {
792
            return '1';
793
        }
794
795 1
        return (string) $min;
796
    }
797
798
    /**
799
     * Gets the value of an environment variable.
800
     */
801 1
    public function getEnv(?string $var): ?string
802
    {
803 1
        $var = $var ?? '';
804
805 1
        return getenv($var) ?: null;
806
    }
807
808
    /**
809
     * Dump variable (or Twig context).
810
     */
811 1
    public function varDump(\Twig\Environment $env, array $context, $var = null, ?array $options = null): void
812
    {
813 1
        if (!$env->isDebug()) {
814
            return;
815
        }
816
817 1
        if ($var === null) {
818
            $var = array();
819
            foreach ($context as $key => $value) {
820
                if (!$value instanceof \Twig\Template && !$value instanceof \Twig\TemplateWrapper) {
821
                    $var[$key] = $value;
822
                }
823
            }
824
        }
825
826 1
        $cloner = new VarCloner();
827 1
        $cloner->setMinDepth(3);
828 1
        $dumper = new HtmlDumper();
829 1
        $dumper->setTheme($options['theme'] ?? 'light');
830
831 1
        $data = $cloner->cloneVar($var)->withMaxDepth(3);
832 1
        $dumper->dump($data, null, ['maxDepth' => 3]);
833
    }
834
835
    /**
836
     * Tests if a variable is an Asset.
837
     */
838
    public function isAsset($variable): bool
839
    {
840
        return $variable instanceof Asset;
841
    }
842
843
    /**
844
     * Returns the dominant hex color of an image asset.
845
     *
846
     * @param string|Asset $asset
847
     *
848
     * @return string
849
     */
850 1
    public function dominantColor($asset): string
851
    {
852 1
        if (!$asset instanceof Asset) {
853
            $asset = new Asset($this->builder, $asset);
854
        }
855
856 1
        return Image::getDominantColor($asset);
857
    }
858
859
    /**
860
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
861
     *
862
     * @param string|Asset $asset
863
     *
864
     * @return string
865
     */
866 1
    public function lqip($asset): string
867
    {
868 1
        if (!$asset instanceof Asset) {
869
            $asset = new Asset($this->builder, $asset);
870
        }
871
872 1
        return Image::getLqip($asset);
873
    }
874
875
    /**
876
     * Converts an hexadecimal color to RGB.
877
     *
878
     * @throws RuntimeException
879
     */
880 1
    public function hexToRgb(?string $variable): array
881
    {
882 1
        $variable = $variable ?? '';
883
884 1
        if (!self::isHex($variable)) {
885
            throw new RuntimeException(sprintf('"%s" is not a valid hexadecimal value.', $variable));
886
        }
887 1
        $hex = ltrim($variable, '#');
888 1
        if (\strlen($hex) == 3) {
889
            $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
890
        }
891 1
        $c = hexdec($hex);
892
893 1
        return [
894 1
            'red'   => $c >> 16 & 0xFF,
895 1
            'green' => $c >> 8 & 0xFF,
896 1
            'blue'  => $c & 0xFF,
897 1
        ];
898
    }
899
900
    /**
901
     * Split a string in multiple lines.
902
     */
903 1
    public function splitLine(?string $variable, int $max = 18): array
904
    {
905 1
        $variable = $variable ?? '';
906
907 1
        return preg_split("/.{0,{$max}}\K(\s+|$)/", $variable, 0, PREG_SPLIT_NO_EMPTY);
908
    }
909
910
    /**
911
     * Is a hexadecimal color is valid?
912
     */
913 1
    private static function isHex(string $hex): bool
914
    {
915 1
        $valid = \is_string($hex);
916 1
        $hex = ltrim($hex, '#');
917 1
        $length = \strlen($hex);
918 1
        $valid = $valid && ($length === 3 || $length === 6);
919 1
        $valid = $valid && ctype_xdigit($hex);
920
921 1
        return $valid;
922
    }
923
}
924