Passed
Pull Request — master (#1665)
by Arnaud
08:44 queued 03:42
created

Core::imageSizes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

243
        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

243
        array_multisort(array_keys(/** @scrutinizer ignore-type */ $collection), $sort, /** @scrutinizer ignore-type */ \SORT_NATURAL | \SORT_FLAG_CASE, $collection);
Loading history...
244
245 1
        return $collection;
246
    }
247
248
    /**
249
     * Sorts a collection by weight.
250
     */
251 1
    public function sortByWeight(\Traversable $collection): array
252
    {
253 1
        $callback = function ($a, $b) {
254 1
            if (!isset($a['weight'])) {
255 1
                $a['weight'] = 0;
256
            }
257 1
            if (!isset($b['weight'])) {
258
                $a['weight'] = 0;
259
            }
260 1
            if ($a['weight'] == $b['weight']) {
261 1
                return 0;
262
            }
263
264 1
            return $a['weight'] < $b['weight'] ? -1 : 1;
265 1
        };
266
267 1
        $collection = iterator_to_array($collection);
268 1
        usort(/** @scrutinizer ignore-type */ $collection, $callback);
269
270 1
        return $collection;
271
    }
272
273
    /**
274
     * Sorts by creation date (or 'updated' date): the most recent first.
275
     */
276 1
    public function sortByDate(\Traversable $collection, string $variable = 'date', bool $descTitle = false): array
277
    {
278 1
        $callback = function ($a, $b) use ($variable, $descTitle) {
279 1
            if ($a[$variable] == $b[$variable]) {
280
                // if dates are equal and "descTitle" is true
281 1
                if ($descTitle && (isset($a['title']) && isset($b['title']))) {
282
                    return strnatcmp($b['title'], $a['title']);
283
                }
284
285 1
                return 0;
286
            }
287
288 1
            return $a[$variable] > $b[$variable] ? -1 : 1;
289 1
        };
290
291 1
        $collection = iterator_to_array($collection);
292 1
        usort(/** @scrutinizer ignore-type */ $collection, $callback);
293
294 1
        return $collection;
295
    }
296
297
    /**
298
     * Creates an URL.
299
     *
300
     * $options[
301
     *     'canonical' => false,
302
     *     'format'    => 'html',
303
     *     'language'  => null,
304
     * ];
305
     *
306
     * @param Page|Asset|string|null $value
307
     * @param array|null             $options
308
     */
309 1
    public function url($value = null, array $options = null): string
310
    {
311 1
        return (new Url($this->builder, $value, $options))->getUrl();
312
    }
313
314
    /**
315
     * Creates an Asset (CSS, JS, images, etc.) from a path or an array of paths.
316
     *
317
     * @param string|array $path    File path or array of files path (relative from `assets/` or `static/` dir).
318
     * @param array|null   $options
319
     *
320
     * @return Asset
321
     */
322 1
    public function asset($path, array $options = null): Asset
323
    {
324 1
        return new Asset($this->builder, $path, $options);
325
    }
326
327
    /**
328
     * Compiles a SCSS asset.
329
     *
330
     * @param string|Asset $asset
331
     *
332
     * @return Asset
333
     */
334 1
    public function toCss($asset): Asset
335
    {
336 1
        if (!$asset instanceof Asset) {
337
            $asset = new Asset($this->builder, $asset);
338
        }
339
340 1
        return $asset->compile();
341
    }
342
343
    /**
344
     * Minifying an asset (CSS or JS).
345
     *
346
     * @param string|Asset $asset
347
     *
348
     * @return Asset
349
     */
350 1
    public function minify($asset): Asset
351
    {
352 1
        if (!$asset instanceof Asset) {
353
            $asset = new Asset($this->builder, $asset);
354
        }
355
356 1
        return $asset->minify();
357
    }
358
359
    /**
360
     * Fingerprinting an asset.
361
     *
362
     * @param string|Asset $asset
363
     *
364
     * @return Asset
365
     */
366 1
    public function fingerprint($asset): Asset
367
    {
368 1
        if (!$asset instanceof Asset) {
369
            $asset = new Asset($this->builder, $asset);
370
        }
371
372 1
        return $asset->fingerprint();
373
    }
374
375
    /**
376
     * Resizes an image.
377
     *
378
     * @param string|Asset $asset
379
     *
380
     * @return Asset
381
     */
382
    public function resize($asset, int $size): Asset
