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

Cecilapp / Cecil / 5046041481

pending completion
5046041481

Pull #1697

github

GitHub
Merge 2cd309b47 into a16355c73
Pull Request #1697: perf: native_function_invocation

322 of 322 new or added lines in 62 files covered. (100.0%)

2784 of 4121 relevant lines covered (67.56%)

0.68 hits per line

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

95.0
/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
        // case of "README" -> index
114
        $basename = (string) str_ireplace('readme', 'index', $basename);
1✔
115
        // case of section's index: "section/index" -> "section"
116
        if (!empty($relativePath) && PrefixSuffix::sub($basename) == 'index') {
1✔
117
            // case of a localized section: "section/index.fr" -> "section.fr"
118
            if (PrefixSuffix::hasSuffix($basename)) {
1✔
119
                return $relativePath . '.' . PrefixSuffix::getSuffix($basename);
1✔
120
            }
121

122
            return $relativePath;
1✔
123
        }
124

125
        return trim(Util::joinPath($relativePath, $basename), '/');
1✔
126
    }
127

128
    /**
129
     * Returns the ID of a page without language suffix.
130
     */
131
    public function getIdWithoutLang(): string
132
    {
133
        return PrefixSuffix::sub($this->getId());
1✔
134
    }
135

136
    /**
137
     * Set file.
138
     */
139
    public function setFile(SplFileInfo $file): self
140
    {
141
        $this->setVirtual(false);
1✔
142
        $this->file = $file;
1✔
143

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

194
        return $this;
1✔
195
    }
196

197
    /**
198
     * Returns file real path.
199
     */
200
    public function getFilePath(): ?string
201
    {
202
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
1✔
203
    }
204

205
    /**
206
     * Parse file content.
207
     */
208
    public function parse(): self
209
    {
210
        $parser = new Parser($this->file);
1✔
211
        $parsed = $parser->parse();
1✔
212
        $this->frontmatter = $parsed->getFrontmatter();
1✔
213
        $this->body = $parsed->getBody();
1✔
214

215
        return $this;
1✔
216
    }
217

218
    /**
219
     * Get front matter.
220
     */
221
    public function getFrontmatter(): ?string
222
    {
223
        return $this->frontmatter;
1✔
224
    }
225

226
    /**
227
     * Get body as raw.
228
     */
229
    public function getBody(): ?string
230
    {
231
        return $this->body;
1✔
232
    }
233

234
    /**
235
     * Set virtual status.
236
     */
237
    public function setVirtual(bool $virtual): self
238
    {
239
        $this->virtual = $virtual;
1✔
240

241
        return $this;
1✔
242
    }
243

244
    /**
245
     * Is current page is virtual?
246
     */
247
    public function isVirtual(): bool
248
    {
249
        return $this->virtual;
1✔
250
    }
251

252
    /**
253
     * Set page type.
254
     */
255
    public function setType(string $type): self
256
    {
257
        $this->type = new Type($type);
1✔
258

259
        return $this;
1✔
260
    }
261

262
    /**
263
     * Get page type.
264
     */
265
    public function getType(): string
266
    {
267
        return (string) $this->type;
1✔
268
    }
269

270
    /**
271
     * Set path without slug.
272
     */
273
    public function setFolder(string $folder): self
274
    {
275
        $this->folder = self::slugify($folder);
1✔
276

277
        return $this;
1✔
278
    }
279

280
    /**
281
     * Get path without slug.
282
     */
283
    public function getFolder(): ?string
284
    {
285
        return $this->folder;
1✔
286
    }
287

288
    /**
289
     * Set slug.
290
     */
291
    public function setSlug(string $slug): self
292
    {
293
        if (!$this->slug) {
1✔
294
            $slug = self::slugify(PrefixSuffix::sub($slug));
1✔
295
        }
296
        // force slug and update path
297
        if ($this->slug && $this->slug != $slug) {
1✔
298
            $this->setPath($this->getFolder() . '/' . $slug);
1✔
299
        }
300
        $this->slug = $slug;
1✔
301

302
        return $this;
1✔
303
    }
304

305
    /**
306
     * Get slug.
307
     */
308
    public function getSlug(): string
309
    {
310
        return $this->slug;
1✔
311
    }
312

313
    /**
314
     * Set path.
315
     */
316
    public function setPath(string $path): self
