Passed
Pull Request — master (#1704)
by Arnaud
11:14 queued 04:29
created

Page::getAncestors()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 2
nop 0
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 3
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 Type Type */
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 self Parent page of a PAGE page or a SECTION page */
75
    protected $parent;
76
77
    /** @var Slugify */
78
    private static $slugifier;
79
80 1
    public function __construct(string $id)
81
    {
82 1
        parent::__construct($id);
83 1
        $this->setVirtual(true);
84 1
        $this->setType(Type::PAGE->value);
85
        // default variables
86 1
        $this->setVariables([
87 1
            'title'            => 'Page Title',
88 1
            'date'             => new \DateTime(),
89 1
            'updated'          => new \DateTime(),
90 1
            'weight'           => null,
91 1
            'filepath'         => null,
92 1
            'published'        => true,
93 1
            'content_template' => 'page.content.twig',
94 1
        ]);
95
    }
96
97
    /**
98
     * toString magic method to prevent Twig get_attribute fatal error.
99
     *
100
     * @return string
101
     */
102 1
    public function __toString()
103
    {
104 1
        return $this->getId();
105
    }
106
107
    /**
108
     * Turns a path (string) into a slug (URI).
109
     */
110 1
    public static function slugify(string $path): string
111
    {
112 1
        if (!self::$slugifier instanceof Slugify) {
113 1
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
114
        }
115
116 1
        return self::$slugifier->slugify($path);
117
    }
118
119
    /**
120
     * Creates the ID from the file (path).
121
     */
122 1
    public static function createIdFromFile(SplFileInfo $file): string
123
    {
124 1
        $fileComponents = self::getFileComponents($file);
125 1
        $relativePath = self::slugify($fileComponents['path']);
126 1
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension())));
127
        // if file is "README.md", ID is "index"
128 1
        $basename = (string) str_ireplace('readme', 'index', $basename);
129
        // if file is section's index: "section/index.md", ID is "section"
130 1
        if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') {
131
            // case of a localized section's index: "section/index.fr.md", ID is "fr/section"
132 1
            if (PrefixSuffix::hasSuffix($basename)) {
133 1
                return PrefixSuffix::getSuffix($basename) . '/' . $relativePath;
134
            }
135
136 1
            return $relativePath;
137
        }
138
        // localized page
139 1
        if (PrefixSuffix::hasSuffix($basename)) {
140 1
            return trim(Util::joinPath(PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename)), '/');
141
        }
142
143 1
        return trim(Util::joinPath($relativePath, $basename), '/');
144
    }
145
146
    /**
147
     * Returns the ID of a page without language.
148
     */
149 1
    public function getIdWithoutLang(): string
150
    {
151 1
        $langPrefix = $this->getVariable('language') . '/';
152 1
        if ($this->hasVariable('language') && Util\Str::startsWith($this->getId(), $langPrefix)) {
153 1
            return substr($this->getId(), \strlen($langPrefix));
154
        }
155
156 1
        return $this->getId();
157
    }
158
159
    /**
160
     * Set file.
161
     */
162 1
    public function setFile(SplFileInfo $file): self
163
    {
164 1
        $this->file = $file;
165 1
        $this->setVirtual(false);
166
167
        /*
168
         * File path components
169
         */
170 1
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
171 1
        $fileExtension = $this->file->getExtension();
172 1
        $fileName = $this->file->getBasename('.' . $fileExtension);
173
        // renames "README" to "index"
174 1
        $fileName = (string) str_ireplace('readme', 'index', $fileName);
175
        // case of "index" = home page
176 1
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName) == 'index') {
177 1
            $this->setType(Type::HOMEPAGE->value);
178
        }
179
        /*
180
         * Set page properties and variables
181
         */
182 1
        $this->setFolder($fileRelativePath);
183 1
        $this->setSlug($fileName);
184 1
        $this->setPath($this->getFolder() . '/' . $this->getSlug());
185 1
        $this->setVariables([
186 1
            'title'    => PrefixSuffix::sub($fileName),
187 1
            'date'     => (new \DateTime())->setTimestamp($this->file->getMTime()),
188 1
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
189 1
            'filepath' => $this->file->getRelativePathname(),
190 1
        ]);
191
        // is a section?
192 1
        if (PrefixSuffix::sub($fileName) == 'index') {
193 1
            $this->setType(Type::SECTION->value);
194 1
            $this->setVariable('title', ucfirst(explode('/', $fileRelativePath)[\count(explode('/', $fileRelativePath)) - 1]));
195
            // is the home page?
196 1
            if (empty($this->getFolder())) {
197 1
                $this->setType(Type::HOMEPAGE->value);
198 1
                $this->setVariable('title', 'Homepage');
199
            }
200
        }
