Passed
Push — assets ( 3e13ea...57a633 )
by Arnaud
13:03 queued 10:25
created

Extension::asset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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