Completed
Push — feature-output-formats ( c119c8...19993f )
by Arnaud
62:02 queued 59:50
created

Extension::filterBy()   B

Complexity

Conditions 7
Paths 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.6666
c 0
b 0
f 0
cc 7
nc 1
nop 3
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']),
77
            new \Twig_SimpleFilter('filterBy', [$this, 'filterBy']),
78
            new \Twig_SimpleFilter('sortByTitle', [$this, 'sortByTitle']),
79
            new \Twig_SimpleFilter('sortByWeight', [$this, 'sortByWeight']),
80
            new \Twig_SimpleFilter('sortByDate', [$this, 'sortByDate']),
81
            new \Twig_SimpleFilter('urlize', [$this, 'slugifyFilter']),
82
            new \Twig_SimpleFilter('minifyCSS', [$this, 'minifyCss']),
83
            new \Twig_SimpleFilter('minifyJS', [$this, 'minifyJs']),
84
            new \Twig_SimpleFilter('SCSStoCSS', [$this, 'scssToCss']),
85
            new \Twig_SimpleFilter('excerpt', [$this, 'excerpt']),
86
            new \Twig_SimpleFilter('excerptHtml', [$this, 'excerptHtml']),
87
        ];
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function getFunctions()
94
    {
95
        return [
96
            new \Twig_SimpleFunction('url', [$this, 'createUrl']),
97
            new \Twig_SimpleFunction('minify', [$this, 'minify']),
98
            new \Twig_SimpleFunction('readtime', [$this, 'readtime']),
99
            new \Twig_SimpleFunction('toCSS', [$this, 'toCss']),
100
            new \Twig_SimpleFunction('hash', [$this, 'hashFile']),
101
            new \Twig_SimpleFunction('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('site.baseurl');
250
        $hash = md5($this->config->get('site.time'));
251
        $base = '';
252
        // handle options
253
        $canonical = null;
254
        $addhash = true;
255
        $format = null;
256
        // backward compatibility
257
        if (is_bool($options)) {
258
            $options = [];
259
            $options['canonical'] = false;
260
            if ($options === true) {
261
                $options['canonical'] = true;
262
            }
263
        }
264
        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...
265
266
        // set baseurl
267
        if ($this->config->get('site.canonicalurl') === true || $canonical === true) {
268
            $base = rtrim($baseurl, '/');
269
        }
270
        if ($canonical === false) {
271
            $base = '';
272
        }
273
274
        // Page item
275
        if ($value instanceof Page) {
276
            if (!$format) {
277
                $format = $value->getVariable('output');
278
                if (is_array($value->getVariable('output'))) {
279
                    $format = $value->getVariable('output')[0];
280
                }
281
                if (!$format) {
282
                    $format = 'html';
283
                }
284
            }
285
            $url = $value->getUrl($format, $this->config);
286
            $url = $base.'/'.ltrim($url, '/');
287
        } else {
288
            // string
289
            if (preg_match('~^(?:f|ht)tps?://~i', $value)) { // external URL
290
                $url = $value;
291
            } else {
292
                if (false !== strpos($value, '.')) { // ressource URL (with a dot for extension)
293
                    $url = $value;
294
                    if ($addhash) {
295
                        $url .= '?'.$hash;
296
                    }
297
                    $url = $base.'/'.ltrim($url, '/');
298
                } else {
299
                    $url = $base.'/';
300
                    if (!empty($value)) { // value == page ID?
301
                        $pageId = $this->slugifyFilter($value);
302
                        if ($this->builder->getPages()->has($pageId)) {
303
                            $page = $this->builder->getPages()->get($pageId);
304
                            $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...
305
                        }
306
                    }
307
                }
308
            }
309
        }
310
311
        return $url;
312
    }
313
314
    /**
315
     * Minify a CSS or a JS file.
316
     *
317
     * @param string $path
318
     *
319
     * @throws Exception
320
     *
321
     * @return string
322
     */
323
    public function minify(string $path): string
324
    {
325
        $filePath = $this->outputPath.'/'.$path;
326
        if (is_file($filePath)) {
327
            $extension = (new \SplFileInfo($filePath))->getExtension();
328
            switch ($extension) {
329
                case 'css':
330
                    $minifier = new Minify\CSS($filePath);
331
                    break;
332
                case 'js':
333
                    $minifier = new Minify\JS($filePath);
334
                    break;
335
                default:
336
                    throw new Exception(sprintf("File '%s' should be a '.css' or a '.js'!", $path));
337
            }
338
            $minifier->minify($filePath);
339
340
            return $path;
341
        }
342
343
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
344
    }
345
346
    /**
347
     * Minify CSS.
348
     *
349
     * @param string $value
350
     *
351
     * @return string
352
     */
353
    public function minifyCss(string $value): string
354
    {
355
        $minifier = new Minify\CSS($value);
356
357
        return $minifier->minify();
358
    }
359
360
    /**
361
     * Minify JS.
362
     *
363
     * @param string $value
364
     *
365
     * @return string
366
     */
367
    public function minifyJs(string $value): string
368
    {
369
        $minifier = new Minify\JS($value);
370
371
        return $minifier->minify();
372
    }
373
374
    /**
375
     * Compile style file to CSS.
376
     *
377
     * @param string $path
378
     *
379
     * @throws Exception
380
     *
381
     * @return string
382
     */
383
    public function toCss(string $path): string
384
    {
385
        $filePath = $this->outputPath.'/'.$path;
386
        $subPath = substr($path, 0, strrpos($path, '/'));
387
388
        if (is_file($filePath)) {
389
            $extension = (new \SplFileInfo($filePath))->getExtension();
390
            switch ($extension) {
391
                case 'scss':
392
                    $scssPhp = new Compiler();
393
                    $scssPhp->setImportPaths($this->outputPath.'/'.$subPath);
394
                    $targetPath = preg_replace('/scss/m', 'css', $path);
395
396
                    // compile if target file doesn't exists
397
                    if (!$this->fileSystem->exists($this->outputPath.'/'.$targetPath)) {
398
                        $scss = file_get_contents($filePath);
399
                        $css = $scssPhp->compile($scss);
400
                        $this->fileSystem->dumpFile($this->outputPath.'/'.$targetPath, $css);
401
                    }
402
403
                    return $targetPath;
404
                default:
405
                    throw new Exception(sprintf("File '%s' should be a '.scss'!", $path));
406
            }
407
        }
408
409
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
410
    }
411
412
    /**
413
     * Compile SCSS string to CSS.
414
     *
415
     * @param string $value
416
     *
417
     * @return string
418
     */
419
    public function scssToCss(string $value): string
420
    {
421
        $scss = new Compiler();
422
423
        return $scss->compile($value);
424
    }
425
426
    /**
427
     * Read $lenght first characters of a string and add a suffix.
428
     *
429
     * @param string|null $string
430
     * @param int         $length
431
     * @param string      $suffix
432
     *
433
     * @return string|null
434
     */
435
    public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string
436
    {
437
        $string = str_replace('</p>', '<br /><br />', $string);
438
        $string = trim(strip_tags($string, '<br>'), '<br />');
439
        if (mb_strlen($string) > $length) {
440
            $string = mb_substr($string, 0, $length);
441
            $string .= $suffix;
442
        }
443
444
        return $string;
445
    }
446
447
    /**
448
     * Read characters before '<!-- excerpt -->'.
449
     *
450
     * @param string|null $string
451
     *
452
     * @return string|null
453
     */
454
    public function excerptHtml(string $string = null): ?string
455
    {
456
        // https://regex101.com/r/mA2mG0/3
457
        $pattern = '^(.*)[\n\r\s]*<!-- excerpt -->[\n\r\s]*(.*)$';
458
        preg_match(
459
            '/'.$pattern.'/s',
460
            $string,
461
            $matches
462
        );
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
     *
491
     * @param string $path
492
     *
493
     * @return string|null
494
     */
495
    public function hashFile(string $path): ?string
496
    {
497
        if (is_file($filePath = $this->outputPath.'/'.$path)) {
498
            return sprintf('sha384-%s', base64_encode(hash_file('sha384', $filePath, true)));
499
        }
500
    }
501
502
    /**
503
     * Gets the value of an environment variable.
504
     *
505
     * @param string $var
506
     *
507
     * @return string|null
508
     */
509
    public function getEnv(string $var): ?string
510
    {
511
        return getenv($var) ?: null;
512
    }
513
}
514