Passed
Pull Request — master (#1704)
by Arnaud
08:15 queued 03:06
created

Page::getFmVariables()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Collection\Page;
15
16
use Cecil\Collection\Item;
17
use Cecil\Exception\RuntimeException;
18
use Cecil\Util;
19
use Cocur\Slugify\Slugify;
20
use Symfony\Component\Finder\SplFileInfo;
21
22
/**
23
 * Class Page.
24
 */
25
class Page extends Item
26
{
27
    public const SLUGIFY_PATTERN = '/(^\/|[^._a-z0-9\/]|-)+/'; // should be '/^\/|[^_a-z0-9\/]+/'
28
29
    /** @var bool True if page is not created from a file. */
30
    protected $virtual;
31
32
    /** @var SplFileInfo */
33
    protected $file;
34
35
    /** @var string Homepage, Page, Section, etc. */
36
    protected $type;
37
38
    /** @var string */
39
    protected $folder;
40
41
    /** @var string */
42
    protected $slug;
43
44
    /** @var string path = folder + slug. */
45
    protected $path;
46
47
    /** @var string */
48
    protected $section;
49
50
    /** @var string */
51
    protected $frontmatter;
52
53
    /** @var array Front matter before conversion. */
54
    protected $fmVariables = [];
55
56
    /** @var string Body before conversion. */
57
    protected $body;
58
59
    /** @var string Body after conversion. */
60
    protected $html;
61
62
    /** @var array Output, by format */
63
    protected $rendered = [];
64
65
    /** @var Collection Subpages of a list page */
66
    protected $subPages;
67
68
    /** @var array */
69
    protected $paginator = [];
70
71
    /** @var \Cecil\Collection\Taxonomy\Vocabulary Terms of a vocabulary */
72
    protected $terms;
73
74
    /** @var Slugify */
75
    private static $slugifier;
76
77 1
    public function __construct(string $id)
78
    {
79 1
        parent::__construct($id);
80 1
        $this->setVirtual(true);
81 1
        $this->setType(Type::PAGE);
82 1
        $this->setVariables([
83 1
            'title'            => 'Page Title',
84 1
            'date'             => new \DateTime(),
85 1
            'updated'          => new \DateTime(),
86 1
            'weight'           => null,
87 1
            'filepath'         => null,
88 1
            'published'        => true,
89 1
            'content_template' => 'page.content.twig',
90 1
        ]);
91
    }
92
93
    /**
94
     * Turns a path (string) into a slug (URI).
95
     */
96 1
    public static function slugify(string $path): string
97
    {
98 1
        if (!self::$slugifier instanceof Slugify) {
99 1
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
100
        }
101
102 1
        return self::$slugifier->slugify($path);
103
    }
104
105
    /**
106
     * Creates the ID from the file path.
107
     */
108 1
    public static function createIdFromFile(SplFileInfo $file): string
109
    {
110 1
        $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
111 1
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension())));
112
        // if file is "README.md", ID is "index"
113 1
        $basename = (string) str_ireplace('readme', 'index', $basename);
114
        // if file is section's index: "section/index.md", ID is "section"
115 1
        if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') {
116
            // case of a localized section's index: "section/index.fr.md", ID is "fr/section"
117 1
            if (PrefixSuffix::hasSuffix($basename)) {
118 1
                return PrefixSuffix::getSuffix($basename) . '/' . $relativePath;
119
            }
120
121 1
            return $relativePath;
122
        }
123
        // localized page
124 1
        if (PrefixSuffix::hasSuffix($basename)) {
125 1
            return trim(Util::joinPath(PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename)), '/');
126
        }
127
128 1
        return trim(Util::joinPath($relativePath, $basename), '/');
129
    }
130
131
    /**
132
     * Returns the ID of a page without language.
133
     */
134 1
    public function getIdWithoutLang(): string
135
    {
136 1
        $langPrefix = $this->getVariable('language') . '/';
137 1
        if ($this->hasVariable('language') && Util\Str::startsWith($this->getId(), $langPrefix)) {
138 1
            return substr($this->getId(), \strlen($langPrefix));
139
        }
140
141 1
        return $this->getId();
142
    }
143
144
    /**
145
     * Set file.
146
     */
147 1
    public function setFile(SplFileInfo $file): self
