Passed
Push — fix/serve-timeout ( 697f77...233bc8 )
by Arnaud
04:27
created

Page::parse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 8
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
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
    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 string Body before conversion. */
54
    protected $body;
55
56
    /** @var array Front matter before conversion. */
57
    protected $fmVariables = [];
58
59
    /** @var string Body after Markdown conversion. */
60
    protected $html;
61
62
    /** @var string */
63
    protected $language = null;
64 1
65
    /** @var Slugify */
66 1
    private static $slugifier;
67 1
68 1
    public function __construct(string $id)
69
    {
70 1
        parent::__construct($id);
71 1
        $this->setVirtual(true);
72 1
        $this->setType(Type::PAGE);
73 1
        // default variables
74
        $this->setVariables([
75
            'title'            => 'Page Title',
76
            'date'             => new \DateTime(),
77 1
            'updated'          => new \DateTime(),
78
            'weight'           => null,
79 1
            'filepath'         => null,
80
            'published'        => true,
81
            'content_template' => 'page.content.twig',
82
        ]);
83
    }
84 1
85
    /**
86 1
     * Turns a path (string) into a slug (URI).
87 1
     */
88
    public static function slugify(string $path): string
89
    {
90 1
        if (!self::$slugifier instanceof Slugify) {
91
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
92
        }
93
94
        return self::$slugifier->slugify($path);
95
    }
96 1
97
    /**
98 1
     * Creates the ID from the file path.
99 1
     */
100
    public static function createId(SplFileInfo $file): string
101 1
    {
102
        $relativepath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
103 1
        $basename = self::slugify(PrefixSuffix::subPrefix($file->getBasename('.'.$file->getExtension())));
104
        // case of "README" -> index
105 1
        $basename = (string) str_ireplace('readme', 'index', $basename);
106
        // case of section's index: "section/index" -> "section"
107
        if (!empty($relativepath) && PrefixSuffix::sub($basename) == 'index') {
108
            // case of a localized section
109 1
            if (PrefixSuffix::hasSuffix($basename)) {
110
                return $relativepath.'.'.PrefixSuffix::getSuffix($basename);
111
            }
112 1
113
            return $relativepath;
114
        }
115
116
        return trim(Util::joinPath($relativepath, $basename), '/');
117
    }
118 1
119
    /**
120 1
     * Returns the ID of a page without language suffix.
121
     */
122
    public function getIdWithoutLang(): string
123
    {
124
        return PrefixSuffix::sub($this->getId());
125
    }
126 1
127
    /**
128 1
     * Set file.
129 1
     */
130
    public function setFile(SplFileInfo $file): self
131
    {
132
        $this->setVirtual(false);
133
        $this->file = $file;
134 1
135 1
        /*
136 1
         * File path components
137
         */
138 1
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
139
        $fileExtension = $this->file->getExtension();
140 1
        $fileName = $this->file->getBasename('.'.$fileExtension);
141 1
        // case of "README" -> "index"
142
        $fileName = (string) str_ireplace('readme', 'index', $fileName);
143
        // case of "index" = home page
144
        if (empty($this->file->getRelativePath()) && PrefixSuffix::sub($fileName) == 'index') {
145
            $this->setType(Type::HOMEPAGE);
146 1
        }
147 1
        /*
148 1
         * Set protected variables
149
         */
150
        $this->setFolder($fileRelativePath); // ie: "blog"
151
        $this->setSlug($fileName); // ie: "post-1"
152 1
        $this->setPath($this->getFolder().'/'.$this->getSlug()); // ie: "blog/post-1"
153 1
        /*
154 1
         * Set default variables
155 1
         */
156 1
        $this->setVariables([
157
            'title'    => PrefixSuffix::sub($fileName),
158
            'date'     => (new \DateTime())->setTimestamp($this->file->getCTime()),
159
            'updated'  => (new \DateTime())->setTimestamp($this->file->getMTime()),
160
            'filepath' => $this->file->getRelativePathname(),
161
        ]);
162 1
        /*
163 1
         * Set specific variables
164 1
         */
165
        // is file has a prefix?
166 1
        if (PrefixSuffix::hasPrefix($fileName)) {
167 1
            $prefix = PrefixSuffix::getPrefix($fileName);
168
            if ($prefix !== null) {
169
                // prefix is a valid date?
170 1
                if (Util\Date::isDateValid($prefix)) {
171
                    $this->setVariable('date', (string) $prefix);
172
                } else {
173
                    // prefix is an integer: used for sorting
174
                    $this->setVariable('weight', (int) $prefix);
175 1
                }
176 1
            }
177
        }
178
        // is file has a language suffix?
179 1
        if (PrefixSuffix::hasSuffix($fileName)) {
180
            $this->setLanguage(PrefixSuffix::getSuffix($fileName));
181 1
        }
182
        // set reference between page's translations, even if it exist in only one language
183
        $this->setVariable('langref', $this->getPath());
184
185
        return $this;
186
    }
187 1
188
    /**
189 1
     * Returns file real path.
190
     */
