Completed
Push — feature-output-formats ( 3ecf64...dbfc4a )
by Arnaud
02:23
created

Page::geSlug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
namespace Cecil\Collection\Page;
10
11
use Cecil\Collection\Item;
12
use Cecil\Config;
13
use Cecil\Page\Parser;
14
use Cecil\Page\Prefix;
15
use Cecil\Page\Type;
16
use Cecil\Util;
17
use Cocur\Slugify\Slugify;
18
use Symfony\Component\Finder\SplFileInfo;
19
20
/**
21
 * Class Page.
22
 */
23
class Page extends Item
24
{
25
    const SLUGIFY_PATTERN = '/(^\/|[^a-z0-9\/]|-)+/';
26
27
    /**
28
     * @var SplFileInfo
29
     */
30
    protected $file;
31
    /**
32
     * @var bool
33
     */
34
    protected $virtual = false;
35
    /**
36
     * @var string
37
     */
38
    protected $type;
39
    /**
40
     * @var string
41
     */
42
    protected $relpath;
43
    /**
44
     * @var string
45
     */
46
    protected $path;
47
    /**
48
     * @var string
49
     */
50
    protected $slug;
51
    /**
52
     * @var string
53
     */
54
    protected $section;
55
    /**
56
     * @var string
57
     */
58
    protected $frontmatter;
59
    /**
60
     * @var string
61
     */
62
    protected $body;
63
    /**
64
     * @var string
65
     */
66
    protected $html;
67
68
    /**
69
     * Constructor.
70
     *
71
     * @param SplFileInfo|null $file
72
     */
73
    public function __construct(SplFileInfo $file = null)
74
    {
75
        $this->file = $file;
76
77
        // physical page
78
        if ($this->file instanceof SplFileInfo) {
79
            /*
80
             * File path components
81
             */
82
            // ie: content/Blog/Post 1.md
83
            //             |    |      └─ fileExtension
84
            //             |    └─ fileName
85
            //             └─ filePath
86
            $fileExtension = pathinfo($this->file, PATHINFO_EXTENSION);
87
            $filePath = str_replace(DIRECTORY_SEPARATOR, '/', $this->file->getRelativePath());
88
            $fileName = $this->file->getBasename('.'.$fileExtension);
89
            // filePathname = filePath + '/' + fileName
90
            // ie: content/Blog/Post 1.md -> "Blog/Post 1"
91
            // ie: content/index.md -> "index"
92
            // ie: content/Blog/index.md -> "Blog/"
93
            $filePathname = ($filePath ? $filePath.'/' : '')
94
                .($filePath && $fileName == 'index' ? '' : $fileName);
95
            /*
96
             * Set properties
97
             *
98
             * id = path = relpath / slug
99
             */
100
            $this->relpath = $this->slugify($filePath); // ie: "blog"
101
            $this->slug = $this->slugify(Prefix::subPrefix($fileName)); // ie: "post-1"
102
            $this->path = $this->slugify(Prefix::subPrefix($filePathname)); // ie: "blog/post-1"
103
            $this->id = $this->path;
104
            /*
105
             * Set protected variables
106
             */
107
            $this->setSection(explode('/', $this->relpath)[0]); // ie: "blog"
108
            /*
109
             * Set overridable variables
110
             */
111
            $this->setVariable('title', Prefix::subPrefix($fileName)); // ie: "Post 1"
112
            $this->setVariable('date', $this->file->getMTime());
113
            $this->setVariable('weight', null);
114
            // special case: file has a prefix
115
            if (Prefix::hasPrefix($filePathname)) {
116
                // prefix is a valid date?
117
                if (Util::isValidDate(Prefix::getPrefix($filePathname))) {
118
                    $this->setVariable('date', (string) Prefix::getPrefix($filePathname));
119
                } else {
120
                    // prefix is an integer, use for sorting
121
                    $this->setVariable('weight', (int) Prefix::getPrefix($filePathname));
122
                }
123
            }
124
            // physical file relative path
125
            $this->setVariable('filepathname', $this->file->getRelativePathname());
126
127
            parent::__construct($this->id);
128
        } else {
129
            // virtual page
130
            $this->virtual = true;
131
            // default variables
132
            $this->setVariables([
133
                'title'  => 'Page Title',
134
                'date'   => time(),
135
                'weight' => null,
136
            ]);
137
138
            parent::__construct();
139
        }
140
        // required
141
        $this->setType(Type::PAGE);
142
        $this->setVariables([
143
            'published'        => true,
144
            'virtual'          => $this->virtual,
145
            'content_template' => 'page.content.twig',
146
        ]);
147
    }
148
149
    /**
150
     * Parse file content.
151
     *
152
     * @return self
153
     */
154
    public function parse(): self
155
    {
156
        $parser = new Parser($this->file);
157
        $parsed = $parser->parse();
158
        $this->frontmatter = $parsed->getFrontmatter();
159
        $this->body = $parsed->getBody();
160
161
        return $this;
162
    }
163
164
    /**
165
     * Get frontmatter.
166
     *
167
     * @return string|null
168
     */
169
    public function getFrontmatter(): ?string
170
    {
171
        return $this->frontmatter;
172
    }
173
174
    /**
175
     * Get body as raw.
176
     *
177
     * @return string
178
     */
179
    public function getBody(): ?string
180
    {
181
        return $this->body;
182
    }
183
184
    /**
185
     * Turn a path (string) into a slug (URL).
186
     *
187
     * @param string $path
188
     *
189
     * @return string
190
     */
191
    public static function slugify(string $path): string
192
    {
193
        return Slugify::create([
194
            'regexp' => self::SLUGIFY_PATTERN,
195
        ])->slugify($path);
196
    }
197
198
    /**
199
     * Is current page is virtual?
200
     *
201
     * @return bool
202
     */
203
    public function isVirtual(): bool
204
    {
205
        return $this->virtual;
206
    }
207
208
    /**
209
     * Set page type.
210
     *
211
     * @param string $type
212
     *
213
     * @return self
214
     */
215
    public function setType(string $type): self
216
    {
217
        $this->type = new Type($type);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Cecil\Page\Type($type) of type object<Cecil\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...
218
219
        return $this;
220
    }
221
222
    /**
223
     * Get page type.
224
     *
225
     * @return string|null
226
     */
227
    public function getType(): ?string
228
    {
229
        return $this->type;
230
    }
231
232
    /**
233
     * Set slug.
234
     *
235
     * @param string $slug
236
     *
237
     * @return self
238
     */
239
    public function setSlug(string $slug): self
240
    {
241
        $this->slug = $slug;
242
243
        return $this;
244
    }
245
246
    /**
247
     * Get slug.
248
     *
249
     * @return string|null
250
     */
251
    public function geSlug(): ?string
252
    {
253
        return $this->slug;
254
    }
255
256
    /**
257
     * Set relative path.
258
     *
259
     * @param $relpath
260
     *
261
     * @return self
262
     */
263
    public function setRelPath(string $relpath): self
264
    {
265
        $this->relpath = $relpath;
266
267
        return $this;
268
    }
269
270
    /**
271
     * Get relative path.
272
     *
273
     * @return string
274
     */
275
    public function getRelPath(): string
276
    {
277
        return $this->relpath;
278
    }
279
280
    /**
281
     * Set path.
282
     *
283
     * @param string $path
284
     *
285
     * @return self
286
     */
287
    public function setPath(string $path): self
288
    {
289
        $this->path = $path;
290
291
        return $this;
292
    }
293
294
    /**
295
     * Get path.
296
     *
297
     * @return string|null
298
     */
299
    public function getPath(): ?string
300
    {
301
        return $this->path;
302
    }
303
304
    /**
305
     * Set section.
306
     *
307
     * @param string $section
308
     *
309
     * @return self
310
     */
311
    public function setSection(string $section): self
312
    {
313
        $this->section = $section;
314
315
        return $this;
316
    }
317
318
    /**
319
     * Get section.
320
     *
321
     * @return string|null
322
     */
323
    public function getSection(): ?string
324
    {
325
        if (empty($this->section) && !empty($this->relpath)) {
326
            $this->setSection(explode('/', $this->relpath)[0]);
327
        }
328
329
        return $this->section;
330
    }
331
332
    /**
333
     * Set body as HTML.
334
     *
335
     * @param string $html
336
     *
337
     * @return self
338
     */
339
    public function setBodyHtml(string $html): self
340
    {
341
        $this->html = $html;
342
343
        return $this;
344
    }
345
346
    /**
347
     * Get body as HTML.
348
     *
349
     * @return string|null
350
     */
351
    public function getBodyHtml(): ?string
352
    {
353
        return $this->html;
354
    }
355
356
    /**
357
     * @see getBodyHtml()
358
     *
359
     * @return string|null
360
     */
361
    public function getContent(): ?string
362
    {
363
        return $this->getBodyHtml();
364
    }
365
366
    /**
367
     * Return output file.
368
     *
369
     * Use cases:
370
     *   - default: path + suffix + extension (ie: blog/post-1/index.html)
371
     *   - subpath: path + subpath + suffix + extension (ie: blog/post-1/amp/index.html)
372
     *   - ugly: path + extension (ie: 404.html, sitemap.xml, robots.txt)
373
     *   - path only (ie: _redirects)
374
     *
375
     * @param string $format
376
     * @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...
377
     *
378
     * @return string
379
     */
380
    public function getOutputFile(string $format, Config $config = null): string
381
    {
382
        $path = $this->getPath();
383
        $subpath = '';
384
        $suffix = '/index';
385
        $extension = 'html';
386
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
387
388
        // site config
389
        if ($config) {
390
            $subpath = $config->get(sprintf('site.output.formats.%s.subpath', $format));
391
            $suffix = $config->get(sprintf('site.output.formats.%s.suffix', $format));
392
            $extension = $config->get(sprintf('site.output.formats.%s.extension', $format));
393
        }
394
        // if ugly URL: not suffix
395
        if ($uglyurl) {
396
            $suffix = '';
397
        }
398
        // format strings
399
        if ($subpath) {
400
            $subpath = sprintf('/%s', ltrim($subpath, '/'));
401
        }
402
        if ($suffix) {
403
            $suffix = sprintf('/%s', ltrim($suffix, '/'));
404
        }
405
        if ($extension) {
406
            $extension = sprintf('.%s', $extension);
407
        }
408
        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...
409
            $path = 'index'; // home page
410
        }
411
412
        return $path.$subpath.$suffix.$extension;
413
    }
