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

Cecilapp / Cecil / 25082537353

28 Apr 2026 11:14PM UTC coverage: 79.921%. First build
25082537353

Pull #2321

github

web-flow
Merge 731f72175 into bd67d9dec
Pull Request #2321: feat: add sub-section support for pages

62 of 196 new or added lines in 4 files covered. (31.63%)

3451 of 4318 relevant lines covered (79.92%)

0.81 hits per line

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

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

3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <arnaud@ligny.fr>
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 Symfony\Component\Finder\SplFileInfo;
20
use Symfony\Component\String\Slugger\AsciiSlugger;
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 Page|null Parent section (for sub-sections). */
75
    protected $parentSection;
76

77
    /** @var Collection|null Child sub-sections collection. */
78
    protected $subSections;
79

80
    /** @var \Cecil\Collection\Taxonomy\Vocabulary Terms of a vocabulary. */
81
    protected $terms;
82

83
    /** @var AsciiSlugger */
84
    private static $slugifier;
85

86
    /**
87
     * @param string[]|null $prefixSeparators Allowed prefix separator characters (from config `pages.prefix.separator`)
88
     */
89
    public function __construct(mixed $id, ?array $prefixSeparators = null)
90
    {
91
        if (!\is_string($id) && !$id instanceof SplFileInfo) {
1✔
92
            throw new RuntimeException('Create a page with a string ID or a SplFileInfo.');
×
93
        }
94

95
        $separators = $prefixSeparators ?? PrefixSuffix::DEFAULT_SEPARATORS;
1✔
96

97
        // default properties
98
        $this->setVirtual(true);
1✔
99
        $this->setType(Type::PAGE->value);
1✔
100
        $this->setVariables([
1✔
101
            'title'            => 'Page Title',
1✔
102
            'date'             => new \DateTime(),
1✔
103
            'weight'           => null,
1✔
104
            'filepath'         => null,
1✔
105
            'published'        => true,
1✔
106
            'status'           => Status::PUBLISHED->value,
1✔
107
            'content_template' => 'page.content.twig',
1✔
108
        ]);
1✔
109

110
        if ($id instanceof SplFileInfo) {
1✔
111
            $file = $id;
1✔
112
            $this->setFile($file, $separators);
1✔
113
            $id = self::createIdFromFile($file, $separators);
1✔
114
        }
115

116
        parent::__construct($id);
1✔
117
    }
118

119
    public function setId(string $id): self
120
    {
121
        return parent::setId($id);
1✔
122
    }
123

124
    /**
125
     * toString magic method to prevent Twig get_attribute fatal error.
126
     *
127
     * @return string
128
     */
129
    public function __toString()
130
    {
131
        return $this->getId();
1✔
132
    }
133

134
    /**
135
     * Turns a path (string) into a slug (URI).
136
     */
137
    public static function slugify(string $path): string
138
    {
139
        if (!self::$slugifier instanceof AsciiSlugger) {
1✔
140
            self::$slugifier = new AsciiSlugger();
1✔
141
        }
142

143
        $placeholders = self::createSlugifyPlaceholders($path);
1✔
144
        $path = strtr($path, $placeholders);
1✔
145

146
        $path = preg_replace_callback('/[^\x00-\x7F]+/u', static function (array $matches): string {
1✔
147
            $locale = preg_match('/\p{Han}/u', $matches[0]) ? 'zh' : null;
1✔
148

149
            return self::$slugifier->slug($matches[0], '-', $locale)->lower()->toString();
1✔
150
        }, $path);
1✔
151
        if ($path === null) {
1✔
152
            throw new RuntimeException('Unable to slugify path.');
×
153
        }
154

155
        $path = preg_replace(self::SLUGIFY_PATTERN, '-', strtolower($path));
1✔
156
        if ($path === null) {
1✔
157
            throw new RuntimeException('Unable to slugify path.');
×
158
        }
159

160
        return ltrim(trim(strtr($path, array_flip($placeholders)), '-'), '/');
1✔
161
    }
162

163
    private static function createSlugifyPlaceholders(string $path): array
