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

Cecilapp / Cecil / 5395046807

pending completion
5395046807

push

github

ArnaudLigny
fix: page.filepath if virtual page

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

2817 of 3420 relevant lines covered (82.37%)

0.82 hits per line

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

94.61
/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 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
    public function __construct(string $id)
78
    {
79
        parent::__construct($id);
1✔
80
        $this->setVirtual(true);
1✔
81
        $this->setType(Type::PAGE);
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
     * Turns a path (string) into a slug (URI).
96
     */
97
    public static function slugify(string $path): string
98
    {
99
        if (!self::$slugifier instanceof Slugify) {
1✔
100
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
1✔
101
        }
102

103
        return self::$slugifier->slugify($path);
1✔
104
    }
105

106
    /**
107
     * Creates the ID from the file path.
108
     */
109
    public static function createIdFromFile(SplFileInfo $file): string
110
    {
111
        $relativePath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
1✔
112
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.' . $file->getExtension())));
1✔
113
        // if file is "README.md", ID is "index"
114
        $basename = (string) str_ireplace('readme', 'index', $basename);
1✔
115
        // if file is section's index: "section/index.md", ID is "section"
116
        if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') {
1✔
117
            // case of a localized section's index: "section/index.fr.md", ID is "fr/section"
118
            if (PrefixSuffix::hasSuffix($basename)) {
1✔
119
                return PrefixSuffix::getSuffix($basename) . '/' . $relativePath;
1✔
120
            }
121

122
            return $relativePath;
1✔
123
        }
124
        // localized page
125
        if (PrefixSuffix::hasSuffix($basename)) {
1✔
126
            return trim(Util::joinPath(PrefixSuffix::getSuffix($basename), $relativePath, PrefixSuffix::sub($basename)), '/');
1✔
127
        }
128

129
        return trim(Util::joinPath($relativePath, $basename), '/');
1✔
130
    }
131

132
    /**
133
     * Returns the ID of a page without language.
134
     */
135
    public function getIdWithoutLang(): string
136
    {
137
        $langPrefix = $this->getVariable('language') . '/';
1✔
138
        if ($this->hasVariable('language') && Util\Str::startsWith($this->getId(), $langPrefix)) {
1✔
139
            return substr($this->getId(), \strlen($langPrefix));
1✔
140
        }
141

142
        return $this->getId();
1✔
143
    }
144

145
    /**
146
     * Set file.
147
     */
148
    public function setFile(SplFileInfo $file): self
149
    {
150
        $this->setVirtual(false);
1✔
151
        $this->file = $file;
1✔
152

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

203
        return $this;
1✔
204
    }
205

206
    /**
207
     * Returns file real path.
208
     */
209
    public function getFilePath(): ?string
210
    {
211
        if ($this->file === null) {
1✔
212
            return null;
×
213
        }
214

215
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
1✔
216
    }
217

218
    /**
219
     * Parse file content.
220
     */
221
    public function parse(): self
222
    {
223
        $parser = new Parser($this->file);
1✔
224
        $parsed = $parser->parse();
1✔
225
        $this->frontmatter = $parsed->getFrontmatter();
1✔
226
        $this->body = $parsed->getBody();
1✔
227

228
        return $this;
1✔
229
    }
230

231
    /**
232
     * Get front matter.
233
     */
234
    public function getFrontmatter(): ?string
235
    {
236
        return $this->frontmatter;
1✔
237
    }
238

239
    /**
240
     * Get body as raw.
241
     */
242
    public function getBody(): ?string
243
    {
244
        return $this->body;
1✔
245
    }
246

247
    /**
248
     * Set virtual status.
249
     */
250
    public function setVirtual(bool $virtual): self
251
    {
252
        $this->virtual = $virtual;
1✔
253

254
        return $this;
1✔
255
    }
256

257
    /**
258
     * Is current page is virtual?
259
     */
260
    public function isVirtual(): bool
261
    {
262
        return $this->virtual;
1✔
263
    }
264

265
    /**
266
     * Set page type.
267
     */
268
    public function setType(string $type): self
269
    {
270
        $this->type = new Type($type);
1✔
271

272
        return $this;
1✔
273
    }
274

275
    /**
276
     * Get page type.
277
     */
278
    public function getType(): string
279
    {
280
        return (string) $this->type;
1✔
281
    }
282

283
    /**
284
     * Set path without slug.
285
     */
286
    public function setFolder(string $folder): self
287
    {
288
        $this->folder = self::slugify($folder);
1✔
289

290
        return $this;
1✔
291
    }
292

293
    /**
294
     * Get path without slug.
295
     */
