Completed
Push — feature-output-formats ( 71c0b9...769ef8 )
by Arnaud
09:01
created

Extension   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 474
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 13
dl 0
loc 474
rs 3.36
c 0
b 0
f 0

20 Methods

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