Passed
Pull Request — master (#2133)
by Arnaud
09:42 queued 04:08
created

Core::html()   D

Complexity

Conditions 19
Paths 93

Size

Total Lines 92
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 37.0495

Importance

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

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Renderer\Extension;
15
16
use Cecil\Assets\Asset;
17
use Cecil\Assets\Cache;
18
use Cecil\Assets\Image;
19
use Cecil\Assets\Url;
20
use Cecil\Builder;
21
use Cecil\Collection\CollectionInterface;
22
use Cecil\Collection\Page\Collection as PagesCollection;
23
use Cecil\Collection\Page\Page;
24
use Cecil\Collection\Page\Type;
25
use Cecil\Config;
26
use Cecil\Converter\Parsedown;
27
use Cecil\Exception\ConfigException;
28
use Cecil\Exception\RuntimeException;
29
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
30
use Cocur\Slugify\Slugify;
31
use MatthiasMullie\Minify;
32
use ScssPhp\ScssPhp\Compiler;
33
use ScssPhp\ScssPhp\OutputStyle;
34
use Symfony\Component\VarDumper\Cloner\VarCloner;
35
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
36
use Symfony\Component\Yaml\Exception\ParseException;
37
use Symfony\Component\Yaml\Yaml;
38
use Twig\DeprecatedCallableInfo;
39
40
/**
41
 * Class Renderer\Extension\Core.
42
 */
43
class Core extends SlugifyExtension
44
{
45
    /** @var Builder */
46
    protected $builder;
47
48
    /** @var Config */
49
    protected $config;
50
51
    /** @var Slugify */
52
    private static $slugifier;
53
54 1
    public function __construct(Builder $builder)
55
    {
56 1
        if (!self::$slugifier instanceof Slugify) {
57 1
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
58
        }
59
60 1
        parent::__construct(self::$slugifier);
61
62 1
        $this->builder = $builder;
63 1
        $this->config = $builder->getConfig();
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function getName(): string
70
    {
71
        return 'CoreExtension';
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 1
    public function getFunctions()
78
    {
79 1
        return [
80 1
            new \Twig\TwigFunction('url', [$this, 'url'], ['needs_context' => true]),
81
            // assets
82 1
            new \Twig\TwigFunction('asset', [$this, 'asset']),
83 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
84 1
            new \Twig\TwigFunction('image_srcset', [$this, 'imageSrcset']),
85 1
            new \Twig\TwigFunction('image_sizes', [$this, 'imageSizes']),
86
            // content
87 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
88
            // others
89 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
90 1
            new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]),
91
            // deprecated
92 1
            new \Twig\TwigFunction(
93 1
                'hash',
94 1
                [$this, 'integrity'],
95 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'integrity')]
96 1
            ),
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('html', [$this, 'html'], ['needs_context' => true]),
124 1
            new \Twig\TwigFilter('inline', [$this, 'inline']),
125 1
            new \Twig\TwigFilter('fingerprint', [$this, 'fingerprint']),
126 1
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
127 1
            new \Twig\TwigFilter('minify', [$this, 'minify']),
128 1
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
129 1
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
130 1
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
131 1
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
132 1
            new \Twig\TwigFilter('resize', [$this, 'resize']),
133 1
            new \Twig\TwigFilter('dataurl', [$this, 'dataurl']),
134 1
            new \Twig\TwigFilter('dominant_color', [$this, 'dominantColor']),
135 1
            new \Twig\TwigFilter('lqip', [$this, 'lqip']),
136 1
            new \Twig\TwigFilter('webp', [$this, 'webp']),
137 1
            new \Twig\TwigFilter('avif', [$this, 'avif']),
138
            // content
139 1
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
140 1
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
141 1
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
142 1
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
143 1
            new \Twig\TwigFilter('toc', [$this, 'markdownToToc']),
144 1
            new \Twig\TwigFilter('json_decode', [$this, 'jsonDecode']),
145 1
            new \Twig\TwigFilter('yaml_parse', [$this, 'yamlParse']),
146 1
            new \Twig\TwigFilter('preg_split', [$this, 'pregSplit']),
147 1
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
148 1
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
149 1
            new \Twig\TwigFilter('splitline', [$this, 'splitLine']),
150
            // date
151 1
            new \Twig\TwigFilter('duration_to_iso8601', ['\Cecil\Util\Date', 'durationToIso8601']),
152 1
        ];
153
    }
154
155
    /**
156
     * {@inheritdoc}
157
     */
158 1
    public function getTests()
159
    {
160 1
        return [
161 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
162 1
        ];
163
    }
164
165
    /**
166
     * Filters by Section.
167
     */
168
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
169
    {
170
        return $this->filterBy($pages, 'section', $section);
171
    }
172
173
    /**
174
     * Filters a pages collection by variable's name/value.
175
     */
176 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
177
    {
178 1
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
179
            // is a dedicated getter exists?
180 1
            $method = 'get' . ucfirst($variable);
181 1
            if (method_exists($page, $method) && $page->$method() == $value) {
182
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
183
            }
184
            // or a classic variable
185 1
            if ($page->getVariable($variable) == $value) {
186 1
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
187
            }
188 1
        });
189
190 1
        return $filteredPages;
191
    }
192
193
    /**
194
     * Sorts a collection by title.
195
     */
196 1
    public function sortByTitle(\Traversable $collection): array
197
    {
198 1
        $sort = \SORT_ASC;
199
200 1
        $collection = iterator_to_array($collection);
201 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

201
        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

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