|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace allejo\stakx\Object; |
|
4
|
|
|
|
|
5
|
|
|
use allejo\stakx\System\Filesystem; |
|
6
|
|
|
use allejo\stakx\Exception\YamlVariableUndefinedException; |
|
7
|
|
|
use Symfony\Component\Filesystem\Exception\FileNotFoundException; |
|
8
|
|
|
use Symfony\Component\Filesystem\Exception\IOException; |
|
9
|
|
|
use Symfony\Component\Yaml\Yaml; |
|
10
|
|
|
|
|
11
|
|
|
abstract class FrontMatterObject |
|
12
|
|
|
{ |
|
13
|
|
|
/** |
|
14
|
|
|
* An array to keep track of collection or data dependencies used inside of a Twig template |
|
15
|
|
|
* |
|
16
|
|
|
* $dataDependencies['collections'] = array() |
|
17
|
|
|
* $dataDependencies['data'] = array() |
|
18
|
|
|
* |
|
19
|
|
|
* @var array |
|
20
|
|
|
*/ |
|
21
|
|
|
protected $dataDependencies; |
|
22
|
|
|
|
|
23
|
|
|
/** |
|
24
|
|
|
* A list of Front Matter values that should not be returned directly from the $frontMatter array. Values listed |
|
25
|
|
|
* here have dedicated functions that handle those Front Matter values and the respective functions should be called |
|
26
|
|
|
* instead. |
|
27
|
|
|
* |
|
28
|
|
|
* @var string[] |
|
29
|
|
|
*/ |
|
30
|
|
|
protected $frontMatterBlacklist; |
|
31
|
|
|
|
|
32
|
|
|
/** |
|
33
|
|
|
* Set to true if the permalink has been sanitized |
|
34
|
|
|
* |
|
35
|
|
|
* @var bool |
|
36
|
|
|
*/ |
|
37
|
|
|
protected $permalinkEvaluated; |
|
38
|
|
|
|
|
39
|
|
|
/** |
|
40
|
|
|
* Set to true if the front matter has already been evaluated with variable interpolation |
|
41
|
|
|
* |
|
42
|
|
|
* @var bool |
|
43
|
|
|
*/ |
|
44
|
|
|
protected $frontMatterEvaluated; |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* An array containing the Yaml of the file |
|
48
|
|
|
* |
|
49
|
|
|
* @var array |
|
50
|
|
|
*/ |
|
51
|
|
|
protected $frontMatter; |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* Set to true if the body has already been parsed as markdown or any other format |
|
55
|
|
|
* |
|
56
|
|
|
* @var bool |
|
57
|
|
|
*/ |
|
58
|
|
|
protected $bodyContentEvaluated; |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* Only the body of the file, i.e. the content |
|
62
|
|
|
* |
|
63
|
|
|
* @var string |
|
64
|
|
|
*/ |
|
65
|
|
|
protected $bodyContent; |
|
66
|
|
|
|
|
67
|
|
|
/** |
|
68
|
|
|
* The extension of the file |
|
69
|
|
|
* |
|
70
|
|
|
* @var string |
|
71
|
|
|
*/ |
|
72
|
|
|
protected $extension; |
|
73
|
|
|
|
|
74
|
|
|
/** |
|
75
|
|
|
* The original file path to the ContentItem |
|
76
|
|
|
* |
|
77
|
|
|
* @var string |
|
78
|
|
|
*/ |
|
79
|
|
|
protected $filePath; |
|
80
|
|
|
|
|
81
|
|
|
/** |
|
82
|
|
|
* A filesystem object |
|
83
|
|
|
* |
|
84
|
|
|
* @var Filesystem |
|
85
|
|
|
*/ |
|
86
|
|
|
protected $fs; |
|
87
|
|
|
|
|
88
|
|
|
/** |
|
89
|
|
|
* ContentItem constructor. |
|
90
|
|
|
* |
|
91
|
|
|
* @param string $filePath The path to the file that will be parsed into a ContentItem |
|
92
|
|
|
* |
|
93
|
|
|
* @throws FileNotFoundException The given file path does not exist |
|
94
|
|
|
* @throws IOException The file was not a valid ContentItem. This would meam there was no front matter or |
|
95
|
|
|
* no body |
|
96
|
|
|
*/ |
|
97
|
28 |
|
public function __construct ($filePath) |
|
98
|
|
|
{ |
|
99
|
28 |
|
$this->frontMatterBlacklist = array('permalink'); |
|
100
|
|
|
|
|
101
|
28 |
|
$this->filePath = $filePath; |
|
102
|
28 |
|
$this->fs = new Filesystem(); |
|
103
|
|
|
|
|
104
|
28 |
|
if (!$this->fs->exists($filePath)) |
|
105
|
28 |
|
{ |
|
106
|
1 |
|
throw new FileNotFoundException("The following file could not be found: ${filePath}"); |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
$this->extension = strtolower($this->fs->getExtension($filePath)); |
|
110
|
|
|
|
|
111
|
|
|
$this->refreshFileContent(); |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
/** |
|
115
|
|
|
* The magic getter returns values from the front matter in order to make these values accessible to Twig templates |
|
116
|
|
|
* in a simple fashion |
|
117
|
|
|
* |
|
118
|
|
|
* @param string $name The key in the front matter |
|
119
|
|
|
* |
|
120
|
|
|
* @return mixed|null |
|
121
|
|
|
*/ |
|
122
|
|
|
public function __get ($name) |
|
123
|
|
|
{ |
|
124
|
3 |
|
return (array_key_exists($name, $this->frontMatter) ? $this->frontMatter[$name] : null); |
|
125
|
|
|
} |
|
126
|
|
|
|
|
127
|
|
|
/** |
|
128
|
|
|
* The magic getter returns true if the value exists in the Front Matter. This is used in conjunction with the __get |
|
129
|
|
|
* function |
|
130
|
|
|
* |
|
131
|
|
|
* @param string $name The name of the Front Matter value being looked for |
|
132
|
|
|
* |
|
133
|
|
|
* @return bool |
|
134
|
|
|
*/ |
|
135
|
|
|
public function __isset ($name) |
|
136
|
|
|
{ |
|
137
|
1 |
|
return (!in_array($name, $this->frontMatterBlacklist)) && array_key_exists($name, $this->frontMatter); |
|
138
|
|
|
} |
|
139
|
|
|
|
|
140
|
|
|
/** |
|
141
|
|
|
* Return the body of the Content Item |
|
142
|
|
|
* |
|
143
|
|
|
* @return string |
|
144
|
|
|
*/ |
|
145
|
|
|
abstract public function getContent (); |
|
146
|
|
|
|
|
147
|
|
|
/** |
|
148
|
|
|
* @param array|null $variables An array of YAML variables to use in evaluating the `$permalink` value |
|
149
|
|
|
*/ |
|
150
|
|
|
final public function evaluateFrontMatter ($variables = null) |
|
151
|
|
|
{ |
|
152
|
2 |
|
if (!is_null($variables)) |
|
153
|
2 |
|
{ |
|
154
|
2 |
|
$this->frontMatter = array_merge($this->frontMatter, $variables); |
|
155
|
2 |
|
$this->handleSpecialFrontMatter(); |
|
156
|
2 |
|
$this->evaluateYaml($this->frontMatter); |
|
157
|
2 |
|
} |
|
158
|
2 |
|
} |
|
159
|
|
|
|
|
160
|
|
|
/** |
|
161
|
|
|
* Get the Front Matter of a ContentItem as an array |
|
162
|
|
|
* |
|
163
|
|
|
* @param bool $evaluateYaml When set to true, the YAML will be evaluated for variables |
|
164
|
|
|
* |
|
165
|
|
|
* @return array |
|
166
|
|
|
*/ |
|
167
|
|
|
final public function getFrontMatter ($evaluateYaml = true) |
|
168
|
|
|
{ |
|
169
|
6 |
|
if ($this->frontMatter === null) |
|
170
|
6 |
|
{ |
|
171
|
1 |
|
$this->frontMatter = array(); |
|
172
|
1 |
|
} |
|
173
|
5 |
|
else if (!$this->frontMatterEvaluated && $evaluateYaml && !empty($evaluateYaml)) |
|
174
|
5 |
|
{ |
|
175
|
5 |
|
$this->evaluateYaml($this->frontMatter); |
|
176
|
4 |
|
$this->frontMatterEvaluated = true; |
|
177
|
4 |
|
} |
|
178
|
|
|
|
|
179
|
5 |
|
return $this->frontMatter; |
|
180
|
|
|
} |
|
181
|
|
|
|
|
182
|
|
|
/** |
|
183
|
|
|
* Get the permalink of this Content Item |
|
184
|
|
|
* |
|
185
|
|
|
* @return string |
|
186
|
|
|
*/ |
|
187
|
|
|
final public function getPermalink () |
|
188
|
|
|
{ |
|
189
|
7 |
|
if ($this->permalinkEvaluated) |
|
190
|
7 |
|
{ |
|
191
|
|
|
return $this->frontMatter['permalink']; |
|
192
|
|
|
} |
|
193
|
|
|
|
|
194
|
7 |
|
$permalink = (is_array($this->frontMatter) && array_key_exists('permalink', $this->frontMatter)) ? |
|
195
|
7 |
|
$this->frontMatter['permalink'] : $this->getPathPermalink(); |
|
196
|
|
|
|
|
197
|
7 |
|
$this->frontMatter['permalink'] = $this->sanitizePermalink($permalink); |
|
198
|
7 |
|
$this->permalinkEvaluated = true; |
|
199
|
|
|
|
|
200
|
7 |
|
return $this->frontMatter['permalink']; |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
/** |
|
204
|
|
|
* Get the destination of where this Content Item would be written to when the website is compiled |
|
205
|
|
|
* |
|
206
|
|
|
* @return string |
|
207
|
|
|
*/ |
|
208
|
|
|
final public function getTargetFile () |
|
209
|
|
|
{ |
|
210
|
5 |
|
$permalink = $this->getPermalink(); |
|
211
|
5 |
|
$extension = $this->fs->getExtension($permalink); |
|
212
|
|
|
|
|
213
|
5 |
|
if (empty($extension)) |
|
214
|
5 |
|
{ |
|
215
|
1 |
|
$permalink = rtrim($permalink, '/') . '/index.html'; |
|
216
|
1 |
|
} |
|
217
|
|
|
|
|
218
|
5 |
|
return ltrim($permalink, '/'); |
|
219
|
|
|
} |
|
220
|
|
|
|
|
221
|
|
|
/** |
|
222
|
|
|
* Get the name of the item, which is just the file name without the extension |
|
223
|
|
|
* |
|
224
|
|
|
* @return string |
|
225
|
|
|
*/ |
|
226
|
|
|
final public function getName () |
|
227
|
|
|
{ |
|
228
|
4 |
|
return $this->fs->getBaseName($this->filePath); |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
/** |
|
232
|
|
|
* Get the original file path |
|
233
|
|
|
* |
|
234
|
|
|
* @return string |
|
235
|
|
|
*/ |
|
236
|
|
|
final public function getFilePath () |
|
237
|
|
|
{ |
|
238
|
1 |
|
return $this->filePath; |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
/** |
|
242
|
|
|
* Get the relative path to this file relative to the root of the Stakx website |
|
243
|
|
|
* |
|
244
|
|
|
* @return string |
|
245
|
|
|
*/ |
|
246
|
|
|
final public function getRelativeFilePath () |
|
247
|
|
|
{ |
|
248
|
4 |
|
return $this->fs->getRelativePath($this->filePath); |
|
249
|
|
|
} |
|
250
|
|
|
|
|
251
|
|
|
/** |
|
252
|
|
|
* Read the file, and parse its contents |
|
253
|
|
|
*/ |
|
254
|
|
|
final public function refreshFileContent () |
|
255
|
|
|
{ |
|
256
|
27 |
|
$rawFileContents = file_get_contents($this->filePath); |
|
257
|
|
|
|
|
258
|
27 |
|
$frontMatter = array(); |
|
259
|
27 |
|
preg_match('/---(.*?)---(.*)/s', $rawFileContents, $frontMatter); |
|
260
|
|
|
|
|
261
|
27 |
View Code Duplication |
if (count($frontMatter) != 3) |
|
|
|
|
|
|
262
|
27 |
|
{ |
|
263
|
1 |
|
throw new IOException(sprintf("'%s' is not a valid ContentItem", |
|
264
|
1 |
|
$this->fs->getFileName($this->filePath)) |
|
265
|
1 |
|
); |
|
266
|
|
|
} |
|
267
|
|
|
|
|
268
|
26 |
View Code Duplication |
if (empty(trim($frontMatter[2]))) |
|
|
|
|
|
|
269
|
26 |
|
{ |
|
270
|
1 |
|
throw new IOException(sprintf('A ContentItem (%s) must have a body to render', |
|
271
|
1 |
|
$this->fs->getFileName($this->filePath)) |
|
272
|
1 |
|
); |
|
273
|
|
|
} |
|
274
|
|
|
|
|
275
|
25 |
|
$this->frontMatter = Yaml::parse($frontMatter[1]); |
|
|
|
|
|
|
276
|
24 |
|
$this->bodyContent = trim($frontMatter[2]); |
|
277
|
|
|
|
|
278
|
24 |
|
$this->frontMatterEvaluated = false; |
|
279
|
24 |
|
$this->bodyContentEvaluated = false; |
|
280
|
24 |
|
$this->permalinkEvaluated = false; |
|
281
|
|
|
|
|
282
|
24 |
|
$this->handleSpecialFrontMatter(); |
|
283
|
24 |
|
$this->findTwigDataDependencies('collections'); |
|
284
|
24 |
|
$this->findTwigDataDependencies('data'); |
|
285
|
24 |
|
} |
|
286
|
|
|
|
|
287
|
|
|
/** |
|
288
|
|
|
* Check whether this object has a reference to a collection or data item |
|
289
|
|
|
* |
|
290
|
|
|
* @param string $namespace 'collections' or 'data' |
|
291
|
|
|
* @param string $needle |
|
292
|
|
|
* |
|
293
|
|
|
* @return bool |
|
294
|
|
|
*/ |
|
295
|
|
|
final public function hasTwigDependency ($namespace, $needle) |
|
296
|
|
|
{ |
|
297
|
|
|
return (in_array($needle, $this->dataDependencies[$namespace])); |
|
298
|
|
|
} |
|
299
|
|
|
|
|
300
|
|
|
/** |
|
301
|
|
|
* Evaluate an array of data for FrontMatter variables. This function will modify the array in place. |
|
302
|
|
|
* |
|
303
|
|
|
* @param array $yaml An array of data containing FrontMatter variables |
|
304
|
|
|
* |
|
305
|
|
|
* @throws YamlVariableUndefinedException A FrontMatter variable used does not exist |
|
306
|
|
|
*/ |
|
307
|
|
|
final protected function evaluateYaml (&$yaml) |
|
308
|
|
|
{ |
|
309
|
7 |
|
foreach ($yaml as $key => $value) |
|
310
|
|
|
{ |
|
311
|
7 |
|
if (is_array($yaml[$key])) |
|
312
|
7 |
|
{ |
|
313
|
1 |
|
$this->evaluateYaml($yaml[$key]); |
|
314
|
1 |
|
} |
|
315
|
|
|
else |
|
316
|
|
|
{ |
|
317
|
7 |
|
$yaml[$key] = $this->evaluateYamlVar($value, $this->frontMatter); |
|
318
|
|
|
} |
|
319
|
6 |
|
} |
|
320
|
6 |
|
} |
|
321
|
|
|
|
|
322
|
|
|
/** |
|
323
|
|
|
* Evaluate an string for FrontMatter variables and replace them with the corresponding values |
|
324
|
|
|
* |
|
325
|
|
|
* @param string $string The string that will be evaluated |
|
326
|
|
|
* @param array $yaml The existing front matter from which the variable values will be pulled from |
|
327
|
|
|
* |
|
328
|
|
|
* @return string The final string with variables evaluated |
|
329
|
|
|
* |
|
330
|
|
|
* @throws YamlVariableUndefinedException A FrontMatter variable used does not exist |
|
331
|
|
|
*/ |
|
332
|
|
|
private function evaluateYamlVar ($string, $yaml) |
|
333
|
|
|
{ |
|
334
|
7 |
|
$variables = array(); |
|
335
|
7 |
|
$varRegex = '/(%[a-zA-Z]+)/'; |
|
336
|
7 |
|
$output = $string; |
|
337
|
|
|
|
|
338
|
7 |
|
preg_match_all($varRegex, $string, $variables); |
|
339
|
|
|
|
|
340
|
|
|
// Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each |
|
341
|
|
|
// matching result individually. |
|
342
|
7 |
|
foreach ($variables[1] as $variable) |
|
343
|
|
|
{ |
|
344
|
6 |
|
$yamlVar = substr($variable, 1); // Trim the '%' from the YAML variable name |
|
345
|
|
|
|
|
346
|
6 |
|
if (!array_key_exists($yamlVar, $yaml)) |
|
347
|
6 |
|
{ |
|
348
|
1 |
|
throw new YamlVariableUndefinedException("Yaml variable `$variable` is not defined"); |
|
349
|
|
|
} |
|
350
|
|
|
|
|
351
|
5 |
|
$output = str_replace($variable, $yaml[$yamlVar], $output); |
|
352
|
6 |
|
} |
|
353
|
|
|
|
|
354
|
6 |
|
return $output; |
|
355
|
|
|
} |
|
356
|
|
|
|
|
357
|
|
|
/** |
|
358
|
|
|
* Handle special front matter values that need special treatment or have special meaning to a Content Item |
|
359
|
|
|
*/ |
|
360
|
|
|
private function handleSpecialFrontMatter () |
|
361
|
|
|
{ |
|
362
|
24 |
|
if (isset($this->frontMatter['date'])) |
|
363
|
24 |
|
{ |
|
364
|
|
|
try |
|
365
|
|
|
{ |
|
366
|
|
|
// Coming from a string variable |
|
367
|
3 |
|
$itemDate = new \DateTime($this->frontMatter['date']); |
|
368
|
|
|
} |
|
369
|
3 |
|
catch (\Exception $e) |
|
370
|
|
|
{ |
|
371
|
|
|
// YAML has parsed them to Epoch time |
|
372
|
1 |
|
$itemDate = \DateTime::createFromFormat('U', $this->frontMatter['date']); |
|
373
|
|
|
} |
|
374
|
|
|
|
|
375
|
3 |
|
if (!$itemDate === false) |
|
376
|
3 |
|
{ |
|
377
|
2 |
|
$this->frontMatter['year'] = $itemDate->format('Y'); |
|
378
|
2 |
|
$this->frontMatter['month'] = $itemDate->format('m'); |
|
379
|
2 |
|
$this->frontMatter['day'] = $itemDate->format('d'); |
|
380
|
2 |
|
} |
|
381
|
3 |
|
} |
|
382
|
24 |
|
} |
|
383
|
|
|
|
|
384
|
|
|
/** |
|
385
|
|
|
* Get all of the references to either DataItems or ContentItems inside of given string |
|
386
|
|
|
* |
|
387
|
|
|
* @param string $filter 'collections' or 'data' |
|
388
|
|
|
*/ |
|
389
|
|
|
private function findTwigDataDependencies ($filter) |
|
390
|
|
|
{ |
|
391
|
24 |
|
$regex = '/{[{%](?:.+)?(?:' . $filter . ')(?:\.|\[\')(\w+)(?:\'\])?.+[%}]}/'; |
|
392
|
24 |
|
$results = array(); |
|
393
|
|
|
|
|
394
|
24 |
|
preg_match_all($regex, $this->bodyContent, $results); |
|
395
|
|
|
|
|
396
|
24 |
|
$this->dataDependencies[$filter] = array_unique($results[1]); |
|
397
|
24 |
|
} |
|
398
|
|
|
|
|
399
|
|
|
/** |
|
400
|
|
|
* Get the permalink based off the location of where the file is relative to the website. This permalink is to be |
|
401
|
|
|
* used as a fallback in the case that a permalink is not explicitly specified in the Front Matter. |
|
402
|
|
|
* |
|
403
|
|
|
* @return string |
|
404
|
|
|
*/ |
|
405
|
|
|
private function getPathPermalink () |
|
406
|
|
|
{ |
|
407
|
|
|
// Remove the protocol of the path, if there is one and prepend a '/' to the beginning |
|
408
|
3 |
|
$cleanPath = preg_replace('/[\w|\d]+:\/\//', '', $this->filePath); |
|
409
|
3 |
|
$cleanPath = ltrim($cleanPath, DIRECTORY_SEPARATOR); |
|
410
|
|
|
|
|
411
|
|
|
// Check the first folder and see if it's a data folder (starts with an underscore) intended for stakx |
|
412
|
3 |
|
$folders = explode('/', $cleanPath); |
|
413
|
|
|
|
|
414
|
3 |
|
if (substr($folders[0], 0, 1) === '_') |
|
415
|
3 |
|
{ |
|
416
|
1 |
|
array_shift($folders); |
|
417
|
1 |
|
} |
|
418
|
|
|
|
|
419
|
3 |
|
$cleanPath = implode(DIRECTORY_SEPARATOR, $folders); |
|
420
|
|
|
|
|
421
|
3 |
|
return $cleanPath; |
|
422
|
|
|
} |
|
423
|
|
|
|
|
424
|
|
|
/** |
|
425
|
|
|
* Sanitize a permalink to remove unsupported characters or multiple '/' and replace spaces with hyphens |
|
426
|
|
|
* |
|
427
|
|
|
* @param string $permalink A permalink |
|
428
|
|
|
* |
|
429
|
|
|
* @return string $permalink The sanitized permalink |
|
430
|
|
|
*/ |
|
431
|
|
|
private function sanitizePermalink ($permalink) |
|
432
|
|
|
{ |
|
433
|
|
|
// Remove multiple '/' together |
|
434
|
7 |
|
$permalink = preg_replace('/\/+/', '/', $permalink); |
|
435
|
|
|
|
|
436
|
|
|
// Replace all spaces with hyphens |
|
437
|
7 |
|
$permalink = str_replace(' ', '-', $permalink); |
|
438
|
|
|
|
|
439
|
|
|
// Remove all disallowed characters |
|
440
|
7 |
|
$permalink = preg_replace('/[^0-9a-zA-Z-_\/\.]/', '', $permalink); |
|
441
|
|
|
|
|
442
|
|
|
// Handle unnecessary extensions |
|
443
|
7 |
|
$extensionsToStrip = array('twig'); |
|
444
|
|
|
|
|
445
|
7 |
|
if (in_array($this->fs->getExtension($permalink), $extensionsToStrip)) |
|
446
|
7 |
|
{ |
|
447
|
3 |
|
$permalink = $this->fs->removeExtension($permalink); |
|
448
|
3 |
|
} |
|
449
|
|
|
|
|
450
|
|
|
// Remove a special './' combination from the beginning of a path |
|
451
|
7 |
|
if (substr($permalink, 0, 2) === './') |
|
452
|
7 |
|
{ |
|
453
|
1 |
|
$permalink = substr($permalink, 2); |
|
454
|
1 |
|
} |
|
455
|
|
|
|
|
456
|
|
|
// Convert permalinks to lower case |
|
457
|
7 |
|
$permalink = mb_strtolower($permalink, 'UTF-8'); |
|
458
|
|
|
|
|
459
|
7 |
|
return $permalink; |
|
460
|
|
|
} |
|
461
|
|
|
} |
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.