383
    {
384
        if (!$asset instanceof Asset) {
385
            $asset = new Asset($this->builder, $asset);
386
        }
387
388
        return $asset->resize($size);
389
    }
390
391
    /**
392
     * Returns the data URL of an image.
393
     *
394
     * @param string|Asset $asset
395
     *
396
     * @return string
397
     */
398 1
    public function dataurl($asset): string
399
    {
400 1
        if (!$asset instanceof Asset) {
401
            $asset = new Asset($this->builder, $asset);
402
        }
403
404 1
        return $asset->dataurl();
405
    }
406
407
    /**
408
     * Hashing an asset with algo (sha384 by default).
409
     *
410
     * @param string|Asset $asset
411
     * @param string       $algo
412
     *
413
     * @return string
414
     */
415 1
    public function integrity($asset, string $algo = 'sha384'): string
416
    {
417 1
        if (!$asset instanceof Asset) {
418 1
            $asset = new Asset($this->builder, $asset);
419
        }
420
421 1
        return $asset->getIntegrity($algo);
422
    }
423
424
    /**
425
     * Minifying a CSS string.
426
     */
427 1
    public function minifyCss(?string $value): string
428
    {
429 1
        $value = $value ?? '';
430
431 1
        if ($this->builder->isDebug()) {
432 1
            return $value;
433
        }
434
435
        $cache = new Cache($this->builder);
436
        $cacheKey = $cache->createKeyFromString($value);
437
        if (!$cache->has($cacheKey)) {
438
            $minifier = new Minify\CSS($value);
439
            $value = $minifier->minify();
440
            $cache->set($cacheKey, $value);
441
        }
442
443
        return $cache->get($cacheKey, $value);
444
    }
445
446
    /**
447
     * Minifying a JavaScript string.
448
     */
449 1
    public function minifyJs(?string $value): string
450
    {
451 1
        $value = $value ?? '';
452
453 1
        if ($this->builder->isDebug()) {
454 1
            return $value;
455
        }
456
457
        $cache = new Cache($this->builder);
458
        $cacheKey = $cache->createKeyFromString($value);
459
        if (!$cache->has($cacheKey)) {
460
            $minifier = new Minify\JS($value);
461
            $value = $minifier->minify();
462
            $cache->set($cacheKey, $value);
463
        }
464
465
        return $cache->get($cacheKey, $value);
466
    }
467
468
    /**
469
     * Compiles a SCSS string.
470
     *
471
     * @throws RuntimeException
472
     */
473
    public function scssToCss(?string $value): string
474
    {
475
        $value = $value ?? '';
476
477
        $cache = new Cache($this->builder);
478
        $cacheKey = $cache->createKeyFromString($value);
479
        if (!$cache->has($cacheKey)) {
480
            $scssPhp = new Compiler();
481
            $outputStyles = ['expanded', 'compressed'];
482
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
483
            if (!in_array($outputStyle, $outputStyles)) {
484
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
485
            }
486
            $scssPhp->setOutputStyle($outputStyle);
487
            $variables = $this->config->get('assets.compile.variables') ?? [];
488
            if (!empty($variables)) {
489
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
490
                $scssPhp->replaceVariables($variables);
491
            }
492
            $value = $scssPhp->compileString($value)->getCss();
493
            $cache->set($cacheKey, $value);
494
        }
495
496
        return $cache->get($cacheKey, $value);
497
    }
498
499
    /**
500
     * Creates the HTML element of an asset.
501
     *
502
     * $options[
503
     *     'preload'    => false,
504
     *     'responsive' => false,
505
     *     'webp'       => false,
506
     * ];
507
     *
508
     * @throws RuntimeException
509
     */
510 1
    public function html(Asset $asset, array $attributes = [], array $options = []): string
