Passed
Push — twig ( 3dc301...77b706 )
by Arnaud
02:26
created

Extension::hashFile()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 7
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\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, 'url']),
76
            new \Twig\TwigFilter('minify', [$this, 'minify']),
77
            new \Twig\TwigFilter('to_css', [$this, 'toCss']),
78
            new \Twig\TwigFilter('html', [$this, 'html']),
79
            new \Twig\TwigFilter('inline', [$this, 'inline']),
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(
91
                'filterBySection',
92
                [$this, 'filterBySection'],
93
                ['deprecated' => true, 'alternative' => 'filter_by']
94
            ),
95
            new \Twig\TwigFilter(
96
                'filterBy',
97
                [$this, 'filterBy'],
98
                ['deprecated' => true, 'alternative' => 'filter_by']
99
            ),
100
            new \Twig\TwigFilter(
101
                'sortByTitle',
102
                [$this, 'sortByTitle'],
103
                ['deprecated' => true, 'alternative' => 'sort_by_title']
104
            ),
105
            new \Twig\TwigFilter(
106
                'sortByWeight',
107
                [$this, 'sortByWeight'],
108
                ['deprecated' => true, 'alternative' => 'sort_by_weight']
109
            ),
110
            new \Twig\TwigFilter(
111
                'sortByDate',
112
                [$this, 'sortByDate'],
113
                ['deprecated' => true, 'alternative' => 'sort_by_date']
114
            ),
115
            new \Twig\TwigFilter(
116
                'minifyCSS',
117
                [$this, 'minifyCss'],
118
                ['deprecated' => true, 'alternative' => 'minifyCss']
119
            ),
120
            new \Twig\TwigFilter(
121
                'minifyJS',
122
                [$this, 'minifyJs'],
123
                ['deprecated' => true, 'alternative' => 'minifyJs']
124
            ),
125
            new \Twig\TwigFilter(
126
                'SCSStoCSS',
127
                [$this, 'scssToCss'],
128
                ['deprecated' => true, 'alternative' => 'scss_to_css']
129
            ),
130
            new \Twig\TwigFilter(
131
                'excerptHtml',
132
                [$this, 'excerptHtml'],
133
                ['deprecated' => true, 'alternative' => 'excerpt_html']
134
            ),
135
            new \Twig\TwigFilter(
136
                'urlize',
137
                [$this, 'slugifyFilter'],
138
                ['deprecated' => true, 'alternative' => 'slugify']
139
            ),
140
        ];
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function getFunctions()
147
    {
148
        return [
149
            // assets
150
            new \Twig\TwigFunction('url', [$this, 'url']),
151
            new \Twig\TwigFunction('asset', [$this, 'asset']),
152
            new \Twig\TwigFunction('hash', [$this, 'hash']),
153
            // content
154
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
155
            // others
156
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
157
            // deprecated
158
            new \Twig\TwigFunction(
159
                'minify',
160
                [$this, 'minify'],
161
                ['deprecated' => true, 'alternative' => 'minify filter']
162
            ),
163
            new \Twig\TwigFunction(
164
                'toCSS',
165
                [$this, 'toCss'],
166
                ['deprecated' => true, 'alternative' => 'to_css filter']
167
            ),
168
        ];
169
    }
170
171
    /**
172
     * Filters by Section.
173
     * Alias of `filterBy('section', $value)`.
174
     *
175
     * @param PagesCollection $pages
176
     * @param string          $section
177
     *
178
     * @return CollectionInterface
179
     */
180
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
181
    {
182
        return $this->filterBy($pages, 'section', $section);
183
    }
184
185
    /**
186
     * Filters by variable's name/value.
187
     *
188
     * @param PagesCollection $pages
189
     * @param string          $variable
190
     * @param string          $value
191
     *
192
     * @return CollectionInterface
193
     */
194
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
195
    {
196
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
197
            $notVirtual = false;
198
            if (!$page->isVirtual()) {
199
                $notVirtual = true;
200
            }
201
            // is a dedicated getter exists?
202
            $method = 'get'.ucfirst($variable);
203
            if (method_exists($page, $method) && $page->$method() == $value) {
204
                return $notVirtual && true;
205
            }
206
            if ($page->getVariable($variable) == $value) {
207
                return $notVirtual && true;
208
            }
209
        });
210
211
        return $filteredPages;
212
    }
213
214
    /**
215
     * Sorts by title.
216
     *
217
     * @param \Traversable $collection
218
     *
219
     * @return array
220
     */
221
    public function sortByTitle(\Traversable $collection): array
