Completed
Push — site-config ( c375ab...402196 )
by Arnaud
01:57
created

Extension::getLanguage()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 3
nc 3
nop 1
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
            new \Twig_SimpleFunction('lang', [$this, 'getLanguage']),
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...
103
        ];
104
    }
105
106
    /**
107
     * Filter by section.
108
     *
109
     * @param PagesCollection $pages
110
     * @param string          $section
111
     *
112
     * @return CollectionInterface
113
     */
114
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
115
    {
116
        return $this->filterBy($pages, 'section', $section);
117
    }
118
119
    /**
120
     * Filter by variable.
121
     *
122
     * @param PagesCollection $pages
123
     * @param string          $variable
124
     * @param string          $value
125
     *
126
     * @return CollectionInterface
127
     */
128
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
129
    {
130
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
131
            $notVirtual = false;
132
            // not virtual only
133
            if (!$page->isVirtual()) {
134
                $notVirtual = true;
135
            }
136
            // dedicated getter?
137
            $method = 'get'.ucfirst($variable);
138
            if (method_exists($page, $method) && $page->$method() == $value) {
139
                return $notVirtual && true;
140
            }
141
            if ($page->getVariable($variable) == $value) {
142
                return $notVirtual && true;
143
            }
144
        });
145
146
        return $filteredPages;
147
    }
148
149
    /**
150
     * Sort by title.
151
     *
152
     * @param CollectionInterface|array $collection
153
     *
154
     * @return array
155
     */
156
    public function sortByTitle($collection): array
157
    {
158
        if ($collection instanceof CollectionInterface) {
159
            $collection = $collection->toArray();
160
        }
161
        if (is_array($collection)) {
162
            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...
163
        }
164
165
        return $collection;
166
    }
167
168
    /**
169
     * Sort by weight.
170
     *
171
     * @param CollectionInterface|array $collection
172
     *
173
     * @return array
174
     */
175
    public function sortByWeight($collection): array
176
    {
177
        $callback = function ($a, $b) {
178
            if (!isset($a['weight'])) {
179
                return 1;
180
            }
181
            if (!isset($b['weight'])) {
182
                return -1;
183
            }
184
            if ($a['weight'] == $b['weight']) {
185
                return 0;
186
            }
187
188
            return ($a['weight'] < $b['weight']) ? -1 : 1;
189
        };
190
191
        if ($collection instanceof CollectionInterface) {
192
            $collection = $collection->toArray();
193
        }
194
        if (is_array($collection)) {
195
            usort($collection, $callback);
196
        }
197
198
        return $collection;
199
    }
200
201
    /**
202
     * Sort by date.
203
     *
204
     * @param CollectionInterface|array $collection
205
     *
206
     * @return mixed
207
     */
208
    public function sortByDate($collection): array
209
    {
210
        $callback = function ($a, $b) {
211
            if (!isset($a['date'])) {
212
                return -1;
213
            }
214
            if (!isset($b['date'])) {
215
                return 1;
216
            }
217
            if ($a['date'] == $b['date']) {
218
                return 0;
219
            }
220
221
            return ($a['date'] > $b['date']) ? -1 : 1;
222
        };
223
224
        if ($collection instanceof CollectionInterface) {
225
            $collection = $collection->toArray();
226
        }
227
        if (is_array($collection)) {
228
            usort($collection, $callback);
229
        }
230
231
        return $collection;
232
    }
233
234
    /**
235
     * Create an URL.
236
     *
237
     * $options[
238
     *     'canonical' => null,
239
     *     'addhash'   => true,
240
     *     'format'    => 'json',
241
     * ];
242
     *
243
     * @param Page|string|null $value
244
     * @param array|null       $options
245
     *
246
     * @return string|null
247
     */
248
    public function createUrl($value = null, $options = null): ?string
