Core::html()   D
last analyzed

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
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
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
 * Core Twig extension.
42
 *
43
 * This extension provides various utility functions and filters for use in Twig templates,
44
 * including URL generation, asset management, content processing, and more.
45
 */
46
class Core extends SlugifyExtension
47
{
48
    /** @var Builder */
49
    protected $builder;
50
51
    /** @var Config */
52
    protected $config;
53
54
    /** @var Slugify */
55
    private static $slugifier;
56
57 1
    public function __construct(Builder $builder)
58
    {
59 1
        if (!self::$slugifier instanceof Slugify) {
60 1
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
61
        }
62
63 1
        parent::__construct(self::$slugifier);
64
65 1
        $this->builder = $builder;
66 1
        $this->config = $builder->getConfig();
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    public function getName(): string
73
    {
74
        return 'CoreExtension';
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 1
    public function getFunctions()
81
    {
82 1
        return [
83 1
            new \Twig\TwigFunction('url', [$this, 'url'], ['needs_context' => true]),
84
            // assets
85 1
            new \Twig\TwigFunction('asset', [$this, 'asset']),
86 1
            new \Twig\TwigFunction('html', [$this, 'html'], ['needs_context' => true]),
87 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
88 1
            new \Twig\TwigFunction('image_srcset', [$this, 'imageSrcset']),
89 1
            new \Twig\TwigFunction('image_sizes', [$this, 'imageSizes']),
90
            // content
91 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
92 1
            new \Twig\TwigFunction('hash', [$this, 'hash']),
93
            // others
94 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
95 1
            new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]),
96
            // deprecated
97 1
            new \Twig\TwigFunction(
98 1
                'minify',
99 1
                [$this, 'minify'],
100 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'minify filter')]
101 1
            ),
102 1
            new \Twig\TwigFunction(
103 1
                'toCSS',
104 1
                [$this, 'toCss'],
105 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'to_css filter')]
106 1
            ),
107 1
        ];
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 1
    public function getFilters(): array
114
    {
115 1
        return [
116 1
            new \Twig\TwigFilter('url', [$this, 'url'], ['needs_context' => true]),
117
            // collections
118 1
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
119 1
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
120 1
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
121 1
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
122
            // assets
123 1
            new \Twig\TwigFilter('inline', [$this, 'inline']),
124 1
            new \Twig\TwigFilter('fingerprint', [$this, 'fingerprint']),
125 1
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
126 1
            new \Twig\TwigFilter('minify', [$this, 'minify']),
127 1
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
128 1
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
129 1
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
130 1
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
131 1
            new \Twig\TwigFilter('resize', [$this, 'resize']),
132 1
            new \Twig\TwigFilter('cover', [$this, 'cover']),
133 1
            new \Twig\TwigFilter('maskable', [$this, 'maskable']),
134 1
            new \Twig\TwigFilter('dataurl', [$this, 'dataurl']),
135 1
            new \Twig\TwigFilter('dominant_color', [$this, 'dominantColor']),
136 1
            new \Twig\TwigFilter('lqip', [$this, 'lqip']),
137 1
            new \Twig\TwigFilter('webp', [$this, 'webp']),
138 1
            new \Twig\TwigFilter('avif', [$this, 'avif']),
139
            // content
140 1
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
141 1
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
142 1
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
143 1
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
144 1
            new \Twig\TwigFilter('toc', [$this, 'markdownToToc']),
145 1
            new \Twig\TwigFilter('json_decode', [$this, 'jsonDecode']),
146 1
            new \Twig\TwigFilter('yaml_parse', [$this, 'yamlParse']),
147 1
            new \Twig\TwigFilter('preg_split', [$this, 'pregSplit']),
148 1
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
149 1
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
150 1
            new \Twig\TwigFilter('splitline', [$this, 'splitLine']),
151 1
            new \Twig\TwigFilter('iterable', [$this, 'iterable']),
152
            // date
153 1
            new \Twig\TwigFilter('duration_to_iso8601', ['\Cecil\Util\Date', 'durationToIso8601']),
154
            // deprecated
155 1
            new \Twig\TwigFilter(
156 1
                'html',
157 1
                [$this, 'html'],
158 1
                [
159 1
                    'needs_context' => true,
160 1
                    'deprecation_info' => new DeprecatedCallableInfo('', '', 'html function')
161 1
                ]
162 1
            ),
163 1
        ];
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169 1
    public function getTests()
170
    {
171 1
        return [
172 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
173 1
        ];
174
    }
175
176
    /**
177
     * Filters by Section.
178
     */
179
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
180
    {
181
        return $this->filterBy($pages, 'section', $section);
182
    }
183
184
    /**
185
     * Filters a pages collection by variable's name/value.
186
     */
187 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
188
    {
189 1
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
190
            // is a dedicated getter exists?
191 1
            $method = 'get' . ucfirst($variable);
192 1
            if (method_exists($page, $method) && $page->$method() == $value) {
193
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
194
            }
195
            // or a classic variable
196 1
            if ($page->getVariable($variable) == $value) {
197 1
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
198
            }
199 1
        });
200
201 1
        return $filteredPages;
202
    }
203
204
    /**
205
     * Sorts a collection by title.
206
     */
207 1
    public function sortByTitle(\Traversable $collection): array
208
    {
209 1
        $sort = \SORT_ASC;
210
211 1
        $collection = iterator_to_array($collection);
212 1
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), $sort, \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
0 ignored issues
show
Bug introduced by
array_keys($collection) cannot be passed to array_multisort() as the parameter $array expects a reference. ( Ignorable by Annotation )

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

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

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

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