Passed
Push — feat/markdown-highlighter ( 1efac5...98434c )
by Arnaud
13:07 queued 08:45
created

Extension::html()   C

Complexity

Conditions 15
Paths 30

Size

Total Lines 78
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 15.271

Importance

Changes 6
Bugs 2 Features 0
Metric Value
cc 15
eloc 46
c 6
b 2
f 0
nc 30
nop 3
dl 0
loc 78
ccs 42
cts 47
cp 0.8936
crap 15.271
rs 5.9166

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\Twig;
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\Config;
25
use Cecil\Converter\Parsedown;
26
use Cecil\Exception\RuntimeException;
27
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
28
use Cocur\Slugify\Slugify;
29
use MatthiasMullie\Minify;
30
use ScssPhp\ScssPhp\Compiler;
31
use Symfony\Component\Yaml\Exception\ParseException;
32
use Symfony\Component\Yaml\Yaml;
33
34
/**
35
 * Class Twig\Extension.
36
 */
37
class Extension extends SlugifyExtension
38
{
39
    /** @var Builder */
40
    protected $builder;
41
42
    /** @var Config */
43
    protected $config;
44
45
    /** @var Slugify */
46
    private static $slugifier;
47
48 1
    public function __construct(Builder $builder)
49
    {
50 1
        if (!self::$slugifier instanceof Slugify) {
51 1
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
52
        }
53
54 1
        parent::__construct(self::$slugifier);
55
56 1
        $this->builder = $builder;
57 1
        $this->config = $this->builder->getConfig();
58 1
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function getName()
64
    {
65
        return 'cecil';
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 1
    public function getFilters()
72
    {
73
        return [
74 1
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
75
            // sort
76 1
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
77 1
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
78 1
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
79
            // assets
80 1
            new \Twig\TwigFilter('url', [$this, 'url']),
81 1
            new \Twig\TwigFilter('html', [$this, 'html']),
82 1
            new \Twig\TwigFilter('inline', [$this, 'inline']),
83 1
            new \Twig\TwigFilter('fingerprint', [$this, 'fingerprint']),
84 1
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
85 1
            new \Twig\TwigFilter('minify', [$this, 'minify']),
86 1
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
87 1
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
88 1
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
89 1
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
90 1
            new \Twig\TwigFilter('resize', [$this, 'resize']),
91 1
            new \Twig\TwigFilter('dataurl', [$this, 'dataurl']),
92
            // content
93 1
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
94 1
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
95 1
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
96 1
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
97 1
            new \Twig\TwigFilter('json_decode', [$this, 'jsonDecode']),
98 1
            new \Twig\TwigFilter('yaml_parse', [$this, 'yamlParse']),
99 1
            new \Twig\TwigFilter('preg_split', [$this, 'pregSplit']),
100 1
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
101 1
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
102
            // deprecated
103 1
            new \Twig\TwigFilter(
104 1
                'filterBySection',
105 1
                [$this, 'filterBySection'],
106 1
                ['deprecated' => true, 'alternative' => 'filter_by']
107
            ),
108 1
            new \Twig\TwigFilter(
109 1
                'filterBy',
110 1
                [$this, 'filterBy'],
111 1
                ['deprecated' => true, 'alternative' => 'filter_by']
112
            ),
113 1
            new \Twig\TwigFilter(
114 1
                'sortByTitle',
115 1
                [$this, 'sortByTitle'],
116 1
                ['deprecated' => true, 'alternative' => 'sort_by_title']
117
            ),
118 1
            new \Twig\TwigFilter(
119 1
                'sortByWeight',
120 1
                [$this, 'sortByWeight'],
121 1
                ['deprecated' => true, 'alternative' => 'sort_by_weight']
122
            ),
123 1
            new \Twig\TwigFilter(
124 1
                'sortByDate',
125 1
                [$this, 'sortByDate'],
126 1
                ['deprecated' => true, 'alternative' => 'sort_by_date']
127
            ),
128 1
            new \Twig\TwigFilter(
129 1
                'minifyCSS',
130 1
                [$this, 'minifyCss'],
131 1
                ['deprecated' => true, 'alternative' => 'minifyCss']
132
            ),
133 1
            new \Twig\TwigFilter(
134 1
                'minifyJS',
135 1
                [$this, 'minifyJs'],
136 1
                ['deprecated' => true, 'alternative' => 'minifyJs']
137
            ),
138 1
            new \Twig\TwigFilter(
139 1
                'SCSStoCSS',
140 1
                [$this, 'scssToCss'],
141 1
                ['deprecated' => true, 'alternative' => 'scss_to_css']
142
            ),
143 1
            new \Twig\TwigFilter(
144 1
                'excerptHtml',
145 1
                [$this, 'excerptHtml'],
146 1
                ['deprecated' => true, 'alternative' => 'excerpt_html']
147
            ),
148 1
            new \Twig\TwigFilter(
149 1
                'urlize',
150 1
                [$this, 'slugifyFilter'],
151 1
                ['deprecated' => true, 'alternative' => 'slugify']
152
            ),
153
        ];
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 1
    public function getFunctions()
160
    {
161
        return [
162
            // assets
163 1
            new \Twig\TwigFunction('url', [$this, 'url']),
164 1
            new \Twig\TwigFunction('asset', [$this, 'asset']),
165 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
166
            // content
167 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
168
            // others
169 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
170
            // deprecated
171 1
            new \Twig\TwigFunction(
172 1
                'minify',
173 1
                [$this, 'minify'],
174 1
                ['deprecated' => true, 'alternative' => 'minify filter']
175
            ),
176 1
            new \Twig\TwigFunction(
177 1
                'toCSS',
178 1
                [$this, 'toCss'],
179 1
                ['deprecated' => true, 'alternative' => 'to_css filter']
180
            ),
181 1
            new \Twig\TwigFunction(
182 1
                'hash',
183 1
                [$this, 'integrity'],
184 1
                ['deprecated' => true, 'alternative' => 'integrity']
185
            ),
186
        ];
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 1
    public function getTests()
193
    {
194
        return [
195 1
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
196
        ];
197
    }
198
199
    /**
200
     * Filters by Section.
201
     * Alias of `filterBy('section', $value)`.
202
     */
203
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
204
    {
205
        return $this->filterBy($pages, 'section', $section);
206
    }
207
208
    /**
209
     * Filters by variable's name/value.
210
     */
211 1
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
212
    {
213
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
214 1
            $notVirtual = false;
215 1
            if (!$page->isVirtual()) {
216 1
                $notVirtual = true;
217
            }
218
            // is a dedicated getter exists?
219 1
            $method = 'get'.ucfirst($variable);
220 1
            if (method_exists($page, $method) && $page->$method() == $value) {
221
                return $notVirtual && true;
222
            }
223 1
            if ($page->getVariable($variable) == $value) {
224 1
                return $notVirtual && true;
225
            }
226 1
        });
227
228 1
        return $filteredPages;
229
    }
230
231
    /**
232
     * Sorts by title.
233
     */
234 1
    public function sortByTitle(\Traversable $collection): array
235
    {
236 1
        $collection = iterator_to_array($collection);
237
        /** @var \array $collection */
238 1
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), \SORT_ASC, \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
0 ignored issues
show
Bug introduced by
SORT_ASC 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

238
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), /** @scrutinizer ignore-type */ \SORT_ASC, \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

238
        array_multisort(/** @scrutinizer ignore-type */ array_keys(/** @scrutinizer ignore-type */ $collection), \SORT_ASC, \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

238
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), \SORT_ASC, /** @scrutinizer ignore-type */ \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
Loading history...
239
240 1
        return $collection;
241
    }
242
243
    /**
244
     * Sorts by weight.
245
     */
246 1
    public function sortByWeight(\Traversable $collection): array
247
    {
248
        $callback = function ($a, $b) {
249 1
            if (!isset($a['weight'])) {
250 1
                $a['weight'] = 0;
251
            }
252 1
            if (!isset($b['weight'])) {
253
                $a['weight'] = 0;
254
            }
255 1
            if ($a['weight'] == $b['weight']) {
256 1
                return 0;
257
            }
258
259 1
            return ($a['weight'] < $b['weight']) ? -1 : 1;
260 1
        };
261
262 1
        $collection = iterator_to_array($collection);
263
        /** @var \array $collection */
264 1
        usort(/** @scrutinizer ignore-type */ $collection, $callback);
265
266 1
        return $collection;
267
    }
268
269
    /**
270
     * Sorts by date: the most recent first.
271
     */
272 1
    public function sortByDate(\Traversable $collection): array
273
    {
274
        $callback = function ($a, $b) {
275 1
            if ($a['date'] == $b['date']) {
276 1
                return 0;
277
            }
278
279 1
            return ($a['date'] > $b['date']) ? -1 : 1;
280 1
        };
281
282 1
        $collection = iterator_to_array($collection);
283
        /** @var \array $collection */
284 1
        usort(/** @scrutinizer ignore-type */ $collection, $callback);
285
286 1
        return $collection;
287
    }
288
289
    /**
290
     * Creates an URL.
291
     *
292
     * $options[
293
     *     'canonical' => true,
294
     *     'addhash'   => false,
295
     *     'format'    => 'json',
296
     * ];
297
     *
298
     * @param Page|Asset|string|null $value
299
     * @param array|null             $options
300
     *
301
     * @return mixed
302
     */
303 1
    public function url($value = null, array $options = null)
304
    {
305 1
        return new Url($this->builder, $value, $options);
306
    }
307
308
    /**
309
     * Creates an asset (CSS, JS, images, etc.).
310
     *
311
     * @param string|array $path    File path (relative from static/ dir).
312
     * @param array|null   $options
313
     *
314
     * @return Asset
315
     */
316 1
    public function asset($path, array $options = null): Asset
317
    {
318 1
        return new Asset($this->builder, $path, $options);
319
    }
320
321
    /**
322
     * Compiles a SCSS asset.
323
     *
324
     * @param string|Asset $asset
325
     *
326
     * @return Asset
327
     */
328 1
    public function toCss($asset): Asset
329
    {
330 1
        if (!$asset instanceof Asset) {
331
            $asset = new Asset($this->builder, $asset);
332
        }
333
334 1
        return $asset->compile();
335
    }
336
337
    /**
338
     * Minifying an asset (CSS or JS).
339
     *
340
     * @param string|Asset $asset
341
     *
342
     * @return Asset
343
     */
344 1
    public function minify($asset): Asset
345
    {
346 1
        if (!$asset instanceof Asset) {
347
            $asset = new Asset($this->builder, $asset);
348
        }
349
350 1
        return $asset->minify();
351
    }
352
353
    /**
354
     * Fingerprinting an asset.
355
     *
356
     * @param string|Asset $asset
357
     *
358
     * @return Asset
359
     */
360 1
    public function fingerprint($asset): Asset
361
    {
362 1
        if (!$asset instanceof Asset) {
363
            $asset = new Asset($this->builder, $asset);
364
        }
365
366 1
        return $asset->fingerprint();
367
    }
368
369
    /**
370
     * Resizes an image.
371
     *
372
     * @param string|Asset $asset
373
     *
374
     * @return Asset
375
     */
376
    public function resize($asset, int $size): Asset
377
    {
378
        if (!$asset instanceof Asset) {
379
            $asset = new Asset($this->builder, $asset);
380
        }
381
382
        return $asset->resize($size);
383
    }
384
385
    /**
386
     * Returns the data URL of an image.
387
     *
388
     * @param string|Asset $asset
389
     *
390
     * @return string
391
     */
392 1
    public function dataurl($asset): string
393
    {
394 1
        if (!$asset instanceof Asset) {
395
            $asset = new Asset($this->builder, $asset);
396
        }
397
398 1
        return $asset->dataurl();
399
    }
400
401
    /**
402
     * Hashing an asset with algo (sha384 by default).
403
     *
404
     * @param string|Asset $path
405
     * @param string       $algo
406
     *
407
     * @return string
408
     */
409 1
    public function integrity($asset, string $algo = 'sha384'): string
410
    {
411 1
        if (!$asset instanceof Asset) {
412 1
            $asset = new Asset($this->builder, $asset);
413
        }
414
415 1
        return $asset->getIntegrity($algo);
416
    }
417
418
    /**
419
     * Minifying a CSS string.
420
     */
421 1
    public function minifyCss(string $value): string
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\CSS($value);
431
            $value = $minifier->minify();
432
            $cache->set($cacheKey, $value);
433
        }
