Completed
Push — Assets/Image ( 7a290d...cdd250 )
by Arnaud
16:11 queued 12:51
created

Extension::sortByDate()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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

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