Passed
Push — master ( 133180...6e23f4 )
by Arnaud
06:22
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 0
Metric Value
cc 19
eloc 54
c 0
b 0
f 0
nc 93
nop 4
dl 0
loc 92
rs 4.5166
ccs 36
cts 57
cp 0.6316
crap 37.0495

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 1
            new \Twig\TwigFunction('hash', [$this, 'hash']),
89
            // others
90 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
91 1
            new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]),
92
            // deprecated
93 1
            new \Twig\TwigFunction(
94 1
                'minify',
95 1
                [$this, 'minify'],
96 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'minify filter')]
97 1
            ),
98 1
            new \Twig\TwigFunction(
99 1
                'toCSS',
100 1
                [$this, 'toCss'],
101 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'to_css filter')]
102 1
            ),
103 1
        ];
104
    }
105
106
    /**
107
     * {@inheritdoc}
108
     */
109 1
    public function getFilters(): array
110
    {
111 1
        return [
112 1
            new \Twig\TwigFilter('url', [$this, 'url'], ['needs_context' => true]),
113
            // collections
114 1
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
115 1
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
116 1
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
117 1
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
118
            // assets
119 1
            new \Twig\TwigFilter('html', [$this, 'html'], ['needs_context' => true]),
120 1
            new \Twig\TwigFilter('inline', [$this, 'inline']),
121 1
            new \Twig\TwigFilter('fingerprint', [$this, 'fingerprint']),
122 1
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
123 1
            new \Twig\TwigFilter('minify', [$this, 'minify']),
124 1
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
125 1
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
126 1
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
127 1
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
128 1
            new \Twig\TwigFilter('resize', [$this, 'resize']),
129 1
            new \Twig\TwigFilter('dataurl', [$this, 'dataurl']),
130 1
            new \Twig\TwigFilter('dominant_color', [$this, 'dominantColor']),
131 1
            new \Twig\TwigFilter('lqip', [$this, 'lqip']),
132 1
            new \Twig\TwigFilter('webp', [$this, 'webp']),
133 1
            new \Twig\TwigFilter('avif', [$this, 'avif']),
134
            // content
135 1
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
136 1
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
137 1
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
138 1
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
139 1
            new \Twig\TwigFilter('toc', [$this, 'markdownToToc']),
140 1
            new \Twig\TwigFilter('json_decode', [$this, 'jsonDecode']),
141 1
            new \Twig\TwigFilter('yaml_parse', [$this, 'yamlParse']),
142 1
            new \Twig\TwigFilter('preg_split', [$this, 'pregSplit']),
143 1
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
144 1
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
145 1
            new \Twig\TwigFilter('splitline', [$this, 'splitLine']),
146
            // date
147 1
            new \Twig\TwigFilter('duration_to_iso8601', ['\Cecil\Util\Date', 'durationToIso8601']),
148 1
        ];
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 1
    public function getTests()
155
    {
156 1
        return [
157 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
158 1
        ];
159
    }
160
161
    /**
162
     * Filters by Section.
163
     */
164
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
165
    {
166
        return $this->filterBy($pages, 'section', $section);
167
    }
168
169
    /**
170
     * Filters a pages collection by variable's name/value.
171
     */
172 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
173
    {
174 1
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
175
            // is a dedicated getter exists?
176 1
            $method = 'get' . ucfirst($variable);
177 1
            if (method_exists($page, $method) && $page->$method() == $value) {
178
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
179
            }
180
            // or a classic variable
181 1
            if ($page->getVariable($variable) == $value) {
182 1
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
183
            }
184 1
        });
185
186 1
        return $filteredPages;
187
    }
188
189
    /**
190
     * Sorts a collection by title.
191
     */
192 1
    public function sortByTitle(\Traversable $collection): array
193
    {
194 1
        $sort = \SORT_ASC;
195
196 1
        $collection = iterator_to_array($collection);
197 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

197
        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

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