Passed
Push — coverage ( 650341...46da49 )
by Arnaud
02:33
created

Extension::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
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 10
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
                $a['weight'] = 0;
248
            }
249
            if (!isset($b['weight'])) {
250
                $a['weight'] = 0;
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
            $outputStyles = ['expanded', 'compressed'];
445
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
446
            if (!in_array($outputStyle, $outputStyles)) {
447
                throw new Exception(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
448
            }
449
            $scssPhp->setOutputStyle($outputStyle);
450
            $scssPhp->setVariables($this->config->get('assets.compile.variables') ?? []);
451
            $value = $scssPhp->compile($value);
452
            $cache->set($cacheKey, $value);
453
        }
454
455
        return $cache->get($cacheKey, $value);
456
    }
457
458
    /**
459
     * Creates an HTML element from an asset.
460
     *
461
     * @param Asset      $asset
462
     * @param array|null $attributes
463
     *
464
     * @return string
465
     */
466
    public function html(Asset $asset, array $attributes = null): string
467
    {
468
        $htmlAttributes = '';
469
        foreach ($attributes as $name => $value) {
470
            if (!empty($value)) {
471
                $htmlAttributes .= \sprintf(' %s="%s"', $name, $value);
472
            } else {
473
                $htmlAttributes .= \sprintf(' %s', $name);
474
            }
475
        }
476
477
        switch ($asset['ext']) {
478
            case 'css':
479
                return \sprintf('<link rel="stylesheet" href="%s"%s>', $asset['path'], $htmlAttributes);
480
            case 'js':
481
                return \sprintf('<script src="%s"%%s></script>', $asset['path'], $htmlAttributes);
482
        }
483
484
        if ($asset['type'] == 'image') {
485
            return \sprintf(
486
                '<img src="%s"%s>',
487
                $asset['path'],
488
                $htmlAttributes
489
            );
490
        }
491
492
        throw new Exception(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
493
    }
494
495
    /**
496
     * Returns the content of an Asset.
497
     *
498
     * @param Asset $asset
499
     *
500
     * @return string
501
     */
502
    public function inline(Asset $asset): string
503
    {
504
        if (is_null($asset['content'])) {
505
            throw new Exception(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
506
        }
507
508
        return $asset['content'];
509
    }
510
511
    /**
512
     * Reads $length first characters of a string and adds a suffix.
513
     *
514
     * @param string|null $string
515
     * @param int         $length
516
     * @param string      $suffix
517
     *
518
     * @return string|null
519
     */
520
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
521
    {
522
        $string = str_replace('</p>', '<br /><br />', $string);
523
        $string = trim(strip_tags($string, '<br>'), '<br />');
524
        if (mb_strlen($string) > $length) {
525
            $string = mb_substr($string, 0, $length);
526
            $string .= $suffix;
527
        }
528
529
        return $string;
530
    }
531
532
    /**
533
     * Reads characters before '<!-- excerpt|break -->'.
534
     *
535
     * @param string|null $string
536
     *
537
     * @return string|null
538
     */
539
    public function excerptHtml(string $string = null): ?string
540
    {
541
        // https://regex101.com/r/Xl7d5I/3
542
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
543
        preg_match('/'.$pattern.'/is', $string, $matches);
544
        if (empty($matches)) {
545
            return $string;
546
        }
547
548
        return trim($matches[1]);
549
    }
550
551
    /**
552
     * Converts a Markdown string to HTML.
553
     *
554
     * @param string|null $markdown
555
     *
556
     * @return string|null
557
     */
558
    public function markdownToHtml(string $markdown): ?string
559
    {
560
        try {
561
            $parsedown = new Parsedown($this->builder);
562
            $html = $parsedown->text($markdown);
563
        } catch (\Exception $e) {
564
            throw new Exception('"markdown" filter can not convert supplied Markdown.');
565
        }
566
567
        return $html;
568
    }
569
570
    /**
571
     * Calculates estimated time to read a text.
572
     *
573
     * @param string|null $text
574
     *
575
     * @return string
576
     */
577
    public function readtime(string $text = null): string
578
    {
579
        $words = str_word_count(strip_tags($text));
580
        $min = floor($words / 200);
581
        if ($min === 0) {
582
            return '1';
583
        }
584
585
        return (string) $min;
586
    }
587
588
    /**
589
     * Gets the value of an environment variable.
590
     *
591
     * @param string $var
592
     *
593
     * @return string|null
594
     */
595
    public function getEnv(string $var): ?string
596
    {
597
        return getenv($var) ?: null;
598
    }
599
}
600