164
    {
165
        $placeholders = [];
1✔
166

167
        foreach (['.' => 'dot', '_' => 'underscore', '/' => 'slash'] as $character => $name) {
1✔
168
            $placeholders[$character] = self::createSlugifyPlaceholder($path, $name);
1✔
169
        }
170

171
        return $placeholders;
1✔
172
    }
173

174
    private static function createSlugifyPlaceholder(string $path, string $name): string
175
    {
176
        $index = 0;
1✔
177

178
        do {
179
            $placeholder = \sprintf('cecil%s%s', $name, substr(hash('sha256', $path . $name . $index), 0, 16));
1✔
180
            ++$index;
1✔
181
        } while (str_contains($path, $placeholder));
1✔
182

183
        return $placeholder;
1✔
184
    }
185

186
    /**
187
     * Returns the ID of a page without language.
188
     */
189
    public function getIdWithoutLang(): string
190
    {
191
        $langPrefix = $this->getVariable('language') . '/';
1✔
192
        if ($this->hasVariable('language') && Util\Str::startsWith($this->getId(), $langPrefix)) {
1✔
193
            return substr($this->getId(), \strlen($langPrefix));
1✔
194
        }
195

196
        return $this->getId();
1✔
197
    }
198

199
    /**
200
     * Set file.
201
     *
202
     * @param string[] $separators Allowed prefix separator characters
203
     */
204
    public function setFile(SplFileInfo $file, array $separators = PrefixSuffix::DEFAULT_SEPARATORS): self
205
    {
206
        $this->file = $file;
1✔
207
        $this->setVirtual(false);
1✔
208

209
        /*
210
         * File path components
211
         */
212
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
1✔
213
        $fileExtension = $this->file->getExtension();
1✔
214
        $fileName = $this->file->getBasename('.' . $fileExtension);
1✔
215
        // renames "README" to "index"
216
        $fileName = strtolower($fileName) == 'readme' ? 'index' : $fileName;
1✔
217
        // case of "index" = home page
218
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName, $separators) == 'index') {
1✔
219
            $this->setType(Type::HOMEPAGE->value);
1✔
220
        }
221
        /*
222
         * Set page properties and variables
223
         */
224
        $this->setFolder($fileRelativePath);
1✔
225
        $this->setSlug($fileName);
1✔
226
        $this->setPath($this->getFolder() . '/' . $this->getSlug());
1✔
227
        $this->setVariables([
1✔
228
            'title'    => PrefixSuffix::sub($fileName, $separators),
1✔
229
            'date'     => (new \DateTime())->setTimestamp($this->file->getMTime()),
1✔
230
            'filepath' => $this->file->getRelativePathname(),
1✔
231
        ]);
1✔
232
        /*
233
         * Set specific variables
234
         */
235
        // is file has a prefix?
236
        if (PrefixSuffix::hasPrefix($fileName, $separators)) {
1✔
237
            $prefix = PrefixSuffix::getPrefix($fileName, $separators);
1✔
238
            if ($prefix !== null) {
1✔
239
                // prefix is an integer: used for sorting
240
                if (is_numeric($prefix)) {
1✔
241
                    $this->setVariable('weight', (int) $prefix);
1✔
242
                }
243
                // prefix is a valid date?
244
                if (Util\Date::isValid($prefix)) {
1✔
245
                    $this->setVariable('date', (string) $prefix);
1✔
246
                }
247
            }
248
        }
249
        // is file has a language suffix?
250
        if (PrefixSuffix::hasSuffix($fileName)) {
1✔
251
            $this->setVariable('language', PrefixSuffix::getSuffix($fileName));
1✔
252
        }
253
        // set reference between page's translations, even if it exist in only one language
254
        $this->setVariable('langref', $this->getPath());
1✔
255

256
        return $this;
1✔
257
    }
258

259
    /**
260
     * Returns file name, with extension.
261
     */
262
    public function getFileName(): ?string
263
    {
264
        if ($this->file === null) {
×
265
            return null;
×
266
        }
267

268
        return $this->file->getBasename();
×
269
    }
270

271
    /**
272
     * Returns file real path.
273
     */
274
    public function getFilePath(): ?string
275
    {
276
        if ($this->file === null) {
1✔
277
            return null;
×
278
        }
279

280
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
1✔
281
    }
282

283
    /**
284
     * Parse file content.
285
     */
