Passed
Push — assets ( e484a3...a30644 )
by Arnaud
25:41 queued 23:17
created

Extension::minify()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 40
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 26
c 1
b 0
f 0
nc 16
nop 1
dl 0
loc 40
rs 8.5706
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\Builder;
17
use Cecil\Collection\CollectionInterface;
18
use Cecil\Collection\Page\Collection as PagesCollection;
19
use Cecil\Collection\Page\Page;
20
use Cecil\Config;
21
use Cecil\Exception\Exception;
22
use Cecil\Util;
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('filterBySection', [$this, 'filterBySection']),
70
            new \Twig\TwigFilter('filterBy', [$this, 'filterBy']),
71
            new \Twig\TwigFilter('sortByTitle', [$this, 'sortByTitle']),
72
            new \Twig\TwigFilter('sortByWeight', [$this, 'sortByWeight']),
73
            new \Twig\TwigFilter('sortByDate', [$this, 'sortByDate']),
74
            new \Twig\TwigFilter('urlize', [$this, 'slugifyFilter']),
75
            new \Twig\TwigFilter('url', [$this, 'createUrl']),
76
            new \Twig\TwigFilter('minify', [$this, 'minify']),
77
            new \Twig\TwigFilter('minifyCSS', [$this, 'minifyCss']),
78
            new \Twig\TwigFilter('minifyJS', [$this, 'minifyJs']),
79
            new \Twig\TwigFilter('SCSStoCSS', [$this, 'scssToCss']),
80
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
81
            new \Twig\TwigFilter('excerptHtml', [$this, 'excerptHtml']),
82
            new \Twig\TwigFilter('resize', [$this, 'resize']),
83
        ];
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function getFunctions()
90
    {
91
        return [
92
            new \Twig\TwigFunction('url', [$this, 'createUrl']),
93
            new \Twig\TwigFunction('minify', [$this, 'minify']),
94
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
95
            new \Twig\TwigFunction('toCSS', [$this, 'toCss']),
96
            new \Twig\TwigFunction('hash', [$this, 'hashFile']),
97
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
98
            new \Twig\TwigFunction('asset', [$this, 'asset']),
99
        ];
100
    }
101
102
    /**
103
     * Filters by Section.
104
     *
105
     * Alias of `filterBy('section', $value)`.
106
     *
107
     * @param PagesCollection $pages
108
     * @param string          $section
109
     *
110
     * @return CollectionInterface
111
     */
112
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
113
    {
114
        return $this->filterBy($pages, 'section', $section);
115
    }
116
117
    /**
118
     * Filters by variable's name/value.
119
     *
120
     * @param PagesCollection $pages
121
     * @param string          $variable
122
     * @param string          $value
123
     *
124
     * @return CollectionInterface
125
     */
126
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
127
    {
128
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
129
            $notVirtual = false;
130
            if (!$page->isVirtual()) {
131
                $notVirtual = true;
132
            }
133
            // is a dedicated getter exists?
134
            $method = 'get'.ucfirst($variable);
135
            if (method_exists($page, $method) && $page->$method() == $value) {
136
                return $notVirtual && true;
137
            }
138
            if ($page->getVariable($variable) == $value) {
139
                return $notVirtual && true;
140
            }
141
        });
142
143
        return $filteredPages;
144
    }
145
146
    /**
147
     * Sorts by title.
148
     *
149
     * @param \Traversable $collection
150
     *
151
     * @return array
152
     */
153
    public function sortByTitle(\Traversable $collection): array
