Passed
Push — analysis-ajLGvB ( bce2e9 )
by Arnaud
05:13 queued 13s
created

Extension::createUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 3
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, '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, 'asset']),
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 Section.
124
     *
125
     * Alias of `filterBy('section', $value)`.
126
     *
127
     * @param PagesCollection $pages
128
     * @param string          $section
129
     *
130
     * @return CollectionInterface
131
     */
132
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
133
    {
134
        return $this->filterBy($pages, 'section', $section);
135
    }
136
137
    /**
138
     * Filters by variable's name/value.
139
     *
140
     * @param PagesCollection $pages
141
     * @param string          $variable
142
     * @param string          $value
143
     *
144
     * @return CollectionInterface
145
     */
146
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
147
    {
148
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
149
            $notVirtual = false;
150
            if (!$page->isVirtual()) {
151
                $notVirtual = true;
152
            }
153
            // is a dedicated getter exists?
154
            $method = 'get'.ucfirst($variable);
155
            if (method_exists($page, $method) && $page->$method() == $value) {
156
                return $notVirtual && true;
157
            }
158
            if ($page->getVariable($variable) == $value) {
159
                return $notVirtual && true;
160
            }
161
        });
162
163
        return $filteredPages;
164
    }
165
166
    /**
167
     * Sorts by title.
168
     *
169
     * @param \Traversable $collection
170
     *
171
     * @return array
172
     */
173
    public function sortByTitle(\Traversable $collection): array
174
    {
175
        $collection = iterator_to_array($collection);
176
        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

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