511
    {
512 1
        $htmlAttributes = '';
513 1
        $preload = false;
514 1
        $responsive = (bool) $this->config->get('assets.images.responsive.enabled') ?? false;
515 1
        $webp = (bool) $this->config->get('assets.images.webp.enabled') ?? false;
516 1
        extract($options, EXTR_IF_EXISTS);
517
518
        // builds HTML attributes
519 1
        foreach ($attributes as $name => $value) {
520 1
            $attribute = \sprintf(' %s="%s"', $name, $value);
521 1
            if (empty($value)) {
522 1
                $attribute = \sprintf(' %s', $name);
523
            }
524 1
            $htmlAttributes .= $attribute;
525
        }
526
527
        // be sure Asset file is saved
528 1
        $asset->save();
529
530
        // CSS or JavaScript
531 1
        switch ($asset['ext']) {
532 1
            case 'css':
533 1
                if ($preload) {
534 1
                    return \sprintf(
535 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>',
536 1
                        $this->url($asset, $options),
537 1
                        $htmlAttributes
538 1
                    );
539
                }
540
541 1
                return \sprintf('<link rel="stylesheet" href="%s"%s>', $this->url($asset, $options), $htmlAttributes);
542 1
            case 'js':
543
                return \sprintf('<script src="%s"%s></script>', $this->url($asset, $options), $htmlAttributes);
544
        }
545
        // image
546 1
        if ($asset['type'] == 'image') {
547
            // responsive
548 1
            $sizes = '';
549
            if (
550 1
                $responsive && $srcset = Image::buildSrcset(
551 1
                    $asset,
552 1
                    $this->config->getAssetsImagesWidths()
553 1
                )
554
            ) {
555
                $htmlAttributes .= \sprintf(' srcset="%s"', $srcset);
556
                $sizes = Image::getSizes($attributes['class'], (array) $this->builder->getConfig()->get('assets.images.responsive.sizes'));
557
                $htmlAttributes .= \sprintf(' sizes="%s"', $sizes);
558
            }
559
560
            // <img> element
561 1
            $img = \sprintf(
562 1
                '<img src="%s" width="' . ($asset['width'] ?: '') . '" height="' . ($asset['height'] ?: '') . '"%s>',
563 1
                $this->url($asset, $options),
564 1
                $htmlAttributes
565 1
            );
566
567
            // WebP conversion?
568 1
            if ($webp && $asset['subtype'] != 'image/webp' && !Image::isAnimatedGif($asset)) {
569
                try {
570 1
                    $assetWebp = $asset->webp();
571
                    // <source> element
572
                    $source = \sprintf('<source type="image/webp" srcset="%s">', $assetWebp);
573
                    // responsive
574
                    if ($responsive) {
575
                        $srcset = Image::buildSrcset(
576
                            $assetWebp,
577
                            $this->config->getAssetsImagesWidths()
578
                        ) ?: (string) $assetWebp;
579
                        // <source> element
580
                        $source = \sprintf(
581
                            '<source type="image/webp" srcset="%s" sizes="%s">',
582
                            $srcset,
583
                            $sizes
584
                        );
585
                    }
586
587
                    return \sprintf("<picture>\n  %s\n  %s\n</picture>", $source, $img);
588 1
                } catch (\Exception $e) {
589 1
                    $this->builder->getLogger()->debug($e->getMessage());
590
                }
591
            }
592
593 1
            return $img;
594
        }
595
596
        throw new RuntimeException(\sprintf('%s is available for CSS, JavaScript and images files only.', '"html" filter'));
597
    }
598
599
    /**
600
     * Creates HTML `srcset` attribute of an image Asset.
601
     *
602
     * @throws RuntimeException
603
     */
604
    public function imageSrcset(Asset $asset): string
605
    {
606
        return Image::buildSrcset($asset, $this->config->getAssetsImagesWidths());
607
    }
608
609
    /**
610
     * Creates HTML `sizes` attribute based of class value.
611
     */
612
    public function imageSizes(string $class): string
613
    {
614
        return Image::getSizes($class, (array) $this->config->get('assets.images.responsive.sizes'));
615
    }
616
617
    /**
618
     * Converts an image Asset to WebP format.
619
     *
620
     * @throws RuntimeException
621
     */
622
    public function webp(Asset $asset): Asset
623
    {
624
        if ($asset['subtype'] == 'image/webp') {
625
            return $asset;
626
        }
627
        if (Image::isAnimatedGif($asset)) {
628
            throw new RuntimeException(sprintf('Can\'t convert "%s" to WebP.', $asset['path']));
629
        }
630
        try {
631
            return $asset->webp();
632
        } catch (\Exception $e) {
633
            return $asset;
634
            $this->builder->getLogger()->debug($e->getMessage());
0 ignored issues
show
Unused Code introduced by
$this->builder->getLogge...debug($e->getMessage()) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
635
        }
636
    }
637
638
    /**
639
     * Returns the content of an asset.
640
     */
641 1
    public function inline(Asset $asset): string
642
    {
643 1
        return $asset['content'];
644
    }
645
646
    /**
647
     * Reads $length first characters of a string and adds a suffix.
648
     */