148
    {
149 1
        $this->setVirtual(false);
150 1
        $this->file = $file;
151
152
        /*
153
         * File path components
154
         */
155 1
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
156 1
        $fileExtension = $this->file->getExtension();
157 1
        $fileName = $this->file->getBasename('.' . $fileExtension);
158 1
        $fileName = (string) str_ireplace('readme', 'index', $fileName); // converts "README" to "index"
159 1
        $this->setFolder($fileRelativePath); // ie: "blog"
160 1
        $this->setSlug($fileName); // ie: "post-1"
161 1
        $this->setPath($this->getFolder() . '/' . $this->getSlug()); // ie: "blog/post-1"
162
        /*
163
         * Update default variables
164
         */
165 1
        $this->setVariables([
166 1
            'title'    => PrefixSuffix::sub($fileName),
167 1
            'date'     => (new \DateTime())->setTimestamp($this->file->getMTime()),
168 1
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
169 1
            'filepath' => $this->file->getRelativePathname(),
170 1
        ]);
171
        // is a section?
172 1
        if (PrefixSuffix::sub($fileName) == 'index') {
173 1
            $this->setType(Type::SECTION);
174 1
            $this->setVariable('title', ucfirst(explode('/', $fileRelativePath)[count(explode('/', $fileRelativePath)) - 1]));
175
            // is the home page?
176 1
            if (empty($this->getFolder())) {
177 1
                $this->setType(Type::HOMEPAGE);
178 1
                $this->setVariable('title', 'Homepage');
179
            }
180
        }
181
        // is file has a prefix?
182 1
        if (PrefixSuffix::hasPrefix($fileName)) {
183 1
            $prefix = PrefixSuffix::getPrefix($fileName);
184 1
            if ($prefix !== null) {
185
                // prefix is a valid date?
186 1
                if (Util\Date::isValid($prefix)) {
187 1
                    $this->setVariable('date', (string) $prefix);
188
                } else {
189
                    // prefix is an integer: used for sorting
190 1
                    $this->setVariable('weight', (int) $prefix);
191
                }
192
            }
193
        }
194
        // is file has a language suffix?
195 1
        if (PrefixSuffix::hasSuffix($fileName)) {
196 1
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
197
        }
198
        // set reference between page's translations, even if it exist in only one language
199 1
        $this->setVariable('langref', $this->getPath());
200
201 1
        return $this;
202
    }
203
204
    /**
205
     * Returns file real path.
206
     */
207 1
    public function getFilePath(): ?string
208
    {
209 1
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
210
    }
211
212
    /**
213
     * Parse file content.
214
     */
215 1
    public function parse(): self
216
    {
217 1
        $parser = new Parser($this->file);
218 1
        $parsed = $parser->parse();
219 1
        $this->frontmatter = $parsed->getFrontmatter();
220 1
        $this->body = $parsed->getBody();
221
222 1
        return $this;
223
    }
224
225
    /**
226
     * Get front matter.
227
     */
228 1
    public function getFrontmatter(): ?string
229
    {
230 1
        return $this->frontmatter;
231
    }
232
233
    /**
234
     * Get body as raw.
235
     */
236 1
    public function getBody(): ?string
237
    {
238 1
        return $this->body;
239
    }
240
241
    /**
242
     * Set virtual status.
243
     */
244 1
    public function setVirtual(bool $virtual): self
245
    {
246 1
        $this->virtual = $virtual;
247
248 1
        return $this;
249
    }
250
251
    /**
252
     * Is current page is virtual?
253
     */
254 1
    public function isVirtual(): bool
255
    {
256 1
        return $this->virtual;
257
    }
258
259
    /**
260
     * Set page type.
261
     */
262 1
    public function setType(string $type): self
263
    {
264 1
        $this->type = new Type($type);
265
266 1
        return $this;
267
    }
268
269
    /**
270
     * Get page type.
271
     */
272 1
    public function getType(): string
273
    {
274 1
        return (string) $this->type;
275
    }
276
277
    /**
278
     * Set path without slug.
279
     */
280 1
    public function setFolder(string $folder): self
281
    {
282 1
        $this->folder = self::slugify($folder);
283
284 1
        return $this;
285
    }
286
287
    /**
288
     * Get path without slug.
289
     */
290 1
    public function getFolder(): ?string
291
    {
292 1
        return $this->folder;
293
    }
294
295
    /**
296
     * Set slug.
297
     */
298 1
    public function setSlug(string $slug): self