249
    {
250
        $baseurl = $this->config->get('baseurl');
251
        $hash = md5($this->config->get('time'));
252
        $base = '';
253
        // handle options
254
        $canonical = null;
255
        $addhash = false;
256
        $format = null;
257
        // backward compatibility
258
        if (is_bool($options)) {
259
            $oldOptions = $options;
260
            $options = [];
261
            $options['canonical'] = false;
262
            if ($oldOptions === true) {
263
                $options['canonical'] = true;
264
            }
265
        }
266
        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...
267
268
        // set baseurl
269
        if ($this->config->get('canonicalurl') === true || $canonical === true) {
270
            $base = rtrim($baseurl, '/');
271
        }
272
        if ($canonical === false) {
273
            $base = '';
274
        }
275
276
        // Page item
277
        if ($value instanceof Page) {
278
            if (!$format) {
279
                $format = $value->getVariable('output');
280
                if (is_array($value->getVariable('output'))) {
281
                    $format = $value->getVariable('output')[0];
282
                }
283
                if (!$format) {
284
                    $format = 'html';
285
                }
286
            }
287
            $url = $value->getUrl($format, $this->config);
288
            $url = $base.'/'.ltrim($url, '/');
289
        } else {
290
            // string
291
            if (preg_match('~^(?:f|ht)tps?://~i', $value)) { // external URL
292
                $url = $value;
293
            } else {
294
                if (false !== strpos($value, '.')) { // ressource URL (with a dot for extension)
295
                    $url = $value;
296
                    if ($addhash) {
297
                        $url .= '?'.$hash;
298
                    }
299
                    $url = $base.'/'.ltrim($url, '/');
300
                } else {
301
                    $url = $base.'/';
302
                    if (!empty($value) && $value != '/') {
303
                        $url = $base.'/'.$value;
304
                        // value == page ID?
305
                        $pageId = $this->slugifyFilter($value);
306
                        if ($this->builder->getPages()->has($pageId)) {
307
                            $page = $this->builder->getPages()->get($pageId);
308
                            $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...
309
                        }
310
                    }
311
                }
312
            }
313
        }
314
315
        return $url;
316
    }
317
318
    /**
319
     * Minify a CSS or a JS file.
320
     *
321
     * @param string $path
322
     *
323
     * @throws Exception
324
     *
325
     * @return string
326
     */
327
    public function minify(string $path): string
328
    {
329
        $filePath = $this->outputPath.'/'.$path;
330
        if (is_file($filePath)) {
331
            $extension = (new \SplFileInfo($filePath))->getExtension();
332
            switch ($extension) {
333
                case 'css':
334
                    $minifier = new Minify\CSS($filePath);
335
                    break;
336
                case 'js':
337
                    $minifier = new Minify\JS($filePath);
338
                    break;
339
                default:
340
                    throw new Exception(sprintf("File '%s' should be a '.css' or a '.js'!", $path));
341
            }
342
            $minifier->minify($filePath);
343
344
            return $path;
345
        }
346
347
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
348
    }
349
350
    /**
351
     * Minify CSS.
352
     *
353
     * @param string $value
354
     *
355
     * @return string
356
     */
357
    public function minifyCss(string $value): string
358
    {
359
        $minifier = new Minify\CSS($value);
360
361
        return $minifier->minify();
362
    }
363
364
    /**
365
     * Minify JS.
366
     *
367
     * @param string $value
368
     *
369
     * @return string
370
     */
371
    public function minifyJs(string $value): string
372
    {
373
        $minifier = new Minify\JS($value);
374
375
        return $minifier->minify();
376
    }
377
378
    /**
379
     * Compile style file to CSS.
380
     *
381
     * @param string $path
382
     *
383
     * @throws Exception
384
     *
385
     * @return string
386
     */
387
    public function toCss(string $path): string
