Passed
Pull Request — master (#2077)
by Arnaud
06:24
created

Core::html()   D

Complexity

Conditions 19
Paths 93

Size

Total Lines 92
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 28.5726

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 19
eloc 54
c 2
b 2
f 0
nc 93
nop 4
dl 0
loc 92
ccs 40
cts 57
cp 0.7018
crap 28.5726
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 Symfony\Component\VarDumper\Cloner\VarCloner;
34
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
35
use Symfony\Component\Yaml\Exception\ParseException;
36
use Symfony\Component\Yaml\Yaml;
37
use Twig\DeprecatedCallableInfo;
38
39
/**
40
 * Class Renderer\Extension\Core.
41
 */
42
class Core extends SlugifyExtension
43
{
44
    /** @var Builder */
45
    protected $builder;
46
47
    /** @var Config */
48
    protected $config;
49
50
    /** @var Slugify */
51
    private static $slugifier;
52
53 1
    public function __construct(Builder $builder)
54
    {
55 1
        if (!self::$slugifier instanceof Slugify) {
56 1
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
57
        }
58
59 1
        parent::__construct(self::$slugifier);
60
61 1
        $this->builder = $builder;
62 1
        $this->config = $builder->getConfig();
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68
    public function getName(): string
69
    {
70
        return 'CoreExtension';
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76 1
    public function getFunctions()
77
    {
78 1
        return [
79 1
            new \Twig\TwigFunction('url', [$this, 'url'], ['needs_context' => true]),
80
            // assets
81 1
            new \Twig\TwigFunction('asset', [$this, 'asset']),
82 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
83 1
            new \Twig\TwigFunction('image_srcset', [$this, 'imageSrcset']),
84 1
            new \Twig\TwigFunction('image_sizes', [$this, 'imageSizes']),
85
            // content
86 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
87
            // others
88 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
89 1
            new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]),
90
            // deprecated
91 1
            new \Twig\TwigFunction(
92 1
                'hash',
93 1
                [$this, 'integrity'],
94 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'integrity')]
95 1
            ),
96 1
            new \Twig\TwigFunction(
97 1
                'minify',
98 1
                [$this, 'minify'],
99 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'minify filter')]
100 1
            ),
101 1
            new \Twig\TwigFunction(
102 1
                'toCSS',
103 1
                [$this, 'toCss'],
104 1
                ['deprecation_info' => new DeprecatedCallableInfo('', '', 'to_css filter')]
105 1
            ),
106 1
        ];
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112 1
    public function getFilters(): array
113
    {
114 1
        return [
115 1
            new \Twig\TwigFilter('url', [$this, 'url'], ['needs_context' => true]),
116
            // collections
117 1
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
118 1
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
119 1
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
120 1
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
121
            // assets
122 1
            new \Twig\TwigFilter('html', [$this, 'html'], ['needs_context' => true]),
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('dataurl', [$this, 'dataurl']),
133 1
            new \Twig\TwigFilter('dominant_color', [$this, 'dominantColor']),
134 1
            new \Twig\TwigFilter('lqip', [$this, 'lqip']),
135 1
            new \Twig\TwigFilter('webp', [$this, 'webp']),
136 1
            new \Twig\TwigFilter('avif', [$this, 'avif']),
137
            // content
138 1
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
139 1
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
140 1
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
141 1
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
142 1
            new \Twig\TwigFilter('toc', [$this, 'markdownToToc']),
143 1
            new \Twig\TwigFilter('json_decode', [$this, 'jsonDecode']),
144 1
            new \Twig\TwigFilter('yaml_parse', [$this, 'yamlParse']),
145 1
            new \Twig\TwigFilter('preg_split', [$this, 'pregSplit']),
146 1
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
147 1
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
148 1
            new \Twig\TwigFilter('splitline', [$this, 'splitLine']),
149
            // date
150 1
            new \Twig\TwigFilter('duration_to_iso8601', ['\Cecil\Util\Date', 'durationToIso8601']),
151 1
        ];
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 1
    public function getTests()
158
    {
159 1
        return [
160 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
161 1
        ];
162
    }
163
164
    /**
165
     * Filters by Section.
166
     */
167
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
168
    {
169
        return $this->filterBy($pages, 'section', $section);
170
    }
171
172
    /**
173
     * Filters a pages collection by variable's name/value.
174
     */
175 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
176
    {
177 1
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
178
            // is a dedicated getter exists?
179 1
            $method = 'get' . ucfirst($variable);
180 1
            if (method_exists($page, $method) && $page->$method() == $value) {
181
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
182
            }
183
            // or a classic variable
184 1
            if ($page->getVariable($variable) == $value) {
185 1
                return $page->getType() == Type::PAGE->value && !$page->isVirtual() && true;
186
            }
187 1
        });
188
189 1
        return $filteredPages;
190
    }
191
192
    /**
193
     * Sorts a collection by title.
194
     */
195 1
    public function sortByTitle(\Traversable $collection): array
196
    {
197 1
        $sort = \SORT_ASC;
198
199 1
        $collection = iterator_to_array($collection);
200 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

200
        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

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