Passed
Push — pr/878 ( f206f5...8aeb4c )
by Arnaud
04:21
created

Extension::markdown()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 9
rs 10
1
<?php
2
/**
3
 * This file is part of the Cecil/Cecil package.
4
 *
5
 * Copyright (c) Arnaud Ligny <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Cecil\Renderer\Twig;
12
13
use Cecil\Assets\Asset;
14
use Cecil\Assets\Cache;
15
use Cecil\Assets\Image;
16
use Cecil\Assets\Url;
17
use Cecil\Builder;
18
use Cecil\Collection\CollectionInterface;
19
use Cecil\Collection\Page\Collection as PagesCollection;
20
use Cecil\Collection\Page\Page;
21
use Cecil\Config;
22
use Cecil\Converter\Parsedown;
23
use Cecil\Exception\Exception;
24
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
25
use Cocur\Slugify\Slugify;
26
use MatthiasMullie\Minify;
27
use ScssPhp\ScssPhp\Compiler;
28
29
/**
30
 * Class Twig\Extension.
31
 */
32
class Extension extends SlugifyExtension
33
{
34
    /** @var Builder */
35
    protected $builder;
36
    /** @var Config */
37
    protected $config;
38
    /** @var Slugify */
39
    private static $slugifier;
40
41
    /**
42
     * @param Builder $builder
43
     */
44
    public function __construct(Builder $builder)
45
    {
46
        if (!self::$slugifier instanceof Slugify) {
47
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
48
        }
49
50
        parent::__construct(self::$slugifier);
51
52
        $this->builder = $builder;
53
        $this->config = $this->builder->getConfig();
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function getName()
60
    {
61
        return 'cecil';
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function getFilters()
68
    {
69
        return [
70
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
71
            // sort
72
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
73
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
74
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
75
            // assets
76
            new \Twig\TwigFilter('url', [$this, 'url']),
77
            new \Twig\TwigFilter('html', [$this, 'html']),
78
            new \Twig\TwigFilter('inline', [$this, 'inline']),
79
            new \Twig\TwigFilter('markdown_to_html', [$this, 'markdownToHtml']),
80
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
81
            new \Twig\TwigFilter('minify', [$this, 'minify']),
82
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
83
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
84
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
85
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
86
            new \Twig\TwigFilter('resize', [$this, 'resize']),
87
            // content
88
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
89
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
90
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
91
            // deprecated
92
            new \Twig\TwigFilter(
93
                'filterBySection',
94
                [$this, 'filterBySection'],
95
                ['deprecated' => true, 'alternative' => 'filter_by']
96
            ),
97
            new \Twig\TwigFilter(
98
                'filterBy',
99
                [$this, 'filterBy'],
100
                ['deprecated' => true, 'alternative' => 'filter_by']
101
            ),
102
            new \Twig\TwigFilter(
103
                'sortByTitle',
104
                [$this, 'sortByTitle'],
105
                ['deprecated' => true, 'alternative' => 'sort_by_title']
106
            ),
107
            new \Twig\TwigFilter(
108
                'sortByWeight',
109
                [$this, 'sortByWeight'],
110
                ['deprecated' => true, 'alternative' => 'sort_by_weight']
111
            ),
112
            new \Twig\TwigFilter(
113
                'sortByDate',
114
                [$this, 'sortByDate'],
115
                ['deprecated' => true, 'alternative' => 'sort_by_date']
116
            ),
117
            new \Twig\TwigFilter(
118
                'minifyCSS',
119
                [$this, 'minifyCss'],
120
                ['deprecated' => true, 'alternative' => 'minifyCss']
121
            ),
122
            new \Twig\TwigFilter(
123
                'minifyJS',
124
                [$this, 'minifyJs'],
125
                ['deprecated' => true, 'alternative' => 'minifyJs']
126
            ),
127
            new \Twig\TwigFilter(
128
                'SCSStoCSS',
129
                [$this, 'scssToCss'],
130
                ['deprecated' => true, 'alternative' => 'scss_to_css']
131
            ),
132
            new \Twig\TwigFilter(
133
                'excerptHtml',
134
                [$this, 'excerptHtml'],
135
                ['deprecated' => true, 'alternative' => 'excerpt_html']
136
            ),
137
            new \Twig\TwigFilter(
138
                'urlize',
139
                [$this, 'slugifyFilter'],
140
                ['deprecated' => true, 'alternative' => 'slugify']
141
            ),
142
        ];
143
    }
144
145
    /**
146
     * {@inheritdoc}
147
     */
148
    public function getFunctions()
149
    {
150
        return [
151
            // assets
152
            new \Twig\TwigFunction('url', [$this, 'url']),
153
            new \Twig\TwigFunction('asset', [$this, 'asset']),
154
            new \Twig\TwigFunction('integrity', [$this, 'integrity']),
155
            // content
156
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
157
            // others
158
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
159
            // deprecated
160
            new \Twig\TwigFunction(
161
                'minify',
162
                [$this, 'minify'],
163
                ['deprecated' => true, 'alternative' => 'minify filter']
164
            ),
165
            new \Twig\TwigFunction(
166
                'toCSS',
167
                [$this, 'toCss'],
168
                ['deprecated' => true, 'alternative' => 'to_css filter']
169
            ),
170
            new \Twig\TwigFunction(
171
                'hash',
172
                [$this, 'integrity'],
173
                ['deprecated' => true, 'alternative' => 'integrity']
174
            ),
175
        ];
176
    }
177
178
    /**
179
     * Filters by Section.
180
     * Alias of `filterBy('section', $value)`.
181
     *
182
     * @param PagesCollection $pages
183
     * @param string          $section
184
     *
185
     * @return CollectionInterface
186
     */
187
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
188
    {
189
        return $this->filterBy($pages, 'section', $section);
190
    }
191
192
    /**
193
     * Filters by variable's name/value.
194
     *
195
     * @param PagesCollection $pages
196
     * @param string          $variable
197
     * @param string          $value
198
     *
199
     * @return CollectionInterface
200
     */
201
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
202
    {
203
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
204
            $notVirtual = false;
205
            if (!$page->isVirtual()) {
206
                $notVirtual = true;
207
            }
208
            // is a dedicated getter exists?
209
            $method = 'get'.ucfirst($variable);
210
            if (method_exists($page, $method) && $page->$method() == $value) {
211
                return $notVirtual && true;
212
            }
213
            if ($page->getVariable($variable) == $value) {
214
                return $notVirtual && true;
215
            }
216
        });
217
218
        return $filteredPages;
219
    }
220
221
    /**
222
     * Sorts by title.
223
     *
224
     * @param \Traversable $collection
225
     *
226
     * @return array
227
     */
228
    public function sortByTitle(\Traversable $collection): array
229
    {
230
        $collection = iterator_to_array($collection);
231
        array_multisort(array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
1 ignored issue
show
Bug introduced by
array_keys($collection) cannot be passed to array_multisort() as the parameter $arr 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

231
        array_multisort(/** @scrutinizer ignore-type */ array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
Loading history...
232
233
        return $collection;
234
    }
235
236
    /**
237
     * Sorts by weight.
238
     *
239
     * @param \Traversable $collection
240
     *
241
     * @return array
242
     */
243
    public function sortByWeight(\Traversable $collection): array
244
    {
245
        $callback = function ($a, $b) {
246
            if (!isset($a['weight'])) {
247
                return 1;
248
            }
249
            if (!isset($b['weight'])) {
250
                return -1;
251
            }
252
            if ($a['weight'] == $b['weight']) {
253
                return 0;
254
            }
255
256
            return ($a['weight'] < $b['weight']) ? -1 : 1;
257
        };
258
259
        $collection = iterator_to_array($collection);
260
        usort($collection, $callback);
261
262
        return $collection;
263
    }
264
265
    /**
266
     * Sorts by date.
267
     *
268
     * @param \Traversable $collection
269
     *
270
     * @return array
271
     */
272
    public function sortByDate(\Traversable $collection): array
273
    {
274
        $callback = function ($a, $b) {
275
            if (!isset($a['date'])) {
276
                return -1;
277
            }
278
            if (!isset($b['date'])) {
279
                return 1;
280
            }
281
            if ($a['date'] == $b['date']) {
282
                return 0;
283
            }
284
285
            return ($a['date'] > $b['date']) ? -1 : 1;
286
        };
287
288
        $collection = iterator_to_array($collection);
289
        usort($collection, $callback);
290
291
        return $collection;
292
    }
293
294
    /**
295
     * Creates an URL.
296
     *
297
     * $options[
298
     *     'canonical' => true,
299
     *     'addhash'   => false,
300
     *     'format'    => 'json',
301
     * ];
302
     *
303
     * @param Page|Asset|string|null $value
304
     * @param array|null             $options
305
     *
306
     * @return mixed
307
     */
308
    public function url($value = null, array $options = null)
309
    {
310
        return new Url($this->builder, $value, $options);
311
    }
312
313
    /**
314
     * Creates an asset (CSS, JS, images, etc.).
315
     *
316
     * @param string|array $path    File path (relative from static/ dir).
317
     * @param array|null   $options
318
     *
319
     * @return Asset
320
     */
321
    public function asset($path, array $options = null): Asset
322
    {
323
        return new Asset($this->builder, $path, $options);
324
    }
325
326
    /**
327
     * Minifying an asset (CSS or JS).
328
     * ie: minify('css/style.css').
329
     *
330
     * @param string|Asset $asset
331
     *
332
     * @return Asset
333
     */
334
    public function minify($asset): Asset
335
    {
336
        if (!$asset instanceof Asset) {
337
            $asset = new Asset($this->builder, $asset);
338
        }
339
340
        return $asset->minify();
341
    }
342
343
    /**
344
     * Compiles a SCSS asset.
345
     *
346
     * @param string|Asset $asset
347
     *
348
     * @return Asset
349
     */
350
    public function toCss($asset): Asset
351
    {
352
        if (!$asset instanceof Asset) {
353
            $asset = new Asset($this->builder, $asset);
354
        }
355
356
        return $asset->compile();
357
    }
358
359
    /**
360
     * Resizes an image.
361
     *
362
     * @param string $path Image path (relative from static/ dir or external).
363
     * @param int    $size Image new size (width).
364
     *
365
     * @return string
366
     */
367
    public function resize(string $path, int $size): string
368
    {
369
        return (new Image($this->builder))
370
            ->load($path)
371
            ->resize($size);
372
    }
373
374
    /**
375
     * Hashing an asset with algo (sha384 by default).
376
     *
377
     * @param string|Asset $path
378
     * @param string       $algo
379
     *
380
     * @return string
381
     */
382
    public function integrity($asset, string $algo = 'sha384'): string
383
    {
384
        if (!$asset instanceof Asset) {
385
            $asset = new Asset($this->builder, $asset);
386
        }
387
388
        return $asset->getIntegrity($algo);
389
    }
390
391
    /**
392
     * Minifying a CSS string.
393
     *
394
     * @param string $value
395
     *
396
     * @return string
397
     */
398
    public function minifyCss(string $value): string
399
    {
400
        $cache = new Cache($this->builder, 'assets');
401
        $cacheKey = $cache->createKeyFromValue($value);
402
        if (!$cache->has($cacheKey)) {
403
            $minifier = new Minify\CSS($value);
404
            $value = $minifier->minify();
405
            $cache->set($cacheKey, $value);
406
        }
407
408
        return $cache->get($cacheKey, $value);
409
    }
410
411
    /**
412
     * Minifying a JavaScript string.
413
     *
414
     * @param string $value
415
     *
416
     * @return string
417
     */
418
    public function minifyJs(string $value): string
419
    {
420
        $cache = new Cache($this->builder, 'assets');
421
        $cacheKey = $cache->createKeyFromValue($value);
422
        if (!$cache->has($cacheKey)) {
423
            $minifier = new Minify\JS($value);
424
            $value = $minifier->minify();
425
            $cache->set($cacheKey, $value);
426
        }
427
428
        return $cache->get($cacheKey, $value);
429
    }
430
431
    /**
432
     * Compiles a SCSS string.
433
     *
434
     * @param string $value
435
     *
436
     * @return string
437
     */
438
    public function scssToCss(string $value): string
439
    {
440
        $cache = new Cache($this->builder, 'assets');
441
        $cacheKey = $cache->createKeyFromValue($value);
442
        if (!$cache->has($cacheKey)) {
443
            $scssPhp = new Compiler();
444
            $formatter = \sprintf(
445
                'ScssPhp\ScssPhp\Formatter\%s',
446
                ucfirst((string) $this->config->get('assets.compile.style'))
447
            );
448
            if (!class_exists($formatter)) {
449
                throw new Exception(\sprintf('Scss formatter "%s" doesn\'t exists.', $formatter));
450
            }
451
            $scssPhp->setFormatter($formatter);
452
            $scssPhp->setVariables($this->config->get('assets.compile.variables') ?? []);
453
            $value = $scssPhp->compile($value);
454
            $cache->set($cacheKey, $value);
455
        }
456
457
        return $cache->get($cacheKey, $value);
458
    }
459
460
    /**
461
     * Creates an HTML element from an asset.
462
     *
463
     * @param Asset      $asset
464
     * @param array|null $attributes
465
     *
466
     * @return string
467
     */
468
    public function html(Asset $asset, array $attributes = null): string
469
    {
470
        $htmlAttributes = '';
471
        foreach ($attributes as $name => $value) {
472
            if (!empty($value)) {
473
                $htmlAttributes .= \sprintf(' %s="%s"', $name, $value);
474
            } else {
475
                $htmlAttributes .= \sprintf(' %s', $name);
476
            }
477
        }
478
479
        switch ($asset['ext']) {
480
            case 'css':
481
                return \sprintf('<link rel="stylesheet" href="%s"%s>', $asset['path'], $htmlAttributes);
482
            case 'js':
483
                return \sprintf('<script src="%s"%%s></script>', $asset['path'], $htmlAttributes);
484
        }
485
486
        if ($asset['type'] == 'image') {
487
            return \sprintf(
488
                '<img src="%s"%s>',
489
                $asset['path'],
490
                $htmlAttributes
491
            );
492
        }
493
494
        throw new Exception(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
495
    }
496
497
    /**
498
     * Returns the content of an Asset.
499
     *
500
     * @param Asset $asset
501
     *
502
     * @return string
503
     */
504
    public function inline(Asset $asset): string
505
    {
506
        if (is_null($asset['content'])) {
507
            throw new Exception(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
508
        }
509
510
        return $asset['content'];
511
    }
512
513
    /**
514
     * Reads $length first characters of a string and adds a suffix.
515
     *
516
     * @param string|null $string
517
     * @param int         $length
518
     * @param string      $suffix
519
     *
520
     * @return string|null
521
     */
522
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
523
    {
524
        $string = str_replace('</p>', '<br /><br />', $string);
525
        $string = trim(strip_tags($string, '<br>'), '<br />');
526
        if (mb_strlen($string) > $length) {
527
            $string = mb_substr($string, 0, $length);
528
            $string .= $suffix;
529
        }
530
531
        return $string;
532
    }
533
534
    /**
535
     * Reads characters before '<!-- excerpt|break -->'.
536
     *
537
     * @param string|null $string
538
     *
539
     * @return string|null
540
     */
541
    public function excerptHtml(string $string = null): ?string
542
    {
543
        // https://regex101.com/r/Xl7d5I/3
544
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
545
        preg_match('/'.$pattern.'/is', $string, $matches);
546
        if (empty($matches)) {
547
            return $string;
548
        }
549
550
        return trim($matches[1]);
551
    }
552
553
    /**
554
     * Converts a Markdown string to HTML.
555
     *
556
     * @param string|null $markdown
557
     *
558
     * @return string|null
559
     */
560
    public function markdownToHtml(string $markdown): ?string
561
    {
562
        try {
563
            $parsedown = new Parsedown($this->builder);
564
            $html = $parsedown->text($markdown);
565
        } catch (\Exception $e) {
566
            throw new Exception('"markdown" filter can not convert supplied Markdown.');
567
        }
568
569
        return $html;
570
    }
571
572
    /**
573
     * Calculates estimated time to read a text.
574
     *
575
     * @param string|null $text
576
     *
577
     * @return string
578
     */
579
    public function readtime(string $text = null): string
580
    {
581
        $words = str_word_count(strip_tags($text));
582
        $min = floor($words / 200);
583
        if ($min === 0) {
584
            return '1';
585
        }
586
587
        return (string) $min;
588
    }
589
590
    /**
591
     * Gets the value of an environment variable.
592
     *
593
     * @param string $var
594
     *
595
     * @return string|null
596
     */
597
    public function getEnv(string $var): ?string
598
    {
599
        return getenv($var) ?: null;
600
    }
601
}
602