191
    public function getFilePath(): ?string
192
    {
193
        return $this->file->getRealPath() === false ? null : $this->file->getRealPath();
194
    }
195 1
196
    /**
197 1
     * Parse file content.
198 1
     */
199 1
    public function parse(): self
200 1
    {
201
        $parser = new Parser($this->file);
202 1
        $parsed = $parser->parse();
203
        $this->frontmatter = $parsed->getFrontmatter();
204
        $this->body = $parsed->getBody();
205
206
        return $this;
207
    }
208 1
209
    /**
210 1
     * Get frontmatter.
211
     */
212
    public function getFrontmatter(): ?string
213
    {
214
        return $this->frontmatter;
215
    }
216 1
217
    /**
218 1
     * Get body as raw.
219
     */
220
    public function getBody(): ?string
221
    {
222
        return $this->body;
223
    }
224 1
225
    /**
226 1
     * Set virtual status.
227
     */
228 1
    public function setVirtual(bool $virtual): self
229
    {
230
        $this->virtual = $virtual;
231
232
        return $this;
233
    }
234 1
235
    /**
236 1
     * Is current page is virtual?
237
     */
238
    public function isVirtual(): bool
239
    {
240
        return $this->virtual;
241
    }
242 1
243
    /**
244 1
     * Set page type.
245
     */
246 1
    public function setType(string $type): self
247
    {
248
        $this->type = new Type($type);
249
250
        return $this;
251
    }
252 1
253
    /**
254 1
     * Get page type.
255
     */
256
    public function getType(): string
257
    {
258
        return (string) $this->type;
259
    }
260 1
261
    /**
262 1
     * Set path without slug.
263
     */
264 1
    public function setFolder(string $folder): self
265
    {
266
        $this->folder = self::slugify($folder);
267
268
        return $this;
269
    }
270 1
271
    /**
272 1
     * Get path without slug.
273
     */
274
    public function getFolder(): ?string
275
    {
276
        return $this->folder;
277
    }
278 1
279
    /**
280 1
     * Set slug.
281 1
     */
282
    public function setSlug(string $slug): self
283
    {
284 1
        if (!$this->slug) {
285 1
            $slug = self::slugify(PrefixSuffix::sub($slug));
286
        }
287 1
        // force slug and update path
288
        if ($this->slug && $this->slug != $slug) {
289 1
            $this->setPath($this->getFolder().'/'.$slug);
290
        }
291
        $this->slug = $slug;
292
293
        return $this;
294
    }
295 1
296
    /**
297 1
     * Get slug.
298
     */
299
    public function getSlug(): string
300
    {
301
        return $this->slug;
302
    }
303 1
304
    /**
305 1
     * Set path.
306
     */
307
    public function setPath(string $path): self
308 1
    {
309 1
        $path = self::slugify(PrefixSuffix::sub($path));
310
311 1
        // case of homepage
312
        if ($path == 'index') {
313
            $this->path = '';
314
315 1
            return $this;
316 1
        }
317
318 1
        // case of custom sections' index (ie: content/section/index.md)
319
        if (substr($path, -6) == '/index') {
320
            $path = substr($path, 0, strlen($path) - 6);
321 1
        }
322 1
        $this->path = $path;
323 1
324
        // case of root pages
325 1
        $lastslash = strrpos($this->path, '/');
326
        if ($lastslash === false) {
327
            $this->slug = $this->path;
328 1
329 1
            return $this;
330
        }
331 1
332 1
        if (!$this->virtual && $this->getSection() === null) {
333
            $this->section = explode('/', $this->path)[0];
334 1
        }
335
        $this->folder = substr($this->path, 0, $lastslash);
336
        $this->slug = substr($this->path, -(strlen($this->path) - $lastslash - 1));
337
338
        return $this;
339
    }
340 1
341
    /**
342 1
     * Get path.
343
     */
344
    public function getPath(): ?string
345
    {
346
        return $this->path;
347
    }
348
349
    /**
350
     * @see getPath()
351
     */
352
    public function getPathname(): ?string
353
    {
354
        return $this->getPath();
355
    }
356 1
357
    /**
358 1
     * Set section.
359
     */
360 1
    public function setSection(string $section): self
361
    {
362
        $this->section = $section;
363
364
        return $this;
365
    }
366 1
367
    /**
368 1
     * Get section.
369
     */
370
    public function getSection(): ?string
371
    {
372
        return !empty($this->section) ? $this->section : null;
373
    }
374 1
375
    /**
376 1
     * Set body as HTML.
377
     */
378 1
    public function setBodyHtml(string $html): self
379
    {
380
        $this->html = $html;
381
382
        return $this;
383
    }
384 1
385
    /**
386 1
     * Get body as HTML.
387
     */
388
    public function getBodyHtml(): ?string
389
    {
390
        return $this->html;
391
    }
392 1
393
    /**
394 1
     * @see getBodyHtml()
395
     */
396
    public function getContent(): ?string
397
    {
398
        return $this->getBodyHtml();
399
    }
400
401
    /**
402
     * Set language.
403
     */
