Completed
Push — site-config ( 2b2097...bb0c13 )
by Arnaud
01:52
created

Extension::getLanguage()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
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\TwigFilter('filterBySection', [$this, 'filterBySection']),
77
            new \Twig\TwigFilter('filterBy', [$this, 'filterBy']),
78
            new \Twig\TwigFilter('sortByTitle', [$this, 'sortByTitle']),
79
            new \Twig\TwigFilter('sortByWeight', [$this, 'sortByWeight']),
80
            new \Twig\TwigFilter('sortByDate', [$this, 'sortByDate']),
81
            new \Twig\TwigFilter('urlize', [$this, 'slugifyFilter']),
82
            new \Twig\TwigFilter('minifyCSS', [$this, 'minifyCss']),
83
            new \Twig\TwigFilter('minifyJS', [$this, 'minifyJs']),
84
            new \Twig\TwigFilter('SCSStoCSS', [$this, 'scssToCss']),
85
            new \Twig\TwigFilter('excerpt', [$this, 'excerpt']),
86
            new \Twig\TwigFilter('excerptHtml', [$this, 'excerptHtml']),
87
        ];
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function getFunctions()
94
    {
95
        return [
96
            new \Twig\TwigFunction('url', [$this, 'createUrl']),
97
            new \Twig\TwigFunction('minify', [$this, 'minify']),
98
            new \Twig\TwigFunction('readtime', [$this, 'readtime']),
99
            new \Twig\TwigFunction('toCSS', [$this, 'toCss']),
100
            new \Twig\TwigFunction('hash', [$this, 'hashFile']),
101
            new \Twig\TwigFunction('getenv', [$this, 'getEnv']),
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('baseurl');
250
        $hash = md5($this->config->get('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('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
        // https://regex101.com/r/Xl7d5I/3
460
        $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
461
        preg_match('/'.$pattern.'/is', $string, $matches);
462
        if (empty($matches)) {
463
            return $string;
464
        }
465
466
        return trim($matches[1]);
467
    }
468
469
    /**
470
     * Calculate estimated time to read a text.
471
     *
472
     * @param string|null $text
473
     *
474
     * @return string
475
     */
476
    public function readtime(string $text = null): string
477
    {
478
        $words = str_word_count(strip_tags($text));
479
        $min = floor($words / 200);
480
        if ($min === 0) {
481
            return '1';
482
        }
483
484
        return (string) $min;
485
    }
486
487
    /**
488
     * Hash file with sha384.
489
     * Useful for SRI (Subresource Integrity).
490
     *
491
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
492
     *
493
     * @param string $path
494
     *
495
     * @return string|null
496
     */
497
    public function hashFile(string $path): ?string
498
    {
499
        if (is_file($filePath = $this->outputPath.'/'.$path)) {
500
            $path = $filePath;
501
        }
502
503
        return sprintf('sha384-%s', base64_encode(hash_file('sha384', $path, true)));
504
    }
505
506
    /**
507
     * Gets the value of an environment variable.
508
     *
509
     * @param string $var
510
     *
511
     * @return string|null
512
     */
513
    public function getEnv(string $var): ?string
514
    {
515
        return getenv($var) ?: null;
516
    }
517
}
518