299
    {
300 1
        if (!$this->slug) {
301 1
            $slug = self::slugify(PrefixSuffix::sub($slug));
302
        }
303
        // force slug and update path
304 1
        if ($this->slug && $this->slug != $slug) {
305 1
            $this->setPath($this->getFolder() . '/' . $slug);
306
        }
307 1
        $this->slug = $slug;
308
309 1
        return $this;
310
    }
311
312
    /**
313
     * Get slug.
314
     */
315 1
    public function getSlug(): string
316
    {
317 1
        return $this->slug;
318
    }
319
320
    /**
321
     * Set path.
322
     */
323 1
    public function setPath(string $path): self
324
    {
325 1
        $path = trim($path, '/');
326
327
        // case of homepage
328 1
        if ($path == 'index') {
329 1
            $this->path = '';
330
331 1
            return $this;
332
        }
333
334
        // case of custom sections' index (ie: content/section/index.md)
335 1
        if (substr($path, -6) == '/index') {
336 1
            $path = substr($path, 0, \strlen($path) - 6);
337
        }
338 1
        $this->path = $path;
339
340
        // case of root pages
341 1
        $lastslash = strrpos($this->path, '/');
342 1
        if ($lastslash === false) {
343 1
            $this->slug = $this->path;
344
345 1
            return $this;
346
        }
347
348 1
        if (!$this->virtual && $this->getSection() === null) {
349 1
            $this->section = explode('/', $this->path)[0];
350
        }
351 1
        $this->folder = substr($this->path, 0, $lastslash);
352 1
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
353
354 1
        return $this;
355
    }
356
357
    /**
358
     * Get path.
359
     */
360 1
    public function getPath(): ?string
361
    {
362 1
        return $this->path;
363
    }
364
365
    /**
366
     * @see getPath()
367
     */
368
    public function getPathname(): ?string
369
    {
370
        return $this->getPath();
371
    }
372
373
    /**
374
     * Set section.
375
     */
376 1
    public function setSection(string $section): self
377
    {
378 1
        $this->section = $section;
379
380 1
        return $this;
381
    }
382
383
    /**
384
     * Get section.
385
     */
386 1
    public function getSection(): ?string
387
    {
388 1
        return !empty($this->section) ? $this->section : null;
389
    }
390
391
    /**
392
     * Set body as HTML.
393
     */
394 1
    public function setBodyHtml(string $html): self
395
    {
396 1
        $this->html = $html;
397
398 1
        return $this;
399
    }
400
401
    /**
402
     * Get body as HTML.
403
     */
404 1
    public function getBodyHtml(): ?string
405
    {
406 1
        return $this->html;
407
    }
408
409
    /**
410
     * @see getBodyHtml()
411
     */
412 1
    public function getContent(): ?string
413
    {
414 1
        return $this->getBodyHtml();
415
    }
416
417
    /**
418
     * Add rendered.
419
     */
420 1
    public function addRendered(array $rendered): self
421
    {
422 1
        $this->rendered += $rendered;
423
424 1
        return $this;
425
    }
426
427
    /**
428
     * Get rendered.
429
     */
430 1
    public function getRendered(): array
431
    {
432 1
        return $this->rendered;
433
    }
434
435
    /**
436
     * Set Subpages.
437
     */
438 1
    public function setPages(\Cecil\Collection\Page\Collection $subPages): self
439
    {
440 1
        $this->subPages = $subPages;
441
442 1
        return $this;
443
    }
444
445
    /**
446
     * Get Subpages.
447
     */
448 1
    public function getPages(): ?\Cecil\Collection\Page\Collection
449
    {
450 1
        return $this->subPages;
451
    }
452
453
    /**
454
     * Set paginator.
455
     */
456 1
    public function setPaginator(array $paginator): self
457
    {
458 1
        $this->paginator = $paginator;
459
460 1
        return $this;
461
    }
462
463
    /**
464
     * Get paginator.
465
     */
466 1
    public function getPaginator(): array
467
    {
468 1
        return $this->paginator;
469
    }
470
471
    /**
472
     * Paginator backward compatibility.
473
     */
474
    public function getPagination(): array
475
    {
476
        return $this->getPaginator();
477
    }
478
479
    /**
480
     * Set vocabulary terms.
481
     */
482 1
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
483
    {
484 1
        $this->terms = $terms;
485
486 1
        return $this;
487
    }
488
489
    /**
490
     * Get vocabulary terms.
491
     */
492 1
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
493
    {
494 1
        return $this->terms;
495
    }
