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

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