414
415
    /**
416
     * Return URL.
417
     *
418
     * @param string $format
419
     * @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...
420
     *
421
     * @return string
422
     */
423
    public function getUrl(string $format = 'html', Config $config = null): string
424
    {
425
        $uglyurl = $this->getVariable('uglyurl') ? true : false;
426
        $output = $this->getOutputFile($format, $config);
427
428
        if (!$uglyurl) {
429
            $output = str_replace('index.html', '', $output);
430
        }
431
432
        return $output;
433
    }
434
435
    /*
436
     * Helper to set and get variables.
437
     */
438
439
    /**
440
     * Set an array as variables.
441
     *
442
     * @param array $variables
443
     *
444
     * @throws \Exception
445
     *
446
     * @return $this
447
     */
448
    public function setVariables($variables)
449
    {
450
        if (!is_array($variables)) {
451
            throw new \Exception('Can\'t set variables: parameter is not an array');
452
        }
453
        foreach ($variables as $key => $value) {
454
            $this->setVariable($key, $value);
455
        }
456
457
        return $this;
458
    }
459
460
    /**
461
     * Get all variables.
462
     *
463
     * @return array
464
     */
465
    public function getVariables(): array
466
    {
467
        return $this->properties;
468
    }
469
470
    /**
471
     * Set a variable.
472
     *
473
     * @param $name
474
     * @param $value
475
     *
476
     * @throws \Exception
477
     *
478
     * @return $this
479
     */