286
    public function parse(): self
287
    {
288
        $parser = new Parser($this->file);
1✔
289
        $parsed = $parser->parse();
1✔
290
        $this->frontmatter = $parsed->getFrontmatter();
1✔
291
        $this->body = $parsed->getBody();
1✔
292

293
        return $this;
1✔
294
    }
295

296
    /**
297
     * Get front matter.
298
     */
299
    public function getFrontmatter(): ?string
300
    {
301
        return $this->frontmatter;
1✔
302
    }
303

304
    /**
305
     * Get body as raw.
306
     */
307
    public function getBody(): ?string
308
    {
309
        return $this->body;
1✔
310
    }
311

312
    /**
313
     * Set virtual status.
314
     */
315
    public function setVirtual(bool $virtual): self
316
    {
317
        $this->virtual = $virtual;
1✔
318

319
        return $this;
1✔
320
    }
321

322
    /**
323
     * Is current page is virtual?
324
     */
325
    public function isVirtual(): bool
326
    {
327
        return $this->virtual;
1✔
328
    }
329

330
    /**
331
     * Set page type.
332
     */
333
    public function setType(string $type): self
334
    {
335
        $this->type = Type::from($type);
1✔
336

337
        return $this;
1✔
338
    }
339

340
    /**
341
     * Get page type.
342
     */
343
    public function getType(): string
344
    {
345
        return $this->type->value;
1✔
346
    }
347

348
    /**
349
     * Set path without slug.
350
     */
351
    public function setFolder(string $folder): self
352
    {
353
        $this->folder = self::slugify($folder);
1✔
354

355
        return $this;
1✔
356
    }
357

358
    /**
359
     * Get path without slug.
360
     */
361
    public function getFolder(): ?string
362
    {
363
        return $this->folder;
1✔
364
    }
365

366
    /**
367
     * Set slug.
368
     */
369
    public function setSlug(string $slug): self
370
    {
371
        if (!$this->slug) {
1✔
372
            $slug = self::slugify(PrefixSuffix::sub($slug));
1✔
373
        }
374
        // force slug and update path
375
        if ($this->slug && $this->slug != $slug) {
1✔
376
            $this->setPath($this->getFolder() . '/' . $slug);
1✔
377
        }
378
        $this->slug = $slug;
1✔
379

380
        return $this;
1✔
381
    }
382

383
    /**
384
     * Get slug.
385
     */
386
    public function getSlug(): string
387
    {
388
        return $this->slug;
1✔
389
    }
390

391
    /**
392
     * Set path.
393
     */
394
    public function setPath(string $path): self
395
    {
396
        $path = trim($path, '/');
1✔
397

398
        // case of homepage
399
        if ($path == 'index') {
1✔
400
            $this->path = '';
1✔
401

402
            return $this;
1✔
403
        }
404

405
        // case of custom sections' index (ie: section/index.md -> section)
406
        if (substr($path, -6) == '/index') {
1✔
407
            $path = substr($path, 0, \strlen($path) - 6);
1✔
408
        }
409
        $this->path = $path;
1✔
410

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

413
        // case of root/top-level pages
414
        if ($lastslash === false) {
1✔
415
            $this->slug = $this->path;
1✔
416

417
            return $this;
1✔
418
        }
419

420
        // case of sections' pages: set section
421
        if (!$this->virtual && $this->getSection() === null) {
1✔
422
            $this->section = explode('/', $this->path)[0];
1✔
423
        }
424
        // set/update folder and slug
425
        $this->folder = substr($this->path, 0, $lastslash);
1✔
426
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
1✔
427

428
        return $this;
1✔
429
    }
430

431
    /**
432
     * Get path.
433
     */
434
    public function getPath(): ?string
435
    {
436
        return $this->path;
1✔
437
    }
438

439
    /**
440
     * @see getPath()
441
     */
442
    public function getPathname(): ?string
443
    {
444
        return $this->getPath();
×
445
    }
446

447
    /**
448
     * Set section.
449
     */
450
    public function setSection(string $section): self
451
    {
452
        $this->section = $section;
1✔
453

454
        return $this;
1✔
455
    }
456

457
    /**
458
     * Get section.
459
     */
460
    public function getSection(): ?string
