Completed
Push — feature-output-formats ( c29a92...505338 )
by Arnaud
02:38
created

Extension::sortByDate()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.5866
c 0
b 0
f 0
cc 7
nc 4
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']),
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
     * @throws Exception
126
     *
127
     * @return CollectionInterface
128
     */
129
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
130
    {
131
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
132
            // dedicated getter?
133
            $method = 'get'.ucfirst($variable);
134
            if (method_exists($page, $method) && $page->$method() == $value) {
135
                return true;
136
            }
137
            if ($page->getVariable($variable) == $value) {
138
                return true;
139
            }
140
        });
141
142
        return $filteredPages;
143
    }
144
145
    /**
146
     * Sort by title.
147
     *
148
     * @param CollectionInterface|array $array
149
     *
150
     * @return mixed
151
     */
152
    public function sortByTitle($array)
153
    {
154
        if ($array instanceof CollectionInterface) {
155
            $array = $array->toArray();
156
        }
157
        if (is_array($array)) {
158
            array_multisort(array_keys($array), SORT_NATURAL | SORT_FLAG_CASE, $array);
159
        }
160
161
        return $array;
162
    }
163
164
    /**
165
     * Sort by weight.
166
     *
167
     * @param CollectionInterface|array $array
168
     *
169
     * @return mixed
170
     */
171
    public function sortByWeight($array)
172
    {
173
        $callback = function ($a, $b) {
174
            if (!isset($a['weight'])) {
175
                return 1;
176
            }
177
            if (!isset($b['weight'])) {
178
                return -1;
179
            }
180
            if ($a['weight'] == $b['weight']) {
181
                return 0;
182
            }
183
184
            return ($a['weight'] < $b['weight']) ? -1 : 1;
185
        };
186
187
        if ($array instanceof CollectionInterface) {
188
            $array = $array->toArray();
189
        }
190
        if (is_array($array)) {
191
            usort($array, $callback);
192
        }
193
194
        return $array;
195
    }
196
197
    /**
198
     * Sort by date.
199
     *
200
     * @param CollectionInterface|array $array
201
     *
202
     * @return mixed
203
     */
204
    public function sortByDate($array)
205
    {
206
        $callback = function ($a, $b) {
207
            if (!isset($a['date'])) {
208
                return -1;
209
            }
210
            if (!isset($b['date'])) {
211
                return 1;
212
            }
213
            if ($a['date'] == $b['date']) {
214
                return 0;
215
            }
216
217
            return ($a['date'] > $b['date']) ? -1 : 1;
218
        };
219
220
        if ($array instanceof CollectionInterface) {
221
            $array = $array->toArray();
222
        }
223
        if (is_array($array)) {
224
            usort($array, $callback);
225
        }
226
227
        return $array;
228
    }
229
230
    /**
231
     * Create an URL.
232
     *
233
     * $options[
234
     *     'canonical' => null,
235
     *     'addhash'   => true,
236
     *     'format'    => 'json',
237
     * ];
238
     *
239
     * @param string|Page|null $value
240
     * @param array|null       $options
241
     *
242
     * @return string|null
243
     */
244
    public function createUrl($value = null, $options = null): ?string
245
    {
246
        $base = '';
247
        $baseurl = $this->config->get('site.baseurl');
248
        $hash = md5($this->config->get('site.time'));
249
        $canonical = null;
250
        $addhash = true;
251
        $format = 'html';
252
253
        // handle options
254
        if (isset($options['canonical'])) {
255
            $canonical = $options['canonical'];
256
        }
257
        if (is_bool($options)) { // backward compatibility
258
            $canonical = $options;
259
        }
260
        if (isset($options['addhash'])) {
261
            $addhash = $options['addhash'];
262
        }
263
        if (isset($options['format'])) {
264
            $format = $options['format'];
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
            $url = $value->getUrl($format, $this->config);
277
            $url = $base.'/'.ltrim($url, '/');
278
        } else {
279
            // string
280
            if (preg_match('~^(?:f|ht)tps?://~i', $value)) { // external URL
281
                $url = $value;
282
            } else {
283
                if (false !== strpos($value, '.')) { // ressource URL (with a dot for extension)
284
                    $url = $value;
285
                    if ($addhash) {
286
                        $url .= '?'.$hash;
287
                    }
288
                    $url = $base.'/'.ltrim($url, '/');
289
                } else {
290
                    $url = $base.'/';
291
                    if (!empty($value)) { // value == page ID
292
                        $pageId = $this->slugifyFilter($value);
293
                        if ($this->builder->getPages()->has($pageId)) {
294
                            $page = $this->builder->getPages()->get($pageId);
295
                            if ($page instanceof Page) {
296
                                $url = $this->createUrl($page, $options);
0 ignored issues
show
Bug introduced by
It seems like $options can also be of type boolean; however, Cecil\Renderer\Twig\Extension::createUrl() does only seem to accept array|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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