Passed
Pull Request — master (#2180)
by Arnaud
04:49
created

Core::html()   D

Complexity

Conditions 20
Paths 39

Size

Total Lines 94
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 29.5651

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 20
eloc 56
c 1
b 1
f 0
nc 39
nop 4
dl 0
loc 94
ccs 42
cts 59
cp 0.7119
crap 29.5651
rs 4.1666

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('html', [$this, 'html'], ['needs_context' => true]),
84 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
85 1
            new \Twig\TwigFunction('image_srcset', [$this, 'imageSrcset']),
86 1
            new \Twig\TwigFunction('image_sizes', [$this, 'imageSizes']),
87
            // content
88 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
89 1
            new \Twig\TwigFunction('hash', [$this, 'hash']),
90
            // others
91 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
92 1
            new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]),
93
            // deprecated
94 1
            new \Twig\TwigFunction(
95 1
                'minify',
96 1
                [$this, 'minify'],
97 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'minify filter')]
98 1
            ),
99 1
            new \Twig\TwigFunction(
100 1
                'toCSS',
101 1
                [$this, 'toCss'],
102 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'to_css filter')]
103 1
            ),
104 1
        ];
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 1
    public function getFilters(): array
111
    {
112 1
        return [
113 1
            new \Twig\TwigFilter('url', [$this, 'url'], ['needs_context' => true]),
114
            // collections
115 1
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
116 1
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
117 1
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
118 1
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
119
            // assets
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('cover', [$this, 'cover']),
130 1
            new \Twig\TwigFilter('dataurl', [$this, 'dataurl']),
131 1
            new \Twig\TwigFilter('dominant_color', [$this, 'dominantColor']),
132 1
            new \Twig\TwigFilter('lqip', [$this, 'lqip']),
133 1
            new \Twig\TwigFilter('webp', [$this, 'webp']),
134 1
            new \Twig\TwigFilter('avif', [$this, 'avif']),
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
            new \Twig\TwigFilter('iterable', [$this, 'iterable']),
148
            // date
149 1
            new \Twig\TwigFilter('duration_to_iso8601', ['\Cecil\Util\Date', 'durationToIso8601']),
150
            // deprecated
151 1
            new \Twig\TwigFilter(
152 1
                'html',
153 1
                [$this, 'html'],
154 1
                [
155 1
                    'needs_context' => true,
156 1
                    'deprecation_info' => new DeprecatedCallableInfo('', '', 'html function')
157 1
                ]
158 1
            ),
159 1
        ];
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 1
    public function getTests()
166
    {
167 1
        return [
168 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
169 1
        ];
170
    }
171
172
    /**
173
     * Filters by Section.
174
     */
175
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
176
    {
177
        return $this->filterBy($pages, 'section', $section);
178
    }
179
180
    /**
181
     * Filters a pages collection by variable's name/value.
182
     */
183 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
184
    {
185 1
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
186
            // is a dedicated getter exists?
187 1
            $method = 'get' . ucfirst($variable);
188 1
            if (method_exists($page, $method) && $page->$method() == $value) {
189
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
190
            }
191
            // or a classic variable
192 1
            if ($page->getVariable($variable) == $value) {
193 1
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
194
            }
195 1
        });
196
197 1
        return $filteredPages;
198
    }
199
200
    /**
201
     * Sorts a collection by title.
202
     */
203 1
    public function sortByTitle(\Traversable $collection): array
204
    {
205 1
        $sort = \SORT_ASC;
206
207 1
        $collection = iterator_to_array($collection);
208 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

208
        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

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