461
    {
462
        return !empty($this->section) ? $this->section : null;
1✔
463
    }
464

465
    /**
466
     * Unset section.
467
     */
468
    public function unSection(): self
469
    {
470
        $this->section = null;
×
471

472
        return $this;
×
473
    }
474

475
    /**
476
     * Set body as HTML.
477
     */
478
    public function setBodyHtml(string $html): self
479
    {
480
        $this->html = $html;
1✔
481

482
        return $this;
1✔
483
    }
484

485
    /**
486
     * Get body as HTML.
487
     */
488
    public function getBodyHtml(): ?string
489
    {
490
        return $this->html;
1✔
491
    }
492

493
    /**
494
     * @see getBodyHtml()
495
     */
496
    public function getContent(): ?string
497
    {
498
        return $this->getBodyHtml();
1✔
499
    }
500

501
    /**
502
     * Add rendered.
503
     */
504
    public function addRendered(array $rendered): self
505
    {
506
        $this->rendered += $rendered;
1✔
507

508
        return $this;
1✔
509
    }
510

511
    /**
512
     * Get rendered.
513
     */
514
    public function getRendered(): array
515
    {
516
        return $this->rendered;
1✔
517
    }
518

519
    /**
520
     * Set pages list.
521
     */
522
    public function setPages(Collection $pages): self
523
    {
524
        $this->pages = $pages;
1✔
525

526
        return $this;
1✔
527
    }
528

529
    /**
530
     * Get pages list.
531
     */
532
    public function getPages(): ?Collection
533
    {
534
        return $this->pages;
1✔
535
    }
536

537
    /**
538
     * Set paginator.
539
     */
540
    public function setPaginator(array $paginator): self
541
    {
542
        $this->paginator = $paginator;
1✔
543

544
        return $this;
1✔
545
    }
546

547
    /**
548
     * Get paginator.
549
     */
550
    public function getPaginator(): array
551
    {
552
        return $this->paginator;
1✔
553
    }
554

555
    /**
556
     * Paginator backward compatibility.
557
     */
558
    public function getPagination(): array
559
    {
560
        return $this->getPaginator();
×
561
    }
562

563
    /**
564
     * Set the parent section (for sub-sections).
565
     */
566
    public function setParentSection(?Page $parent): self
567
    {
NEW
568
        $this->parentSection = $parent;
×
569

NEW
570
        return $this;
×
571
    }
572

573
    /**
574
     * Get the parent section (for sub-sections).
575
     */
576
    public function getParentSection(): ?Page
577
    {
NEW
578
        return $this->parentSection;
×
579
    }
580

581
    /**
582
     * Does this section have a parent section?
583
     */
584
    public function hasParentSection(): bool
585
    {
NEW
586
        return $this->parentSection !== null;
×
587
    }
588

589
    /**
590
     * Get child sub-sections collection.
591
     */
592
    public function getSubSections(): ?Collection
593
    {
NEW
594
        return $this->subSections;
×
595
    }
596

597
    /**
598
     * Set child sub-sections collection.
599
     */
600
    public function setSubSections(Collection $subSections): self
601
    {
NEW
602
        $this->subSections = $subSections;
×
603

NEW
604
        return $this;
×
605
    }
606

607
    /**
608
     * Add a child sub-section.
609
     */
610
    public function addSubSection(Page $child): self
611
    {
NEW
612
        if ($this->subSections === null) {
×
NEW
613
            $this->subSections = new Collection(\sprintf('%s-subsections', $this->getId()));
×
614
        }
615

616
        try {
NEW
617
            $this->subSections->add($child);
×
NEW
618
        } catch (\DomainException) {
×
NEW
619
            $this->subSections->replace($child->getId(), $child);
×
620
        }
621

NEW
622
        return $this;
×
623
    }
624

625
    /**
626
     * Does this section have child sub-sections?
627
     */
628
    public function hasSubSections(): bool
629
    {
NEW
630
        return $this->subSections !== null && \count($this->subSections) > 0;
×
631
    }
632

633
    /**
634
     * Is this a sub-section (has a parent section)?
635
     */
636
    public function isSubSection(): bool
637
    {
NEW
638
        return $this->type === Type::SECTION && $this->parentSection !== null;
×
639
    }