317
    {
318
        // case of homepage
319
        if ($path == 'index') {
1✔
320
            $this->path = '';
×
321

322
            return $this;
×
323
        }
324

325
        // case of custom sections' index (ie: content/section/index.md)
326
        if (substr($path, -6) == '/index') {
1✔
327
            $path = substr($path, 0, \strlen($path) - 6);
1✔
328
        }
329
        $this->path = $path;
1✔
330

331
        // case of root pages
332
        $lastslash = strrpos($this->path, '/');
1✔
333
        if ($lastslash === false) {
1✔
334
            $this->slug = $this->path;
1✔
335

336
            return $this;
1✔
337
        }
338

339
        if (!$this->virtual && $this->getSection() === null) {
1✔
340
            $this->section = explode('/', $this->path)[0];
1✔
341
        }
342
        $this->folder = substr($this->path, 0, $lastslash);
1✔
343
        $this->slug = substr($this->path, -(\strlen($this->path) - $lastslash - 1));
1✔
344

345
        return $this;
1✔
346
    }
347

348
    /**
349
     * Get path.
350
     */
351
    public function getPath(): ?string
352
    {
353
        return $this->path;
1✔
354
    }
355

356
    /**
357
     * @see getPath()
358
     */
359
    public function getPathname(): ?string
360
    {
361
        return $this->getPath();
×
362
    }
363

364
    /**
365
     * Set section.
366
     */
367
    public function setSection(string $section): self
368
    {
369
        $this->section = $section;
1✔
370

371
        return $this;
1✔
372
    }
373

374
    /**
375
     * Get section.
376
     */
377
    public function getSection(): ?string
378
    {
379
        return !empty($this->section) ? $this->section : null;
1✔
380
    }
381

382
    /**
383
     * Set body as HTML.
384
     */
385
    public function setBodyHtml(string $html): self
386
    {
387
        $this->html = $html;
1✔
388

389
        return $this;
1✔
390
    }
391

392
    /**
393
     * Get body as HTML.
394
     */
395
    public function getBodyHtml(): ?string
396
    {
397
        return $this->html;
1✔
398
    }
399

400
    /**
401
     * @see getBodyHtml()
402
     */
403
    public function getContent(): ?string
404
    {
405
        return $this->getBodyHtml();
1✔
406
    }
407

408
    /**
409
     * Add rendered.
410
     */
411
    public function addRendered(array $rendered): self
412
    {
413
        $this->rendered += $rendered;
1✔
414

415
        return $this;
1✔
416
    }
417

418
    /**
419
     * Get rendered.
420
     */
421
    public function getRendered(): array
422
    {
423
        return $this->rendered;
1✔
424
    }
425

426
    /**
427
     * Set Subpages.
428
     */
429
    public function setPages(\Cecil\Collection\Page\Collection $subPages): self
430
    {
431
        $this->subPages = $subPages;
1✔
432

433
        return $this;
1✔
434
    }
435

436
    /**
437
     * Get Subpages.
438
     */
439
    public function getPages(): ?\Cecil\Collection\Page\Collection
440
    {
441
        return $this->subPages;
1✔
442
    }
443

444
    /**
445
     * Set paginator.
446
     */
447
    public function setPaginator(array $paginator): self
448
    {
449
        $this->paginator = $paginator;
1✔
450

451
        return $this;
1✔
452
    }
453

454
    /**
455
     * Get paginator.
456
     */
457
    public function getPaginator(): array
458
    {
459
        return $this->paginator;
1✔
460
    }
461

462
    /**
463
     * Paginator backward compatibility.
464
     */
465
    public function getPagination(): array
466
    {
467
        return $this->getPaginator();
×
468
    }
469

470
    /**
471
     * Set vocabulary terms.
472
     */
473
    public function setTerms(\Cecil\Collection\Taxonomy\Vocabulary $terms): self
474
    {
475
        $this->terms = $terms;
1✔
476

477
        return $this;
1✔
478
    }
479

480
    /**
481
     * Get vocabulary terms.
482
     */
483
    public function getTerms(): \Cecil\Collection\Taxonomy\Vocabulary
484
    {
485
        return $this->terms;
1✔
486
    }
487

488
    /*
489
     * Helpers to set and get variables.
490
     */
491

492
    /**
493
     * Set an array as variables.
494
     *
495
     * @throws RuntimeException
496
     */
497
    public function setVariables(array $variables): self
498
    {
499
        foreach ($variables as $key => $value) {
1✔
500
            $this->setVariable($key, $value);
1✔
501
        }
502

503
        return $this;
1✔
504
    }
505

506
    /**
507
     * Get all variables.
508
     */
509
    public function getVariables(): array
510
    {
511
        return $this->properties;
1✔
512
    }
