Passed
Pull Request — master (#2148)
by Arnaud
09:31 queued 04:12
created

Core::html()   D

Complexity

Conditions 19
Paths 93

Size

Total Lines 92
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 28.5726

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 19
eloc 54
c 1
b 0
f 0
nc 93
nop 4
dl 0
loc 92
ccs 40
cts 57
cp 0.7018
crap 28.5726
rs 4.5166

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

198
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), $sort, /** @scrutinizer ignore-type */ \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
Loading history...
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

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