201
        // is file has a prefix?
202 1
        if (PrefixSuffix::hasPrefix($fileName)) {
203 1
            $prefix = PrefixSuffix::getPrefix($fileName);
204 1
            if ($prefix !== null) {
205
                // prefix is a valid date?
206 1
                if (Util\Date::isValid($prefix)) {
207 1
                    $this->setVariable('date', (string) $prefix);
208
                } else {
209
                    // prefix is an integer: used for sorting
210 1
                    $this->setVariable('weight', (int) $prefix);
211
                }
212
            }
213
        }
214
        // is file has a language suffix?
215 1
        if (PrefixSuffix::hasSuffix($fileName)) {
216 1
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
217
        }
218
        // set reference between page's translations, even if it exist in only one language
219 1
        $this->setVariable('langref', $this->getPath());
220
221 1
        return $this;
222
    }
223
224
    /**
225
     * Returns file real path.
226
     */
227 1
    public function getFilePath(): ?string
228
    {
229 1
        if ($this->file === null) {
230
            return null;
231
        }
232
233 1
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
234
    }
235
236
    /**
237
     * Parse file content.
238
     */
239 1
    public function parse(): self
240
    {
241 1
        $parser = new Parser($this->file);
242 1
        $parsed = $parser->parse();
243 1
        $this->frontmatter = $parsed->getFrontmatter();
244 1
        $this->body = $parsed->getBody();
245
246 1
        return $this;
247
    }
248
249
    /**
250
     * Get front matter.
251
     */
252 1
    public function getFrontmatter(): ?string
253
    {
254 1
        return $this->frontmatter;
255
    }
256
257
    /**
258
     * Get body as raw.
259
     */
260 1
    public function getBody(): ?string
261
    {
262 1
        return $this->body;
263
    }
264
265
    /**
266
     * Set virtual status.
267
     */
268 1
    public function setVirtual(bool $virtual): self
269
    {
270 1
        $this->virtual = $virtual;
271
272 1
        return $this;
273
    }
274
275
    /**
276
     * Is current page is virtual?
277
     */
278 1
    public function isVirtual(): bool
279
    {
280 1
        return $this->virtual;
281
    }
282
283
    /**
284
     * Set page type.
285
     */
286 1
    public function setType(string $type): self
287
    {
288 1
        $this->type = Type::from($type);
289
290 1
        return $this;
291
    }
292
293
    /**
294
     * Get page type.
295
     */
296 1
    public function getType(): string
297
    {
298 1
        return $this->type->value;
299
    }
300
301
    /**
302
     * Set path without slug.
303
     */
304 1
    public function setFolder(string $folder): self
305
    {
306 1
        $this->folder = self::slugify($folder);
307
308 1
        return $this;
309
    }
310
311
    /**
312
     * Get path without slug.
313
     */
314 1
    public function getFolder(): ?string
315
    {
316 1
        return $this->folder;
317
    }
318
319
    /**
320
     * Set slug.
321
     */
322 1
    public function setSlug(string $slug): self
323
    {
324 1
        if (!$this->slug) {
325 1
            $slug = self::slugify(PrefixSuffix::sub($slug));
326
        }
327
        // force slug and update path
328 1
        if ($this->slug && $this->slug != $slug) {
329 1
            $this->setPath($this->getFolder() . '/' . $slug);
330
        }
331 1
        $this->slug = $slug;
332
333 1
        return $this;
334
    }
335
336
    /**
337
     * Get slug.
338
     */
339 1
    public function getSlug(): string
340
    {
341 1
        return $this->slug;
342
    }
343
344
    /**
345
     * Set path.
346
     */
347 1
    public function setPath(string $path): self
348
    {
349 1
        $path = trim($path, '/');
350
        // case of homepage
351 1
        if ($path == 'index') {
352 1
            $this->path = '';
353 1
            return $this;
354
        }
355
        // case of custom sections' index (ie: section/index.md -> section)
356 1
        if (substr($path, -6) == '/index') {
357 1
            $path = substr($path, 0, \strlen($path) - 6);
358
        }
359 1
        $this->path = $path;
360 1
        $lastslash = strrpos($this->path, '/');
361
        // case of root/top-level pages
362 1
        if ($lastslash === false) {
363 1
            $this->slug = $this->path;
364 1
            return $this;
365
        }
366
        // case of sections' pages: set section
367 1
        if (!$this->virtual && $this->getSection() === null) {
368 1
            $this->section = explode('/', $this->path)[0];
369
        }
370
        // set/update folder and slug
371 1
        $this->folder = substr($this->path, 0, $lastslash);
372 1
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
373 1
        return $this;
374
    }