640

641
    /**
642
     * Get depth level in the section tree (0 = root section).
643
     * Uses the parent section chain rather than path slashes for robustness
644
     * when custom path patterns are configured.
645
     */
646
    public function getSectionDepth(): int
647
    {
NEW
648
        $depth = 0;
×
NEW
649
        $current = $this;
×
650

NEW
651
        while ($current->hasParentSection()) {
×
NEW
652
            $depth++;
×
NEW
653
            $current = $current->getParentSection();
×
654
        }
655

NEW
656
        return $depth;
×
657
    }
658

659
    /**
660
     * Returns the breadcrumb from root section to this section.
661
     *
662
     * @return Page[]
663
     */
664
    public function getSectionBreadcrumb(): array
665
    {
NEW
666
        $breadcrumb = [$this];
×
NEW
667
        $current = $this;
×
668

NEW
669
        while ($current->hasParentSection()) {
×
NEW
670
            $current = $current->getParentSection();
×
NEW
671
            array_unshift($breadcrumb, $current);
×
672
        }
673

NEW
674
        return $breadcrumb;
×
675
    }
676

677
    /**
678
     * Get all pages recursively, including pages from sub-sections.
679
     */
680
    public function getAllPagesRecursive(): Collection
681
    {
NEW
682
        $allPages = new Collection(\sprintf('%s-all-pages', $this->getId()));
×
683

684
        // Add direct pages
NEW
685
        if ($this->getPages() !== null) {
×
NEW
686
            foreach ($this->getPages() as $page) {
×
687
                try {
NEW
688
                    $allPages->add($page);
×
NEW
689
                } catch (\DomainException) {
×
690
                    // skip duplicates
691
                }
692
            }
693
        }
694

695
        // Add pages from sub-sections recursively
NEW
696
        if ($this->hasSubSections()) {
×
NEW
697
            foreach ($this->getSubSections() as $subSection) {
×
NEW
698
                foreach ($subSection->getAllPagesRecursive() as $page) {
×
699
                    try {
NEW
700
                        $allPages->add($page);
×
NEW
701
                    } catch (\DomainException) {
×
702
                        // skip duplicates
703
                    }
704
                }
705
            }
706
        }
707

NEW
708
        return $allPages;
×
709
    }
710

711
    /**
712
     * Set vocabulary terms.
713
     */
714
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
715
    {
716
        $this->terms = $terms;
1✔
717

718
        return $this;
1✔
719
    }
720

721
    /**
722
     * Get vocabulary terms.
723
     */
724
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
725
    {
726
        return $this->terms;
1✔
727
    }
728

729
    /*
730
     * Helpers to set and get variables.
731
     */
732

733
    /**
734
     * Set an array as variables.
735
     *
736
     * @throws RuntimeException
737
     */
738
    public function setVariables(array $variables): self
739
    {
740
        foreach ($variables as $key => $value) {
1✔
741
            $this->setVariable($key, $value);
1✔
742
        }
743

744
        return $this;
1✔
745
    }
746

747
    /**
748
     * Get all variables.
749
     */
750
    public function getVariables(): array
751
    {
752
        return $this->properties;
1✔
753
    }
754

755
    /**
756
     * Set a variable.
757
     *
758
     * @param string $name  Name of the variable
759
     * @param mixed  $value Value of the variable
760
     *
761
     * @throws RuntimeException
762
     */
763
    public function setVariable(string $name, $value): self
764
    {
765
        $this->filterBool($value);
1✔
766
        switch ($name) {
767
            case 'date':
1✔
768
            case 'updated':
1✔
769
            case 'lastmod':
1✔
770
                try {
771
                    $date = Util\Date::toDatetime($value);
1✔
772
                } catch (\Exception) {
×
773
                    throw new \Exception(\sprintf('The value of "%s" is not a valid date: "%s".', $name, var_export($value, true)));
×
774
                }
775
                $this->offsetSet($name == 'lastmod' ? 'updated' : $name, $date);
1✔
776
                break;
1✔
777

778
            case 'schedule':
1✔
779
                /*
780
                 * publish: 2012-10-08
781
                 * expiry: 2012-10-09
782
                 */
783
                $this->offsetSet('published', false);
1✔
784
                if (\is_array($value)) {
1✔
785
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
1✔
786
                        $this->offsetSet('published', true);
1✔
787
                    }
788
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
1✔
789
                        $this->offsetSet('published', true);
×
790
                    }
791
                }
