Passed
Push — analysis-peWDDV ( 08939e )
by Arnaud
05:35 queued 15s
created

Extension::sortByWeight()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 11
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 20
rs 9.6111
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\Exception\Exception;
23
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
24
use Cocur\Slugify\Slugify;
25
use MatthiasMullie\Minify;
26
use ScssPhp\ScssPhp\Compiler;
27
28
/**
29
 * Class Twig\Extension.
30
 */
31
class Extension extends SlugifyExtension
32
{
33
    /** @var Builder */
34
    protected $builder;
35
    /** @var Config */
36
    protected $config;
37
    /** @var Slugify */
38
    private static $slugifier;
39
40
    /**
41
     * @param Builder $builder
42
     */
43
    public function __construct(Builder $builder)
44
    {
45
        if (!self::$slugifier instanceof Slugify) {
46
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
47
        }
48
49
        parent::__construct(self::$slugifier);
50
51
        $this->builder = $builder;
52
        $this->config = $this->builder->getConfig();
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function getName()
59
    {
60
        return 'cecil';
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function getFilters()
67
    {
68
        return [
69
            new \Twig\TwigFilter('filter_by', [$this, 'filterBy']),
70
            // sort
71
            new \Twig\TwigFilter('sort_by_title', [$this, 'sortByTitle']),
72
            new \Twig\TwigFilter('sort_by_weight', [$this, 'sortByWeight']),
73
            new \Twig\TwigFilter('sort_by_date', [$this, 'sortByDate']),
74
            // assets
75
            new \Twig\TwigFilter('url', [$this, 'createUrl']),
76
            new \Twig\TwigFilter('minify', [$this, 'minify']),
77
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
78
            new \Twig\TwigFilter('html', [$this, 'createHtmlElement']),
79
            new \Twig\TwigFilter('inline', [$this, 'getContent']),
80
            new \Twig\TwigFilter('minify_css', [$this, 'minifyCss']),
81
            new \Twig\TwigFilter('minify_js', [$this, 'minifyJs']),
82
            new \Twig\TwigFilter('scss_to_css', [$this, 'scssToCss']),
83
            new \Twig\TwigFilter('sass_to_css', [$this, 'scssToCss']),
84
            new \Twig\TwigFilter('resize', [$this, 'resize']),
85
            // content
86
            new \Twig\TwigFilter('slugify', [$this, 'slugifyFilter']),
87
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
88
            new \Twig\TwigFilter('excerpt_html', [$this, 'excerptHtml']),
89
            // deprecated
90
            new \Twig\TwigFilter('filterBy', [$this, 'filterBy'], ['deprecated' => true, 'alternative' => 'filter_by']),
91
            new \Twig\TwigFilter('sortByTitle', [$this, 'sortByTitle'], ['deprecated' => true, 'alternative' => 'sort_by_title']),
92
            new \Twig\TwigFilter('sortByWeight', [$this, 'sortByWeight'], ['deprecated' => true, 'alternative' => 'sort_by_weight']),
93
            new \Twig\TwigFilter('sortByDate', [$this, 'sortByDate'], ['deprecated' => true, 'alternative' => 'sort_by_date']),
94
            new \Twig\TwigFilter('minifyCSS', [$this, 'minifyCss'], ['deprecated' => true, 'alternative' => 'minifyCss']),
95
            new \Twig\TwigFilter('minifyJS', [$this, 'minifyJs'], ['deprecated' => true, 'alternative' => 'minifyJs']),
96
            new \Twig\TwigFilter('SCSStoCSS', [$this, 'scssToCss'], ['deprecated' => true, 'alternative' => 'scss_to_css']),
97
            new \Twig\TwigFilter('excerptHtml', [$this, 'excerptHtml'], ['deprecated' => true, 'alternative' => 'excerpt_html']),
98
            new \Twig\TwigFilter('urlize', [$this, 'slugifyFilter'], ['deprecated' => true, 'alternative' => 'slugify']),
99
        ];
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105
    public function getFunctions()
106
    {
107
        return [
108
            // assets
109
            new \Twig\TwigFunction('url', [$this, 'createUrl']),
110
            new \Twig\TwigFunction('asset', [$this, 'createAsset']),
111
            new \Twig\TwigFunction('hash', [$this, 'hashFile']),
112
            // content
113
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
114
            // others
115
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
116
            // deprecated
117
            new \Twig\TwigFunction('minify', [$this, 'minify'], ['deprecated' => true, 'alternative' => 'minify filter']),
118
            new \Twig\TwigFunction('toCSS', [$this, 'toCss'], ['deprecated' => true, 'alternative' => 'to_css filter']),
119
        ];
120
    }
121
122
    /**
123
     * Filters by variable's name/value.
124
     *
125
     * @param PagesCollection $pages
126
     * @param string          $variable
127
     * @param string          $value
128
     *
129
     * @return CollectionInterface
130
     */
131
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
132
    {
133
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
134
            $notVirtual = false;
135
            if (!$page->isVirtual()) {
136
                $notVirtual = true;
137
            }
138
            // is a dedicated getter exists?
139
            $method = 'get'.ucfirst($variable);
140
            if (method_exists($page, $method) && $page->$method() == $value) {
141
                return $notVirtual && true;
142
            }
143
            if ($page->getVariable($variable) == $value) {
144
                return $notVirtual && true;
145
            }
146
        });
147
148
        return $filteredPages;
149
    }
150
151
    /**
152
     * Sorts by title.
153
     *
154
     * @param \Traversable $collection
155
     *
156
     * @return array
157
     */
158
    public function sortByTitle(\Traversable $collection): array
159
    {
160
        $collection = iterator_to_array($collection);
161
        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

161
        array_multisort(/** @scrutinizer ignore-type */ array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
Loading history...
162
163
        return $collection;
164
    }
165
166
    /**
167
     * Sorts by weight.
168
     *
169
     * @param \Traversable $collection
170
     *
171
     * @return array
172
     */
173
    public function sortByWeight(\Traversable $collection): array
174
    {
175
        $callback = function ($a, $b) {
176
            if (!isset($a['weight'])) {
177
                return 1;
178
            }
179
            if (!isset($b['weight'])) {
180
                return -1;
181
            }
182
            if ($a['weight'] == $b['weight']) {
183
                return 0;
184
            }
185
186
            return ($a['weight'] < $b['weight']) ? -1 : 1;
187
        };
188
189
        $collection = iterator_to_array($collection);
190
        usort($collection, $callback);
191
192
        return $collection;
193
    }
194
195
    /**
196
     * Sorts by date.
197
     *
198
     * @param \Traversable $collection
199
     *
200
     * @return array
201
     */
202
    public function sortByDate(\Traversable $collection): array
203
    {
204
        $callback = function ($a, $b) {
205
            if (!isset($a['date'])) {
206
                return -1;
207
            }
208
            if (!isset($b['date'])) {
209
                return 1;
210
            }
211
            if ($a['date'] == $b['date']) {
212
                return 0;
213
            }
214
215
            return ($a['date'] > $b['date']) ? -1 : 1;
216
        };
217
218
        $collection = iterator_to_array($collection);
219
        usort($collection, $callback);
220
221
        return $collection;
222
    }
223
224
    /**
225
     * Creates an URL.
226
     *
227
     * $options[
228
     *     'canonical' => true,
229
     *     'addhash'   => false,
230
     *     'format'    => 'json',
231
     * ];
232
     *
233
     * @param Page|Asset|string|null $value
234
     * @param array|null             $options
235
     *
236
     * @return mixed
237
     */
238
    public function createUrl($value = null, $options = null)
239
    {
240
        if ($options !== null && !is_array($options)) {
0 ignored issues
show
introduced by
The condition is_array($options) is always true.
Loading history...
241
            throw new Exception('Options of "url()" must be an array.');
242
        }
243
244
        return new Url($this->builder, $value, $options);
245
    }
246
247
    /**
248
     * Creates an asset (CSS, JS, images, etc.).
249
     *
250
     * @param string     $path    File path (relative from static/ dir).
251
     * @param array|null $options
252
     *
253
     * @return Asset
254
     */
255
    public function createAsset(string $path, $options = null): Asset
256
    {
257
        if ($options !== null && !is_array($options)) {
0 ignored issues
show
introduced by
The condition is_array($options) is always true.
Loading history...
258
            throw new Exception('Options of "asset()" must be an array.');
259
        }
260
261
        return new Asset($this->builder, $path, $options);
262
    }
263
264
    /**
265
     * Minifying an asset (CSS or JS).
266
     * ie: minify('css/style.css').
267
     *
268
     * @param string|Asset $asset
269
     *
270
     * @return Asset
271
     */
272
    public function minify($asset): Asset
273
    {
274
        if (!$asset instanceof Asset) {
275
            $asset = new Asset($this->builder, $asset);
276
        }
277
278
        return $asset->minify();
279
    }
280
281
    /**
282
     * Compiles a SCSS asset.
283
     *
284
     * @param string|Asset $asset
285
     *
286
     * @return Asset
287
     */
288
    public function toCss($asset): Asset
289
    {
290
        if (!$asset instanceof Asset) {
291
            $asset = new Asset($this->builder, $asset);
292
        }
293
294
        return $asset->compile();
295
    }
296
297
    /**
298
     * Resizes an image.
299
     *
300
     * @param string $path Image path (relative from static/ dir or external).
301
     * @param int    $size Image new size (width).
302
     *
303
     * @return string
304
     */
305
    public function resize(string $path, int $size): string
306
    {
307
        return (new Image($this->builder))->resize($path, $size);
308
    }
309
310
    /**
311
     * Hashing an asset with sha384.
312
     * Useful for SRI (Subresource Integrity).
313
     *
314
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
315
     *
316
     * @param string|Asset $path
317
     *
318
     * @return string|null
319
     */
320
    public function hashFile($asset): ?string
321
    {
322
        if (!$asset instanceof Asset) {
323
            $asset = new Asset($this->builder, $asset);
324
        }
325
326
        return sprintf('sha384-%s', base64_encode(hash('sha384', $asset['content'], true)));
327
    }
328
329
    /**
330
     * Minifying a CSS string.
331
     *
332
     * @param string $value
333
     *
334
     * @return string
335
     */
336
    public function minifyCss(string $value): string
337
    {
338
        $cache = new Cache($this->builder, 'assets');
339
        $cacheKey = $cache->createKeyFromValue($value);
340
        if (!$cache->has($cacheKey)) {
341
            $minifier = new Minify\CSS($value);
342
            $value = $minifier->minify();
343
            $cache->set($cacheKey, $value);
344
        }
345
346
        return $cache->get($cacheKey, $value);
347
    }
348
349
    /**
350
     * Minifying a JavaScript string.
351
     *
352
     * @param string $value
353
     *
354
     * @return string
355
     */
356
    public function minifyJs(string $value): string
357
    {
358
        $cache = new Cache($this->builder, 'assets');
359
        $cacheKey = $cache->createKeyFromValue($value);
360
        if (!$cache->has($cacheKey)) {
361
            $minifier = new Minify\JS($value);
362
            $value = $minifier->minify();
363
            $cache->set($cacheKey, $value);
364
        }
365
366
        return $cache->get($cacheKey, $value);
367
    }
368
369
    /**
370
     * Compiles a SCSS string.
371
     *
372
     * @param string $value
373
     *
374
     * @return string
375
     */
376
    public function scssToCss(string $value): string
377
    {
378
        $cache = new Cache($this->builder, 'assets');
379
        $cacheKey = $cache->createKeyFromValue($value);
380
        if (!$cache->has($cacheKey)) {
381
            $scss = new Compiler();
382
            $value = $scss->compile($value);
383
            $cache->set($cacheKey, $value);
384
        }
385
386
        return $cache->get($cacheKey, $value);
387
    }
388
389
    /**
390
     * Creates an HTML element from an asset.
391
     *
392
     * @param Asset $asset
393
     *
394
     * @return string
395
     */
396
    public function createHtmlElement(Asset $asset): string
397
    {
398
        if ($asset['type'] == 'image') {
399
            $attributes = $asset['attributes'] ?? [];
400
            $title = array_key_exists('title', $attributes) ? $attributes['title'] : null;
401
            $alt = array_key_exists('alt', $attributes) ? $attributes['alt'] : null;
402
403
            return \sprintf(
404
                '<img src="%s"%s%s>',
405
                $asset['path'],
406
                !is_null($title) ? \sprintf(' title="%s"', $title) : '',
407
                !is_null($alt) ? \sprintf(' alt="%s"', $alt) : ''
408
            );
409
        }
410
411
        switch ($asset['ext']) {
412
            case 'css':
413
                return \sprintf('<link rel="stylesheet" href="%s">', $asset['path']);
414
            case 'js':
415
                return \sprintf('<script src="%s"></script>', $asset['path']);
416
        }
417
418
        throw new Exception(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
419
    }
420
421
    /**
422
     * Returns the content of an Asset.
423
     *
424
     * @param Asset $asset
425
     *
426
     * @return string
427
     */
428
    public function getContent(Asset $asset): string
429
    {
430
        if (is_null($asset['content'])) {
431
            throw new Exception(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
432
        }
433
434
        return $asset['content'];
435
    }
436
437
    /**
438
     * Reads $length first characters of a string and adds a suffix.
439
     *
440
     * @param string|null $string
441
     * @param int         $length
442
     * @param string      $suffix
443
     *
444
     * @return string|null
445
     */
446
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
447
    {
448
        $string = str_replace('</p>', '<br /><br />', $string);
449
        $string = trim(strip_tags($string, '<br>'), '<br />');
450
        if (mb_strlen($string) > $length) {
451
            $string = mb_substr($string, 0, $length);
452
            $string .= $suffix;
453
        }
454
455
        return $string;
456
    }
457
458
    /**
459
     * Reads characters before '<!-- excerpt|break -->'.
460
     *
461
     * @param string|null $string
462
     *
463
     * @return string|null
464
     */
465
    public function excerptHtml(string $string = null): ?string
466
    {
467
        // https://regex101.com/r/Xl7d5I/3
468
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
469
        preg_match('/'.$pattern.'/is', $string, $matches);
470
        if (empty($matches)) {
471
            return $string;
472
        }
473
474
        return trim($matches[1]);
475
    }
476
477
    /**
478
     * Calculates estimated time to read a text.
479
     *
480
     * @param string|null $text
481
     *
482
     * @return string
483
     */
484
    public function readtime(string $text = null): string
485
    {
486
        $words = str_word_count(strip_tags($text));
487
        $min = floor($words / 200);
488
        if ($min === 0) {
489
            return '1';
490
        }
491
492
        return (string) $min;
493
    }
494
495
    /**
496
     * Gets the value of an environment variable.
497
     *
498
     * @param string $var
499
     *
500
     * @return string|null
501
     */
502
    public function getEnv(string $var): ?string
503
    {
504
        return getenv($var) ?: null;
505
    }
506
}
507