Completed
Push — feature-output-formats ( 62e575...6f55bd )
by Arnaud
02:00
created

Extension   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 13

Importance

Changes 0
Metric Value
wmc 59
lcom 2
cbo 13
dl 0
loc 469
rs 4.08
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A getName() 0 4 1
A getFilters() 0 16 1
A getFunctions() 0 11 1
A filterBySection() 0 4 1
A filterBy() 0 15 4
A sortByTitle() 0 11 3
B sortByWeight() 0 25 7
B sortByDate() 0 25 7
F createUrl() 0 55 13
A minify() 0 22 4
A minifyCss() 0 6 1
A minifyJs() 0 6 1
A toCss() 0 28 4
A scssToCss() 0 6 1
A excerpt() 0 11 2
A excerptHtml() 0 15 2
A readtime() 0 10 2
A hashFile() 0 6 2
A getEnv() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Extension often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Extension, and based on these observations, apply Extract Interface, too.

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\Collection\CollectionInterface;
12
use Cecil\Collection\Page\Collection as PagesCollection;
13
use Cecil\Collection\Page\Page;
14
use Cecil\Config;
15
use Cecil\Exception\Exception;
16
use Cocur\Slugify\Bridge\Twig\SlugifyExtension;
17
use Cocur\Slugify\Slugify;
18
use Leafo\ScssPhp\Compiler;
19
use MatthiasMullie\Minify;
20
use Symfony\Component\Filesystem\Filesystem;
21
22
/**
23
 * Class Twig\Extension.
24
 */