513

514
    /**
515
     * Set a variable.
516
     *
517
     * @param string $name  Name of the variable
518
     * @param mixed  $value Value of the variable
519
     *
520
     * @throws RuntimeException
521
     */
522
    public function setVariable(string $name, $value): self
523
    {
524
        $this->filterBool($value);
1✔
525
        switch ($name) {
526
            case 'date':
1✔
527
            case 'updated':
1✔
528
                try {
529
                    $date = Util\Date::toDatetime($value);
1✔
530
                } catch (\Exception $e) {
×
531
                    throw new \Exception(sprintf('Expected date format for variable "%s" must be "YYYY-MM-DD" instead of "%s".', $name, (string) $value));
×
532
                }
533
                $this->offsetSet($name, $date);
1✔
534
                break;
1✔
535

536
            case 'schedule':
1✔
537
                /*
538
                 * publish: 2012-10-08
539
                 * expiry: 2012-10-09
540
                 */
541
                $this->offsetSet('published', false);
1✔
542
                if (\is_array($value)) {
1✔
543
                    if (\array_key_exists('publish', $value) && Util\Date::toDatetime($value['publish']) <= Util\Date::toDatetime('now')) {
1✔
544
                        $this->offsetSet('published', true);
1✔
545
                    }
546
                    if (\array_key_exists('expiry', $value) && Util\Date::toDatetime($value['expiry']) >= Util\Date::toDatetime('now')) {
1✔
547
                        $this->offsetSet('published', true);
×
548
                    }
549
                }
550
                break;
1✔
551
            case 'draft':
1✔
552
                // draft: true = published: false
553
                if ($value === true) {
1✔
554
                    $this->offsetSet('published', false);
1✔
555
                }
556
                break;
1✔
557
            case 'path':
1✔
558
            case 'slug':
1✔
559
                $slugify = self::slugify((string) $value);
1✔
560
                if ($value != $slugify) {
1✔
561
                    throw new RuntimeException(sprintf('"%s" variable should be "%s" (not "%s") in "%s".', $name, $slugify, (string) $value, $this->getId()));
×
562
                }
563
                $method = 'set' . ucfirst($name);
1✔
564
                $this->$method($value);
1✔
565
                break;
1✔
566
            default:
567
                $this->offsetSet($name, $value);
1✔
568
        }
569

570
        return $this;
1✔
571
    }
572

573
    /**
574
     * Is variable exists?
575
     *
576
     * @param string $name Name of the variable
577
     */
578
    public function hasVariable(string $name): bool
579
    {
580
        return $this->offsetExists($name);
1✔
581
    }
582

583
    /**
584
     * Get a variable.
585
     *
586
     * @param string     $name    Name of the variable
587
     * @param mixed|null $default Default value
588
     *
589
     * @return mixed|null
590
     */
591
    public function getVariable(string $name, $default = null)
592
    {
593
        if ($this->offsetExists($name)) {
1✔
594
            return $this->offsetGet($name);
1✔
595
        }
596

597
        return $default;
1✔
598
    }
599

600
    /**
601
     * Unset a variable.
602
     *
603
     * @param string $name Name of the variable
604
     */
605
    public function unVariable(string $name): self
606
    {
607
        if ($this->offsetExists($name)) {
1✔
608
            $this->offsetUnset($name);
1✔
609
        }
610

611
        return $this;
1✔
612
    }
613

614
    /**
615
     * Set front matter (only) variables.
616
     */
617
    public function setFmVariables(array $variables): self
618
    {
619
        $this->fmVariables = $variables;
1✔
620

621
        return $this;
1✔
622
    }
623

624
    /**
625
     * Get front matter variables.
626
     */
627
    public function getFmVariables(): array
628
    {
629
        return $this->fmVariables;
1✔
630
    }
631

632
    /**
633
     * Cast "boolean" string (or array of strings) to boolean.
634
     *
635
     * @param mixed $value Value to filter
636
     *
637
     * @return bool|mixed
638
     *
639
     * @see strToBool()
640
     */
641
    private function filterBool(&$value)
642
    {
643
        \Cecil\Util\Str::strToBool($value);
1✔
644
        if (\is_array($value)) {
1✔
645
            array_walk_recursive($value, '\Cecil\Util\Str::strToBool');
1✔
646
        }
647
    }
648

649
    /**
650
     * {@inheritdoc}
651
     */
652
    public function setId(string $id): self
653
    {
654
        return parent::setId($id);
1✔
655
    }
656
}
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