792
                break;
1✔
793
            case 'draft':
1✔
794
                // draft: true = published: false
795
                if ($value === true) {
1✔
796
                    $this->offsetSet('published', false);
1✔
797
                }
798
                break;
1✔
799
            case 'path':
1✔
800
            case 'slug':
1✔
801
                $slugify = self::slugify((string) $value);
1✔
802
                if ($value != $slugify) {
1✔
803
                    throw new RuntimeException(\sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
×
804
                }
805
                $method = 'set' . ucfirst($name);
1✔
806
                $this->$method($value);
1✔
807
                break;
1✔
808
            default:
809
                $this->offsetSet($name, $value);
1✔
810
        }
811

812
        return $this;
1✔
813
    }
814

815
    /**
816
     * Is variable exists?
817
     *
818
     * @param string $name Name of the variable
819
     */
820
    public function hasVariable(string $name): bool
821
    {
822
        return $this->offsetExists($name);
1✔
823
    }
824

825
    /**
826
     * Get a variable.
827
     *
828
     * @param string     $name    Name of the variable
829
     * @param mixed|null $default Default value
830
     *
831
     * @return mixed|null
832
     */
833
    public function getVariable(string $name, $default = null)
834
    {
835
        if ($this->offsetExists($name)) {
1✔
836
            return $this->offsetGet($name);
1✔
837
        }
838

839
        return $default;
1✔
840
    }
841

842
    /**
843
     * Unset a variable.
844
     *
845
     * @param string $name Name of the variable
846
     */
847
    public function unVariable(string $name): self
848
    {
849
        if ($this->offsetExists($name)) {
1✔
850
            $this->offsetUnset($name);
1✔
851
        }
852

853
        return $this;
1✔
854
    }
855

856
    /**
857
     * Set front matter (only) variables.
858
     */
859
    public function setFmVariables(array $variables): self
860
    {
861
        $this->fmVariables = $variables;
1✔
862

863
        return $this;
1✔
864
    }
865

866
    /**
867
     * Get front matter variables.
868
     */
869
    public function getFmVariables(): array
870
    {
871
        return $this->fmVariables;
1✔
872
    }
873

874
    /**
875
     * Creates a page ID from a file (based on path).
876
     */
877
    /**
878
     * @param string[] $separators Allowed prefix separator characters
879
     */
880
    private static function createIdFromFile(SplFileInfo $file, array $separators = PrefixSuffix::DEFAULT_SEPARATORS): string
881
    {
882
        $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
1✔
883
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension()), $separators));
1✔
884
        // if file is "README.md", ID is "index"
885
        $basename = strtolower($basename) == 'readme' ? 'index' : $basename;
1✔
886
        // if file is section's index: "section/index.md", ID is "section"
887
        if (!empty($relativePath) && PrefixSuffix::sub($basename, $separators) == 'index') {
1✔
888
            // case of a localized section's index: "section/index.fr.md", ID is "fr/section"
889
            if (PrefixSuffix::hasSuffix($basename)) {
1✔
890
                return PrefixSuffix::getSuffix($basename) . '/' . $relativePath;
1✔
891
            }
892

893
            return $relativePath;
1✔
894
        }
895
        // localized page
896
        if (PrefixSuffix::hasSuffix($basename)) {
1✔
897
            return trim(Util::joinPath(/** @scrutinizer ignore-type */ PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename, $separators)), '/');
1✔
898
        }
899

900
        return trim(Util::joinPath($relativePath, $basename), '/');
1✔
901
    }
902

903
    /**
904
     * Cast "boolean" string (or array of strings) to boolean.
905
     *
906
     * @param mixed $value Value to filter
907
     *
908
     * @return bool|mixed
909
     *
910
     * @see strToBool()
911
     */
912
    private function filterBool(&$value)
913
    {
914
        \Cecil\Util\Str::strToBool($value);
1✔
915
        if (\is_array($value)) {
1✔
916
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
1✔
917
        }
918
    }
919
}
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