25
class Extension extends SlugifyExtension
26
{
27
    /**
28
     * @var Config
29
     */
30
    protected $config;
31
    /**
32
     * @var string
33
     */
34
    protected $destPath;
35
    /**
36
     * @var Filesystem
37
     */
38
    protected $fileSystem;
39
40
    /**
41
     * Constructor.
42
     *
43
     * @param Config $config
44
     */
45
    public function __construct(Config $config)
46
    {
47
        parent::__construct(Slugify::create([
48
            'regexp' => Page::SLUGIFY_PATTERN,
49
        ]));
50
51
        $this->config = $config;
52
        $this->destPath = $config->getOutputPath();
53
        $this->fileSystem = new Filesystem();
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function getName()
60
    {
61
        return 'cecil';
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function getFilters()
68
    {
69
        return [
70
            new \Twig_SimpleFilter('filterBySection', [$this, 'filterBySection']),
71
            new \Twig_SimpleFilter('filterBy', [$this, 'filterBy']),
72
            new \Twig_SimpleFilter('sortByTitle', [$this, 'sortByTitle']),
73
            new \Twig_SimpleFilter('sortByWeight', [$this, 'sortByWeight']),
74
            new \Twig_SimpleFilter('sortByDate', [$this, 'sortByDate']),
75
            new \Twig_SimpleFilter('urlize', [$this, 'slugifyFilter']),
76
            new \Twig_SimpleFilter('minifyCSS', [$this, 'minifyCss']),
77
            new \Twig_SimpleFilter('minifyJS', [$this, 'minifyJs']),
78
            new \Twig_SimpleFilter('SCSStoCSS', [$this, 'scssToCss']),
79
            new \Twig_SimpleFilter('excerpt', [$this, 'excerpt']),
80
            new \Twig_SimpleFilter('excerptHtml', [$this, 'excerptHtml']),
81
        ];
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function getFunctions()
88
    {
89
        return [
90
            new \Twig_SimpleFunction('url', [$this, 'createUrl']),
91
            new \Twig_SimpleFunction('minify', [$this, 'minify']),
92
            new \Twig_SimpleFunction('readtime', [$this, 'readtime']),
93
            new \Twig_SimpleFunction('toCSS', [$this, 'toCss']),
94
            new \Twig_SimpleFunction('hash', [$this, 'hashFile']),
95
            new \Twig_SimpleFunction('getenv', [$this, 'getEnv']),
96
        ];
97
    }
98
99
    /**
100
     * Filter by section.
101
     *
102
     * @param PagesCollection $pages
103
     * @param string          $section
104
     *
105
     * @return CollectionInterface
106
     */
107
    public function filterBySection(PagesCollection $pages, string $section): CollectionInterface
108
    {
109
        return $this->filterBy($pages, 'section', $section);
110
    }
111
112
    /**
113
     * Filter by variable.
114
     *
115
     * @param PagesCollection $pages
116
     * @param string          $variable
117
     * @param string          $value
118
     *
119
     * @throws Exception
120
     *
121
     * @return CollectionInterface
122
     */
123
    public function filterBy(PagesCollection $pages, string $variable, string $value): CollectionInterface
124
    {
125
        $filteredPages = $pages->filter(function (Page $page) use ($variable, $value) {
126
            // dedicated getter?
127
            $method = 'get'.ucfirst($variable);
128
            if (method_exists($page, $method) && $page->$method() == $value) {
129
                return true;
130
            }
131
            if ($page->getVariable($variable) == $value) {
132
                return true;
133
            }
134
        });
135
136
        return $filteredPages;
137
    }
138
139
    /**
140
     * Sort by title.
141
     *
142
     * @param CollectionInterface|array $array
143
     *
144
     * @return mixed
145
     */
146
    public function sortByTitle($array)
147
    {
148
        if ($array instanceof CollectionInterface) {
149
            $array = $array->toArray();
150
        }
151
        if (is_array($array)) {
152
            array_multisort(array_keys($array), SORT_NATURAL | SORT_FLAG_CASE, $array);
153
        }
154
155
        return $array;
156
    }
157
158
    /**
159
     * Sort by weight.
160
     *
161
     * @param CollectionInterface|array $array
162
     *
163
     * @return mixed
164
     */
165
    public function sortByWeight($array)
166
    {
167
        $callback = function ($a, $b) {
168
            if (!isset($a['weight'])) {
169
                return 1;
170
            }
171
            if (!isset($b['weight'])) {
172
                return -1;
173
            }
174
            if ($a['weight'] == $b['weight']) {
175
                return 0;
176
            }
177
178
            return ($a['weight'] < $b['weight']) ? -1 : 1;
179
        };
180
181
        if ($array instanceof CollectionInterface) {
182
            $array = $array->toArray();
183
        }
184
        if (is_array($array)) {
185
            usort($array, $callback);
186
        }
187
188
        return $array;
189
    }
190
191
    /**
192
     * Sort by date.
193
     *
194
     * @param CollectionInterface|array $array
195
     *
196
     * @return mixed
197
     */
198
    public function sortByDate($array)
199
    {
200
        $callback = function ($a, $b) {
201
            if (!isset($a['date'])) {
202
                return -1;
203
            }
204
            if (!isset($b['date'])) {
205
                return 1;
206
            }
207
            if ($a['date'] == $b['date']) {
208
                return 0;
209
            }
210
211
            return ($a['date'] > $b['date']) ? -1 : 1;
212
        };
213
214
        if ($array instanceof CollectionInterface) {
215
            $array = $array->toArray();
216
        }
217
        if (is_array($array)) {
218
            usort($array, $callback);
219
        }
220
221
        return $array;
222
    }
223
224
    /**
225
     * Create an URL.
226
     *
227
     * $options[
228
     *     'canonical' => null,
229
     *     'addhash'   => true,
230
     *     'format'    => 'json',
231
     * ];
232
     *
233
     * @param string|Page|null  $value
234
     * @param array|null        $options
235
     *
236
     * @return string|null
237
     */
238
    public function createUrl($value = null, $options = null): ?string
239
    {
240
        $base = '';
241
        $baseurl = $this->config->get('site.baseurl');
242
        $hash = md5($this->config->get('site.time'));
243
        $canonical = null;
244
        $addhash = true;
245
        $format = 'html';
246
247
        // handle options
248
        if (isset($options['canonical'])) {
249
            $canonical = $options['canonical'];
250
        }
251
        if (is_bool($options)) { // backward compatibility
252
            $canonical = $options;
253
        }
254
        if (isset($options['addhash'])) {
255
            $addhash = $options['addhash'];
256
        }
257
        if (isset($options['format'])) {
258
            $format = $options['format'];
259
        }
260
        // set baseurl
261
        if ($this->config->get('site.canonicalurl') === true || $canonical === true) {
262
            $base = rtrim($baseurl, '/');
263
        }
264
        if ($canonical === false) {
265
            $base = '';
266
        }
267
268
        // Page item
269
        if ($value instanceof Page) {
270
            $url = $value->getUrl($format, $this->config);
271
            $url = $base.'/'.ltrim($url, '/');
272
        } else {
273
            // string
274
            $url = $value;
275
            if (!preg_match('~^(?:f|ht)tps?://~i', $url)) { // external URL
276
                if (false !== strpos($url, '.')) { // file URL (with a dot for extension)
277
                    if ($addhash) {
278
                        $url .= '?'.$hash;
279
                    }
280
                    $url = $base.'/'.ltrim($url, '/');
281
                } else {
282
                    if (!empty($url)) {
283
                        $url = $this->slugifyFilter($url);
284
                        $url = rtrim($url, '/').'/';
285
                    }
286
                    $url = $base.'/'.ltrim($url, '/');
287
                }
288
            }
289
        }
290
291
        return $url;
292
    }
293
294
    /**
295
     * Minify a CSS or a JS file.
296
     *
297
     * @param string $path
298
     *
299
     * @throws Exception
300
     *
301
     * @return string
302
     */
303
    public function minify(string $path): string
304
    {
305
        $filePath = $this->destPath.'/'.$path;
306
        if (is_file($filePath)) {
307
            $extension = (new \SplFileInfo($filePath))->getExtension();
308
            switch ($extension) {
309
                case 'css':
310
                    $minifier = new Minify\CSS($filePath);
311
                    break;
312
                case 'js':
313
                    $minifier = new Minify\JS($filePath);
314
                    break;
315
                default:
316
                    throw new Exception(sprintf("File '%s' should be a '.css' or a '.js'!", $path));
317
            }
318
            $minifier->minify($filePath);
319
320
            return $path;
321
        }
322
323
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
324
    }
325
326
    /**
327
     * Minify CSS.
328
     *
329
     * @param string $value
330
     *
331
     * @return string
332
     */
333
    public function minifyCss(string $value): string
334
    {
335
        $minifier = new Minify\CSS($value);
336
337
        return $minifier->minify();
338
    }
339
340
    /**
341
     * Minify JS.
342
     *
343
     * @param string $value
344
     *
345
     * @return string
346
     */
347
    public function minifyJs(string $value): string
348
    {
349
        $minifier = new Minify\JS($value);
350
351
        return $minifier->minify();
352
    }
353
354
    /**
355
     * Compile style file to CSS.
356
     *
357
     * @param string $path
358
     *
359
     * @throws Exception
360
     *
361
     * @return string
362
     */
363
    public function toCss(string $path): string
364
    {
365
        $filePath = $this->destPath.'/'.$path;
366
        $subPath = substr($path, 0, strrpos($path, '/'));
367
368
        if (is_file($filePath)) {
369
            $extension = (new \SplFileInfo($filePath))->getExtension();
370
            switch ($extension) {
371
                case 'scss':
372
                    $scssPhp = new Compiler();
373
                    $scssPhp->setImportPaths($this->destPath.'/'.$subPath);
374
                    $targetPath = preg_replace('/scss/m', 'css', $path);
375
376
                    // compile if target file doesn't exists
377
                    if (!$this->fileSystem->exists($this->destPath.'/'.$targetPath)) {
378
                        $scss = file_get_contents($filePath);
379
                        $css = $scssPhp->compile($scss);
380
                        $this->fileSystem->dumpFile($this->destPath.'/'.$targetPath, $css);
381
                    }
382
383
                    return $targetPath;
384
                default:
385
                    throw new Exception(sprintf("File '%s' should be a '.scss'!", $path));
386
            }
387
        }
388
389
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
390
    }
391
392
    /**
393
     * Compile SCSS string to CSS.
394
     *
395
     * @param string $value
396
     *
397
     * @return string
398
     */
399
    public function scssToCss(string $value): string
400
    {
401
        $scss = new Compiler();
402
403
        return $scss->compile($value);
404
    }
405
406
    /**
407
     * Read $lenght first characters of a string and add a suffix.
408
     *
409
     * @param string $string
410
     * @param int    $length
411
     * @param string $suffix
412
     *
413
     * @return string
414
     */
415
    public function excerpt(string $string, int $length = 450, string $suffix = ' …'): string
416
    {
417
        $string = str_replace('</p>', '<br /><br />', $string);
418
        $string = trim(strip_tags($string, '<br>'), '<br />');
419
        if (mb_strlen($string) > $length) {
420
            $string = mb_substr($string, 0, $length);
421
            $string .= $suffix;
422
        }
423
424
        return $string;
425
    }
426
427
    /**
428
     * Read characters before '<!-- excerpt -->'.
429
     *
430
     * @param string $string
431
     *
432
     * @return string
433
     */
434
    public function excerptHtml(string $string): string
435
    {
436
        // https://regex101.com/r/mA2mG0/3
437
        $pattern = '^(.*)[\n\r\s]*<!-- excerpt -->[\n\r\s]*(.*)$';
438
        preg_match(
439
            '/'.$pattern.'/s',
440
            $string,
441
            $matches
442
        );
443
        if (empty($matches)) {
444
            return $string;
445
        }
446
447
        return trim($matches[1]);
448
    }
449
450
    /**
451
     * Calculate estimated time to read a text.
452
     *
453
     * @param string $text
454
     *
455
     * @return string
456
     */
457
    public function readtime(string $text): string
458
    {
459
        $words = str_word_count(strip_tags($text));
460
        $min = floor($words / 200);
461
        if ($min === 0) {
462
            return '1';
463
        }
464
465
        return (string) $min;
466
    }
467
468
    /**
469
     * Hash file with sha384.
470
     *
471
     * @param string $path
472
     *
473
     * @return string|null
474
     */
475
    public function hashFile(string $path): ?string
476
    {
477
        if (is_file($filePath = $this->destPath.'/'.$path)) {
478
            return sprintf('sha384-%s', base64_encode(hash_file('sha384', $filePath, true)));
479
        }
480
    }
481
482
    /**
483
     * Gets the value of an environment variable.
484
     *
485
     * @param string $var
486
     *
487
     * @return string|false
488
     */
489
    public function getEnv($var): ?string
490
    {
491
        return getenv($var);
492
    }
493
}
494