Passed
Push — 8.x-dev ( 640d34...2250ec )
by Arnaud
04:51 queued 16s
created

Core::html()   C

Complexity

Conditions 17
Paths 66

Size

Total Lines 83
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 21.5156

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 17
eloc 48
c 1
b 1
f 0
nc 66
nop 4
dl 0
loc 83
ccs 30
cts 40
cp 0.75
crap 21.5156
rs 5.2166

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\RuntimeException;
28
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
29
use Cocur\Slugify\Slugify;
30
use MatthiasMullie\Minify;
31
use ScssPhp\ScssPhp\Compiler;
32
use Symfony\Component\VarDumper\Cloner\VarCloner;
33
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
34
use Symfony\Component\Yaml\Exception\ParseException;
35
use Symfony\Component\Yaml\Yaml;
36
37
/**
38
 * Class Renderer\Extension\Core.
39
 */
40
class Core extends SlugifyExtension
41
{
42
    /** @var Builder */
43
    protected $builder;
44
45
    /** @var Config */
46
    protected $config;
47
48
    /** @var Slugify */
49 1
    private static $slugifier;
50
51 1
    public function __construct(Builder $builder)
52 1
    {
53
        if (!self::$slugifier instanceof Slugify) {
54
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
55 1
        }
56
57 1
        parent::__construct(self::$slugifier);
58 1
59
        $this->builder = $builder;
60
        $this->config = $builder->getConfig();
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function getName(): string
67
    {
68
        return 'CoreExtension';
69
    }
70
71
    /**
72 1
     * {@inheritdoc}
73
     */
74 1
    public function getFunctions()
75 1
    {
76
        return [
77 1
            new \Twig\TwigFunction('url', [$this, 'url'], ['needs_context' => true]),
78 1
            // assets
79 1
            new \Twig\TwigFunction('asset', [$this, 'asset']),
80 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
81
            new \Twig\TwigFunction('image_srcset', [$this, 'imageSrcset']),
82 1
            new \Twig\TwigFunction('image_sizes', [$this, 'imageSizes']),
83
            // content
84 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
85 1
            // others
86
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
87
            new \Twig\TwigFunction('d', [$this, 'varDump'], ['needs_context' => true, 'needs_environment' => true]),
88
            // deprecated
89
            new \Twig\TwigFunction(
90
                'hash',
91 1
                [$this, 'integrity'],
92
                ['deprecated' => true, 'alternative' => 'integrity']
93 1
            ),
94 1
            new \Twig\TwigFunction(
95
                'minify',
96 1
                [$this, 'minify'],
97 1
                ['deprecated' => true, 'alternative' => 'minify filter']
98 1
            ),
99 1
            new \Twig\TwigFunction(
100
                'toCSS',
101 1
                [$this, 'toCss'],
102 1
                ['deprecated' => true, 'alternative' => 'to_css filter']
103 1
            ),
104 1
        ];
105 1
    }
106 1
107 1
    /**
108 1
     * {@inheritdoc}
109 1
     */
110 1
    public function getFilters(): array
111 1
    {
112 1
        return [
113 1
            new \Twig\TwigFilter('url', [$this, 'url'], ['needs_context' => true]),
114 1
            // collections
115
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
116 1
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
117 1
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
118 1
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
119 1
            // assets
120 1
            new \Twig\TwigFilter('html', [$this, 'html'], ['needs_context' => true]),
121 1
            new \Twig\TwigFilter('inline', [$this, 'inline']),
122 1
            new \Twig\TwigFilter('fingerprint', [$this, 'fingerprint']),
123 1
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
124 1
            new \Twig\TwigFilter('minify', [$this, 'minify']),
125 1
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
126 1
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
127 1
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
128
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
129
            new \Twig\TwigFilter('resize', [$this, 'resize']),
130
            new \Twig\TwigFilter('dataurl', [$this, 'dataurl']),
131
            new \Twig\TwigFilter('dominant_color', [$this, 'dominantColor']),
132
            new \Twig\TwigFilter('lqip', [$this, 'lqip']),
133 1
            new \Twig\TwigFilter('webp', [$this, 'webp']),
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
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
139
            new \Twig\TwigFilter('toc', [$this, 'markdownToToc']),
140
            new \Twig\TwigFilter('json_decode', [$this, 'jsonDecode']),
141
            new \Twig\TwigFilter('yaml_parse', [$this, 'yamlParse']),
142
            new \Twig\TwigFilter('preg_split', [$this, 'pregSplit']),
143
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
144
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
145
            new \Twig\TwigFilter('splitline', [$this, 'splitLine']),
146
        ];
147
    }
148
149
    /**
150
     * {@inheritdoc}
151 1
     */
152
    public function getTests()
153 1
    {
154
        return [
155 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
156 1
        ];
157
    }
158
159
    /**
160 1
     * Filters by Section.
161 1
     */
162
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
163 1
    {
164
        return $this->filterBy($pages, 'section', $section);
165 1
    }
166
167
    /**
168
     * Filters a pages collection by variable's name/value.
169
     */
170
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
171 1
    {
172
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
173 1
            // is a dedicated getter exists?
174
            $method = 'get' . ucfirst($variable);
175 1
            if (method_exists($page, $method) && $page->$method() == $value) {
176 1
                return $page->getType() == Type::PAGE() && !$page->isVirtual() && true;
177
            }
178 1
            // or a classic variable
179
            if ($page->getVariable($variable) == $value) {
180
                return $page->getType() == Type::PAGE() && !$page->isVirtual() && true;
181
            }
182
        });
183
184 1
        return $filteredPages;
185
    }
186 1
187 1
    /**
188 1
     * Sorts a collection by title.
189
     */
190 1
    public function sortByTitle(\Traversable $collection): array
191
    {
192
        $sort = \SORT_ASC;
193 1
194 1
        $collection = iterator_to_array($collection);
195
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), $sort, \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
0 ignored issues
show
Bug introduced by
array_keys($collection) cannot be passed to array_multisort() as the parameter $array expects a reference. ( Ignorable by Annotation )

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

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

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

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