Completed
Push — fix-serve-baseurl ( 42568d )
by Arnaud
02:28
created

Extension::createUrl()   D

Complexity

Conditions 17
Paths 132

Size

Total Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 69
rs 4.95
c 0
b 0
f 0
cc 17
nc 132
nop 2

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
 * 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 Cocur\Slugify\Bridge\Twig\SlugifyExtension;
18
use Cocur\Slugify\Slugify;
19
use Leafo\ScssPhp\Compiler;
20
use MatthiasMullie\Minify;
21
use Symfony\Component\Filesystem\Filesystem;
22
23
/**
24
 * Class Twig\Extension.
25
 */
26
class Extension extends SlugifyExtension
27
{
28
    /**
29
     * @var Builder
30
     */
31
    protected $builder;
32
    /**
33
     * @var Config
34
     */
35
    protected $config;
36
    /**
37
     * @var string
38
     */
39
    protected $outputPath;
40
    /**
41
     * @var Filesystem
42
     */
43
    protected $fileSystem;
44
45
    /**
46
     * Constructor.
47
     *
48
     * @param Builder $builder
49
     */
50
    public function __construct(Builder $builder)
51
    {
52
        parent::__construct(Slugify::create([
53
            'regexp' => Page::SLUGIFY_PATTERN,
54
        ]));
55
56
        $this->builder = $builder;
57
        $this->config = $this->builder->getConfig();
58
        $this->outputPath = $this->config->getOutputPath();
59
        $this->fileSystem = new Filesystem();
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function getName()
66
    {
67
        return 'cecil';
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function getFilters()
74
    {
75
        return [
76
            new \Twig_SimpleFilter('filterBySection', [$this, 'filterBySection']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
77
            new \Twig_SimpleFilter('filterBy', [$this, 'filterBy']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
78
            new \Twig_SimpleFilter('sortByTitle', [$this, 'sortByTitle']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
79
            new \Twig_SimpleFilter('sortByWeight', [$this, 'sortByWeight']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
80
            new \Twig_SimpleFilter('sortByDate', [$this, 'sortByDate']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
81
            new \Twig_SimpleFilter('urlize', [$this, 'slugifyFilter']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
82
            new \Twig_SimpleFilter('minifyCSS', [$this, 'minifyCss']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
83
            new \Twig_SimpleFilter('minifyJS', [$this, 'minifyJs']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
84
            new \Twig_SimpleFilter('SCSStoCSS', [$this, 'scssToCss']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
85
            new \Twig_SimpleFilter('excerpt', [$this, 'excerpt']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
86
            new \Twig_SimpleFilter('excerptHtml', [$this, 'excerptHtml']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFilter has been deprecated with message: since Twig 2.7, use "Twig\TwigFilter" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
87
        ];
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function getFunctions()
94
    {
95
        return [
96
            new \Twig_SimpleFunction('url', [$this, 'createUrl']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFunction has been deprecated with message: since Twig 2.7, use "Twig\TwigFunction" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
97
            new \Twig_SimpleFunction('minify', [$this, 'minify']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFunction has been deprecated with message: since Twig 2.7, use "Twig\TwigFunction" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
98
            new \Twig_SimpleFunction('readtime', [$this, 'readtime']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFunction has been deprecated with message: since Twig 2.7, use "Twig\TwigFunction" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
99
            new \Twig_SimpleFunction('toCSS', [$this, 'toCss']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFunction has been deprecated with message: since Twig 2.7, use "Twig\TwigFunction" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
100
            new \Twig_SimpleFunction('hash', [$this, 'hashFile']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFunction has been deprecated with message: since Twig 2.7, use "Twig\TwigFunction" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
101
            new \Twig_SimpleFunction('getenv', [$this, 'getEnv']),
0 ignored issues
show
Deprecated Code introduced by
The class Twig_SimpleFunction has been deprecated with message: since Twig 2.7, use "Twig\TwigFunction" instead

This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.

Loading history...
102
        ];
103
    }
104
105
    /**
106
     * Filter by section.
107
     *
108
     * @param PagesCollection $pages
109
     * @param string          $section
110
     *
111
     * @return CollectionInterface
112
     */
113
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
114
    {
115
        return $this->filterBy($pages, 'section', $section);
116
    }
117
118
    /**
119
     * Filter by variable.
120
     *
121
     * @param PagesCollection $pages
122
     * @param string          $variable
123
     * @param string          $value
124
     *
125
     * @return CollectionInterface
126
     */
127
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
128
    {
129
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
130
            $notVirtual = false;
131
            // not virtual only
132
            if (!$page->isVirtual()) {
133
                $notVirtual = true;
134
            }
135
            // dedicated getter?
136
            $method = 'get'.ucfirst($variable);
137
            if (method_exists($page, $method) && $page->$method() == $value) {
138
                return $notVirtual && true;
139
            }
140
            if ($page->getVariable($variable) == $value) {
141
                return $notVirtual && true;
142
            }
143
        });
144
145
        return $filteredPages;
146
    }
147
148
    /**
149
     * Sort by title.
150
     *
151
     * @param CollectionInterface|array $collection
152
     *
153
     * @return array
154
     */
155
    public function sortByTitle($collection): array
156
    {
157
        if ($collection instanceof CollectionInterface) {
158
            $collection = $collection->toArray();
159
        }
160
        if (is_array($collection)) {
161
            array_multisort(array_keys($collection), SORT_NATURAL | SORT_FLAG_CASE, $collection);
0 ignored issues
show
Bug introduced by
array_keys($collection) cannot be passed to array_multisort() as the parameter $arr expects a reference.
Loading history...
162
        }
163
164
        return $collection;
165
    }
166
167
    /**
168
     * Sort by weight.
169
     *
170
     * @param CollectionInterface|array $collection
171
     *
172
     * @return array
173
     */
174
    public function sortByWeight($collection): array
175
    {
176
        $callback = function ($a, $b) {
177
            if (!isset($a['weight'])) {
178
                return 1;
179
            }
180
            if (!isset($b['weight'])) {
181
                return -1;
182
            }
183
            if ($a['weight'] == $b['weight']) {
184
                return 0;
185
            }
186
187
            return ($a['weight'] < $b['weight']) ? -1 : 1;
188
        };
189
190
        if ($collection instanceof CollectionInterface) {
191
            $collection = $collection->toArray();
192
        }
193
        if (is_array($collection)) {
194
            usort($collection, $callback);
195
        }
196
197
        return $collection;
198
    }
199
200
    /**
201
     * Sort by date.
202
     *
203
     * @param CollectionInterface|array $collection
204
     *
205
     * @return mixed
206
     */
207
    public function sortByDate($collection): array
208
    {
209
        $callback = function ($a, $b) {
210
            if (!isset($a['date'])) {
211
                return -1;
212
            }
213
            if (!isset($b['date'])) {
214
                return 1;
215
            }
216
            if ($a['date'] == $b['date']) {
217
                return 0;
218
            }
219
220
            return ($a['date'] > $b['date']) ? -1 : 1;
221
        };
222
223
        if ($collection instanceof CollectionInterface) {
224
            $collection = $collection->toArray();
225
        }
226
        if (is_array($collection)) {
227
            usort($collection, $callback);
228
        }
229
230
        return $collection;
231
    }
232
233
    /**
234
     * Create an URL.
235
     *
236
     * $options[
237
     *     'canonical' => null,
238
     *     'addhash'   => true,
239
     *     'format'    => 'json',
240
     * ];
241
     *
242
     * @param Page|string|null $value
243
     * @param array|null       $options
244
     *
245
     * @return string|null
246
     */
247
    public function createUrl($value = null, $options = null): ?string
248
    {
249
        $baseurl = $this->config->get('site.baseurl');
250
        $hash = md5($this->config->get('site.time'));
251
        $base = '';
252
        // handle options
253
        $canonical = null;
254
        $addhash = false;
255
        $format = null;
256
        // backward compatibility
257
        if (is_bool($options)) {
258
            $oldOptions = $options;
259
            $options = [];
260
            $options['canonical'] = false;
261
            if ($oldOptions === true) {
262
                $options['canonical'] = true;
263
            }
264
        }
265
        extract($options ?: []);
0 ignored issues
show
Bug introduced by
$options ?: array() cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
266
267
        // set baseurl
268
        if ($this->config->get('site.canonicalurl') === true || $canonical === true) {
269
            $base = rtrim($baseurl, '/');
270
        }
271
        if ($canonical === false) {
272
            $base = '';
273
        }
274
275
        // Page item
276
        if ($value instanceof Page) {
277
            if (!$format) {
278
                $format = $value->getVariable('output');
279
                if (is_array($value->getVariable('output'))) {
280
                    $format = $value->getVariable('output')[0];
281
                }
282
                if (!$format) {
283
                    $format = 'html';
284
                }
285
            }
286
            $url = $value->getUrl($format, $this->config);
287
            $url = $base.'/'.ltrim($url, '/');
288
        } else {
289
            // string
290
            if (preg_match('~^(?:f|ht)tps?://~i', $value)) { // external URL
291
                $url = $value;
292
            } else {
293
                if (false !== strpos($value, '.')) { // ressource URL (with a dot for extension)
294
                    $url = $value;
295
                    if ($addhash) {
296
                        $url .= '?'.$hash;
297
                    }
298
                    $url = $base.'/'.ltrim($url, '/');
299
                } else {
300
                    $url = $base.'/';
301
                    if (!empty($value) && $value != '/') {
302
                        $url = $base.'/'.$value;
303
                        // value == page ID?
304
                        $pageId = $this->slugifyFilter($value);
305
                        if ($this->builder->getPages()->has($pageId)) {
306
                            $page = $this->builder->getPages()->get($pageId);
307
                            $url = $this->createUrl($page, $options);
0 ignored issues
show
Documentation introduced by
$page is of type object<Cecil\Collection\ItemInterface>, but the function expects a object<Cecil\Collection\Page\Page>|string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
308
                        }
309
                    }
310
                }
311
            }
312
        }
313
314
        return $url;
315
    }
316
317
    /**
318
     * Minify a CSS or a JS file.
319
     *
320
     * @param string $path
321
     *
322
     * @throws Exception
323
     *
324
     * @return string
325
     */
326
    public function minify(string $path): string
327
    {
328
        $filePath = $this->outputPath.'/'.$path;
329
        if (is_file($filePath)) {
330
            $extension = (new \SplFileInfo($filePath))->getExtension();
331
            switch ($extension) {
332
                case 'css':
333
                    $minifier = new Minify\CSS($filePath);
334
                    break;
335
                case 'js':
336
                    $minifier = new Minify\JS($filePath);
337
                    break;
338
                default:
339
                    throw new Exception(sprintf("File '%s' should be a '.css' or a '.js'!", $path));
340
            }
341
            $minifier->minify($filePath);
342
343
            return $path;
344
        }
345
346
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
347
    }
348
349
    /**
350
     * Minify CSS.
351
     *
352
     * @param string $value
353
     *
354
     * @return string
355
     */
356
    public function minifyCss(string $value): string
357
    {
358
        $minifier = new Minify\CSS($value);
359
360
        return $minifier->minify();
361
    }
362
363
    /**
364
     * Minify JS.
365
     *
366
     * @param string $value
367
     *
368
     * @return string
369
     */
370
    public function minifyJs(string $value): string
371
    {
372
        $minifier = new Minify\JS($value);
373
374
        return $minifier->minify();
375
    }
376
377
    /**
378
     * Compile style file to CSS.
379
     *
380
     * @param string $path
381
     *
382
     * @throws Exception
383
     *
384
     * @return string
385
     */
386
    public function toCss(string $path): string
387
    {
388
        $filePath = $this->outputPath.'/'.$path;
389
        $subPath = substr($path, 0, strrpos($path, '/'));
390
391
        if (is_file($filePath)) {
392
            $extension = (new \SplFileInfo($filePath))->getExtension();
393
            switch ($extension) {
394
                case 'scss':
395
                    $scssPhp = new Compiler();
396
                    $scssPhp->setImportPaths($this->outputPath.'/'.$subPath);
397
                    $targetPath = preg_replace('/scss/m', 'css', $path);
398
399
                    // compile if target file doesn't exists
400
                    if (!$this->fileSystem->exists($this->outputPath.'/'.$targetPath)) {
401
                        $scss = file_get_contents($filePath);
402
                        $css = $scssPhp->compile($scss);
403
                        $this->fileSystem->dumpFile($this->outputPath.'/'.$targetPath, $css);
404
                    }
405
406
                    return $targetPath;
407
                default:
408
                    throw new Exception(sprintf("File '%s' should be a '.scss'!", $path));
409
            }
410
        }
411
412
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
413
    }
414
415
    /**
416
     * Compile SCSS string to CSS.
417
     *
418
     * @param string $value
419
     *
420
     * @return string
421
     */
422
    public function scssToCss(string $value): string
423
    {
424
        $scss = new Compiler();
425
426
        return $scss->compile($value);
427
    }
428
429
    /**
430
     * Read $lenght first characters of a string and add a suffix.
431
     *
432
     * @param string|null $string
433
     * @param int         $length
434
     * @param string      $suffix
435
     *
436
     * @return string|null
437
     */
438
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
439
    {
440
        $string = str_replace('</p>', '<br /><br />', $string);
441
        $string = trim(strip_tags($string, '<br>'), '<br />');
442
        if (mb_strlen($string) > $length) {
443
            $string = mb_substr($string, 0, $length);
444
            $string .= $suffix;
445
        }
446
447
        return $string;
448
    }
449
450
    /**
451
     * Read characters before '<!-- excerpt|break -->'.
452
     *
453
     * @param string|null $string
454
     *
455
     * @return string|null
456
     */
457
    public function excerptHtml(string $string = null): ?string
458
    {
459
        $pattern = '^(.*)[\n\r\s]*<!--[[:blank:]]?excerpt|break[[:blank:]]?-->[\n\r\s]*(.*)$';
460
        preg_match(
461
            '/'.$pattern.'/s',
462
            $string,
463
            $matches
464
        );
465
        if (empty($matches)) {
466
            return $string;
467
        }
468
469
        return trim($matches[1]);
470
    }
471
472
    /**
473
     * Calculate estimated time to read a text.
474
     *
475
     * @param string|null $text
476
     *
477
     * @return string
478
     */
479
    public function readtime(string $text = null): string
480
    {
481
        $words = str_word_count(strip_tags($text));
482
        $min = floor($words / 200);
483
        if ($min === 0) {
484
            return '1';
485
        }
486
487
        return (string) $min;
488
    }
489
490
    /**
491
     * Hash file with sha384.
492
     * Useful for SRI (Subresource Integrity).
493
     *
494
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
495
     *
496
     * @param string $path
497
     *
498
     * @return string|null
499
     */
500
    public function hashFile(string $path): ?string
501
    {
502
        if (is_file($filePath = $this->outputPath.'/'.$path)) {
503
            $path = $filePath;
504
        }
505
506
        return sprintf('sha384-%s', base64_encode(hash_file('sha384', $path, true)));
507
    }
508
509
    /**
510
     * Gets the value of an environment variable.
511
     *
512
     * @param string $var
513
     *
514
     * @return string|null
515
     */
516
    public function getEnv(string $var): ?string
517
    {
518
        return getenv($var) ?: null;
519
    }
520
}
521