Completed
Push — internationalization ( 50fb1f )
by Arnaud
02:01
created

Page::setFile()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 47
rs 8.8452
c 0
b 0
f 0
cc 5
nc 6
nop 1
1
<?php
2
/*
3
 * Copyright (c) Arnaud Ligny <[email protected]>
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
declare(strict_types=1);
10
11
namespace Cecil\Collection\Page;
12
13
use Cecil\Collection\Item;
14
use Cecil\Config;
15
use Cecil\Util;
16
use Cocur\Slugify\Slugify;
17
use Symfony\Component\Finder\SplFileInfo;
18
19
/**
20
 * Class Page.
21
 */
22
class Page extends Item
23
{
24
    const SLUGIFY_PATTERN = '/(^\/|[^_a-z0-9\/]|-)+/'; // should be '/^\/|[^_a-z0-9\/]+/'
25
26
    /**
27
     * @var bool
28
     */
29
    protected $virtual;
30
    /**
31
     * @var SplFileInfo
32
     */
33
    protected $file;
34
    /**
35
     * @var string
36
     */
37
    protected $type;
38
    /**
39
     * @var string
40
     */
41
    protected $folder;
42
    /**
43
     * @var string
44
     */
45
    protected $slug;
46
    /**
47
     * @var string
48
     */
49
    protected $path;
50
    /**
51
     * @var string
52
     */
53
    protected $section;
54
    /**
55
     * @var string
56
     */
57
    protected $frontmatter;
58
    /**
59
     * @var string
60
     */
61
    protected $body;
62
    /**
63
     * @var string
64
     */
65
    protected $html;
66
    /**
67
     * @var Slugify
68
     */
69
    private static $slugifier;
70
71
    /**
72
     * Constructor.
73
     *
74
     * @param string $id
75
     */
76
    public function __construct(string $id)
77
    {
78
        parent::__construct($id);
79
        $this->setVirtual(true);
80
        $this->setType(Type::PAGE);
81
        // default variables
82
        $this->setVariables([
83
            'title'            => 'Page Title',
84
            'date'             => new \DateTime(),
85
            'updated'          => new \DateTime(),
86
            'weight'           => null,
87
            'filepath'         => null,
88
            'published'        => true,
89
            'content_template' => 'page.content.twig',
90
        ]);
91
    }
92
93
    /**
94
     * Turn a path (string) into a slug (URI).
95
     *
96
     * @param string $path
97
     *
98
     * @return string
99
     */
100
    public static function slugify(string $path): string
101
    {
102
        if (!self::$slugifier instanceof Slugify) {
103
            self::$slugifier = Slugify::create(['regexp' => self::SLUGIFY_PATTERN]);
104
        }
105
106
        return self::$slugifier->slugify($path);
107
    }
108
109
    /**
110
     * Create ID from file.
111
     *
112
     * @param SplFileInfo $file
113
     *
114
     * @return string
115
     */
116
    public static function createId(SplFileInfo $file): string
117
    {
118
        $relpath = self::slugify(str_replace(DIRECTORY_SEPARATOR, '/', $file->getRelativePath()));
119
        $basename = self::slugify(FileStr::subPrefix($file->getBasename('.'.$file->getExtension())));
120
121
        // kill me with your fucking index!
122
        if ($relpath && $basename == 'index') {
123
            return $relpath;
124
        }
125
126
        return trim($relpath.'/'.$basename, '/');
127
    }
128
129
    /**
130
     * Set file.
131
     *
132
     * @param SplFileInfo $file
133
     *
134
     * @return self
135
     */
136
    public function setFile(SplFileInfo $file): self
137
    {
138
        $this->setVirtual(false);
139
        $this->file = $file;
140
141
        /*
142
         * File path components
143
         */
144
        $fileRelativePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
145
        $fileExtension = $this->file->getExtension();
146
        $fileName = $this->file->getBasename('.'.$fileExtension);
147
        /*
148
         * Set protected variables
149
         */
150
        $this->setFolder($fileRelativePath); // ie: "blog"
151
        $this->setSlug($fileName); // ie: "post-1"
152
        $this->setPath($this->getFolder().'/'.$this->getSlug()); // ie: "blog/post-1"
153
        /*
154
         * Set default variables
155
         */
156
        $this->setVariables([
157
            'title'    => FileStr::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
        // special case: file has a prefix
163
        if (FileStr::hasPrefix($fileName)) {
164
            $prefix = FileStr::getPrefix($fileName);
165
            // prefix is a valid date?
166
            if (Util::dateIsValid($prefix)) {
167
                $this->setVariable('date', (string) $prefix);
168
            } else {
169
                // prefix is an integer, use for sorting
170
                $this->setVariable('weight', (int) $prefix);
171
            }
172
        }
173
        // special case: file has a suffix
174
        if (FileStr::hasSuffix($fileName)) {
175
            $suffix = FileStr::getSuffix($fileName);
176
            $this->setVariable('language', $suffix);
177
            $this->setPath($suffix.(null !== $this->getFolder() ? '/'.$this->getFolder() : '').'/'.$this->getSlug());
178
179
        }
180
181
        return $this;
182
    }
183
184
    /**
185
     * Parse file content.
186
     *
187
     * @return self
188
     */
189
    public function parse(): self
190
    {
191
        $parser = new Parser($this->file);
192
        $parsed = $parser->parse();
193
        $this->frontmatter = $parsed->getFrontmatter();
194
        $this->body = $parsed->getBody();
195
196
        return $this;
197
    }
198
199
    /**
200
     * Get frontmatter.
201
     *
202
     * @return string|null
203
     */
204
    public function getFrontmatter(): ?string
205
    {
206
        return $this->frontmatter;
207
    }
208
209
    /**
210
     * Get body as raw.
211
     *
212
     * @return string
213
     */
214
    public function getBody(): ?string
215
    {
216
        return $this->body;
217
    }
218
219
    /**
220
     * Set virtual status.
221
     *
222
     * @param bool $virtual
223
     *
224
     * @return self
225
     */
226
    public function setVirtual(bool $virtual): self
227
    {
228
        $this->virtual = $virtual;
229
230
        return $this;
231
    }
232
233
    /**
234
     * Is current page is virtual?
235
     *
236
     * @return bool
237
     */
238
    public function isVirtual(): bool
239
    {
240
        return $this->virtual;
241
    }
242
243
    /**
244
     * Set page type.
245
     *
246
     * @param string $type
247
     *
248
     * @return self
249
     */
250
    public function setType(string $type): self
251
    {
252
        $this->type = new Type($type);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Cecil\Collection\Page\Type($type) of type object<Cecil\Collection\Page\Type> is incompatible with the declared type string of property $type.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
253
254
        return $this;
255
    }
256
257
    /**
258
     * Get page type.
259
     *
260
     * @return string
261
     */
262
    public function getType(): string
263
    {
264
        return (string) $this->type;
265
    }
266
267
    /**
268
     * Set path without slug.
269
     *
270
     * @param string $folder
271
     *
272
     * @return self
273
     */
274
    public function setFolder(string $folder): self
275
    {
276
        $this->folder = self::slugify($folder);
277
278
        return $this;
279
    }
280
281
    /**
282
     * Get path without slug.
283
     *
284
     * @return string|null
285
     */
286
    public function getFolder(): ?string
287
    {
288
        return $this->folder;
289
    }
290
291
    /**
292
     * Set slug.
293
     *
294
     * @param string $slug
295
     *
296
     * @return self
297
     */
298
    public function setSlug(string $slug): self
299
    {
300
        if (!$this->slug) {
301
            $slug = self::slugify(FileStr::sub($slug));
302
        }
303
        // force slug and update path
304
        if ($this->slug && $this->slug != $slug) {
305
            $this->setPath($this->getFolder().'/'.$slug);
306
        }
307
        $this->slug = $slug;
308
309
        return $this;
310
    }
311
312
    /**
313
     * Get slug.
314
     *
315
     * @return string
316
     */
317
    public function getSlug(): string
318
    {
319
        return $this->slug;
320
    }
321
322
    /**
323
     * Set path.
324
     *
325
     * @param string $path
326
     *
327
     * @return self
328
     */
329
    public function setPath(string $path): self
330
    {
331
        $path = self::slugify(FileStr::sub($path));
332
        // special case: homepage
333
        if ($path == 'index') {
334
            $this->path = '';
335
336
            return $this;
337
        }
338
        // special case: custom section index (ie: content/section/index.md)
339
        if (substr($path, -6) == '/index') {
340
            $path = substr($path, 0, strlen($path) - 6);
341
        }
342
        $this->path = $path;
343
        // explode path by slash
344
        $lastslash = strrpos($this->path, '/');
345
        if ($lastslash === false) {
346
            $this->section = null;
347
            $this->folder = null;
348
            $this->slug = $this->path;
349
        } else {
350
            if (!$this->virtual) {
351
                $this->section = explode('/', $this->path)[0];
352
            }
353
            $this->folder = substr($this->path, 0, $lastslash);
354
            $this->slug = substr($this->path, -(strlen($this->path) - $lastslash - 1));
355
        }
356
357
        return $this;
358
    }
359
360
    /**
361
     * Get path.
362
     *
363
     * @return string|null
364
     */
365
    public function getPath(): ?string
366
    {
367
        return $this->path;
368
    }
369
370
    /**
371
     * @see getPath()
372
     *
373
     * @return string|null
374
     */
375
    public function getPathname(): ?string
376
    {
377
        return $this->getPath();
378
    }
379
380
    /**
381
     * Set section.
382
     *
383
     * @param string $section
384
     *
385
     * @return self
386
     */
387
    public function setSection(string $section): self
388
    {
389
        $this->section = $section;
390
391
        return $this;
392
    }
393
394
    /**
395
     * Get section.
396
     *
397
     * @return string|null
398
     */
399
    public function getSection(): ?string
400
    {
401
        return $this->section;
402
    }
403
404
    /**
405
     * Set body as HTML.
406
     *
407
     * @param string $html
408
     *
409
     * @return self
410
     */
411
    public function setBodyHtml(string $html): self
412
    {
413
        $this->html = $html;
414
415
        return $this;
416
    }
417
418
    /**
419
     * Get body as HTML.
420
     *
421
     * @return string|null
422
     */
423
    public function getBodyHtml(): ?string
424
    {
425
        return $this->html;
426
    }
427
428
    /**
429
     * @see getBodyHtml()
430
     *
431
     * @return string|null
432
     */
433
    public function getContent(): ?string
434
    {
435
        return $this->getBodyHtml();
436
    }
437
438
    /**
439
     * Return output file.
440
     *
441
     * Use cases:
442
     *   - default: path + suffix + extension (ie: blog/post-1/index.html)
443
     *   - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
444
     *   - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
445
     *   - path only (ie: _redirects)
446
     *
447
     * @param string $format
448
     * @param Config $config
0 ignored issues
show
Documentation introduced by
Should the type for parameter $config not be null|Config?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
449
     *
450
     * @return string
451
     */
452
    public function getOutputFile(string $format, Config $config = null): string
453
    {
454
        $path = $this->getPath();
455
        $subpath = '';
456
        $suffix = '/index';
457
        $extension = 'html';
458
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
459
460
        // site config
461
        if ($config) {
462
            $subpath = $config->get(sprintf('output.formats.%s.subpath', $format));
463
            $suffix = $config->get(sprintf('output.formats.%s.suffix', $format));
464
            $extension = $config->get(sprintf('output.formats.%s.extension', $format));
465
        }
466
        // if ugly URL: not suffix
467
        if ($uglyurl) {
468
            $suffix = '';
469
        }
470
        // format strings
471
        if ($subpath) {
472
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
473
        }
474
        if ($suffix) {
475
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
476
        }
477
        if ($extension) {
478
            $extension = sprintf('.%s', $extension);
479
        }
480
        // special case: homepage ('index' from hell!)
481
        if (!$path && !$suffix) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
482
            $path = 'index';
483
        }
484
485
        return $path.$subpath.$suffix.$extension;
486
    }
487
488
    /**
489
     * Return URL.
490
     *
491
     * @param string $format
492
     * @param Config $config
0 ignored issues
show
Documentation introduced by
Should the type for parameter $config not be null|Config?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
493
     *
494
     * @return string
495
     */
496
    public function getUrl(string $format = 'html', Config $config = null): string
497
    {
498
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
499
        $output = $this->getOutputFile($format, $config);
500
501
        if (!$uglyurl) {
502
            $output = str_replace('index.html', '', $output);
503
        }
504
505
        return $output;
506
    }
507
508
    /*
509
     * Helper to set and get variables.
510
     */
511
512
    /**
513
     * Set an array as variables.
514
     *
515
     * @param array $variables
516
     *
517
     * @throws \Exception
518
     *
519
     * @return $this
520
     */
521
    public function setVariables($variables)
522
    {
523
        if (!is_array($variables)) {
524
            throw new \Exception(sprintf(
525
                'Can\'t set variables in "%s": array expected, not %s',
526
                $this->getId(),
527
                gettype($variables)
528
            ));
529
        }
530
        foreach ($variables as $key => $value) {
531
            $this->setVariable($key, $value);
532
        }
533
534
        return $this;
535
    }
536
537
    /**
538
     * Get all variables.
539
     *
540
     * @return array
541
     */
542
    public function getVariables(): array
543
    {
544
        return $this->properties;
545
    }
546
547
    /**
548
     * Set a variable.
549
     *
550
     * @param $name
551
     * @param $value
552
     *
553
     * @throws \Exception
554
     *
555
     * @return $this
556
     */
557
    public function setVariable($name, $value)
558
    {
559
        if (is_bool($value)) {
560
            $value = $value ?: 0;
561
        }
562
        switch ($name) {
563
            case 'date':
564
                try {
565
                    $date = Util::dateToDatetime($value);
566
                } catch (\Exception $e) {
567
                    throw new \Exception(sprintf(
568
                        'Expected date format (ie: "2012-10-08") for "date" in "%s" instead of "%s"',
569
                        $this->getId(),
570
                        $value
571
                    ));
572
                }
573
                $this->offsetSet('date', $date);
574
                break;
575
            case 'draft':
576
                if ($value === true) {
577
                    $this->offsetSet('published', false);
578
                }
579
                break;
580
            case 'path':
581
            case 'slug':
582
                $slugify = self::slugify($value);
583
                if ($value != $slugify) {
584
                    throw new \Exception(sprintf(
585
                        '"%s" variable should be "%s" (not "%s") in "%s"',
586
                        $name,
587
                        $slugify,
588
                        $value,
589
                        $this->getId()
590
                    ));
591
                }
592
                // @see setPath()
593
                // @see setSlug()
594
                $method = 'set'.\ucfirst($name);
595
                $this->$method($value);
596
                break;
597
            default:
598
                $this->offsetSet($name, $value);
599
        }
600
601
        return $this;
602
    }
603
604
    /**
605
     * Is variable exist?
606
     *
607
     * @param string $name
608
     *
609
     * @return bool
610
     */
611
    public function hasVariable(string $name): bool
612
    {
613
        return $this->offsetExists($name);
614
    }
615
616
    /**
617
     * Get a variable.
618
     *
619
     * @param string $name
620
     *
621
     * @return mixed|null
622
     */
623
    public function getVariable(string $name)
624
    {
625
        if ($this->offsetExists($name)) {
626
            return $this->offsetGet($name);
627
        }
628
    }
629
630
    /**
631
     * Unset a variable.
632
     *
633
     * @param string $name
634
     *
635
     * @return $this
636
     */
637
    public function unVariable(string $name): self
638
    {
639
        if ($this->offsetExists($name)) {
640
            $this->offsetUnset($name);
641
        }
642
643
        return $this;
644
    }
645
}
646