375
376
    /**
377
     * Get path.
378
     */
379 1
    public function getPath(): ?string
380
    {
381 1
        return $this->path;
382
    }
383
384
    /**
385
     * @see getPath()
386
     */
387
    public function getPathname(): ?string
388
    {
389
        return $this->getPath();
390
    }
391
392
    /**
393
     * Set section.
394
     */
395 1
    public function setSection(string $section): self
396
    {
397 1
        $this->section = $section;
398
399 1
        return $this;
400
    }
401
402
    /**
403
     * Get section.
404
     */
405 1
    public function getSection(): ?string
406
    {
407 1
        return !empty($this->section) ? $this->section : null;
408
    }
409
410
    /**
411
     * Unset section.
412
     */
413
    public function unSection(): self
414
    {
415
        $this->section = null;
416
417
        return $this;
418
    }
419
420
    /**
421
     * Set body as HTML.
422
     */
423 1
    public function setBodyHtml(string $html): self
424
    {
425 1
        $this->html = $html;
426
427 1
        return $this;
428
    }
429
430
    /**
431
     * Get body as HTML.
432
     */
433 1
    public function getBodyHtml(): ?string
434
    {
435 1
        return $this->html;
436
    }
437
438
    /**
439
     * @see getBodyHtml()
440
     */
441 1
    public function getContent(): ?string
442
    {
443 1
        return $this->getBodyHtml();
444
    }
445
446
    /**
447
     * Add rendered.
448
     */
449 1
    public function addRendered(array $rendered): self
450
    {
451 1
        $this->rendered += $rendered;
452
453 1
        return $this;
454
    }
455
456
    /**
457
     * Get rendered.
458
     */
459 1
    public function getRendered(): array
460
    {
461 1
        return $this->rendered;
462
    }
463
464
    /**
465
     * Set Subpages.
466
     */
467 1
    public function setPages(\Cecil\Collection\Page\Collection $subPages): self
468
    {
469 1
        $this->subPages = $subPages;
470
471 1
        return $this;
472
    }
473
474
    /**
475
     * Get Subpages.
476
     */
477 1
    public function getPages(): ?\Cecil\Collection\Page\Collection
478
    {
479 1
        return $this->subPages;
480
    }
481
482
    /**
483
     * Set paginator.
484
     */
485 1
    public function setPaginator(array $paginator): self
486
    {
487 1
        $this->paginator = $paginator;
488
489 1
        return $this;
490
    }
491
492
    /**
493
     * Get paginator.
494
     */
495 1
    public function getPaginator(): array
496
    {
497 1
        return $this->paginator;
498
    }
499
500
    /**
501
     * Paginator backward compatibility.
502
     */
503
    public function getPagination(): array
504
    {
505
        return $this->getPaginator();
506
    }
507
508
    /**
509
     * Set vocabulary terms.
510
     */
511 1
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
512
    {
513 1
        $this->terms = $terms;
514
515 1
        return $this;
516
    }
517
518
    /**
519
     * Get vocabulary terms.
520
     */
521 1
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
522
    {
523 1
        return $this->terms;
524
    }
525
526
    /*
527
     * Helpers to set and get variables.
528
     */
529
530
    /**
531
     * Set an array as variables.
532
     *
533
     * @throws RuntimeException
534
     */
535 1
    public function setVariables(array $variables): self
536
    {
537 1
        foreach ($variables as $key => $value) {
538 1
            $this->setVariable($key, $value);
539
        }
540
541 1
        return $this;
542
    }
543
544
    /**
545
     * Get all variables.
546
     */
547 1
    public function getVariables(): array
548
    {
549 1
        return $this->properties;
550
    }
551
552
    /**
553
     * Set a variable.
554
     *
555
     * @param string $name  Name of the variable
556
     * @param mixed  $value Value of the variable
557
     *
558
     * @throws RuntimeException
559
     */
560 1
    public function setVariable(string $name, $value): self