434
435
        return $cache->get($cacheKey, $value);
436
    }
437
438
    /**
439
     * Minifying a JavaScript string.
440
     */
441 1
    public function minifyJs(string $value): string
442
    {
443 1
        if ($this->builder->isDebug()) {
444 1
            return $value;
445
        }
446
447
        $cache = new Cache($this->builder);
448
        $cacheKey = $cache->createKeyFromString($value);
449
        if (!$cache->has($cacheKey)) {
450
            $minifier = new Minify\JS($value);
451
            $value = $minifier->minify();
452
            $cache->set($cacheKey, $value);
453
        }
454
455
        return $cache->get($cacheKey, $value);
456
    }
457
458
    /**
459
     * Compiles a SCSS string.
460
     */
461 1
    public function scssToCss(string $value): string
462
    {
463 1
        $cache = new Cache($this->builder);
464 1
        $cacheKey = $cache->createKeyFromString($value);
465 1
        if (!$cache->has($cacheKey)) {
466 1
            $scssPhp = new Compiler();
467 1
            $outputStyles = ['expanded', 'compressed'];
468 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
469 1
            if (!in_array($outputStyle, $outputStyles)) {
470
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
471
            }
472 1
            $scssPhp->setOutputStyle($outputStyle);
473 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
474 1
            if (!empty($variables)) {
475 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
476 1
                $scssPhp->replaceVariables($variables);
477
            }
478 1
            $value = $scssPhp->compileString($value)->getCss();
479 1
            $cache->set($cacheKey, $value);
480
        }
481
482 1
        return $cache->get($cacheKey, $value);
483
    }
