Passed
Push — fix/filter-collection ( 4cf3e9 )
by Arnaud
05:22
created

Extension::sortByDate()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 7
eloc 14
c 3
b 0
f 0
nc 4
nop 1
dl 0
loc 25
rs 8.8333
1
<?php
2
/*
3
 * Copyright (c) Arnaud Ligny <[email protected]>
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace Cecil\Renderer\Twig;
10
11
use Cecil\Assets\Image;
12
use Cecil\Builder;
13
use Cecil\Collection\CollectionInterface;
14
use Cecil\Collection\Page\Collection as PagesCollection;
15
use Cecil\Collection\Page\Page;
16
use Cecil\Config;
17
use Cecil\Exception\Exception;
18
use Cecil\Util;
19
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
20
use Cocur\Slugify\Slugify;
21
use MatthiasMullie\Minify;
22
use ScssPhp\ScssPhp\Compiler;
23
use Symfony\Component\Filesystem\Filesystem;
24
25
/**
26
 * Class Twig\Extension.
27
 */
28
class Extension extends SlugifyExtension
29
{
30
    /**
31
     * @var Builder
32
     */
33
    protected $builder;
34
    /**
35
     * @var Config
36
     */
37
    protected $config;
38
    /**
39
     * @var string
40
     */
41
    protected $outputPath;
42
    /**
43
     * @var Filesystem
44
     */
45
    protected $fileSystem;
46
    /**
47
     * @var Slugify
48
     */
49
    private static $slugifier;
50
51
    /**
52
     * Constructor.
53
     *
54
     * @param Builder $builder
55
     */
56
    public function __construct(Builder $builder)
57
    {
58
        if (!self::$slugifier instanceof Slugify) {
59
            self::$slugifier = Slugify::create(['regexp' => Page::SLUGIFY_PATTERN]);
60
        }
61
62
        parent::__construct(self::$slugifier);
63
64
        $this->builder = $builder;
65
        $this->config = $this->builder->getConfig();
66
        $this->outputPath = $this->config->getOutputPath();
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    public function getName()
73
    {
74
        return 'cecil';
75
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80
    public function getFilters()
81
    {
82
        return [
83
            new \Twig\TwigFilter('filterBySection', [$this, 'filterBySection']),
84
            new \Twig\TwigFilter('filterBy', [$this, 'filterBy']),
85
            new \Twig\TwigFilter('sortByTitle', [$this, 'sortByTitle']),
86
            new \Twig\TwigFilter('sortByWeight', [$this, 'sortByWeight']),
87
            new \Twig\TwigFilter('sortByDate', [$this, 'sortByDate']),
88
            new \Twig\TwigFilter('urlize', [$this, 'slugifyFilter']),
89
            new \Twig\TwigFilter('minifyCSS', [$this, 'minifyCss']),
90
            new \Twig\TwigFilter('minifyJS', [$this, 'minifyJs']),
91
            new \Twig\TwigFilter('SCSStoCSS', [$this, 'scssToCss']),
92
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
93
            new \Twig\TwigFilter('excerptHtml', [$this, 'excerptHtml']),
94
            new \Twig\TwigFilter('resize', [$this, 'resize']),
95
        ];
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function getFunctions()
102
    {
103
        return [
104
            new \Twig\TwigFunction('url', [$this, 'createUrl']),
105
            new \Twig\TwigFunction('minify', [$this, 'minify']),
106
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
107
            new \Twig\TwigFunction('toCSS', [$this, 'toCss']),
108
            new \Twig\TwigFunction('hash', [$this, 'hashFile']),
109
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
110
        ];
111
    }
112
113
    /**
114
     * Filter by section.
115
     *
116
     * @param PagesCollection $pages
117
     * @param string          $section
118
     *
119
     * @return CollectionInterface
120
     */
121
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
122
    {
123
        return $this->filterBy($pages, 'section', $section);
124
    }
125
126
    /**
127
     * Filter by variable.
128
     *
129
     * @param PagesCollection $pages
130
     * @param string          $variable
131
     * @param string          $value
132
     *
133
     * @return CollectionInterface
134
     */
135
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
136
    {
137
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
138
            $notVirtual = false;
139
            // not virtual only
140
            if (!$page->isVirtual()) {
141
                $notVirtual = true;
142
            }
143
            // dedicated getter?
144
            $method = 'get'.ucfirst($variable);
145
            if (method_exists($page, $method) && $page->$method() == $value) {
146
                return $notVirtual && true;
147
            }
148
            if ($page->getVariable($variable) == $value) {
149
                return $notVirtual && true;
150
            }
151
        });
152
153
        return $filteredPages;
154
    }
155
156
    /**
157
     * Sort by title.
158
     *
159
     * @param CollectionInterface|array $collection
160
     *
161
     * @return array
162
     */
163
    public function sortByTitle($collection): array
164
    {
165
        if ($collection instanceof CollectionInterface) {
166
            $collection = $collection->toArray();
167
        }
168
        if (!is_array($collection)) {
0 ignored issues
show
introduced by
The condition is_array($collection) is always true.
Loading history...
169
            $collection = iterator_to_array($collection);
170
        }
171
        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

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