Passed
Push — clean-code ( 5aba14...836038 )
by Arnaud
08:50 queued 03:05
created

Extension::createUrl()   F

Complexity

Conditions 19
Paths 240

Size

Total Lines 84
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 19
eloc 47
c 2
b 0
f 0
nc 240
nop 2
dl 0
loc 84
rs 3.1833

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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