• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Cecilapp / Cecil / 15444845771

04 Jun 2025 02:19PM UTC coverage: 82.811% (+0.04%) from 82.775%
15444845771

push

github

ArnaudLigny
refactor: code quality

5 of 5 new or added lines in 1 file covered. (100.0%)

17 existing lines in 3 files now uncovered.

3117 of 3764 relevant lines covered (82.81%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.18
/src/Collection/Page/Page.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <arnaud@ligny.fr>
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
    public function __construct(string $id)
78
    {
79
        parent::__construct($id);
1✔
80
        $this->setVirtual(true);
1✔
81
        $this->setType(Type::PAGE->value);
1✔
82
        // default variables
83
        $this->setVariables([
1✔
84
            'title'            => 'Page Title',
1✔
85
            'date'             => new \DateTime(),
1✔
86
            'updated'          => new \DateTime(),
1✔
87
            'weight'           => null,
1✔
88
            'filepath'         => null,
1✔
89
            'published'        => true,
1✔
90
            'content_template' => 'page.content.twig',
1✔
91
        ]);
1✔
92
    }
93

94
    /**
95
     * toString magic method to prevent Twig get_attribute fatal error.
96
     *
97
     * @return string
98
     */
99
    public function __toString()
100
    {
101
        return $this->getId();
1✔
102
    }
103

104
    /**
105
     * Turns a path (string) into a slug (URI).
106
     */
107
    public static function slugify(string $path): string
108
    {
109
        if (!self::$slugifier instanceof Slugify) {
1✔
110
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
1✔
111
        }
112

113
        return self::$slugifier->slugify($path);
1✔
114
    }
115

116
    /**
117
     * Creates the ID from the file (path).
118
     */
119
    public static function createIdFromFile(SplFileInfo $file): string
120
    {
121
        $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
1✔
122
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension())));
1✔
123
        // if file is "README.md", ID is "index"
124
        $basename = (string) str_ireplace('readme', 'index', $basename);
1✔
125
        // if file is section's index: "section/index.md", ID is "section"
126
        if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') {
1✔
127
            // case of a localized section's index: "section/index.fr.md", ID is "fr/section"
128
            if (PrefixSuffix::hasSuffix($basename)) {
1✔
129
                return PrefixSuffix::getSuffix($basename) . '/' . $relativePath;
1✔
130
            }
131

132
            return $relativePath;
1✔
133
        }
134
        // localized page
135
        if (PrefixSuffix::hasSuffix($basename)) {
1✔
136
            return trim(Util::joinPath(PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename)), '/');
1✔
137
        }
138

139
        return trim(Util::joinPath($relativePath, $basename), '/');
1✔
140
    }
141

142
    /**
143
     * Returns the ID of a page without language.
144
     */
145
    public function getIdWithoutLang(): string
146
    {
147
        $langPrefix = $this->getVariable('language') . '/';
1✔
148
        if ($this->hasVariable('language') && Util\Str::startsWith($this->getId(), $langPrefix)) {
1✔
149
            return substr($this->getId(), \strlen($langPrefix));
1✔
150
        }
151

152
        return $this->getId();
1✔
153
    }
154

155
    /**
156
     * Set file.
157
     */
158
    public function setFile(SplFileInfo $file): self
159
    {
160
        $this->file = $file;
1✔
161
        $this->setVirtual(false);
1✔
162

163
        /*
164
         * File path components
165
         */
166
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
1✔
167
        $fileExtension = $this->file->getExtension();
1✔
168
        $fileName = $this->file->getBasename('.' . $fileExtension);
1✔
169
        // renames "README" to "index"
170
        $fileName = (string) str_ireplace('readme', 'index', $fileName);
1✔
171
        // case of "index" = home page
172
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName) == 'index') {
1✔
173
            $this->setType(Type::HOMEPAGE->value);
1✔
174
        }
175
        /*
176
         * Set page properties and variables
177
         */
178
        $this->setFolder($fileRelativePath);
1✔
179
        $this->setSlug($fileName);
1✔
180
        $this->setPath($this->getFolder() . '/' . $this->getSlug());
1✔
181
        $this->setVariables([
1✔
182
            'title'    => PrefixSuffix::sub($fileName),
1✔
183
            'date'     => (new \DateTime())->setTimestamp($this->file->getMTime()),
1✔
184
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
1✔
185
            'filepath' => $this->file->getRelativePathname(),
1✔
186
        ]);
