Passed
Push — master ( 218aa4...34ad8f )
by Arnaud
05:11
created

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