Passed
Push — fallback-intl ( 2f4c17...1f6a03 )
by Arnaud
03:32
created

Extension::html()   C

Complexity

Conditions 16
Paths 66

Size

Total Lines 82
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 75.142

Importance

Changes 7
Bugs 3 Features 0
Metric Value
cc 16
eloc 49
c 7
b 3
f 0
nc 66
nop 3
dl 0
loc 82
ccs 17
cts 44
cp 0.3864
crap 75.142
rs 5.5666

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 1
    protected $config;
44
45 1
    /** @var Slugify */
46 1
    private static $slugifier;
47
48
    public function __construct(Builder $builder)
49 1
    {
50
        if (!self::$slugifier instanceof Slugify) {
51 1
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
52 1
        }
53 1
54
        parent::__construct(self::$slugifier);
55
56
        $this->builder = $builder;
57
        $this->config = $this->builder->getConfig();
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function getName()
64
    {
65
        return 'cecil';
66 1
    }
67
68
    /**
69 1
     * {@inheritdoc}
70
     */
71 1
    public function getFilters()
72 1
    {
73 1
        return [
74
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
75 1
            // 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 1
            // 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
            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 1
            // content
93 1
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
94 1
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
95
            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
            new \Twig\TwigFilter('preg_match_all', [$this, 'pregMatchAll']),
101 1
            new \Twig\TwigFilter('hex_to_rgb', [$this, 'hexToRgb']),
102 1
            // deprecated
103 1
            new \Twig\TwigFilter(
104 1
                'filterBySection',
105
                [$this, 'filterBySection'],
106 1
                ['deprecated' => true, 'alternative' => 'filter_by']
107 1
            ),
108 1
            new \Twig\TwigFilter(
109 1
                'filterBy',
110
                [$this, 'filterBy'],
111 1
                ['deprecated' => true, 'alternative' => 'filter_by']
112 1
            ),
113 1
            new \Twig\TwigFilter(
114 1
                'sortByTitle',
115
                [$this, 'sortByTitle'],
116 1
                ['deprecated' => true, 'alternative' => 'sort_by_title']
117 1
            ),
118 1
            new \Twig\TwigFilter(
119 1
                'sortByWeight',
120
                [$this, 'sortByWeight'],
121 1
                ['deprecated' => true, 'alternative' => 'sort_by_weight']
122 1
            ),
123 1
            new \Twig\TwigFilter(
124 1
                'sortByDate',
125
                [$this, 'sortByDate'],
126 1
                ['deprecated' => true, 'alternative' => 'sort_by_date']
127 1
            ),
128 1
            new \Twig\TwigFilter(
129 1
                'minifyCSS',
130
                [$this, 'minifyCss'],
131 1
                ['deprecated' => true, 'alternative' => 'minifyCss']
132 1
            ),
133 1
            new \Twig\TwigFilter(
134 1
                'minifyJS',
135
                [$this, 'minifyJs'],
136 1
                ['deprecated' => true, 'alternative' => 'minifyJs']
137 1
            ),
138 1
            new \Twig\TwigFilter(
139 1
                'SCSStoCSS',
140
                [$this, 'scssToCss'],
141 1
                ['deprecated' => true, 'alternative' => 'scss_to_css']
142 1
            ),
143 1
            new \Twig\TwigFilter(
144 1
                'excerptHtml',
145
                [$this, 'excerptHtml'],
146
                ['deprecated' => true, 'alternative' => 'excerpt_html']
147
            ),
148
            new \Twig\TwigFilter(
149
                'urlize',
150
                [$this, 'slugifyFilter'],
151
                ['deprecated' => true, 'alternative' => 'slugify']
152 1
            ),
153
        ];
154
    }
155
156 1
    /**
157 1
     * {@inheritdoc}
158 1
     */
159
    public function getFunctions()
160 1
    {
161
        return [
162 1
            // assets
163
            new \Twig\TwigFunction('url', [$this, 'url']),
164 1
            new \Twig\TwigFunction('asset', [$this, 'asset']),
165 1
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
166 1
            // content
167 1
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
168
            // others
169 1
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
170 1
            // deprecated
171 1
            new \Twig\TwigFunction(
172 1
                'minify',
173
                [$this, 'minify'],
174 1
                ['deprecated' => true, 'alternative' => 'minify filter']
175 1
            ),
176 1
            new \Twig\TwigFunction(
177 1
                'toCSS',
178
                [$this, 'toCss'],
179
                ['deprecated' => true, 'alternative' => 'to_css filter']
180
            ),
181
            new \Twig\TwigFunction(
182
                'hash',
183
                [$this, 'integrity'],
184
                ['deprecated' => true, 'alternative' => 'integrity']
185
            ),
186
        ];
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function getTests()
193
    {
194 1
        return [
195
            new \Twig\TwigTest('asset', [$this, 'isAsset']),
196
        ];
197 1
    }
198 1
199 1
    /**
200
     * Filters by Section.
201
     * Alias of `filterBy('section', $value)`.
202 1
     */
203 1
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
204
    {
205
        return $this->filterBy($pages, 'section', $section);
206 1
    }
207 1
208
    /**
209 1
     * 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
            $notVirtual = false;
215
            if (!$page->isVirtual()) {
216
                $notVirtual = true;
217 1
            }
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 1
            }
223
            if ($page->getVariable($variable) == $value) {
224
                return $notVirtual && true;
225
            }
226
        });
227
228 1
        return $filteredPages;
229
    }
230
231 1
    /**
232 1
     * Sorts by title.
233
     */
234 1
    public function sortByTitle(\Traversable $collection): array
235 1
    {
236
        $collection = iterator_to_array($collection);
237 1
        /** @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
        return $collection;
241 1
    }
242 1
243
    /**
244 1
     * Sorts by weight.
245 1
     */
246
    public function sortByWeight(\Traversable $collection): array
247 1
    {
248
        $callback = function ($a, $b) {
249
            if (!isset($a['weight'])) {
250
                $a['weight'] = 0;
251
            }
252
            if (!isset($b['weight'])) {
253 1
                $a['weight'] = 0;
254
            }
255
            if ($a['weight'] == $b['weight']) {
256 1
                return 0;
257 1
            }
258
259
            return ($a['weight'] < $b['weight']) ? -1 : 1;
260 1
        };
261 1
262
        $collection = iterator_to_array($collection);
263 1
        /** @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
    public function sortByDate(\Traversable $collection): array
273
    {
274
        $callback = function ($a, $b) {
275
            if ($a['date'] == $b['date']) {
276
                return 0;
277
            }
278
279
            return ($a['date'] > $b['date']) ? -1 : 1;
280
        };
281
282
        $collection = iterator_to_array($collection);
283 1
        /** @var \array $collection */
284
        usort(/** @scrutinizer ignore-type */ $collection, $callback);
285 1
286
        return $collection;
287
    }
288
289
    /**
290
     * Creates an URL.
291
     *
292
     * $options[
293
     *     'canonical' => true,
294
     *     'addhash'   => false,
295
     *     'format'    => 'json',
296 1
     * ];
297
     *
298 1
     * @param Page|Asset|string|null $value
299
     * @param array|null             $options
300
     *
301
     * @return mixed
302
     */
303
    public function url($value = null, array $options = null)
304
    {
305
        return new Url($this->builder, $value, $options);
306
    }
307
308 1
    /**
309
     * Creates an asset (CSS, JS, images, etc.).
310 1
     *
311
     * @param string|array $path    File path (relative from static/ dir).
312
     * @param array|null   $options
313
     *
314 1
     * @return Asset
315
     */
316
    public function asset($path, array $options = null): Asset
317
    {
318
        return new Asset($this->builder, $path, $options);
319
    }
320
321
    /**
322
     * Compiles a SCSS asset.
323
     *
324 1
     * @param string|Asset $asset
325
     *
326 1
     * @return Asset
327
     */
328
    public function toCss($asset): Asset
329
    {
330 1
        if (!$asset instanceof Asset) {
331
            $asset = new Asset($this->builder, $asset);
332
        }
333
334
        return $asset->compile();
335
    }
336
337
    /**
338
     * Minifying an asset (CSS or JS).
339
     *
340 1
     * @param string|Asset $asset
341
     *
342 1
     * @return Asset
343
     */
344
    public function minify($asset): Asset
345
    {
346 1
        if (!$asset instanceof Asset) {
347
            $asset = new Asset($this->builder, $asset);
348
        }
349
350
        return $asset->minify();
351
    }
352
353
    /**
354
     * Fingerprinting an asset.
355
     *
356
     * @param string|Asset $asset
357
     *
358
     * @return Asset
359
     */
360
    public function fingerprint($asset): Asset
361
    {
362
        if (!$asset instanceof Asset) {
363
            $asset = new Asset($this->builder, $asset);
364
        }
365
366
        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 1
     *
390
     * @return string
391 1
     */
392 1
    public function dataurl($asset): string
393
    {
394
        if (!$asset instanceof Asset) {
395 1
            $asset = new Asset($this->builder, $asset);
396
        }
397
398
        return $asset->dataurl();
399
    }
400
401 1
    /**
402
     * Hashing an asset with algo (sha384 by default).
403 1
     *
404 1
     * @param string|Asset $path
405
     * @param string       $algo
406
     *
407
     * @return string
408
     */
409
    public function integrity($asset, string $algo = 'sha384'): string
410
    {
411
        if (!$asset instanceof Asset) {
412
            $asset = new Asset($this->builder, $asset);
413
        }
414
415
        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 1
        }
446 1
447 1
        $cache = new Cache($this->builder);
448 1
        $cacheKey = $cache->createKeyFromString($value);
449 1
        if (!$cache->has($cacheKey)) {
450
            $minifier = new Minify\JS($value);
451
            $value = $minifier->minify();
452 1
            $cache->set($cacheKey, $value);
453 1
        }
454 1
455 1
        return $cache->get($cacheKey, $value);
456 1
    }
457
458 1
    /**
459 1
     * Compiles a SCSS string.
460
     */
461
    public function scssToCss(string $value): string
462 1
    {
463
        $cache = new Cache($this->builder);
464
        $cacheKey = $cache->createKeyFromString($value);
465
        if (!$cache->has($cacheKey)) {
466
            $scssPhp = new Compiler();
467
            $outputStyles = ['expanded', 'compressed'];
468
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
469
            if (!in_array($outputStyle, $outputStyles)) {
470
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
471
            }
472
            $scssPhp->setOutputStyle($outputStyle);
473 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
474
            if (!empty($variables)) {
475 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
476 1
                $scssPhp->replaceVariables($variables);
477 1
            }
478 1
            $value = $scssPhp->compileString($value)->getCss();
479 1
            $cache->set($cacheKey, $value);
480
        }
481 1
482 1
        return $cache->get($cacheKey, $value);
483 1
    }
484 1
485
    /**
486 1
     * Returns the HTML version of an asset.
487
     *
488
     * $options[
489 1
     *     'preload'    => false,
490 1
     *     'responsive' => false,
491 1
     * ];
492 1
     *
493 1
     * @throws RuntimeException
494
     */
495 1
    public function html(Asset $asset, array $attributes = [], array $options = []): string
496 1
    {
497
        $htmlAttributes = '';
498
        $preload = false;
499
        $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
        foreach ($attributes as $name => $value) {
504
            $attribute = \sprintf(' %s="%s"', $name, $value);
505 1
            if (empty($value)) {
506 1
                $attribute = \sprintf(' %s', $name);
507
            }
508
            $htmlAttributes .= $attribute;
509
        }
510 1
511
        $asset->save();
512
513
        /* CSS or JavaScript */
514
        switch ($asset['ext']) {
515
            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
                        $htmlAttributes
521
                    );
522 1
                }
523
524
                return \sprintf('<link rel="stylesheet" href="%s"%s>', $this->url($asset['path'], $options), $htmlAttributes);
525
            case 'js':
526
                return \sprintf('<script src="%s"%s></script>', $this->url($asset['path'], $options), $htmlAttributes);
527
        }
528
529
        /* Image */
530
        if ($asset['type'] == 'image') {
531
            // responsive
532
            if ($responsive && $srcset = Image::buildSrcset(
533
                $asset,
534
                $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 1
540
            // <img>
541
            $img = \sprintf(
542
                '<img src="%s" width="'.($asset->getWidth() ?: 0).'" height="'.($asset->getHeight() ?: 0).'"%s>',
543
                $this->url($asset['path'], $options),
544
                $htmlAttributes
545
            );
546
547
            // WebP transformation?
548 1
            if ($webp && !Image::isAnimatedGif($asset)) {
549
                try {
550 1
                    $assetWebp = Image::convertTopWebp($asset, $this->config->get('assets.images.quality') ?? 75);
551
                    // <source>
552
                    $source = \sprintf('<source type="image/webp" srcset="%s">', $assetWebp);
553
                    // responsive
554 1
                    if ($responsive) {
555
                        $srcset = Image::buildSrcset(
556
                            $assetWebp,
557
                            $this->config->get('assets.images.responsive.widths') ?? [480, 640, 768, 1024, 1366, 1600, 1920]
558
                        ) ?: (string) $assetWebp;
559
                        // <source>
560
                        $source = \sprintf(
561
                            '<source type="image/webp" srcset="%s" sizes="%s">',
562
                            $srcset,
563
                            $this->config->get('assets.images.responsive.sizes.default') ?? '100vw'
564
                        );
565
                    }
566
567
                    return \sprintf("<picture>\n  %s\n  %s\n</picture>", $source, $img);
568
                } catch (\Exception $e) {
569
                    $this->builder->getLogger()->debug($e->getMessage());
570
                }
571
            }
572
573
            return $img;
574
        }
575 1
576
        throw new RuntimeException(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
577
    }
578 1
579 1
    /**
580 1
     * Returns the content of an asset.
581 1
     *
582
     * @throws RuntimeException
583
     */
584 1
    public function inline(Asset $asset): string
585
    {
586
        if (is_null($asset['content'])) {
587
            throw new RuntimeException(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
588
        }
589
590 1
        return $asset['content'];
591
    }
592
593 1
    /**
594 1
     * Reads $length first characters of a string and adds a suffix.
595
     */
596
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
597
    {
598
        $string = str_replace('</p>', '<br /><br />', $string);
599 1
        $string = trim(strip_tags($string, '<br>'), '<br />');
600
        if (mb_strlen($string) > $length) {
601
            $string = mb_substr($string, 0, $length);
602
            $string .= $suffix;
603
        }
604
605
        return $string;
606
    }
607
608
    /**
609
     * Reads characters before '<!-- excerpt|break -->'.
610
     * Options:
611
     *  - separator: string to use as separator
612
     *  - capture: string to capture, 'before' (default) or 'after'.
613
     */
614
    public function excerptHtml(string $string, array $options = []): string
615
    {
616
        $separator = 'excerpt|break';
617
        $capture = 'before';
618
        extract($options, EXTR_IF_EXISTS);
619
620
        // https://regex101.com/r/n9TWHF/1
621
        $pattern = '(.*)<!--[[:blank:]]?('.$separator.')[[:blank:]]?-->(.*)';
622
        preg_match('/'.$pattern.'/is', $string, $matches);
623
624
        if (empty($matches)) {
625
            return $string;
626
        }
627
        if ($capture == 'after') {
628
            return trim($matches[3]);
629
        }
630
        // remove footnotes
631
        return preg_replace('/<sup[^>]*>[^u]*<\/sup>/', '', trim($matches[1]));
632
    }
633
634
    /**
635
     * Converts a Markdown string to HTML.
636
     *
637
     * @throws RuntimeException
638
     */
639
    public function markdownToHtml(string $markdown): ?string
640
    {
641
        try {
642
            $parsedown = new Parsedown($this->builder);
643
            $html = $parsedown->text($markdown);
644
        } catch (\Exception $e) {
645
            throw new RuntimeException('"markdown_to_html" filter can not convert supplied Markdown.');
646
        }
647
648
        return $html;
649
    }
650
651
    /**
652
     * Converts a JSON string to an array.
653
     *
654
     * @throws RuntimeException
655
     */
656
    public function jsonDecode(string $json): ?array
657
    {
658
        try {
659
            $array = json_decode($json, true);
660
            if ($array === null && json_last_error() !== JSON_ERROR_NONE) {
661
                throw new \Exception('JSON error.');
662
            }
663
        } catch (\Exception $e) {
664
            throw new RuntimeException('"json_decode" filter can not parse supplied JSON.');
665
        }
666
667
        return $array;
668
    }
669
670 1
    /**
671
     * Converts a YAML string to an array.
672 1
     *
673
     * @throws RuntimeException
674
     */
675
    public function yamlParse(string $yaml): ?array
676
    {
677
        try {
678
            $array = Yaml::parse($yaml);
679
            if (!is_array($array)) {
680
                throw new ParseException('YAML error.');
681
            }
682
        } catch (ParseException $e) {
683
            throw new RuntimeException(\sprintf('"yaml_parse" filter can not parse supplied YAML: %s', $e->getMessage()));
684
        }
685
686
        return $array;
687
    }
688
689
    /**
690
     * Split a string into an array using a regular expression.
691
     *
692
     * @throws RuntimeException
693
     */
694
    public function pregSplit(string $value, string $pattern, int $limit = 0): ?array
695
    {
696
        try {
697
            $array = preg_split($pattern, $value, $limit);
698
            if ($array === false) {
699
                throw new RuntimeException('PREG split error.');
700
            }
701
        } catch (\Exception $e) {
702
            throw new RuntimeException('"preg_split" filter can not split supplied string.');
703
        }
704
705
        return $array;
706
    }
707
708
    /**
709
     * Perform a regular expression match and return the group for all matches.
710
     *
711
     * @throws RuntimeException
712
     */
713
    public function pregMatchAll(string $value, string $pattern, int $group = 0): ?array
714
    {
715
        try {
716
            $array = preg_match_all($pattern, $value, $matches, PREG_PATTERN_ORDER);
717
            if ($array === false) {
718
                throw new RuntimeException('PREG match all error.');
719
            }
720
        } catch (\Exception $e) {
721
            throw new RuntimeException('"preg_match_all" filter can not match in supplied string.');
722
        }
723
724
        return $matches[$group];
725
    }
726
727
    /**
728
     * Calculates estimated time to read a text.
729
     */
730
    public function readtime(string $text): string
731
    {
732
        $words = str_word_count(strip_tags($text));
733
        $min = floor($words / 200);
734
        if ($min === 0) {
735
            return '1';
736
        }
737
738
        return (string) $min;
739
    }
740
741
    /**
742
     * Gets the value of an environment variable.
743
     */
744
    public function getEnv(string $var): ?string
745
    {
746
        return getenv($var) ?: null;
747
    }
748
749
    /**
750
     * Tests if a variable is an Asset.
751
     */
752
    public function isAsset($variable): bool
753
    {
754
        return $variable instanceof Asset;
755
    }
756
757
    /**
758
     * Converts an hexadecimal color to RGB.
759
     */
760
    public function hexToRgb($variable): array
761
    {
762
        if (!self::isHex($variable)) {
763
            throw new RuntimeException(\sprintf('"%s" is not a valid hexadecimal value.', $variable));
764
        }
765
        $hex = ltrim($variable, '#');
766
        if (strlen($hex) == 3) {
767
            $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
768
        }
769
        $c = hexdec($hex);
770
771
        return [
772
            'red'   => $c >> 16 & 0xFF,
773
            'green' => $c >> 8 & 0xFF,
774
            'blue'  => $c & 0xFF,
775
        ];
776
    }
777
778
    /**
779
     * Is a hexadecimal color is valid?
780
     */
781
    private static function isHex(string $hex): bool
782
    {
783
        $valid = is_string($hex);
784
        $hex = ltrim($hex, '#');
785
        $length = strlen($hex);
786
        $valid = $valid && ($length === 3 || $length === 6);
787
        $valid = $valid && ctype_xdigit($hex);
788
789
        return $valid;
790
    }
791
}
792