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

Cecilapp / Cecil / 14676530927

26 Apr 2025 01:56AM UTC coverage: 83.736% (-0.05%) from 83.782%
14676530927

push

github

ArnaudLigny
fix: Page getFileName if file is null

0 of 2 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

3048 of 3640 relevant lines covered (83.74%)

0.84 hits per line

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

93.14
/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 a valid date?
195
                if (Util\Date::isValid($prefix)) {
1✔
196
                    $this->setVariable('date', (string) $prefix);
1✔
197
                } else {
198
                    // prefix is an integer: used for sorting
199
                    $this->setVariable('weight', (int) $prefix);
1✔
200
                }
201
            }
202
        }
203
        // is file has a language suffix?
204
        if (PrefixSuffix::hasSuffix($fileName)) {
1✔
205
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
1✔
206
        }
207
        // set reference between page's translations, even if it exist in only one language
208
        $this->setVariable('langref', $this->getPath());
1✔
209

210
        return $this;
1✔
211
    }
212

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

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

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

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

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

247
        return $this;
1✔
248
    }
249

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

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

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

273
        return $this;
1✔
274
    }
275

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

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

291
        return $this;
1✔
292
    }
293

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

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

309
        return $this;
1✔
310
    }
311

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

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

334
        return $this;
1✔
335
    }
336

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

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

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

356
            return $this;
1✔
357
        }
358

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

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

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

371
            return $this;
1✔
372
        }
373

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

382
        return $this;
1✔
383
    }
384

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

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

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

408
        return $this;
1✔
409
    }
410

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

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

426
        return $this;
×
427
    }
428

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

436
        return $this;
1✔
437
    }
438

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

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

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

462
        return $this;
1✔
463
    }
464

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

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

480
        return $this;
1✔
481
    }
482

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

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

498
        return $this;
1✔
499
    }
500

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

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

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

524
        return $this;
1✔
525
    }
526

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

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

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

550
        return $this;
1✔
551
    }
552

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

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

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

618
        return $this;
1✔
619
    }
620

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

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

645
        return $default;
1✔
646
    }
647

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

659
        return $this;
1✔
660
    }
661

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

669
        return $this;
1✔
670
    }
671

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

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

697
    /**
698
     * {@inheritdoc}
699
     */
700
    public function setId(string $id): self
701
    {
702
        return parent::setId($id);
1✔
703
    }
704
}
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