388
    {
389
        $filePath = $this->outputPath.'/'.$path;
390
        $subPath = substr($path, 0, strrpos($path, '/'));
391
392
        if (is_file($filePath)) {
393
            $extension = (new \SplFileInfo($filePath))->getExtension();
394
            switch ($extension) {
395
                case 'scss':
396
                    $scssPhp = new Compiler();
397
                    $scssPhp->setImportPaths($this->outputPath.'/'.$subPath);
398
                    $targetPath = preg_replace('/scss/m', 'css', $path);
399
400
                    // compile if target file doesn't exists
401
                    if (!$this->fileSystem->exists($this->outputPath.'/'.$targetPath)) {
402
                        $scss = file_get_contents($filePath);
403
                        $css = $scssPhp->compile($scss);
404
                        $this->fileSystem->dumpFile($this->outputPath.'/'.$targetPath, $css);
405
                    }
406
407
                    return $targetPath;
408
                default:
409
                    throw new Exception(sprintf("File '%s' should be a '.scss'!", $path));
410
            }
411
        }
412
413
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
414
    }
415
416
    /**
417
     * Compile SCSS string to CSS.
418
     *
419
     * @param string $value
420
     *
421
     * @return string
422
     */
423
    public function scssToCss(string $value): string
424
    {
425
        $scss = new Compiler();
426
427
        return $scss->compile($value);
428
    }
429
430
    /**
431
     * Read $lenght first characters of a string and add a suffix.
432
     *
433
     * @param string|null $string
434
     * @param int         $length
435
     * @param string      $suffix
436
     *
437
     * @return string|null
438
     */
439
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
440
    {
441
        $string = str_replace('</p>', '<br /><br />', $string);
442
        $string = trim(strip_tags($string, '<br>'), '<br />');
443
        if (mb_strlen($string) > $length) {
444
            $string = mb_substr($string, 0, $length);
445
            $string .= $suffix;
446
        }
447
448
        return $string;
449
    }
450
451
    /**
452
     * Read characters before '<!-- excerpt|break -->'.
453
     *
454
     * @param string|null $string
455
     *
456
     * @return string|null
457
     */
458
    public function excerptHtml(string $string = null): ?string
459
    {
460
        // https://regex101.com/r/Xl7d5I/3
461
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
462
        preg_match('/'.$pattern.'/is', $string, $matches);
463
        if (empty($matches)) {
464
            return $string;
465
        }
466
467
        return trim($matches[1]);
468
    }
469
470
    /**
471
     * Calculate estimated time to read a text.
472
     *
473
     * @param string|null $text
474
     *
475
     * @return string
476
     */
477
    public function readtime(string $text = null): string
478
    {
479
        $words = str_word_count(strip_tags($text));
480
        $min = floor($words / 200);
481
        if ($min === 0) {
482
            return '1';
483
        }
484
485
        return (string) $min;
486
    }
487
488
    /**
489
     * Hash file with sha384.
490
     * Useful for SRI (Subresource Integrity).
491
     *
492
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
493
     *
494
     * @param string $path
495
     *
496
     * @return string|null
497
     */
498
    public function hashFile(string $path): ?string
499
    {
500
        if (is_file($filePath = $this->outputPath.'/'.$path)) {
501
            $path = $filePath;
502
        }
503
504
        return sprintf('sha384-%s', base64_encode(hash_file('sha384', $path, true)));
505
    }
506
507
    /**
508
     * Gets the value of an environment variable.
509
     *
510
     * @param string $var
511
     *
512
     * @return string|null
513
     */
514
    public function getEnv(string $var): ?string
515
    {
516
        return getenv($var) ?: null;
517
    }
518
519
    /**
520
     * Language helper.
521
     *
522
     * @param string $variable
523
     *
524
     * @return string|null
525
     */
526
    public function getLanguage(string $variable): ?string
527
    {
528
        switch ($variable) {
529
            case 'name':
530
                return $this->config->getLanguageProperty('name');
531
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
532
            case 'locale':
533
                return $this->config->getLanguageProperty('locale');
534
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
535
            default:
536
                return $this->config->getLanguageDefaultKey();
537
        }
538
    }
539
}
540