Passed
Push — 8.x-dev ( 640d34...2250ec )
by Arnaud
04:51 queued 16s
created

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