649
    public function excerpt(?string $string, int $length = 450, string $suffix = ' …'): string
650
    {
651
        $string = $string ?? '';
652
653
        $string = str_replace('</p>', '<br /><br />', $string);
654
        $string = trim(strip_tags($string, '<br>'), '<br />');
655
        if (mb_strlen($string) > $length) {
656
            $string = mb_substr($string, 0, $length);
657
            $string .= $suffix;
658
        }
659
660
        return $string;
661
    }
662
663
    /**
664
     * Reads characters before or after '<!-- separator -->'.
665
     * Options:
666
     *  - separator: string to use as separator (`excerpt|break` by default)
667
     *  - capture: part to capture, `before` or `after` the separator (`before` by default).
668
     */
669 1
    public function excerptHtml(?string $string, array $options = []): string
670
    {
671 1
        $string = $string ?? '';
672
673 1
        $separator = (string) $this->builder->getConfig()->get('body.excerpt.separator');
674 1
        $capture = (string) $this->builder->getConfig()->get('body.excerpt.capture');
675 1
        extract($options, EXTR_IF_EXISTS);
676
677
        // https://regex101.com/r/n9TWHF/1
678 1
        $pattern = '(.*)<!--[[:blank:]]?(' . $separator . ')[[:blank:]]?-->(.*)';
679 1
        preg_match('/' . $pattern . '/is', $string, $matches);
680
681 1
        if (empty($matches)) {
682 1
            return $string;
683
        }
684 1
        $result = trim($matches[1]);
685 1
        if ($capture == 'after') {
686 1
            $result = trim($matches[3]);
687
        }
688
        // removes footnotes and returns result
689 1
        return preg_replace('/<sup[^>]*>[^u]*<\/sup>/', '', $result);
690
    }
691
692
    /**
693
     * Converts a Markdown string to HTML.
694
     *
695
     * @throws RuntimeException
696
     */
697 1
    public function markdownToHtml(?string $markdown): ?string
698
    {
699 1
        $markdown = $markdown ?? '';
700
701
        try {
702 1
            $parsedown = new Parsedown($this->builder);
703 1
            $html = $parsedown->text($markdown);
704
        } catch (\Exception $e) {
705
            throw new RuntimeException('"markdown_to_html" filter can not convert supplied Markdown.');
706
        }
707
708 1
        return $html;
709
    }
710
711
    /**
712
     * Extract table of content of a Markdown string,
713
     * in the given format ("html" or "json", "html" by default).
714
     *
715
     * @throws RuntimeException
716
     */
717
    public function markdownToToc(?string $markdown, $format = 'html', $url = ''): ?string
718
    {
719
        $markdown = $markdown ?? '';
720
721
        try {
722
            $parsedown = new Parsedown($this->builder, ['selectors' => ['h2'], 'url' => $url]);
723
            $parsedown->body($markdown);
724
            $return = $parsedown->contentsList($format);
725
        } catch (\Exception $e) {
726
            throw new RuntimeException('"toc" filter can not convert supplied Markdown.');
727
        }
728
729
        return $return;
730
    }
731
732
    /**
733
     * Converts a JSON string to an array.
734
     *
735
     * @throws RuntimeException
736
     */
737 1
    public function jsonDecode(?string $json): ?array
738
    {
739 1
        $json = $json ?? '';
740
741
        try {
742 1
            $array = json_decode($json, true);
743 1
            if ($array === null && json_last_error() !== JSON_ERROR_NONE) {
744 1
                throw new \Exception('JSON error.');
745
            }
746
        } catch (\Exception $e) {
747
            throw new RuntimeException('"json_decode" filter can not parse supplied JSON.');
748
        }
749
750 1
        return $array;
751
    }
752
753
    /**
754
     * Converts a YAML string to an array.
755
     *
756
     * @throws RuntimeException
757
     */
758 1
    public function yamlParse(?string $yaml): ?array
759
    {
760 1
        $yaml = $yaml ?? '';
761
762
        try {
763 1
            $array = Yaml::parse($yaml);
764 1
            if (!is_array($array)) {
765 1
                throw new ParseException('YAML error.');
766
            }
767
        } catch (ParseException $e) {
768
            throw new RuntimeException(\sprintf('"yaml_parse" filter can not parse supplied YAML: %s', $e->getMessage()));
769
        }
770
771 1
        return $array;
772
    }
