Completed
Push — master ( e6222a...4bc239 )
by Vladimir
02:26
created

ContentItem::getPathPermalink()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 2
eloc 8
c 2
b 1
f 0
nc 2
nop 0
dl 0
loc 18
rs 9.4285
ccs 0
cts 9
cp 0
crap 6
1
<?php
2
3
namespace allejo\stakx\Object;
4
5
use allejo\stakx\Engines\MarkdownEngine;
6
use allejo\stakx\Engines\RstEngine;
7
use allejo\stakx\System\Filesystem;
8
use allejo\stakx\Exception\YamlVariableUndefinedException;
9
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
10
use Symfony\Component\Filesystem\Exception\IOException;
11
use Symfony\Component\Yaml\Yaml;
12
13
class ContentItem
14
{
15
    /**
16
     * Set to true if the permalink has been sanitized
17
     *
18
     * @var bool
19
     */
20
    protected $permalinkEvaluated;
21
22
    /**
23
     * Set to true if the front matter has already been evaluated with variable interpolation
24
     *
25
     * @var bool
26
     */
27
    protected $frontMatterEvaluated;
28
29
    /**
30
     * An array containing the Yaml of the file
31
     *
32
     * @var array
33
     */
34
    protected $frontMatter;
35
36
    /**
37
     * Set to true if the body has already been parsed as markdown or any other format
38
     *
39
     * @var bool
40
     */
41
    protected $bodyContentEvaluated;
42
43
    /**
44
     * Only the body of the file, i.e. the content
45
     *
46
     * @var string
47
     */
48
    protected $bodyContent;
49
50
    /**
51
     * The extension of the file
52
     *
53
     * @var string
54
     */
55
    protected $extension;
56
57
    /**
58
     * The original file path to the ContentItem
59
     *
60
     * @var string
61
     */
62
    protected $filePath;
63
64
    /**
65
     * A filesystem object
66
     *
67
     * @var Filesystem
68
     */
69
    protected $fs;
70
71
    /**
72
     * ContentItem constructor.
73
     *
74
     * @param string $filePath The path to the file that will be parsed into a ContentItem
75
     *
76
     * @throws FileNotFoundException The given file path does not exist
77
     * @throws IOException           The file was not a valid ContentItem. This would meam there was no front matter or
78
     *                               no body
79
     */
80 24
    public function __construct ($filePath)
81
    {
82 24
        $this->filePath = $filePath;
83 24
        $this->fs       = new Filesystem();
84
85 24
        if (!$this->fs->exists($filePath))
86 24
        {
87 1
            throw new FileNotFoundException("The following file could not be found: ${filePath}");
88
        }
89
90
        $this->extension = strtolower($this->fs->getExtension($filePath));
91
        $rawFileContents = file_get_contents($filePath);
92
93
        $frontMatter = array();
94
        preg_match('/---(.*?)---(.*)/s', $rawFileContents, $frontMatter);
95
96 View Code Duplication
        if (count($frontMatter) != 3)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
97
        {
98
            throw new IOException(sprintf("'%s' is not a valid ContentItem",
99
                $this->fs->getFileName($filePath))
100
            );
101
        }
102
103 View Code Duplication
        if (empty(trim($frontMatter[2])))
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
104
        {
105
            throw new IOException(sprintf('A ContentItem (%s) must have a body to render',
106
                $this->fs->getFileName($filePath))
107
            );
108
        }
109
110
        $this->frontMatter = Yaml::parse($frontMatter[1]);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Symfony\Component\Yaml\...:parse($frontMatter[1]) can also be of type string or object<stdClass>. However, the property $frontMatter is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
111
        $this->bodyContent = trim($frontMatter[2]);
112
113
        $this->handleSpecialFrontMatter();
114
    }
115
116
    /**
117
     * The magic getter returns values from the front matter in order to make these values accessible to Twig templates
118
     * in a simple fashion
119
     *
120
     * @param  string $name The key in the front matter
121
     *
122
     * @return mixed|null
123
     */
124
    public function __get ($name)
125
    {
126 2
        return (array_key_exists($name, $this->frontMatter) ? $this->frontMatter[$name] : null);
127
    }
128
129
    /**
130
     * The magic getter returns true if the value exists in the Front Matter. This is used in conjunction with the __get
131
     * function
132
     *
133
     * @param  string $name The name of the Front Matter value being looked for
134
     *
135
     * @return bool
136
     */
137
    public function __isset ($name)
138
    {
139 1
        return array_key_exists($name, $this->frontMatter);
140
    }
141
142
    /**
143
     * @param array|null $variables An array of YAML variables to use in evaluating the `$permalink` value
144
     */
145
    public function evaluateFrontMatter ($variables = null)
146
    {
147 2
        if (!is_null($variables))
148 2
        {
149 2
            $this->frontMatter = array_merge($this->frontMatter, $variables);
150 2
            $this->handleSpecialFrontMatter();
151 2
            $this->evaluateYaml($this->frontMatter);
152 2
        }
153 2
    }
154
155
    /**
156
     * Return the body of the Content Item parsed as markdown
157
     *
158
     * @return string
159
     */
160
    public function getContent ()
161
    {
162 3
        if (!$this->bodyContentEvaluated)
163 3
        {
164 3
            $twig = Website::getTwigInstance();
165
166 3
            if ($twig instanceof \Twig_Environment)
167 3
            {
168
                $template = $twig->createTemplate($this->bodyContent);
169
                $this->bodyContent = $template->render(array());
170
            }
171
172 3
            switch ($this->extension)
173
            {
174 3
                case "md":
175 3
                case "markdown":
176 1
                    $pd = new MarkdownEngine();
177 1
                    break;
178
179 2
                case "rst":
180 1
                    $pd = new RstEngine();
181 1
                    break;
182
183 1
                default:
184 1
                    $pd = null;
185 1
                    break;
186 3
            }
187
188 3
            if (!is_null($pd)) // No parser needed
189 3
            {
190 2
                $this->bodyContent = $pd->parse($this->bodyContent);
191 2
            }
192
193 3
            $this->bodyContentEvaluated = true;
194 3
        }
195
196 3
        return (string)$this->bodyContent;
197
    }
198
199
    /**
200
     * Get the Front Matter of a ContentItem as an array
201
     *
202
     * @param  bool $evaluateYaml When set to true, the YAML will be evaluated for variables
203
     *
204
     * @return array
205
     */
206
    final public function getFrontMatter ($evaluateYaml = true)
207
    {
208 6
        if ($this->frontMatter === null)
209 6
        {
210 1
            $this->frontMatter = array();
211 1
        }
212 5
        else if (!$this->frontMatterEvaluated && $evaluateYaml && !empty($evaluateYaml))
213 5
        {
214 5
            $this->evaluateYaml($this->frontMatter);
215 4
            $this->frontMatterEvaluated = true;
216 4
        }
217
218 5
        return $this->frontMatter;
219
    }
220
221
    /**
222
     * Get the permalink of this Content Item
223
     *
224
     * @return string
225
     */
226
    final public function getPermalink ()
227
    {
228 7
        if ($this->permalinkEvaluated)
229 7
        {
230 2
            return $this->frontMatter['permalink'];
231
        }
232
233 7
        $permalink = (!array_key_exists('permalink', $this->frontMatter)) ?  $this->getPathPermalink() : $this->frontMatter['permalink'];
234
235 4
        $this->frontMatter['permalink'] = $this->sanitizePermalink($permalink);
236 4
        $this->permalinkEvaluated = true;
237
238 4
        return $this->frontMatter['permalink'];
239
    }
240
241
    /**
242
     * Get the destination of where this Content Item would be written to when the website is compiled
243
     *
244
     * @return string
245
     */
246
    final public function getTargetFile ()
247
    {
248 5
        $extension  = $this->fs->getExtension($this->getPermalink());
249 2
        $targetFile = $this->getPermalink();
250
251 2
        if (empty($extension))
252 2
        {
253 1
            $targetFile = rtrim($this->getPermalink(), '/') . '/index.html';
254 1
        }
255
256 2
        return ltrim($targetFile, '/');
257
    }
258
259
    /**
260
     * Get the original file path
261
     *
262
     * @return string
263
     */
264
    final public function getFilePath ()
265
    {
266 1
        return $this->filePath;
267
    }
268
269
    /**
270
     * Evaluate an array of data for FrontMatter variables. This function will modify the array in place.
271
     *
272
     * @param  array $yaml An array of data containing FrontMatter variables
273
     *
274
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
275
     */
276
    final protected function evaluateYaml (&$yaml)
277
    {
278 7
        foreach ($yaml as $key => $value)
279
        {
280 7
            if (is_array($yaml[$key]))
281 7
            {
282 1
                $this->evaluateYaml($yaml[$key]);
283 1
            }
284
            else
285
            {
286 7
                $yaml[$key] = $this->evaluateYamlVar($value, $this->frontMatter);
287
            }
288 6
        }
289 6
    }
290
291
    /**
292
     * Evaluate an string for FrontMatter variables and replace them with the corresponding values
293
     *
294
     * @param  string $string The string that will be evaluated
295
     * @param  array  $yaml   The existing front matter from which the variable values will be pulled from
296
     *
297
     * @return string The final string with variables evaluated
298
     *
299
     * @throws YamlVariableUndefinedException A FrontMatter variable used does not exist
300
     */
301
    final protected static function evaluateYamlVar ($string, $yaml)
302
    {
303 7
        $variables = array();
304 7
        $varRegex  = '/(%[a-zA-Z]+)/';
305 7
        $output    = $string;
306
307 7
        preg_match_all($varRegex, $string, $variables);
308
309
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
310
        // matching result individually.
311 7
        foreach ($variables[1] as $variable)
312
        {
313 6
            $yamlVar = substr($variable, 1); // Trim the '%' from the YAML variable name
314
315 6
            if (!array_key_exists($yamlVar, $yaml))
316 6
            {
317 1
                throw new YamlVariableUndefinedException("Yaml variable `$variable` is not defined");
318
            }
319
320 5
            $output = str_replace($variable, $yaml[$yamlVar], $output);
321 6
        }
322
323 6
        return $output;
324
    }
325
326
    /**
327
     * Handle special front matter values that need special treatment or have special meaning to a Content Item
328
     */
329
    private function handleSpecialFrontMatter ()
330
    {
331 20
        if (isset($this->frontMatter['date']))
332 20
        {
333
            try
334
            {
335
                // Coming from a string variable
336 3
                $itemDate = new \DateTime($this->frontMatter['date']);
337
            }
338 3
            catch (\Exception $e)
339
            {
340
                // YAML has parsed them to Epoch time
341 1
                $itemDate = \DateTime::createFromFormat('U', $this->frontMatter['date']);
342
            }
343
344 3
            if (!$itemDate === false)
345 3
            {
346 2
                $this->frontMatter['year']  = $itemDate->format('Y');
347 2
                $this->frontMatter['month'] = $itemDate->format('m');
348 2
                $this->frontMatter['day']   = $itemDate->format('d');
349 2
            }
350 3
        }
351 20
    }
352
353
    /**
354
     * Get the permalink based off the location of where the file is relative to the website. This permalink is to be
355
     * used as a fallback in the case that a permalink is not explicitly specified in the Front Matter.
356
     *
357
     * @return string
358
     */
359
    private function getPathPermalink ()
360
    {
361
        // Remove the protocol of the path, if there is one and prepend a '/' to the beginning
362
        $cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->filePath);
363
        $cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR);