484
485
    /**
486
     * Returns the HTML version of an asset.
487
     *
488
     * $options[
489
     *     'preload'    => false,
490
     *     'responsive' => false,
491
     * ];
492
     *
493
     * @throws RuntimeException
494
     */
495 1
    public function html(Asset $asset, array $attributes = [], array $options = []): string
496
    {
497 1
        $htmlAttributes = '';
498 1
        $preload = false;
499 1
        $responsive = $this->config->get('assets.images.responsive.enabled') ?? false;
500 1
        $webp = $this->config->get('assets.images.webp.enabled') ?? false;
501 1
        extract($options, EXTR_IF_EXISTS);
502
503 1
        foreach ($attributes as $name => $value) {
504 1
            $attribute = \sprintf(' %s="%s"', $name, $value);
505 1
            if (empty($value)) {
506 1
                $attribute = \sprintf(' %s', $name);
507
            }
508 1
            $htmlAttributes .= $attribute;
509
        }
510
511 1
        $asset->save();
512
513
        /* CSS or JavaScript */
514 1
        switch ($asset['ext']) {
515 1
            case 'css':
516 1
                if ($preload) {
517 1
                    return \sprintf(
518 1
                        '<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>',
519 1
                        $this->url($asset['path'], $options),
520 1
                        $htmlAttributes
521
                    );
522
                }
523
524 1
                return \sprintf('<link rel="stylesheet" href="%s"%s>', $this->url($asset['path'], $options), $htmlAttributes);
525 1
            case 'js':
526
                return \sprintf('<script src="%s"%s></script>', $this->url($asset['path'], $options), $htmlAttributes);
527
        }
528
529
        /* Image */
530 1
        if ($asset['type'] == 'image') {
531
            // responsive
532 1
            if ($responsive && $srcset = Image::buildSrcset(
533 1
                $asset,
534 1
                $this->config->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
535
            )) {
536
                $htmlAttributes .= \sprintf(' srcset="%s"', $srcset);
537
                $htmlAttributes .= \sprintf(' sizes="%s"', $this->config->get('assets.images.responsive.sizes.default') ?? '100vw');
538
            }
539
540
            // <img>
541 1
            $img = \sprintf(
542 1
                '<img src="%s" width="'.($asset->getWidth() ?: 0).'" height="'.($asset->getHeight() ?: 0).'"%s>',
543 1
                $this->url($asset['path'], $options),
544 1
                $htmlAttributes
545
            );
546
547
            // WebP transformation?
548 1
            if ($webp && !Image::isAnimatedGif($asset)) {
549 1
                $assetWebp = Image::convertTopWebp($asset, $this->config->get('assets.images.quality') ?? 75);
550
                // <source>
551 1
                $source = \sprintf('<source type="image/webp" srcset="%s">', $assetWebp);
552
                // responsive
553 1
                if ($responsive) {
554 1
                    $srcset = Image::buildSrcset(
555 1
                        $assetWebp,
556 1
                        $this->config->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
557 1
                    ) ?: (string) $assetWebp;
558
                    // <source>
559 1
                    $source = \sprintf(
560 1
                        '<source type="image/webp" srcset="%s" sizes="%s">',
561 1
                        $srcset,
562 1
                        $this->config->get('assets.images.responsive.sizes.default') ?? '100vw'
563
                    );
564
                }
565
566 1
                return \sprintf("<picture>\n  %s\n  %s\n</picture>", $source, $img);
567
            }
568
569
            return $img;
570
        }
571
572
        throw new RuntimeException(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
573
    }
574
575
    /**
576
     * Returns the content of an asset.
577
     *
578
     * @throws RuntimeException
579
     */
580 1
    public function inline(Asset $asset): string
581
    {
582 1
        if (is_null($asset['content'])) {
583
            throw new RuntimeException(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
584
        }
585
586 1
        return $asset['content'];
587
    }
588
589
    /**
590
     * Reads $length first characters of a string and adds a suffix.
591
     */
592
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
593
    {
594
        $string = str_replace('</p>', '<br /><br />', $string);
595
        $string = trim(strip_tags($string, '<br>'), '<br />');
596
        if (mb_strlen($string) > $length) {
597
            $string = mb_substr($string, 0, $length);
598
            $string .= $suffix;
599
        }
600
601
        return $string;
602
    }
603
604
    /**
605
     * Reads characters before '<!-- excerpt|break -->'.
606
     * Options:
607
     *  - separator: string to use as separator
608
     *  - capture: string to capture, 'before' (default) or 'after'.
609
     */
610 1
    public function excerptHtml(string $string, array $options = []): string
611
    {
612 1
        $separator = 'excerpt|break';
613 1
        $capture = 'before';
614 1
        extract($options, EXTR_IF_EXISTS);
615
616
        // https://regex101.com/r/n9TWHF/1
617 1
        $pattern = '(.*)<!--[[:blank:]]?('.$separator.')[[:blank:]]?-->(.*)';
618 1
        preg_match('/'.$pattern.'/is', $string, $matches);
619
620 1
        if (empty($matches)) {
621 1
            return $string;
622
        }
623 1
        if ($capture == 'after') {
624
            return trim($matches[3]);
625
        }
626
        // remove footnotes
627 1
        return preg_replace('/<sup[^>]*>[^u]*<\/sup>/', '', trim($matches[1]));
628
    }
629
630
    /**
631
     * Converts a Markdown string to HTML.
632
     *
633
     * @throws RuntimeException
634
     */
635 1
    public function markdownToHtml(string $markdown): ?string
636
    {
637
        try {
638 1
            $parsedown = new Parsedown($this->builder);
639 1
            $html = $parsedown->text($markdown);
640
        } catch (\Exception $e) {
641
            throw new RuntimeException('"markdown_to_html" filter can not convert supplied Markdown.');
642
        }
643
644 1
        return $html;
645
    }
646
647
    /**
648
     * Converts a JSON string to an array.
649
     *
650
     * @throws RuntimeException
651
     */
652 1
    public function jsonDecode(string $json): ?array
653
    {
654
        try {
655 1
            $array = json_decode($json, true);
656 1
            if ($array === null && json_last_error() !== JSON_ERROR_NONE) {
657 1
                throw new \Exception('JSON error.');
658
            }
659
        } catch (\Exception $e) {
660
            throw new RuntimeException('"json_decode" filter can not parse supplied JSON.');
661
        }
662
663 1
        return $array;
664
    }
665
666
    /**
667
     * Converts a YAML string to an array.
668
     *
669
     * @throws RuntimeException
670
     */
671 1
    public function yamlParse(string $yaml): ?array
672
    {
673
        try {
674 1
            $array = Yaml::parse($yaml);
675 1
            if (!is_array($array)) {
676 1
                throw new ParseException('YAML error.');
677
            }
678
        } catch (ParseException $e) {
679
            throw new RuntimeException(\sprintf('"yaml_parse" filter can not parse supplied YAML: %s', $e->getMessage()));
680
        }
681
682 1
        return $array;
683
    }
684
685
    /**
686
     * Split a string into an array using a regular expression.
687
     *
688
     * @throws RuntimeException
689
     */
690
    public function pregSplit(string $value, string $pattern, int $limit = 0): ?array
691
    {
692
        try {
693
            $array = preg_split($pattern, $value, $limit);
694
            if ($array === false) {
695
                throw new RuntimeException('PREG split error.');
696
            }
697
        } catch (\Exception $e) {
698
            throw new RuntimeException('"preg_split" filter can not split supplied string.');
699
        }
700
701
        return $array;
702
    }
703
704
    /**
705
     * Perform a regular expression match and return the group for all matches.
706
     *
707
     * @throws RuntimeException
708
     */
709
    public function pregMatchAll(string $value, string $pattern, int $group = 0): ?array
710
    {
711
        try {
712
            $array = preg_match_all($pattern, $value, $matches, PREG_PATTERN_ORDER);
713
            if ($array === false) {
714
                throw new RuntimeException('PREG match all error.');
715
            }
716
        } catch (\Exception $e) {
717
            throw new RuntimeException('"preg_match_all" filter can not match in supplied string.');
718
        }
719
720
        return $matches[$group];
721
    }
722
723
    /**
724
     * Calculates estimated time to read a text.
725
     */
726
    public function readtime(string $text): string
727
    {
728
        $words = str_word_count(strip_tags($text));
729
        $min = floor($words / 200);
730
        if ($min === 0) {
731
            return '1';
732
        }
733
734
        return (string) $min;
735
    }
736
737
    /**
738
     * Gets the value of an environment variable.
739
     */
740 1
    public function getEnv(string $var): ?string
741
    {
742 1
        return getenv($var) ?: null;
743
    }
744
745
    /**
746
     * Tests if a variable is an Asset.
747
     */
748
    public function isAsset($variable): bool
749
    {
750
        return $variable instanceof Asset;
751
    }
752
753
    /**
754
     * Converts an hexadecimal color to RGB.
755
     */
756
    public function hexToRgb($variable): array
757
    {
758
        if (!self::isHex($variable)) {
759
            throw new RuntimeException(\sprintf('"%s" is not a valid hexadecimal value.', $variable));
760
        }
761
        $hex = ltrim($variable, '#');
762
        if (strlen($hex) == 3) {
763
            $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
764
        }
765
        $c = hexdec($hex);
766
767
        return [
768
            'red'   => $c >> 16 & 0xFF,
769
            'green' => $c >> 8 & 0xFF,
770
            'blue'  => $c & 0xFF,
771
        ];
772
    }
773
774
    /**
775
     * Is a hexadecimal color is valid?
776
     */
777
    private static function isHex(string $hex): bool
778
    {
779
        $valid = is_string($hex);
780
        $hex = ltrim($hex, '#');
781
        $length = strlen($hex);
782
        $valid = $valid && ($length === 3 || $length === 6);
783
        $valid = $valid && ctype_xdigit($hex);
784
785
        return $valid;
786
    }
787
}
788