404
    public function setLanguage(string $language = null): self
405
    {
406 1
        $this->language = $language;
407
408 1
        return $this;
409 1
    }
410
411
    /**
412 1
     * Get language.
413
     */
414
    public function getLanguage(): ?string
415
    {
416
        return $this->language;
417
    }
418 1
419
    /*
420 1
     * Helpers to set and get variables.
421
     */
422
423
    /**
424
     * Set an array as variables.
425
     *
426
     * @throws RuntimeException
427
     */
428
    public function setVariables(array $variables): self
429
    {
430
        foreach ($variables as $key => $value) {
431 1
            $this->setVariable($key, $value);
432
        }
433 1
434 1
        return $this;
435
    }
436
437 1
    /**
438
     * Get all variables.
439 1
     */
440
    public function getVariables(): array
441
    {
442
        return $this->properties;
443 1
    }
444 1
445 1
    /**
446 1
     * Set a variable.
447 1
     *
448
     * @param string $name
449 1
     * @param mixed  $value
450 1
     *
451 1
     * @throws RuntimeException
452 1
     */
453 1
    public function setVariable(string $name, $value): self
454
    {
455
        // cast some strings to boolean
456
        $this->filterBool($value);
457
        if (is_array($value)) {
458 1
            array_walk_recursive($value, [$this, 'filterBool']);
459 1
        }
460 1
        // behavior for specific named variables
461
        switch ($name) {
462 1
            /**
463
             * date: 2012-10-08.
464
             */
465 1
            case 'date':
466
                try {
467
                    $date = Util\Date::dateToDatetime($value);
468
                } catch (\Exception $e) {
469
                    throw new RuntimeException(\sprintf('Expected date format for "date" in "%s" must be "YYYY-MM-DD" instead of "%s"', $this->getId(), (string) $value));
470
                }
471 1
                $this->offsetSet('date', $date);
472
                break;
473 1
            /**
474
             * schedule:
475
             *   publish: 2012-10-08
476
             *   expiry: 2012-10-09.
477
             */
478
            case 'schedule':
479
                $this->offsetSet('published', false);
480
                if (is_array($value)) {
481 1
                    if (array_key_exists('publish', $value) && Util\Date::dateToDatetime($value['publish']) <= Util\Date::dateToDatetime('now')) {
482
                        $this->offsetSet('published', true);
483 1
                    }
484 1
                    if (array_key_exists('expiry', $value) && Util\Date::dateToDatetime($value['expiry']) >= Util\Date::dateToDatetime('now')) {
485
                        $this->offsetSet('published', true);
486 1
                    }
487
                }
488
                break;
489
            /**
490
             * draft: true.
491 1
             */
492
            case 'draft':
493 1
                if ($value === true) {
494 1
                    $this->offsetSet('published', 0);
495
                }
496
                break;
497 1
            /**
498
             * path: about/about
499
             * slug: about.
500
             */
501
            case 'path':
502
            case 'slug':
503 1
                $slugify = self::slugify((string) $value);
504
                if ($value != $slugify) {
505 1
                    throw new RuntimeException(\sprintf('"%s" variable should be "%s" (not "%s") in "%s"', $name, $slugify, (string) $value, $this->getId()));
506
                }
507 1
                /** @see setPath() */
508
                /** @see setSlug() */
509
                $method = 'set'.\ucfirst($name);
510
                $this->$method($value);
511
                break;
512
            default:
513 1
                $this->offsetSet($name, $value);
514
        }
515 1
516
        return $this;
517
    }
518
519
    /**
520
     * Is variable exists?
521
     */
522
    public function hasVariable(string $name): bool
523
    {
524
        return $this->offsetExists($name);
525
    }
526
527
    /**
528
     * Get a variable.
529
     *
530
     * @return mixed|null
531
     */
532
    public function getVariable(string $name)
533
    {
534
        if ($this->offsetExists($name)) {
535
            return $this->offsetGet($name);
536
        }
537
    }
538
539
    /**
540
     * Unset a variable.
541
     */
542
    public function unVariable(string $name): self
543
    {
544
        if ($this->offsetExists($name)) {
545
            $this->offsetUnset($name);
546
        }
547
548
        return $this;
549
    }
550
551
    /**
552
     * Set front matter (only) variables.
553
     */
554
    public function setFmVariables(array $variables): self
555
    {
556
        $this->fmVariables = $variables;
557
558
        return $this;
559
    }
560
561
    /**
562
     * Get front matter variables.
563
     */
564
    public function getFmVariables(): array
565
    {
566
        return $this->fmVariables;
567
    }
568
569
    /**
570
     * Filter 'true', 'false', 'on', 'off', 'yes', 'no' to boolean.
571
     */
572
    private function filterBool(&$value)
573
    {
574
        if (is_string($value)) {
575
            if (in_array($value, ['true', 'on', 'yes'])) {
576
                $value = true;
577
            }
578
            if (in_array($value, ['false', 'off', 'no'])) {
579
                $value = false;
580
            }
581
        }
582
    }
583
}
584