480
    public function setVariable($name, $value)
481
    {
482
        if (is_bool($value)) {
483
            $value = $value ?: 0;
484
        }
485
        switch ($name) {
486
            case 'date':
487
                try {
488
                    if ($value instanceof \DateTime) {
489
                        $date = $value;
490
                    } else {
491
                        // timestamp
492
                        if (is_numeric($value)) {
493
                            $date = (new \DateTime())->setTimestamp($value);
494
                        } else {
495
                            // ie: 2019-01-01
496
                            if (is_string($value)) {
497
                                $date = new \DateTime($value);
498
                            }
499
                        }
500
                    }
501
                } catch (\Exception $e) {
502
                    throw new \Exception(sprintf("Expected date string in page '%s'", $this->getId()));
503
                }
504
                $this->offsetSet('date', $date);
0 ignored issues
show
Bug introduced by
The variable $date does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
505
                break;
506
            case 'draft':
507
                if ($value === true) {
508
                    $this->offsetSet('published', false);
509
                }
510
                break;
511
            case 'url':
512
                $slug = self::slugify($value);
513
                if ($value != $slug) {
514
                    throw new \Exception(sprintf("'url' variable should be '%s', not '%s', in page '%s'", $slug, $value, $this->getId()));
515
                }
516
                break;
517
            default:
518
                $this->offsetSet($name, $value);
519
        }
520
521
        return $this;
522
    }
523
524
    /**
525
     * Is variable exist?
526
     *
527
     * @param $name
528
     *
529
     * @return bool
530
     */
531
    public function hasVariable($name)
532
    {
533
        return $this->offsetExists($name);
534
    }
535
536
    /**
537
     * Get a variable.
538
     *
539
     * @param string $name
540
     *
541
     * @return mixed|null
542
     */
543
    public function getVariable($name)
544
    {
545
        if ($this->offsetExists($name)) {
546
            return $this->offsetGet($name);
547
        }
548
    }
549
550
    /**
551
     * Unset a variable.
552
     *
553
     * @param $name
554
     *
555
     * @return $this
556
     */
557
    public function unVariable($name)
558
    {
559
        if ($this->offsetExists($name)) {
560
            $this->offsetUnset($name);
561
        }
562
563
        return $this;
564
    }
565
}
566