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); |
|
|
|
|
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 = false; |
255
|
|
|
$format = null; |
256
|
|
|
// backward compatibility |
257
|
|
|
if (is_bool($options)) { |
258
|
|
|
$oldOptions = $options; |
259
|
|
|
$options = []; |
260
|
|
|
$options['canonical'] = false; |
261
|
|
|
if ($oldOptions === true) { |
262
|
|
|
$options['canonical'] = true; |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
extract($options ?: []); |
|
|
|
|
266
|
|
|
|
267
|
|
|
// set baseurl |
268
|
|
|
if ($this->config->get('site.canonicalurl') === true || $canonical === true) { |
269
|
|
|
$base = rtrim($baseurl, '/'); |
270
|
|
|
} |
271
|
|
|
if ($canonical === false) { |
272
|
|
|
$base = ''; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
// Page item |
276
|
|
|
if ($value instanceof Page) { |
277
|
|
|
if (!$format) { |
278
|
|
|
$format = $value->getVariable('output'); |
279
|
|
|
if (is_array($value->getVariable('output'))) { |
280
|
|
|
$format = $value->getVariable('output')[0]; |
281
|
|
|
} |
282
|
|
|
if (!$format) { |
283
|
|
|
$format = 'html'; |
284
|
|
|
} |
285
|
|
|
} |
286
|
|
|
$url = $value->getUrl($format, $this->config); |
287
|
|
|
$url = $base.'/'.ltrim($url, '/'); |
288
|
|
|
} else { |
289
|
|
|
// string |
290
|
|
|
if (preg_match('~^(?:f|ht)tps?://~i', $value)) { // external URL |
291
|
|
|
$url = $value; |
292
|
|
|
} else { |
293
|
|
|
if (false !== strpos($value, '.')) { // ressource URL (with a dot for extension) |
294
|
|
|
$url = $value; |
295
|
|
|
if ($addhash) { |
296
|
|
|
$url .= '?'.$hash; |
297
|
|
|
} |
298
|
|
|
$url = $base.'/'.ltrim($url, '/'); |
299
|
|
|
} else { |
300
|
|
|
$url = $base.'/'; |
301
|
|
|
if (!empty($value) && $value != '/') { |
302
|
|
|
$url = $base.'/'.$value; |
303
|
|
|
// value == page ID? |
304
|
|
|
$pageId = $this->slugifyFilter($value); |
305
|
|
|
if ($this->builder->getPages()->has($pageId)) { |
306
|
|
|
$page = $this->builder->getPages()->get($pageId); |
307
|
|
|
$url = $this->createUrl($page, $options); |
|
|
|
|
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
return $url; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* Minify a CSS or a JS file. |
319
|
|
|
* |
320
|
|
|
* @param string $path |
321
|
|
|
* |
322
|
|
|
* @throws Exception |
323
|
|
|
* |
324
|
|
|
* @return string |
325
|
|
|
*/ |
326
|
|
|
public function minify(string $path): string |
327
|
|
|
{ |
328
|
|
|
$filePath = $this->outputPath.'/'.$path; |
329
|
|
|
if (is_file($filePath)) { |
330
|
|
|
$extension = (new \SplFileInfo($filePath))->getExtension(); |
331
|
|
|
switch ($extension) { |
332
|
|
|
case 'css': |
333
|
|
|
$minifier = new Minify\CSS($filePath); |
334
|
|
|
break; |
335
|
|
|
case 'js': |
336
|
|
|
$minifier = new Minify\JS($filePath); |
337
|
|
|
break; |
338
|
|
|
default: |
339
|
|
|
throw new Exception(sprintf("File '%s' should be a '.css' or a '.js'!", $path)); |
340
|
|
|
} |
341
|
|
|
$minifier->minify($filePath); |
342
|
|
|
|
343
|
|
|
return $path; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
throw new Exception(sprintf("File '%s' doesn't exist!", $path)); |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Minify CSS. |
351
|
|
|
* |
352
|
|
|
* @param string $value |
353
|
|
|
* |
354
|
|
|
* @return string |
355
|
|
|
*/ |
356
|
|
|
public function minifyCss(string $value): string |
357
|
|
|
{ |
358
|
|
|
$minifier = new Minify\CSS($value); |
359
|
|
|
|
360
|
|
|
return $minifier->minify(); |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Minify JS. |
365
|
|
|
* |
366
|
|
|
* @param string $value |
367
|
|
|
* |
368
|
|
|
* @return string |
369
|
|
|
*/ |
370
|
|
|
public function minifyJs(string $value): string |
371
|
|
|
{ |
372
|
|
|
$minifier = new Minify\JS($value); |
373
|
|
|
|
374
|
|
|
return $minifier->minify(); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
/** |
378
|
|
|
* Compile style file to CSS. |
379
|
|
|
* |
380
|
|
|
* @param string $path |
381
|
|
|
* |
382
|
|
|
* @throws Exception |
383
|
|
|
* |
384
|
|
|
* @return string |
385
|
|
|
*/ |
386
|
|
|
public function toCss(string $path): string |
387
|
|
|
{ |
388
|
|
|
$filePath = $this->outputPath.'/'.$path; |
389
|
|
|
$subPath = substr($path, 0, strrpos($path, '/')); |
390
|
|
|
|
391
|
|
|
if (is_file($filePath)) { |
392
|
|
|
$extension = (new \SplFileInfo($filePath))->getExtension(); |
393
|
|
|
switch ($extension) { |
394
|
|
|
case 'scss': |
395
|
|
|
$scssPhp = new Compiler(); |
396
|
|
|
$scssPhp->setImportPaths($this->outputPath.'/'.$subPath); |
397
|
|
|
$targetPath = preg_replace('/scss/m', 'css', $path); |
398
|
|
|
|
399
|
|
|
// compile if target file doesn't exists |
400
|
|
|
if (!$this->fileSystem->exists($this->outputPath.'/'.$targetPath)) { |
401
|
|
|
$scss = file_get_contents($filePath); |
402
|
|
|
$css = $scssPhp->compile($scss); |
403
|
|
|
$this->fileSystem->dumpFile($this->outputPath.'/'.$targetPath, $css); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
return $targetPath; |
407
|
|
|
default: |
408
|
|
|
throw new Exception(sprintf("File '%s' should be a '.scss'!", $path)); |
409
|
|
|
} |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
throw new Exception(sprintf("File '%s' doesn't exist!", $path)); |
413
|
|
|
} |
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* Compile SCSS string to CSS. |
417
|
|
|
* |
418
|
|
|
* @param string $value |
419
|
|
|
* |
420
|
|
|
* @return string |
421
|
|
|
*/ |
422
|
|
|
public function scssToCss(string $value): string |
423
|
|
|
{ |
424
|
|
|
$scss = new Compiler(); |
425
|
|
|
|
426
|
|
|
return $scss->compile($value); |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* Read $lenght first characters of a string and add a suffix. |
431
|
|
|
* |
432
|
|
|
* @param string|null $string |
433
|
|
|
* @param int $length |
434
|
|
|
* @param string $suffix |
435
|
|
|
* |
436
|
|
|
* @return string|null |
437
|
|
|
*/ |
438
|
|
|
public function excerpt(string $string = null, int $length = 450, string $suffix = ' …'): ?string |
439
|
|
|
{ |
440
|
|
|
$string = str_replace('</p>', '<br /><br />', $string); |
441
|
|
|
$string = trim(strip_tags($string, '<br>'), '<br />'); |
442
|
|
|
if (mb_strlen($string) > $length) { |
443
|
|
|
$string = mb_substr($string, 0, $length); |
444
|
|
|
$string .= $suffix; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
return $string; |
448
|
|
|
} |
449
|
|
|
|
450
|
|
|
/** |
451
|
|
|
* Read characters before '<!-- excerpt|break -->'. |
452
|
|
|
* |
453
|
|
|
* @param string|null $string |
454
|
|
|
* |
455
|
|
|
* @return string|null |
456
|
|
|
*/ |
457
|
|
|
public function excerptHtml(string $string = null): ?string |
458
|
|
|
{ |
459
|
|
|
$pattern = '^(.*)[\n\r\s]*<!--[[:blank:]]?excerpt|break[[:blank:]]?-->[\n\r\s]*(.*)$'; |
460
|
|
|
preg_match( |
461
|
|
|
'/'.$pattern.'/s', |
462
|
|
|
$string, |
463
|
|
|
$matches |
464
|
|
|
); |
465
|
|
|
if (empty($matches)) { |
466
|
|
|
return $string; |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
return trim($matches[1]); |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
/** |
473
|
|
|
* Calculate estimated time to read a text. |
474
|
|
|
* |
475
|
|
|
* @param string|null $text |
476
|
|
|
* |
477
|
|
|
* @return string |
478
|
|
|
*/ |
479
|
|
|
public function readtime(string $text = null): string |
480
|
|
|
{ |
481
|
|
|
$words = str_word_count(strip_tags($text)); |
482
|
|
|
$min = floor($words / 200); |
483
|
|
|
if ($min === 0) { |
484
|
|
|
return '1'; |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
return (string) $min; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
/** |
491
|
|
|
* Hash file with sha384. |
492
|
|
|
* Useful for SRI (Subresource Integrity). |
493
|
|
|
* |
494
|
|
|
* @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity |
495
|
|
|
* |
496
|
|
|
* @param string $path |
497
|
|
|
* |
498
|
|
|
* @return string|null |
499
|
|
|
*/ |
500
|
|
|
public function hashFile(string $path): ?string |
501
|
|
|
{ |
502
|
|
|
if (is_file($filePath = $this->outputPath.'/'.$path)) { |
503
|
|
|
$path = $filePath; |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
return sprintf('sha384-%s', base64_encode(hash_file('sha384', $path, true))); |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
/** |
510
|
|
|
* Gets the value of an environment variable. |
511
|
|
|
* |
512
|
|
|
* @param string $var |
513
|
|
|
* |
514
|
|
|
* @return string|null |
515
|
|
|
*/ |
516
|
|
|
public function getEnv(string $var): ?string |
517
|
|
|
{ |
518
|
|
|
return getenv($var) ?: null; |
519
|
|
|
} |
520
|
|
|
} |
521
|
|
|
|
This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.