222
    {
223
        $collection = iterator_to_array($collection);
224
        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

224
        array_multisort(/** @scrutinizer ignore-type */ array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
Loading history...
225
226
        return $collection;
227
    }
228
229
    /**
230
     * Sorts by weight.
231
     *
232
     * @param \Traversable $collection
233
     *
234
     * @return array
235
     */
236
    public function sortByWeight(\Traversable $collection): array
237
    {
238
        $callback = function ($a, $b) {
239
            if (!isset($a['weight'])) {
240
                return 1;
241
            }
242
            if (!isset($b['weight'])) {
243
                return -1;
244
            }
245
            if ($a['weight'] == $b['weight']) {
246
                return 0;
247
            }
248
249
            return ($a['weight'] < $b['weight']) ? -1 : 1;
250
        };
251
252
        $collection = iterator_to_array($collection);
253
        usort($collection, $callback);
254
255
        return $collection;
256
    }
257
258
    /**
259
     * Sorts by date.
260
     *
261
     * @param \Traversable $collection
262
     *
263
     * @return array
264
     */
265
    public function sortByDate(\Traversable $collection): array
266
    {
267
        $callback = function ($a, $b) {
268
            if (!isset($a['date'])) {
269
                return -1;
270
            }
271
            if (!isset($b['date'])) {
272
                return 1;
273
            }
274
            if ($a['date'] == $b['date']) {
275
                return 0;
276
            }
277
278
            return ($a['date'] > $b['date']) ? -1 : 1;
279
        };
280
281
        $collection = iterator_to_array($collection);
282
        usort($collection, $callback);
283
284
        return $collection;
285
    }
286
287
    /**
288
     * Creates an URL.
289
     *
290
     * $options[
291
     *     'canonical' => true,
292
     *     'addhash'   => false,
293
     *     'format'    => 'json',
294
     * ];
295
     *
296
     * @param Page|Asset|string|null $value
297
     * @param array|null             $options
298
     *
299
     * @return mixed
300
     */
301
    public function url($value = null, array $options = null)
302
    {
303
        return new Url($this->builder, $value, $options);
304
    }
305
306
    /**
307
     * Creates an asset (CSS, JS, images, etc.).
308
     *
309
     * @param string     $path    File path (relative from static/ dir).
310
     * @param array|null $options
311
     *
312
     * @return Asset
313
     */
314
    public function asset(string $path, array $options = null): Asset
315
    {
316
        return new Asset($this->builder, $path, $options);
317
    }
318
319
    /**
320
     * Minifying an asset (CSS or JS).
321
     * ie: minify('css/style.css').
322
     *
323
     * @param string|Asset $asset
324
     *
325
     * @return Asset
326
     */
327
    public function minify($asset): Asset
328
    {
329
        if (!$asset instanceof Asset) {
330
            $asset = new Asset($this->builder, $asset);
331
        }
332
333
        return $asset->minify();
334
    }
335
336
    /**
337
     * Compiles a SCSS asset.
338
     *
339
     * @param string|Asset $asset
340
     *
341
     * @return Asset
342
     */
343
    public function toCss($asset): Asset
344
    {
345
        if (!$asset instanceof Asset) {
346
            $asset = new Asset($this->builder, $asset);
347
        }
348
349
        return $asset->compile();
350
    }
351
352
    /**
353
     * Resizes an image.
354
     *
355
     * @param string $path Image path (relative from static/ dir or external).
356
     * @param int    $size Image new size (width).
357
     *
358
     * @return string
359
     */
360
    public function resize(string $path, int $size): string
361
    {
362
        return (new Image($this->builder))->resize($path, $size);
363
    }
364
365
    /**
366
     * Hashing an asset with sha384.
367
     *
368
     * @param string|Asset $path
369
     *
370
     * @return string
371
     */
372
    public function hash($asset): string
373
    {
374
        if (!$asset instanceof Asset) {
375
            $asset = new Asset($this->builder, $asset);
376
        }
377
378
        return $asset->getHash();
379
    }
380
381
    /**
382
     * Minifying a CSS string.
383
     *
384
     * @param string $value
385
     *
386
     * @return string
387
     */
388
    public function minifyCss(string $value): string
389
    {
390
        $cache = new Cache($this->builder, 'assets');
391
        $cacheKey = $cache->createKeyFromValue($value);
392
        if (!$cache->has($cacheKey)) {
393
            $minifier = new Minify\CSS($value);
394
            $value = $minifier->minify();
395
            $cache->set($cacheKey, $value);
396
        }
397
398
        return $cache->get($cacheKey, $value);
399
    }
400
401
    /**
402
     * Minifying a JavaScript string.
403
     *
404
     * @param string $value
405
     *
406
     * @return string
407
     */
408
    public function minifyJs(string $value): string
409
    {
410
        $cache = new Cache($this->builder, 'assets');
411
        $cacheKey = $cache->createKeyFromValue($value);
412
        if (!$cache->has($cacheKey)) {
413
            $minifier = new Minify\JS($value);
414
            $value = $minifier->minify();
415
            $cache->set($cacheKey, $value);
416
        }
417
418
        return $cache->get($cacheKey, $value);
419
    }
420
421
    /**
422
     * Compiles a SCSS string.
423
     *
424
     * @param string $value
425
     *
426
     * @return string
427
     */
428
    public function scssToCss(string $value): string
429
    {
430
        $cache = new Cache($this->builder, 'assets');
431
        $cacheKey = $cache->createKeyFromValue($value);
432
        if (!$cache->has($cacheKey)) {
433
            $scssPhp = new Compiler();
434
            $scssPhp->setVariables($this->config->get('assets.sass.variables') ?? []);
435
            $scssPhp->setFormatter('ScssPhp\ScssPhp\Formatter\\'.ucfirst($this->config->get('assets.sass.style')));
436
            $value = $scssPhp->compile($value);
437
            $cache->set($cacheKey, $value);
438
        }
439
440
        return $cache->get($cacheKey, $value);
441
    }
442
443
    /**
444
     * Creates an HTML element from an asset.
445
     *
446
     * @param Asset $asset
447
     *
448
     * @return string
449
     */
450
    public function html(Asset $asset): string
451
    {
452
        if ($asset['type'] == 'image') {
453
            $attributes = $asset['attributes'] ?? [];
454
            $title = array_key_exists('title', $attributes) ? $attributes['title'] : null;
455
            $alt = array_key_exists('alt', $attributes) ? $attributes['alt'] : null;
456
457
            return \sprintf(
458
                '<img src="%s"%s%s>',
459
                $asset['path'],
460
                !is_null($title) ? \sprintf(' title="%s"', $title) : '',
461
                !is_null($alt) ? \sprintf(' alt="%s"', $alt) : ''
462
            );
463
        }
464
465
        switch ($asset['ext']) {
466
            case 'css':
467
                return \sprintf('<link rel="stylesheet" href="%s">', $asset['path']);
468
            case 'js':
469
                return \sprintf('<script src="%s"></script>', $asset['path']);
470
        }
471
472
        throw new Exception(\sprintf('%s is available with CSS, JS and images files only.', '"html" filter'));
473
    }
474
475
    /**
476
     * Returns the content of an Asset.
477
     *
478
     * @param Asset $asset
479
     *
480
     * @return string
481
     */
482
    public function inline(Asset $asset): string
483
    {
484
        if (is_null($asset['content'])) {
485
            throw new Exception(\sprintf('%s is available with CSS et JS files only.', '"inline" filter'));
486
        }
487
488
        return $asset['content'];
489
    }
490
491
    /**
492
     * Reads $length first characters of a string and adds a suffix.
493
     *
494
     * @param string|null $string
495
     * @param int         $length
496
     * @param string      $suffix
497
     *
498
     * @return string|null
499
     */
500
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
501
    {
502
        $string = str_replace('</p>', '<br /><br />', $string);
503
        $string = trim(strip_tags($string, '<br>'), '<br />');
504
        if (mb_strlen($string) > $length) {
505
            $string = mb_substr($string, 0, $length);
506
            $string .= $suffix;
507
        }
508
509
        return $string;
510
    }
511
512
    /**
513
     * Reads characters before '<!-- excerpt|break -->'.
514
     *
515
     * @param string|null $string
516
     *
517
     * @return string|null
518
     */
519
    public function excerptHtml(string $string = null): ?string
520
    {
521
        // https://regex101.com/r/Xl7d5I/3
522
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
523
        preg_match('/'.$pattern.'/is', $string, $matches);
524
        if (empty($matches)) {
525
            return $string;
526
        }
527
528
        return trim($matches[1]);
529
    }
530
531
    /**
532
     * Calculates estimated time to read a text.
533
     *
534
     * @param string|null $text
535
     *
536
     * @return string
537
     */
538
    public function readtime(string $text = null): string
539
    {
540
        $words = str_word_count(strip_tags($text));
541
        $min = floor($words / 200);
542
        if ($min === 0) {
543
            return '1';
544
        }
545
546
        return (string) $min;
547
    }
548
549
    /**
550
     * Gets the value of an environment variable.
551
     *
552
     * @param string $var
553
     *
554
     * @return string|null
555
     */
556
    public function getEnv(string $var): ?string
557
    {
558
        return getenv($var) ?: null;
559
    }
560
}
561