Completed
Push — analysis-D2M1DL ( 825c2b )
by Arnaud
06:16 queued 11s
created

Extension::resize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
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
        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

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