773
774
    /**
775
     * Split a string into an array using a regular expression.
776
     *
777
     * @throws RuntimeException
778
     */
779
    public function pregSplit(?string $value, string $pattern, int $limit = 0): ?array
780
    {
781
        $value = $value ?? '';
782
783
        try {
784
            $array = preg_split($pattern, $value, $limit);
785
            if ($array === false) {
786
                throw new RuntimeException('PREG split error.');
787
            }
788
        } catch (\Exception $e) {
789
            throw new RuntimeException('"preg_split" filter can not split supplied string.');
790
        }
791
792
        return $array;
793
    }
794
795
    /**
796
     * Perform a regular expression match and return the group for all matches.
797
     *
798
     * @throws RuntimeException
799
     */
800
    public function pregMatchAll(?string $value, string $pattern, int $group = 0): ?array
801
    {
802
        $value = $value ?? '';
803
804
        try {
805
            $array = preg_match_all($pattern, $value, $matches, PREG_PATTERN_ORDER);
806
            if ($array === false) {
807
                throw new RuntimeException('PREG match all error.');
808
            }
809
        } catch (\Exception $e) {
810
            throw new RuntimeException('"preg_match_all" filter can not match in supplied string.');
811
        }
812
813
        return $matches[$group];
814
    }
815
816
    /**
817
     * Calculates estimated time to read a text.
818
     */
819
    public function readtime(?string $text): string
820
    {
821
        $text = $text ?? '';
822
823
        $words = str_word_count(strip_tags($text));
824
        $min = floor($words / 200);
825
        if ($min === 0) {
826
            return '1';
827
        }
828
829
        return (string) $min;
830
    }
831
832
    /**
833
     * Gets the value of an environment variable.
834
     */
835 1
    public function getEnv(?string $var): ?string
836
    {
837 1
        $var = $var ?? '';
838
839 1
        return getenv($var) ?: null;
840
    }
841
842
    /**
843
     * Tests if a variable is an Asset.
844
     */
845
    public function isAsset($variable): bool
846
    {
847
        return $variable instanceof Asset;
848
    }
849
850
    /**
851
     * Returns the dominant hex color of an image asset.
852
     *
853
     * @param string|Asset $asset
854
     *
855
     * @return string
856
     */
857 1
    public function dominantColor($asset): string
858
    {
859 1
        if (!$asset instanceof Asset) {
860
            $asset = new Asset($this->builder, $asset);
861
        }
862
863 1
        return Image::getDominantColor($asset);
864
    }
865
866
    /**
867
     * Returns a Low Quality Image Placeholder (LQIP) as data URL.
868
     *
869
     * @param string|Asset $asset
870
     *
871
     * @return string
872
     */
873 1
    public function lqip($asset): string
874
    {
875 1
        if (!$asset instanceof Asset) {
876
            $asset = new Asset($this->builder, $asset);
877
        }
878
879 1
        return Image::getLqip($asset);
880
    }
881
882
    /**
883
     * Converts an hexadecimal color to RGB.
884
     *
885
     * @throws RuntimeException
886
     */
887
    public function hexToRgb(?string $variable): array
888
    {
889
        $variable = $variable ?? '';
890
891
        if (!self::isHex($variable)) {
892
            throw new RuntimeException(\sprintf('"%s" is not a valid hexadecimal value.', $variable));
893
        }
894
        $hex = ltrim($variable, '#');
895
        if (strlen($hex) == 3) {
896
            $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
897
        }
898
        $c = hexdec($hex);
899
900
        return [
901
            'red'   => $c >> 16 & 0xFF,
902
            'green' => $c >> 8 & 0xFF,
903
            'blue'  => $c & 0xFF,
904
        ];
905
    }
906
907
    /**
908
     * Split a string in multiple lines.
909
     */
910
    public function splitLine(?string $variable, int $max = 18): array
911
    {
912
        $variable = $variable ?? '';
913
914
        return preg_split("/.{0,{$max}}\K(\s+|$)/", $variable, 0, PREG_SPLIT_NO_EMPTY);
915
    }
916
917
    /**
918
     * Is a hexadecimal color is valid?
919
     */
920
    private static function isHex(string $hex): bool
921
    {
922
        $valid = is_string($hex);
923
        $hex = ltrim($hex, '#');
924
        $length = strlen($hex);
925
        $valid = $valid && ($length === 3 || $length === 6);
926
        $valid = $valid && ctype_xdigit($hex);
927
928
        return $valid;
929
    }
930
}
931