1✔
187
        /*
188
         * Set specific variables
189
         */
190
        // is file has a prefix?
191
        if (PrefixSuffix::hasPrefix($fileName)) {
1✔
192
            $prefix = PrefixSuffix::getPrefix($fileName);
1✔
193
            if ($prefix !== null) {
1✔
194
                // prefix is an integer: used for sorting
195
                if (is_numeric($prefix)) {
1✔
196
                    $this->setVariable('weight', (int) $prefix);
1✔
197
                }
198
                // prefix is a valid date?
199
                if (Util\Date::isValid($prefix)) {
1✔
200
                    $this->setVariable('date', (string) $prefix);
1✔
201
                }
202
            }
203
        }
204
        // is file has a language suffix?
205
        if (PrefixSuffix::hasSuffix($fileName)) {
1✔
206
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
1✔
207
        }
208
        // set reference between page's translations, even if it exist in only one language
209
        $this->setVariable('langref', $this->getPath());
1✔
210

211
        return $this;
1✔
212
    }
213

214
    /**
215
     * Returns file name, with extension.
216
     */
217
    public function getFileName(): ?string
218
    {
219
        if ($this->file === null) {
×
UNCOV
220
            return null;
×
221
        }
222

UNCOV
223
        return $this->file->getBasename();
×
224
    }
225

226
    /**
227
     * Returns file real path.
228
     */
229
    public function getFilePath(): ?string
230
    {
231
        if ($this->file === null) {
1✔
UNCOV
232
            return null;
×
233
        }
234

235
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
1✔
236
    }
237

238
    /**
239
     * Parse file content.
240
     */
241
    public function parse(): self
242
    {
243
        $parser = new Parser($this->file);
1✔
244
        $parsed = $parser->parse();
1✔
245
        $this->frontmatter = $parsed->getFrontmatter();
1✔
246
        $this->body = $parsed->getBody();
1✔
247

248
        return $this;
1✔
249
    }
250

251
    /**
252
     * Get front matter.
253
     */
254
    public function getFrontmatter(): ?string
255
    {
256
        return $this->frontmatter;
1✔
257
    }
258

259
    /**
260
     * Get body as raw.
261
     */
262
    public function getBody(): ?string
263
    {
264
        return $this->body;
1✔
265
    }
266

267
    /**
268
     * Set virtual status.
269
     */
270
    public function setVirtual(bool $virtual): self
271
    {
272
        $this->virtual = $virtual;
1✔
273

274
        return $this;
1✔
275
    }
276

277
    /**
278
     * Is current page is virtual?
279
     */
280
    public function isVirtual(): bool
281
    {
282
        return $this->virtual;
1✔
283
    }
284

285
    /**
286
     * Set page type.
287
     */
288
    public function setType(string $type): self
289
    {
290
        $this->type = Type::from($type);
1✔
291

292
        return $this;
1✔
293
    }
294

295
    /**
296
     * Get page type.
297
     */
298
    public function getType(): string
299
    {
300
        return $this->type->value;
1✔
301
    }
302

303
    /**
304
     * Set path without slug.
305
     */
306
    public function setFolder(string $folder): self
307
    {
308
        $this->folder = self::slugify($folder);
1✔
309

310
        return $this;
1✔
311
    }
312

313
    /**
314
     * Get path without slug.
315
     */
316
    public function getFolder(): ?string
317
    {
318
        return $this->folder;
1✔
319
    }
320

321
    /**
322
     * Set slug.
323
     */
324
    public function setSlug(string $slug): self
325
    {
326
        if (!$this->slug) {
1✔
327
            $slug = self::slugify(PrefixSuffix::sub($slug));
1✔
328
        }
329
        // force slug and update path
330
        if ($this->slug && $this->slug != $slug) {
1✔
331
            $this->setPath($this->getFolder() . '/' . $slug);
1✔
332
        }
333
        $this->slug = $slug;
1✔
334

335
        return $this;
1✔
336
    }
337

338
    /**
339
     * Get slug.
340
     */
341
    public function getSlug(): string
342
    {
343
        return $this->slug;
1✔
344
    }
345

346
    /**
347
     * Set path.
348
     */
349
    public function setPath(string $path): self
