Page::getFileName()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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