561
    {
562 1
        $this->filterBool($value);
563
        switch ($name) {
564 1
            case 'date':
565 1
            case 'updated':
566 1
            case 'lastmod':
567
                try {
568 1
                    $date = Util\Date::toDatetime($value);
569
                } catch (\Exception) {
570
                    throw new \Exception(sprintf('The value of "%s" is not a valid date: "%s".', $name, var_export($value, true)));
571
                }
572 1
                $this->offsetSet($name == 'lastmod' ? 'updated' : $name, $date);
573 1
                break;
574
575 1
            case 'schedule':
576
                /*
577
                 * publish: 2012-10-08
578
                 * expiry: 2012-10-09
579
                 */
580 1
                $this->offsetSet('published', false);
581 1
                if (\is_array($value)) {
582 1
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
583 1
                        $this->offsetSet('published', true);
584
                    }
585 1
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
586
                        $this->offsetSet('published', true);
587
                    }
588
                }
589 1
                break;
590 1
            case 'draft':
591
                // draft: true = published: false
592 1
                if ($value === true) {
593 1
                    $this->offsetSet('published', false);
594
                }
595 1
                break;
596 1
            case 'path':
597 1
            case 'slug':
598 1
                $slugify = self::slugify((string) $value);
599 1
                if ($value != $slugify) {
600
                    throw new RuntimeException(sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
601
                }
602 1
                $method = 'set' . ucfirst($name);
603 1
                $this->$method($value);
604 1
                break;
605
            default:
606 1
                $this->offsetSet($name, $value);
607
        }
608
609 1
        return $this;
610
    }
611
612
    /**
613
     * Is variable exists?
614
     *
615
     * @param string $name Name of the variable
616
     */
617 1
    public function hasVariable(string $name): bool
618
    {
619 1
        return $this->offsetExists($name);
620
    }
621
622
    /**
623
     * Get a variable.
624
     *
625
     * @param string     $name    Name of the variable
626
     * @param mixed|null $default Default value
627
     *
628
     * @return mixed|null
629
     */
630 1
    public function getVariable(string $name, $default = null)
631
    {
632 1
        if ($this->offsetExists($name)) {
633 1
            return $this->offsetGet($name);
634
        }
635
636 1
        return $default;
637
    }
638
639
    /**
640
     * Unset a variable.
641
     *
642
     * @param string $name Name of the variable
643
     */
644 1
    public function unVariable(string $name): self
645
    {
646 1
        if ($this->offsetExists($name)) {
647 1
            $this->offsetUnset($name);
648
        }
649
650 1
        return $this;
651
    }
652
653
    /**
654
     * Set front matter (only) variables.
655
     */
656 1
    public function setFmVariables(array $variables): self
657
    {
658 1
        $this->fmVariables = $variables;
659
660 1
        return $this;
661
    }
662
663
    /**
664
     * Get front matter variables.
665
     */
666 1
    public function getFmVariables(): array
667
    {
668 1
        return $this->fmVariables;
669
    }
670
671
    /**
672
     * {@inheritdoc}
673
     */
674 1
    public function setId(string $id): self
675
    {
676 1
        return parent::setId($id);
677
    }
678
679
    /**
680
     * Set parent page.
681
     */
682 1
    public function setParent(self $page): self
683
    {
684 1
        $this->parent = $page;
685
686 1
        return $this;
687
    }
688
689
    /**
690
     * Returns parent page if exists.
691
     */
692 1
    public function getParent(): ?self
693
    {
694 1
        return $this->parent;
695
    }
696
697
    /**
698
     * Returns array of ancestors pages.
699
     */
700 1
    public function getAncestors(): array
701
    {
702 1
        $ancestors = [];
703
704 1
        if (null !== $parent = $this->getParent()) {
705 1
            $ancestors[] = $parent;
706 1
            while (null !== $parent = $parent->getParent()) {
707 1
                $ancestors[] = $parent;
708
            };
709
        }
710
711 1
        return $ancestors;
712
    }
713
714
    /**
715
     * Cast "boolean" string (or array of strings) to boolean.
716
     *
717
     * @param mixed $value Value to filter
718
     *
719
     * @return bool|mixed
720
     *
721
     * @see strToBool()
722
     */
723 1
    private function filterBool(&$value)
724
    {
725 1
        \Cecil\Util\Str::strToBool($value);
726 1
        if (\is_array($value)) {
727 1
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
728
        }
729
    }
730
731
    /**
732
     * Get file components.
733
     *
734
     * [
735
     *   path => relative path,
736
     *   name => name,
737
     *   ext  => extension,
738
     * ]
739
     */
740 1
    private static function getFileComponents(SplFileInfo $file): array
741
    {
742 1
        return [
743 1
            'path' => str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()),
744 1
            'name' => (string) str_ireplace('readme', 'index', $file->getBasename('.' . $file->getExtension())),
745 1
            'ext'  => $file->getExtension(),
746 1
        ];
747
    }
748
}
749