Passed
Push — master ( da6944...a34110 )
by
unknown
05:24
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('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
            // deprecated
150 1
            new \Twig\TwigFilter(
151 1
                'html',
152 1
                [$this, 'html'],
153 1
                [
154 1
                    'needs_context' => true,
155 1
                    'deprecation_info' => new DeprecatedCallableInfo('', '', 'html function')
156 1
                ]
157 1
            ),
158 1
        ];
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 1
    public function getTests()
165
    {
166 1
        return [
167 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
168 1
        ];
169
    }
170
171
    /**
172
     * Filters by Section.
173
     */
174
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
175
    {
176
        return $this->filterBy($pages, 'section', $section);
177
    }
178
179
    /**
180
     * Filters a pages collection by variable's name/value.
181
     */
182 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
183
    {
184 1
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
185
            // is a dedicated getter exists?
186 1
            $method = 'get' . ucfirst($variable);
187 1
            if (method_exists($page, $method) && $page->$method() == $value) {
188
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
189
            }
190
            // or a classic variable
191 1
            if ($page->getVariable($variable) == $value) {
192 1
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
193
            }
194 1
        });
195
196 1
        return $filteredPages;
197
    }
198
199
    /**
200
     * Sorts a collection by title.
201
     */
202 1
    public function sortByTitle(\Traversable $collection): array
203
    {
204 1
        $sort = \SORT_ASC;
205
206 1
        $collection = iterator_to_array($collection);
207 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

207
        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

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