296
    public function getFolder(): ?string
297
    {
298
        return $this->folder;
1✔
299
    }
300

301
    /**
302
     * Set slug.
303
     */
304
    public function setSlug(string $slug): self
305
    {
306
        if (!$this->slug) {
1✔
307
            $slug = self::slugify(PrefixSuffix::sub($slug));
1✔
308
        }
309
        // force slug and update path
310
        if ($this->slug && $this->slug != $slug) {
1✔
311
            $this->setPath($this->getFolder() . '/' . $slug);
1✔
312
        }
313
        $this->slug = $slug;
1✔
314

315
        return $this;
1✔
316
    }
317

318
    /**
319
     * Get slug.
320
     */
321
    public function getSlug(): string
322
    {
323
        return $this->slug;
1✔
324
    }
325

326
    /**
327
     * Set path.
328
     */
329
    public function setPath(string $path): self
330
    {
331
        // case of homepage
332
        if ($path == 'index') {
1✔
333
            $this->path = '';
×
334

335
            return $this;
×
336
        }
337

338
        // case of custom sections' index (ie: content/section/index.md)
339
        if (substr($path, -6) == '/index') {
1✔
340
            $path = substr($path, 0, \strlen($path) - 6);
1✔
341
        }
342
        $this->path = $path;
1✔
343

344
        // case of root pages
345
        $lastslash = strrpos($this->path, '/');
1✔
346
        if ($lastslash === false) {
1✔
347
            $this->slug = $this->path;
1✔
348

349
            return $this;
1✔
350
        }
351

352
        if (!$this->virtual && $this->getSection() === null) {
1✔
353
            $this->section = explode('/', $this->path)[0];
1✔
354
        }
355
        $this->folder = substr($this->path, 0, $lastslash);
1✔
356
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
1✔
357

358
        return $this;
1✔
359
    }
360

361
    /**
362
     * Get path.
363
     */
364
    public function getPath(): ?string
365
    {
366
        return $this->path;
1✔
367
    }
368

369
    /**
370
     * @see getPath()
371
     */
372
    public function getPathname(): ?string
373
    {
374
        return $this->getPath();
×
375
    }
376

377
    /**
378
     * Set section.
379
     */
380
    public function setSection(string $section): self
381
    {
382
        $this->section = $section;
1✔
383

384
        return $this;
1✔
385
    }
386

387
    /**
388
     * Get section.
389
     */
390
    public function getSection(): ?string
391
    {
392
        return !empty($this->section) ? $this->section : null;
1✔
393
    }
394

395
    /**
396
     * Set body as HTML.
397
     */
398
    public function setBodyHtml(string $html): self
399
    {
400
        $this->html = $html;
1✔
401

402
        return $this;
1✔
403
    }
404

405
    /**
406
     * Get body as HTML.
407
     */
408
    public function getBodyHtml(): ?string
409
    {
410
        return $this->html;
1✔
411
    }
412

413
    /**
414
     * @see getBodyHtml()
415
     */
416
    public function getContent(): ?string
417
    {
418
        return $this->getBodyHtml();
1✔
419
    }
420

421
    /**
422
     * Add rendered.
423
     */
424
    public function addRendered(array $rendered): self
425
    {
426
        $this->rendered += $rendered;
1✔
427

428
        return $this;
1✔
429
    }
430

431
    /**
432
     * Get rendered.
433
     */
434
    public function getRendered(): array
435
    {
436
        return $this->rendered;
1✔
437
    }
438

439
    /**
440
     * Set Subpages.
441
     */
442
    public function setPages(\Cecil\Collection\Page\Collection $subPages): self
443
    {
444
        $this->subPages = $subPages;
1✔
445

446
        return $this;
1✔
447
    }
448

449
    /**
450
     * Get Subpages.
451
     */
452
    public function getPages(): ?\Cecil\Collection\Page\Collection
453
    {
454
        return $this->subPages;
1✔
455
    }
456

457
    /**
458
     * Set paginator.
459
     */
460
    public function setPaginator(array $paginator): self
461
    {
462
        $this->paginator = $paginator;
1✔
463

464
        return $this;
1✔
465
    }
466

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

475
    /**
476
     * Paginator backward compatibility.
477
     */
478
    public function getPagination(): array
479
    {
480
        return $this->getPaginator();
×
481
    }
482

483
    /**
484
     * Set vocabulary terms.
485
     */
486
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
487
    {
488
        $this->terms = $terms;
1✔
489

490
        return $this;
1✔
491
    }
492

493
    /**
494
     * Get vocabulary terms.
495
     */
496
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
497
    {
498
        return $this->terms;
1✔
499
    }
