Passed
Push — master ( d1c6a8...c93284 )
by Arnaud
06:11
created

Page::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

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