364
365
        // Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx
366
        $folders = explode('/', $cleanPath);
367
368
        if (substr($folders[0], 0, 1) === '_')
369
        {
370
            array_shift($folders);
371
        }
372
373
        $cleanPath = implode(DIRECTORY_SEPARATOR, $folders);
374
375
        return $cleanPath;
376
    }
377
378
    /**
379
     * Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens
380
     *
381
     * @param  string $permalink A permalink
382
     *
383
     * @return string $permalink The sanitized permalink
384
     */
385
    private function sanitizePermalink ($permalink)
386
    {
387
        // Remove multiple '/' together
388 4
        $permalink = preg_replace('/\/+/', '/', $permalink);
389
390
        // Replace all spaces with hyphens
391 4
        $permalink = str_replace(' ', '-', $permalink);
392
393
        // Remove all disallowed characters
394 4
        $permalink = preg_replace('/[^0-9a-zA-Z-_\/\.]/', '', $permalink);
395
396
        // Handle unnecessary extensions
397 4
        $extensionsToStrip = array('twig');
398
399 4
        if (in_array($this->fs->getExtension($permalink), $extensionsToStrip))
400 4
        {
401
            $permalink = $this->fs->removeExtension($permalink);
402
        }
403
404
        // Remove a special './' combination from the beginning of a path
405 4
        if (substr($permalink, 0, 2) === './')
406 4
        {
407
            $permalink = substr($permalink, 2);
408
        }
409
410 4
        return $permalink;
411
    }
412
}