Passed
Push — analysis-bQL3PQ ( 1b2f0d )
by Arnaud
05:14 queued 12s
created

Extension::createAsset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
        return new Url($this->builder, $value, $options);
0 ignored issues
show
Bug introduced by
It seems like $options can also be of type null; however, parameter $options of Cecil\Assets\Url::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

240
        return new Url($this->builder, $value, /** @scrutinizer ignore-type */ $options);
Loading history...
241
    }
242
243
    /**
244
     * Creates an asset (CSS, JS, images, etc.).
245
     *
246
     * @param string     $path    File path (relative from static/ dir).
247
     * @param array|null $options
248
     *
249
     * @return Asset
250
     */
251
    public function createAsset(string $path, array $options = null): Asset
252
    {
253
        return new Asset($this->builder, $path, $options);
254
    }
255
256
    /**
257
     * Minifying an asset (CSS or JS).
258
     * ie: minify('css/style.css').
259
     *
260
     * @param string|Asset $asset
261
     *
262
     * @return Asset
263
     */
264
    public function minify($asset): Asset
265
    {
266
        if (!$asset instanceof Asset) {
267
            $asset = new Asset($this->builder, $asset);
268
        }
269
270
        return $asset->minify();
271
    }
272
273
    /**
274
     * Compiles a SCSS asset.
275
     *
276
     * @param string|Asset $asset
277
     *
278
     * @return Asset
279
     */
280
    public function toCss($asset): Asset
281
    {
282
        if (!$asset instanceof Asset) {
283
            $asset = new Asset($this->builder, $asset);
284
        }
285
286
        return $asset->compile();
287
    }
288
289
    /**
290
     * Resizes an image.
291
     *
292
     * @param string $path Image path (relative from static/ dir or external).
293
     * @param int    $size Image new size (width).
294
     *
295
     * @return string
296
     */
297
    public function resize(string $path, int $size): string
298
    {
299
        return (new Image($this->builder))->resize($path, $size);
300
    }
301
302
    /**
303
     * Hashing an asset with sha384.
304
     * Useful for SRI (Subresource Integrity).
305
     *
306
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
307
     *
308
     * @param string|Asset $path
309
     *
310
     * @return string|null
311
     */
312
    public function hashFile($asset): ?string
313
    {
314
        if (!$asset instanceof Asset) {
315
            $asset = new Asset($this->builder, $asset);
316
        }
317
318
        return sprintf('sha384-%s', base64_encode(hash('sha384', $asset['content'], true)));
319
    }
320
321
    /**
322
     * Minifying a CSS string.
323
     *
324
     * @param string $value
325
     *
326
     * @return string
327
     */
328
    public function minifyCss(string $value): string
329
    {
330
        $cache = new Cache($this->builder, 'assets');
331
        $cacheKey = $cache->createKeyFromValue($value);
332
        if (!$cache->has($cacheKey)) {
333
            $minifier = new Minify\CSS($value);
334
            $value = $minifier->minify();
335
            $cache->set($cacheKey, $value);
336
        }
337
338
        return $cache->get($cacheKey, $value);
339
    }
340
341
    /**
342
     * Minifying a JavaScript string.
343
     *
344
     * @param string $value
345
     *
346
     * @return string
347
     */
348
    public function minifyJs(string $value): string
349
    {
350
        $cache = new Cache($this->builder, 'assets');
351
        $cacheKey = $cache->createKeyFromValue($value);
352
        if (!$cache->has($cacheKey)) {
353
            $minifier = new Minify\JS($value);
354
            $value = $minifier->minify();
355
            $cache->set($cacheKey, $value);
356
        }
357
358
        return $cache->get($cacheKey, $value);
359
    }
360
361
    /**
362
     * Compiles a SCSS string.
363
     *
364
     * @param string $value
365
     *
366
     * @return string
367
     */
368
    public function scssToCss(string $value): string
369
    {
370
        $cache = new Cache($this->builder, 'assets');
371
        $cacheKey = $cache->createKeyFromValue($value);
372
        if (!$cache->has($cacheKey)) {
373
            $scss = new Compiler();
374
            $value = $scss->compile($value);
375
            $cache->set($cacheKey, $value);
376
        }
377
378
        return $cache->get($cacheKey, $value);
379
    }
380
381
    /**
382
     * Creates an HTML element from an asset.
383
     *
384
     * @param Asset $asset
385
     *
386
     * @return string
387
     */
388
    public function createHtmlElement(Asset $asset): string
389
    {
390
        if ($asset['type'] == 'image') {
391
            $attributes = $asset['attributes'] ?? [];
392
            $title = array_key_exists('title', $attributes) ? $attributes['title'] : null;
393
            $alt = array_key_exists('alt', $attributes) ? $attributes['alt'] : null;
394
395
            return \sprintf(
396
                '<img src="%s"%s%s>',
397
                $asset['path'],
398
                !is_null($title) ? \sprintf(' title="%s"', $title) : '',
399
                !is_null($alt) ? \sprintf(' alt="%s"', $alt) : ''
400
            );
401
        }
402
403
        switch ($asset['ext']) {
404
            case 'css':
405
                return \sprintf('<link rel="stylesheet" href="%s">', $asset['path']);
406
            case 'js':
407
                return \sprintf('<script src="%s"></script>', $asset['path']);
408
        }
409
410
        throw new Exception(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
411
    }
412
413
    /**
414
     * Returns the content of an Asset.
415
     *
416
     * @param Asset $asset
417
     *
418
     * @return string
419
     */
420
    public function getContent(Asset $asset): string
421
    {
422
        if (is_null($asset['content'])) {
423
            throw new Exception(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
424
        }
425
426
        return $asset['content'];
427
    }
428
429
    /**
430
     * Reads $length first characters of a string and adds a suffix.
431
     *
432
     * @param string|null $string
433
     * @param int         $length
434
     * @param string      $suffix
435
     *
436
     * @return string|null
437
     */
438
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
439
    {
440
        $string = str_replace('</p>', '<br /><br />', $string);
441
        $string = trim(strip_tags($string, '<br>'), '<br />');
442
        if (mb_strlen($string) > $length) {
443
            $string = mb_substr($string, 0, $length);
444
            $string .= $suffix;
445
        }
446
447
        return $string;
448
    }
449
450
    /**
451
     * Reads characters before '<!-- excerpt|break -->'.
452
     *
453
     * @param string|null $string
454
     *
455
     * @return string|null
456
     */
457
    public function excerptHtml(string $string = null): ?string
458
    {
459
        // https://regex101.com/r/Xl7d5I/3
460
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
461
        preg_match('/'.$pattern.'/is', $string, $matches);
462
        if (empty($matches)) {
463
            return $string;
464
        }
465
466
        return trim($matches[1]);
467
    }
468
469
    /**
470
     * Calculates estimated time to read a text.
471
     *
472
     * @param string|null $text
473
     *
474
     * @return string
475
     */
476
    public function readtime(string $text = null): string
477
    {
478
        $words = str_word_count(strip_tags($text));
479
        $min = floor($words / 200);
480
        if ($min === 0) {
481
            return '1';
482
        }
483
484
        return (string) $min;
485
    }
486
487
    /**
488
     * Gets the value of an environment variable.
489
     *
490
     * @param string $var
491
     *
492
     * @return string|null
493
     */
494
    public function getEnv(string $var): ?string
495
    {
496
        return getenv($var) ?: null;
497
    }
498
}
499