350
    {
351
        $path = trim($path, '/');
1✔
352

353
        // case of homepage
354
        if ($path == 'index') {
1✔
355
            $this->path = '';
1✔
356

357
            return $this;
1✔
358
        }
359

360
        // case of custom sections' index (ie: section/index.md -> section)
361
        if (substr($path, -6) == '/index') {
1✔
362
            $path = substr($path, 0, \strlen($path) - 6);
1✔
363
        }
364
        $this->path = $path;
1✔
365

366
        $lastslash = strrpos($this->path, '/');
1✔
367

368
        // case of root/top-level pages
369
        if ($lastslash === false) {
1✔
370
            $this->slug = $this->path;
1✔
371

372
            return $this;
1✔
373
        }
374

375
        // case of sections' pages: set section
376
        if (!$this->virtual && $this->getSection() === null) {
1✔
377
            $this->section = explode('/', $this->path)[0];
1✔
378
        }
379
        // set/update folder and slug
380
        $this->folder = substr($this->path, 0, $lastslash);
1✔
381
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
1✔
382

383
        return $this;
1✔
384
    }
385

386
    /**
387
     * Get path.
388
     */
389
    public function getPath(): ?string
390
    {
391
        return $this->path;
1✔
392
    }
393

394
    /**
395
     * @see getPath()
396
     */
397
    public function getPathname(): ?string
398
    {
UNCOV
399
        return $this->getPath();
×
400
    }
401

402
    /**
403
     * Set section.
404
     */
405
    public function setSection(string $section): self
406
    {
407
        $this->section = $section;
1✔
408

409
        return $this;
1✔
410
    }
411

412
    /**
413
     * Get section.
414
     */
415
    public function getSection(): ?string
416
    {
417
        return !empty($this->section) ? $this->section : null;
1✔
418
    }
419

420
    /**
421
     * Unset section.
422
     */
423
    public function unSection(): self
424
    {
UNCOV
425
        $this->section = null;
×
426

UNCOV
427
        return $this;
×
428
    }
429

430
    /**
431
     * Set body as HTML.
432
     */
433
    public function setBodyHtml(string $html): self
434
    {
435
        $this->html = $html;
1✔
436

437
        return $this;
1✔
438
    }
439

440
    /**
441
     * Get body as HTML.
442
     */
443
    public function getBodyHtml(): ?string
444
    {
445
        return $this->html;
1✔
446
    }
447

448
    /**
449
     * @see getBodyHtml()
450
     */
451
    public function getContent(): ?string
452
    {
453
        return $this->getBodyHtml();
1✔
454
    }
455

456
    /**
457
     * Add rendered.
458
     */
459
    public function addRendered(array $rendered): self
460
    {
461
        $this->rendered += $rendered;
1✔
462

463
        return $this;
1✔
464
    }
465

466
    /**
467
     * Get rendered.
468
     */
469
    public function getRendered(): array
470
    {
471
        return $this->rendered;
1✔
472
    }
473

474
    /**
475
     * Set Subpages.
476
     */
477
    public function setPages(\Cecil\Collection\Page\Collection $subPages): self
478
    {
479
        $this->subPages = $subPages;
1✔
480

481
        return $this;
1✔
482
    }
483

484
    /**
485
     * Get Subpages.
486
     */
487
    public function getPages(): ?\Cecil\Collection\Page\Collection
488
    {
489
        return $this->subPages;
1✔
490
    }
491

492
    /**
493
     * Set paginator.
494
     */
495
    public function setPaginator(array $paginator): self
496
    {
497
        $this->paginator = $paginator;
1✔
498

499
        return $this;
1✔
500
    }
501

502
    /**
503
     * Get paginator.
504
     */
505
    public function getPaginator(): array
506
    {
507
        return $this->paginator;
1✔
508
    }
509

510
    /**
511
     * Paginator backward compatibility.
512
     */
513
    public function getPagination(): array
514
    {
UNCOV
515
        return $this->getPaginator();
×
516
    }
517

518
    /**
519
     * Set vocabulary terms.
520
     */
521
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
522
    {
523
        $this->terms = $terms;
1✔
524

525
        return $this;
1✔
526
    }
527

528
    /**
529
     * Get vocabulary terms.
530
     */
531
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
532
    {
533
        return $this->terms;
1✔
534
    }
535

536
    /*
537
     * Helpers to set and get variables.
538
     */
539

540
    /**
541
     * Set an array as variables.
542
     *
543
     * @throws RuntimeException
544
     */
545
    public function setVariables(array $variables): self
