Passed
Pull Request — master (#1704)
by Arnaud
22:27 queued 14:59
created

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