500

501
    /*
502
     * Helpers to set and get variables.
503
     */
504

505
    /**
506
     * Set an array as variables.
507
     *
508
     * @throws RuntimeException
509
     */
510
    public function setVariables(array $variables): self
511
    {
512
        foreach ($variables as $key => $value) {
1✔
513
            $this->setVariable($key, $value);
1✔
514
        }
515

516
        return $this;
1✔
517
    }
518

519
    /**
520
     * Get all variables.
521
     */
522
    public function getVariables(): array
523
    {
524
        return $this->properties;
1✔
525
    }
526

527
    /**
528
     * Set a variable.
529
     *
530
     * @param string $name  Name of the variable
531
     * @param mixed  $value Value of the variable
532
     *
533
     * @throws RuntimeException
534
     */
535
    public function setVariable(string $name, $value): self
536
    {
537
        $this->filterBool($value);
1✔
538
        switch ($name) {
539
            case 'date':
1✔
540
            case 'updated':
1✔
541
                try {
542
                    $date = Util\Date::toDatetime($value);
1✔
543
                } catch (\Exception $e) {
×
544
                    throw new \Exception(sprintf('Expected date format for variable "%s" must be "YYYY-MM-DD" instead of "%s".', $name, (string) $value));
×
545
                }
546
                $this->offsetSet($name, $date);
1✔
547
                break;
1✔
548

549
            case 'schedule':
1✔
550
                /*
551
                 * publish: 2012-10-08
552
                 * expiry: 2012-10-09
553
                 */
554
                $this->offsetSet('published', false);
1✔
555
                if (\is_array($value)) {
1✔
556
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
1✔
557
                        $this->offsetSet('published', true);
1✔
558
                    }
559
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
1✔
560
                        $this->offsetSet('published', true);
×
561
                    }
562
                }
563
                break;
1✔
564
            case 'draft':
1✔
565
                // draft: true = published: false
566
                if ($value === true) {
1✔
567
                    $this->offsetSet('published', false);
1✔
568
                }
569
                break;
1✔
570
            case 'path':
1✔
571
            case 'slug':
1✔
572
                $slugify = self::slugify((string) $value);
1✔
573
                if ($value != $slugify) {
1✔
574
                    throw new RuntimeException(sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
×
575
                }
576
                $method = 'set' . ucfirst($name);
1✔
577
                $this->$method($value);
1✔
578
                break;
1✔
579
            default:
580
                $this->offsetSet($name, $value);
1✔
581
        }
582

583
        return $this;
1✔
584
    }
585

586
    /**
587
     * Is variable exists?
588
     *
589
     * @param string $name Name of the variable
590
     */
591
    public function hasVariable(string $name): bool
592
    {
593
        return $this->offsetExists($name);
1✔
594
    }
595

596
    /**
597
     * Get a variable.
598
     *
599
     * @param string     $name    Name of the variable
600
     * @param mixed|null $default Default value
601
     *
602
     * @return mixed|null
603
     */
604
    public function getVariable(string $name, $default = null)
605
    {
606
        if ($this->offsetExists($name)) {
1✔
607
            return $this->offsetGet($name);
1✔
608
        }
609

610
        return $default;
1✔
611
    }
612

613
    /**
614
     * Unset a variable.
615
     *
616
     * @param string $name Name of the variable
617
     */
618
    public function unVariable(string $name): self
619
    {
620
        if ($this->offsetExists($name)) {
1✔
621
            $this->offsetUnset($name);
1✔
622
        }
623

624
        return $this;
1✔
625
    }
626

627
    /**
628
     * Set front matter (only) variables.
629
     */
630
    public function setFmVariables(array $variables): self
631
    {
632
        $this->fmVariables = $variables;
1✔
633

634
        return $this;
1✔
635
    }
636

637
    /**
638
     * Get front matter variables.
639
     */
640
    public function getFmVariables(): array
641
    {
642
        return $this->fmVariables;
1✔
643
    }
644

645
    /**
646
     * Cast "boolean" string (or array of strings) to boolean.
647
     *
648
     * @param mixed $value Value to filter
649
     *
650
     * @return bool|mixed
651
     *
652
     * @see strToBool()
653
     */
654
    private function filterBool(&$value)
655
    {
656
        \Cecil\Util\Str::strToBool($value);
1✔
657
        if (\is_array($value)) {
1✔
658
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
1✔
659
        }
660
    }
661

662
    /**
663
     * {@inheritdoc}
664
     */
665
    public function setId(string $id): self
666
    {
667
        return parent::setId($id);
1✔
668
    }
669
}
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