154
    {
155
        $collection = iterator_to_array($collection);
156
        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

156
        array_multisort(/** @scrutinizer ignore-type */ array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
Loading history...
157
158
        return $collection;
159
    }
160
161
    /**
162
     * Sorts by weight.
163
     *
164
     * @param \Traversable $collection
165
     *
166
     * @return array
167
     */
168
    public function sortByWeight(\Traversable $collection): array
169
    {
170
        $callback = function ($a, $b) {
171
            if (!isset($a['weight'])) {
172
                return 1;
173
            }
174
            if (!isset($b['weight'])) {
175
                return -1;
176
            }
177
            if ($a['weight'] == $b['weight']) {
178
                return 0;
179
            }
180
181
            return ($a['weight'] < $b['weight']) ? -1 : 1;
182
        };
183
184
        $collection = iterator_to_array($collection);
185
        usort($collection, $callback);
186
187
        return $collection;
188
    }
189
190
    /**
191
     * Sorts by date.
192
     *
193
     * @param \Traversable $collection
194
     *
195
     * @return array
196
     */
197
    public function sortByDate(\Traversable $collection): array
198
    {
199
        $callback = function ($a, $b) {
200
            if (!isset($a['date'])) {
201
                return -1;
202
            }
203
            if (!isset($b['date'])) {
204
                return 1;
205
            }
206
            if ($a['date'] == $b['date']) {
207
                return 0;
208
            }
209
210
            return ($a['date'] > $b['date']) ? -1 : 1;
211
        };
212
213
        $collection = iterator_to_array($collection);
214
        usort($collection, $callback);
215
216
        return $collection;
217
    }
218
219
    /**
220
     * Creates an URL.
221
     *
222
     * $options[
223
     *     'canonical' => null,
224
     *     'addhash'   => true,
225
     *     'format'    => 'json',
226
     * ];
227
     *
228
     * @param Page|string|null $value
229
     * @param array|bool|null  $options
230
     *
231
     * @return string|null
232
     */
233
    public function createUrl($value = null, $options = null): ?string
234
    {
235
        $baseurl = (string) $this->config->get('baseurl');
236
        $hash = md5((string) $this->config->get('time'));
237
        $base = '';
238
        // handles options
239
        $canonical = null;
240
        $addhash = false;
241
        $format = null;
242
        extract(is_array($options) ? $options : []);
243
244
        // set baseurl
245
        if ((bool) $this->config->get('canonicalurl') || $canonical === true) {
246
            $base = rtrim($baseurl, '/');
247
        }
248
        if ($canonical === false) {
249
            $base = '';
250
        }
251
252
        // value is a Page item
253
        if ($value instanceof Page) {
254
            if (!$format) {
255
                $format = $value->getVariable('output');
256
                if (is_array($value->getVariable('output'))) {
257
                    $format = $value->getVariable('output')[0];
258
                }
259
                if (!$format) {
260
                    $format = 'html';
261
                }
262
            }
263
            $url = $value->getUrl($format, $this->config);
264
            $url = $base.'/'.ltrim($url, '/');
265
266
            return $url;
267
        }
268
269
        // value is an external URL
270
        if ($value !== null) {
271
            if (Util::isExternalUrl($value)) {
272
                $url = $value;
273
274
                return $url;
275
            }
276
        }
277
278
        // value is a string
279
        if (!is_null($value)) {
280
            // value is an external URL
281
            if (Util::isExternalUrl($value)) {
282
                $url = $value;
283
284
                return $url;
285
            }
286
            $value = Util::joinPath($value);
287
        }
288
289
        // value is a ressource URL (ie: 'path/style.css')
290
        if (false !== strpos($value, '.')) {
291
            $url = $value;
292
            if ($addhash) {
293
                $url .= '?'.$hash;
294
            }
295
            $url = $base.'/'.ltrim($url, '/');
296
297
            return $url;
298
        }
299
300
        // others cases
301
        $url = $base.'/';
302
        if (!empty($value) && $value != '/') {
303
            $url = $base.'/'.$value;
304
305
            // value is a page ID (ie: 'path/my-page')
306
            try {
307
                $pageId = $this->slugifyFilter($value);
308
                $page = $this->builder->getPages()->get($pageId);
309
                $url = $this->createUrl($page, $options);
310
            } catch (\DomainException $e) {
311
                // nothing to do
312
            }
313
        }
314
315
        return $url;
316
    }
317
318
    /**
319
     * Manages assets (CSS, JS and images).
320
     *
321
     * @param string     $path    File path (relative from static/ dir).
322
     * @param array|null $options
323
     *
324
     * @return Asset
325
     */
326
    public function asset(string $path, array $options = null): Asset
327
    {
328
        return new Asset($this->builder, $path, $options);
329
    }
330
331
    /**
332
     * Minifying an asset (CSS or JS).
333
     * ie: minify('css/style.css').
334
     *
335
     * @param string|Asset $asset
336
     *
337
     * @throws Exception
338
     *
339
     * @return Asset
340
     */
341
    public function minify($asset): Asset
342
    {
343
        if (!$asset instanceof Asset) {
344
            $asset = new Asset($this->builder, $asset);
345
        }
346
347
        // ie: minify('css/style.min.css')
348
        $pathMinified = \sprintf('%s.min.%s', substr($asset['path'], 0, -strlen('.'.$asset['ext'])), $asset['ext']);
349
        $filePathMinified = Util::joinFile($this->config->getOutputPath(), $pathMinified);
350
351
        if (is_file($asset['file'])) {
352
            $cache = new Cache($this->builder, 'assets');
353
            $cacheKey = $asset['path'];
354
            if (!$cache->has($cacheKey)) {
355
                switch ($asset['ext']) {
356
                    case 'css':
357
                        $minifier = new Minify\CSS($asset['file']);
358
                        break;
359
                    case 'js':
360
                        $minifier = new Minify\JS($asset['file']);
361
                        break;
362
                    default:
363
                        throw new Exception(sprintf('%s() error: not able to process "%s"', __FUNCTION__, $asset));
364
                }
365
                $minified = $minifier->minify();
366
                $asset['content'] = $minified;
367
                $cache->set($cacheKey, $asset['content']);
368
            }
369
            $asset['content'] = $cache->get($cacheKey, $asset['content']);
370
            $asset['path'] = $pathMinified;
371
372
            // save?
373
            if (!$this->builder->getBuildOptions()['dry-run']) {
374
                Util::getFS()->dumpFile($filePathMinified, $asset['content']);
375
            }
376
377
            return $asset;
378
        }
379
380
        throw new Exception(sprintf('%s() error: "%s" doesn\'t exist', __FUNCTION__, $asset));
381
    }
382
383
    /**
384
     * Compiles a SCSS asset.
385
     *
386
     * @param string|Asset $path
387
     *
388
     * @throws Exception
389
     *
390
     * @return Asset
391
     */
392
    public function toCss($asset): Asset
393
    {
394
        if (!$asset instanceof Asset) {
395
            $asset = new Asset($this->builder, $asset);
396
        }
397
398
        $subPath = substr($asset['path'], 0, strrpos($asset['path'], '/'));
399
400
        if (is_file($asset['file'])) {
401
            switch ($asset['ext']) {
402
                case 'scss':
403
                    $scssPhp = new Compiler();
404
                    $scssPhp->setImportPaths(Util::joinFile($this->config->getOutputPath(), $subPath));
405
                    $targetPath = preg_replace('/scss/m', 'css', $asset['path']);
406
                    $css = $scssPhp->compile($asset['content']);
407
408
                    // save?
409
                    if (!$this->builder->getBuildOptions()['dry-run']) {
410
                        Util::getFS()->dumpFile(Util::joinFile($this->config->getOutputPath(), $targetPath), $css);
411
                    }
412
413
                    return $asset;
414
                default:
415
                    throw new Exception(sprintf('%s() error: not able to process "%s"', __FUNCTION__, $asset));
416
            }
417
        }
418
419
        throw new Exception(sprintf('%s() error: "%s" doesn\'t exist', __FUNCTION__, $asset));
420
    }
421
422
    /**
423
     * Resizes an image.
424
     *
425
     * @param string $path Image path (relative from static/ dir or external).
426
     * @param int    $size Image new size (width).
427
     *
428
     * @return string
429
     */
430
    public function resize(string $path, int $size): string
431
    {
432
        return (new Image($this->builder))->resize($path, $size);
433
    }
434
435
    /**
436
     * Hashing an asset with sha384.
437
     * Useful for SRI (Subresource Integrity).
438
     *
439
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
440
     *
441
     * @param string|Asset $path
442
     *
443
     * @return string|null
444
     */
445
    public function hashFile($asset): ?string
446
    {
447
        if (!$asset instanceof Asset) {
448
            $asset = new Asset($this->builder, $asset);
449
        }
450
451
        return sprintf('sha384-%s', base64_encode(hash('sha384', $asset['content'], true)));
452
    }
453
454
    /**
455
     * Minifying a CSS string.
456
     *
457
     * @param string $value
458
     *
459
     * @return string
460
     */
461
    public function minifyCss(string $value): string
462
    {
463
        $minifier = new Minify\CSS($value);
464
465
        return $minifier->minify();
466
    }
467
468
    /**
469
     * Minifying a JavaScript string.
470
     *
471
     * @param string $value
472
     *
473
     * @return string
474
     */
475
    public function minifyJs(string $value): string
476
    {
477
        $minifier = new Minify\JS($value);
478
479
        return $minifier->minify();
480
    }
481
482
    /**
483
     * Compiles a SCSS string.
484
     *
485
     * @param string $value
486
     *
487
     * @return string
488
     */
489
    public function scssToCss(string $value): string
490
    {
491
        $scss = new Compiler();
492
493
        return $scss->compile($value);
494
    }
495
496
    /**
497
     * Reads $length first characters of a string and adds a suffix.
498
     *
499
     * @param string|null $string
500
     * @param int         $length
501
     * @param string      $suffix
502
     *
503
     * @return string|null
504
     */
505
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
506
    {
507
        $string = str_replace('</p>', '<br /><br />', $string);
508
        $string = trim(strip_tags($string, '<br>'), '<br />');
509
        if (mb_strlen($string) > $length) {
510
            $string = mb_substr($string, 0, $length);
511
            $string .= $suffix;
512
        }
513
514
        return $string;
515
    }
516
517
    /**
518
     * Reads characters before '<!-- excerpt|break -->'.
519
     *
520
     * @param string|null $string
521
     *
522
     * @return string|null
523
     */
524
    public function excerptHtml(string $string = null): ?string
525
    {
526
        // https://regex101.com/r/Xl7d5I/3
527
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
528
        preg_match('/'.$pattern.'/is', $string, $matches);
529
        if (empty($matches)) {
530
            return $string;
531
        }
532
533
        return trim($matches[1]);
534
    }
535
536
    /**
537
     * Calculates estimated time to read a text.
538
     *
539
     * @param string|null $text
540
     *
541
     * @return string
542
     */
543
    public function readtime(string $text = null): string
544
    {
545
        $words = str_word_count(strip_tags($text));
546
        $min = floor($words / 200);
547
        if ($min === 0) {
548
            return '1';
549
        }
550
551
        return (string) $min;
552
    }
553
554
    /**
555
     * Gets the value of an environment variable.
556
     *
557
     * @param string $var
558
     *
559
     * @return string|null
560
     */
561
    public function getEnv(string $var): ?string
562
    {
563
        return getenv($var) ?: null;
564
    }
565
}
566