496
497
    /*
498
     * Helpers to set and get variables.
499
     */
500
501
    /**
502
     * Set an array as variables.
503
     *
504
     * @throws RuntimeException
505
     */
506 1
    public function setVariables(array $variables): self
507
    {
508 1
        foreach ($variables as $key => $value) {
509 1
            $this->setVariable($key, $value);
510
        }
511
512 1
        return $this;
513
    }
514
515
    /**
516
     * Get all variables.
517
     */
518 1
    public function getVariables(): array
519
    {
520 1
        return $this->properties;
521
    }
522
523
    /**
524
     * Set a variable.
525
     *
526
     * @param string $name  Name of the variable
527
     * @param mixed  $value Value of the variable
528
     *
529
     * @throws RuntimeException
530
     */
531 1
    public function setVariable(string $name, $value): self
532
    {
533 1
        $this->filterBool($value);
534
        switch ($name) {
535 1
            case 'date':
536 1
            case 'updated':
537
                try {
538 1
                    $date = Util\Date::toDatetime($value);
539
                } catch (\Exception $e) {
540
                    throw new \Exception(sprintf('Expected date format for variable "%s" must be "YYYY-MM-DD" instead of "%s".', $name, (string) $value));
541
                }
542 1
                $this->offsetSet($name, $date);
543 1
                break;
544
545 1
            case 'schedule':
546
                /*
547
                 * publish: 2012-10-08
548
                 * expiry: 2012-10-09
549
                 */
550 1
                $this->offsetSet('published', false);
551 1
                if (\is_array($value)) {
552 1
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
553 1
                        $this->offsetSet('published', true);
554
                    }
555 1
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
556
                        $this->offsetSet('published', true);
557
                    }
558
                }
559 1
                break;
560 1
            case 'draft':
561
                // draft: true = published: false
562 1
                if ($value === true) {
563 1
                    $this->offsetSet('published', false);
564
                }
565 1
                break;
566 1
            case 'path':
567 1
            case 'slug':
568 1
                $slugify = self::slugify((string) $value);
569 1
                if ($value != $slugify) {
570
                    throw new RuntimeException(sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
571
                }
572 1
                $method = 'set' . ucfirst($name);
573 1
                $this->$method($value);
574 1
                break;
575
            default:
576 1
                $this->offsetSet($name, $value);
577
        }
578
579 1
        return $this;
580
    }
581
582
    /**
583
     * Is variable exists?
584
     *
585
     * @param string $name Name of the variable
586
     */
587 1
    public function hasVariable(string $name): bool
588
    {
589 1
        return $this->offsetExists($name);
590
    }
591
592
    /**
593
     * Get a variable.
594
     *
595
     * @param string     $name    Name of the variable
596
     * @param mixed|null $default Default value
597
     *
598
     * @return mixed|null
599
     */
600 1
    public function getVariable(string $name, $default = null)
601
    {
602 1
        if ($this->offsetExists($name)) {
603 1
            return $this->offsetGet($name);
604
        }
605
606 1
        return $default;
607
    }
608
609
    /**
610
     * Unset a variable.
611
     *
612
     * @param string $name Name of the variable
613
     */
614 1
    public function unVariable(string $name): self
615
    {
616 1
        if ($this->offsetExists($name)) {
617 1
            $this->offsetUnset($name);
618
        }
619
620 1
        return $this;
621
    }
622
623
    /**
624
     * Set front matter (only) variables.
625
     */
626 1
    public function setFmVariables(array $variables): self
627
    {
628 1
        $this->fmVariables = $variables;
629
630 1
        return $this;
631
    }
632
633
    /**
634
     * Get front matter variables.
635
     */
636 1
    public function getFmVariables(): array
637
    {
638 1
        return $this->fmVariables;
639
    }
640
641
    /**
642
     * Cast "boolean" string (or array of strings) to boolean.
643
     *
644
     * @param mixed $value Value to filter
645
     *
646
     * @return bool|mixed
647
     *
648
     * @see strToBool()
649
     */
650 1
    private function filterBool(&$value)
651
    {
652 1
        \Cecil\Util\Str::strToBool($value);
653 1
        if (\is_array($value)) {
654 1
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
655
        }
656
    }
657
658
    /**
659
     * {@inheritdoc}
660
     */
661 1
    public function setId(string $id): self
662
    {
663 1
        return parent::setId($id);
664
    }
665
}
666