546
    {
547
        foreach ($variables as $key => $value) {
1✔
548
            $this->setVariable($key, $value);
1✔
549
        }
550

551
        return $this;
1✔
552
    }
553

554
    /**
555
     * Get all variables.
556
     */
557
    public function getVariables(): array
558
    {
559
        return $this->properties;
1✔
560
    }
561

562
    /**
563
     * Set a variable.
564
     *
565
     * @param string $name  Name of the variable
566
     * @param mixed  $value Value of the variable
567
     *
568
     * @throws RuntimeException
569
     */
570
    public function setVariable(string $name, $value): self
571
    {
572
        $this->filterBool($value);
1✔
573
        switch ($name) {
574
            case 'date':
1✔
575
            case 'updated':
1✔
576
            case 'lastmod':
1✔
577
                try {
578
                    $date = Util\Date::toDatetime($value);
1✔
579
                } catch (\Exception) {
×
UNCOV
580
                    throw new \Exception(\sprintf('The value of "%s" is not a valid date: "%s".', $name, var_export($value, true)));
×
581
                }
582
                $this->offsetSet($name == 'lastmod' ? 'updated' : $name, $date);
1✔
583
                break;
1✔
584

585
            case 'schedule':
1✔
586
                /*
587
                 * publish: 2012-10-08
588
                 * expiry: 2012-10-09
589
                 */
590
                $this->offsetSet('published', false);
1✔
591
                if (\is_array($value)) {
1✔
592
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
1✔
593
                        $this->offsetSet('published', true);
1✔
594
                    }
595
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
1✔
UNCOV
596
                        $this->offsetSet('published', true);
×
597
                    }
598
                }
599
                break;
1✔
600
            case 'draft':
1✔
601
                // draft: true = published: false
602
                if ($value === true) {
1✔
603
                    $this->offsetSet('published', false);
1✔
604
                }
605
                break;
1✔
606
            case 'path':
1✔
607
            case 'slug':
1✔
608
                $slugify = self::slugify((string) $value);
1✔
609
                if ($value != $slugify) {
1✔
UNCOV
610
                    throw new RuntimeException(\sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
×
611
                }
612
                $method = 'set' . ucfirst($name);
1✔
613
                $this->$method($value);
1✔
614
                break;
1✔
615
            default:
616
                $this->offsetSet($name, $value);
1✔
617
        }
618

619
        return $this;
1✔
620
    }
621

622
    /**
623
     * Is variable exists?
624
     *
625
     * @param string $name Name of the variable
626
     */
627
    public function hasVariable(string $name): bool
628
    {
629
        return $this->offsetExists($name);
1✔
630
    }
631

632
    /**
633
     * Get a variable.
634
     *
635
     * @param string     $name    Name of the variable
636
     * @param mixed|null $default Default value
637
     *
638
     * @return mixed|null
639
     */
640
    public function getVariable(string $name, $default = null)
641
    {
642
        if ($this->offsetExists($name)) {
1✔
643
            return $this->offsetGet($name);
1✔
644
        }
645

646
        return $default;
1✔
647
    }
648

649
    /**
650
     * Unset a variable.
651
     *
652
     * @param string $name Name of the variable
653
     */
654
    public function unVariable(string $name): self
655
    {
656
        if ($this->offsetExists($name)) {
1✔
657
            $this->offsetUnset($name);
1✔
658
        }
659

660
        return $this;
1✔
661
    }
662

663
    /**
664
     * Set front matter (only) variables.
665
     */
666
    public function setFmVariables(array $variables): self
667
    {
668
        $this->fmVariables = $variables;
1✔
669

670
        return $this;
1✔
671
    }
672

673
    /**
674
     * Get front matter variables.
675
     */
676
    public function getFmVariables(): array
677
    {
678
        return $this->fmVariables;
1✔
679
    }
680

681
    /**
682
     * Cast "boolean" string (or array of strings) to boolean.
683
     *
684
     * @param mixed $value Value to filter
685
     *
686
     * @return bool|mixed
687
     *
688
     * @see strToBool()
689
     */
690
    private function filterBool(&$value)
691
    {
692
        \Cecil\Util\Str::strToBool($value);
1✔
693
        if (\is_array($value)) {
1✔
694
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
1✔
695
        }
696
    }
697

698
    /**
699
     * {@inheritdoc}
700
     */
701
    public function setId(string $id): self
702
    {
703
        return parent::setId($id);
1✔
704
    }
705
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc