Completed
Push — feature-output-formats ( 77af1f...1f46d0 )
by Arnaud
02:22
created

Extension::getEnv()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
nc 2
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
     * @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
            // dedicated getter?
131
            $method = 'get'.ucfirst($variable);
132
            if (method_exists($page, $method) && $page->$method() == $value) {
133
                return true;
134
            }
135
            if ($page->getVariable($variable) == $value) {
136
                return true;
137
            }
138
        });
139
140
        return $filteredPages;
141
    }
142
143
    /**
144
     * Sort by title.
145
     *
146
     * @param CollectionInterface|array $collection
147
     *
148
     * @return array
149
     */
150
    public function sortByTitle($collection): array
151
    {
152
        if ($collection instanceof CollectionInterface) {
153
            $collection = $collection->toArray();
154
        }
155
        if (is_array($collection)) {
156
            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...
157
        }
158
159
        return $collection;
160
    }
161
162
    /**
163
     * Sort by weight.
164
     *
165
     * @param CollectionInterface|array $collection
166
     *
167
     * @return array
168
     */
169
    public function sortByWeight($collection): array
170
    {
171
        $callback = function ($a, $b) {
172
            if (!isset($a['weight'])) {
173
                return 1;
174
            }
175
            if (!isset($b['weight'])) {
176
                return -1;
177
            }
178
            if ($a['weight'] == $b['weight']) {
179
                return 0;
180
            }
181
182
            return ($a['weight'] < $b['weight']) ? -1 : 1;
183
        };
184
185
        if ($collection instanceof CollectionInterface) {
186
            $collection = $collection->toArray();
187
        }
188
        if (is_array($collection)) {
189
            usort($collection, $callback);
190
        }
191
192
        return $collection;
193
    }
194
195
    /**
196
     * Sort by date.
197
     *
198
     * @param CollectionInterface|array $collection
199
     *
200
     * @return mixed
201
     */
202
    public function sortByDate($collection): array
203
    {
204
        $callback = function ($a, $b) {
205
            if (!isset($a['date'])) {
206
                return -1;
207
            }
208
            if (!isset($b['date'])) {
209
                return 1;
210
            }
211
            if ($a['date'] == $b['date']) {
212
                return 0;
213
            }
214
215
            return ($a['date'] > $b['date']) ? -1 : 1;
216
        };
217
218
        if ($collection instanceof CollectionInterface) {
219
            $collection = $collection->toArray();
220
        }
221
        if (is_array($collection)) {
222
            usort($collection, $callback);
223
        }
224
225
        return $collection;
226
    }
227
228
    /**
229
     * Create an URL.
230
     *
231
     * $options[
232
     *     'canonical' => null,
233
     *     'addhash'   => true,
234
     *     'format'    => 'json',
235
     * ];
236
     *
237
     * @param Page|string|null $value
238
     * @param array|null       $options
239
     *
240
     * @return string|null
241
     */
242
    public function createUrl($value = null, $options = null): ?string
243
    {
244
        $baseurl = $this->config->get('site.baseurl');
245
        $hash = md5($this->config->get('site.time'));
246
        $base = '';
247
        // handle options
248
        $canonical = null;
249
        $addhash = true;
250
        $format = 'html';
251
        // backward compatibility
252
        if (is_bool($options)) {
253
            $options = [];
254
            $options['canonical'] = false;
255
            if ($options === true) {
256
                $options['canonical'] = true;
257
            }
258
        }
259
        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...
260
261
        // set baseurl
262
        if ($this->config->get('site.canonicalurl') === true || $canonical === true) {
263
            $base = rtrim($baseurl, '/');
264
        }
265
        if ($canonical === false) {
266
            $base = '';
267
        }
268
269
        // Page item
270
        if ($value instanceof Page) {
271
            $url = $value->getUrl($format, $this->config);
272
            $url = $base.'/'.ltrim($url, '/');
273
        } else {
274
            // string
275
            if (preg_match('~^(?:f|ht)tps?://~i', $value)) { // external URL
276
                $url = $value;
277
            } else {
278
                if (false !== strpos($value, '.')) { // ressource URL (with a dot for extension)
279
                    $url = $value;
280
                    if ($addhash) {
281
                        $url .= '?'.$hash;
282
                    }
283
                    $url = $base.'/'.ltrim($url, '/');
284
                } else {
285
                    $url = $base.'/';
286
                    if (!empty($value)) { // value == page ID?
287
                        $pageId = $this->slugifyFilter($value);
288
                        if ($this->builder->getPages()->has($pageId)) {
289
                            $page = $this->builder->getPages()->get($pageId);
290
                            $url = $this->createUrl($page, $options);
0 ignored issues
show
Documentation introduced by
$page is of type object<Cecil\Collection\ItemInterface>|boolean, 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...
291
                        }
292
                    }
293
                }
294
            }
295
        }
296
297
        return $url;
298
    }
299
300
    /**
301
     * Minify a CSS or a JS file.
302
     *
303
     * @param string $path
304
     *
305
     * @throws Exception
306
     *
307
     * @return string
308
     */
309
    public function minify(string $path): string
310
    {
311
        $filePath = $this->outputPath.'/'.$path;
312
        if (is_file($filePath)) {
313
            $extension = (new \SplFileInfo($filePath))->getExtension();
314
            switch ($extension) {
315
                case 'css':
316
                    $minifier = new Minify\CSS($filePath);
317
                    break;
318
                case 'js':
319
                    $minifier = new Minify\JS($filePath);
320
                    break;
321
                default:
322
                    throw new Exception(sprintf("File '%s' should be a '.css' or a '.js'!", $path));
323
            }
324
            $minifier->minify($filePath);
325
326
            return $path;
327
        }
328
329
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
330
    }
331
332
    /**
333
     * Minify CSS.
334
     *
335
     * @param string $value
336
     *
337
     * @return string
338
     */
339
    public function minifyCss(string $value): string
340
    {
341
        $minifier = new Minify\CSS($value);
342
343
        return $minifier->minify();
344
    }
345
346
    /**
347
     * Minify JS.
348
     *
349
     * @param string $value
350
     *
351
     * @return string
352
     */
353
    public function minifyJs(string $value): string
354
    {
355
        $minifier = new Minify\JS($value);
356
357
        return $minifier->minify();
358
    }
359
360
    /**
361
     * Compile style file to CSS.
362
     *
363
     * @param string $path
364
     *
365
     * @throws Exception
366
     *
367
     * @return string
368
     */
369
    public function toCss(string $path): string
370
    {
371
        $filePath = $this->outputPath.'/'.$path;
372
        $subPath = substr($path, 0, strrpos($path, '/'));
373
374
        if (is_file($filePath)) {
375
            $extension = (new \SplFileInfo($filePath))->getExtension();
376
            switch ($extension) {
377
                case 'scss':
378
                    $scssPhp = new Compiler();
379
                    $scssPhp->setImportPaths($this->outputPath.'/'.$subPath);
380
                    $targetPath = preg_replace('/scss/m', 'css', $path);
381
382
                    // compile if target file doesn't exists
383
                    if (!$this->fileSystem->exists($this->outputPath.'/'.$targetPath)) {
384
                        $scss = file_get_contents($filePath);
385
                        $css = $scssPhp->compile($scss);
386
                        $this->fileSystem->dumpFile($this->outputPath.'/'.$targetPath, $css);
387
                    }
388
389
                    return $targetPath;
390
                default:
391
                    throw new Exception(sprintf("File '%s' should be a '.scss'!", $path));
392
            }
393
        }
394
395
        throw new Exception(sprintf("File '%s' doesn't exist!", $path));
396
    }
397
398
    /**
399
     * Compile SCSS string to CSS.
400
     *
401
     * @param string $value
402
     *
403
     * @return string
404
     */
405
    public function scssToCss(string $value): string
406
    {
407
        $scss = new Compiler();
408
409
        return $scss->compile($value);
410
    }
411
412
    /**
413
     * Read $lenght first characters of a string and add a suffix.
414
     *
415
     * @param string $string
416
     * @param int    $length
417
     * @param string $suffix
418
     *
419
     * @return string
420
     */
421
    public function excerpt(string $string, int $length = 450, string $suffix = ' …'): string
422
    {
423
        $string = str_replace('</p>', '<br /><br />', $string);
424
        $string = trim(strip_tags($string, '<br>'), '<br />');
425
        if (mb_strlen($string) > $length) {
426
            $string = mb_substr($string, 0, $length);
427
            $string .= $suffix;
428
        }
429
430
        return $string;
431
    }
432
433
    /**
434
     * Read characters before '<!-- excerpt -->'.
435
     *
436
     * @param string $string
437
     *
438
     * @return string
439
     */
440
    public function excerptHtml(string $string): string
441
    {
442
        // https://regex101.com/r/mA2mG0/3
443
        $pattern = '^(.*)[\n\r\s]*<!-- excerpt -->[\n\r\s]*(.*)$';
444
        preg_match(
445
            '/'.$pattern.'/s',
446
            $string,
447
            $matches
448
        );
449
        if (empty($matches)) {
450
            return $string;
451
        }
452
453
        return trim($matches[1]);
454
    }
455
456
    /**
457
     * Calculate estimated time to read a text.
458
     *
459
     * @param string $text
460
     *
461
     * @return string
462
     */
463
    public function readtime(string $text): string
464
    {
465
        $words = str_word_count(strip_tags($text));
466
        $min = floor($words / 200);
467
        if ($min === 0) {
468
            return '1';
469
        }
470
471
        return (string) $min;
472
    }
473
474
    /**
475
     * Hash file with sha384.
476
     *
477
     * @param string $path
478
     *
479
     * @return string|null
480
     */
481
    public function hashFile(string $path): ?string
482
    {
483
        if (is_file($filePath = $this->outputPath.'/'.$path)) {
484
            return sprintf('sha384-%s', base64_encode(hash_file('sha384', $filePath, true)));
485
        }
486
    }
487
488
    /**
489
     * Gets the value of an environment variable.
490
     *
491
     * @param string $var
492
     *
493
     * @return string|null
494
     */
495
    public function getEnv(string $var): ?string
496
    {
497
        return getenv($var) ?: null;
498
    }
499
}
500