1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @copyright 2017 Vladimir Jimenez |
5
|
|
|
* @license https://github.com/allejo/stakx/blob/master/LICENSE.md MIT |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace allejo\stakx\FrontMatter; |
9
|
|
|
|
10
|
|
|
use allejo\stakx\Document\JailedDocumentInterface; |
11
|
|
|
use allejo\stakx\Document\PermalinkDocument; |
12
|
|
|
use allejo\stakx\Exception\FileAwareException; |
13
|
|
|
use allejo\stakx\Exception\InvalidSyntaxException; |
14
|
|
|
use allejo\stakx\FrontMatter\Exception\YamlVariableUndefinedException; |
15
|
|
|
use Symfony\Component\Filesystem\Exception\FileNotFoundException; |
16
|
|
|
use Symfony\Component\Filesystem\Exception\IOException; |
17
|
|
|
use Symfony\Component\Yaml\Exception\ParseException; |
18
|
|
|
use Symfony\Component\Yaml\Yaml; |
19
|
|
|
|
20
|
|
|
abstract class FrontMatterDocument extends PermalinkDocument implements |
|
|
|
|
21
|
|
|
\ArrayAccess, |
22
|
|
|
JailedDocumentInterface, |
23
|
|
|
WritableDocumentInterface |
24
|
|
|
{ |
25
|
|
|
const TEMPLATE = "---\n%s\n---\n\n%s"; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* The names of FrontMatter keys that are specially defined for all Documents |
29
|
|
|
* |
30
|
|
|
* @var array |
31
|
|
|
*/ |
32
|
|
|
public static $specialFrontMatterKeys = array( |
|
|
|
|
33
|
|
|
'filename', 'basename' |
34
|
|
|
); |
35
|
|
|
|
36
|
|
|
protected static $whiteListFunctions = array( |
37
|
|
|
'getPermalink', 'getRedirects', 'getTargetFile', 'getName', 'getFilePath', 'getRelativeFilePath', 'getContent', |
38
|
|
|
'getExtension', 'getFrontMatter' |
39
|
|
|
); |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* An array to keep track of collection or data dependencies used inside of a Twig template. |
43
|
|
|
* |
44
|
|
|
* $dataDependencies['collections'] = array() |
45
|
|
|
* $dataDependencies['data'] = array() |
46
|
|
|
* |
47
|
|
|
* @var array |
48
|
|
|
*/ |
49
|
|
|
protected $dataDependencies; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* FrontMatter values that can be injected or set after the file has been parsed. Values in this array will take |
53
|
|
|
* precedence over values in $frontMatter. |
54
|
|
|
* |
55
|
|
|
* @var array |
56
|
|
|
*/ |
57
|
|
|
protected $writableFrontMatter; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* A list of Front Matter values that should not be returned directly from the $frontMatter array. Values listed |
61
|
|
|
* here have dedicated functions that handle those Front Matter values and the respective functions should be called |
62
|
|
|
* instead. |
63
|
|
|
* |
64
|
|
|
* @var string[] |
65
|
|
|
*/ |
66
|
|
|
protected $frontMatterBlacklist; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Set to true if the front matter has already been evaluated with variable interpolation. |
70
|
|
|
* |
71
|
|
|
* @var bool |
72
|
|
|
*/ |
73
|
|
|
protected $frontMatterEvaluated; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* @var Parser |
77
|
|
|
*/ |
78
|
|
|
protected $frontMatterParser; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* An array containing the Yaml of the file. |
82
|
|
|
* |
83
|
|
|
* @var array |
84
|
|
|
*/ |
85
|
|
|
protected $frontMatter; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Set to true if the body has already been parsed as markdown or any other format. |
89
|
|
|
* |
90
|
|
|
* @var bool |
91
|
|
|
*/ |
92
|
|
|
protected $bodyContentEvaluated; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Only the body of the file, i.e. the content. |
96
|
|
|
* |
97
|
|
|
* @var string |
98
|
|
|
*/ |
99
|
|
|
protected $bodyContent; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* The number of lines that Twig template errors should offset. |
103
|
|
|
* |
104
|
|
|
* @var int |
105
|
|
|
*/ |
106
|
|
|
private $lineOffset; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* ContentItem constructor. |
110
|
|
|
* |
111
|
|
|
* @param string $filePath The path to the file that will be parsed into a ContentItem |
112
|
|
|
* |
113
|
|
|
* @throws FileNotFoundException The given file path does not exist |
114
|
|
|
* @throws IOException The file was not a valid ContentItem. This would meam there was no front matter or |
115
|
|
|
* no body |
116
|
|
|
*/ |
117
|
116 |
|
public function __construct($filePath) |
118
|
|
|
{ |
119
|
116 |
|
$this->frontMatterBlacklist = array('permalink', 'redirects'); |
120
|
116 |
|
$this->writableFrontMatter = array(); |
121
|
|
|
|
122
|
116 |
|
parent::__construct($filePath); |
123
|
104 |
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Return the body of the Content Item. |
127
|
|
|
* |
128
|
|
|
* @return string |
129
|
|
|
*/ |
130
|
|
|
abstract public function getContent(); |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* The number of lines that are taken up by FrontMatter and white space. |
134
|
|
|
* |
135
|
|
|
* @return int |
136
|
|
|
*/ |
137
|
|
|
final public function getLineOffset() |
138
|
|
|
{ |
139
|
|
|
return $this->lineOffset; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Get the name of the item, which is just the filename without the extension. |
144
|
|
|
* |
145
|
|
|
* @return string |
146
|
|
|
*/ |
147
|
62 |
|
final public function getName() |
148
|
|
|
{ |
149
|
62 |
|
return $this->getBaseName(); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Check whether this object has a reference to a collection or data item. |
154
|
|
|
* |
155
|
|
|
* @param string $namespace 'collections' or 'data' |
156
|
|
|
* @param string $needle |
157
|
|
|
* |
158
|
|
|
* @return bool |
159
|
|
|
*/ |
160
|
|
|
final public function hasTwigDependency($namespace, $needle) |
161
|
|
|
{ |
162
|
|
|
return in_array($needle, $this->dataDependencies[$namespace]); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Read the file, and parse its contents. |
167
|
|
|
*/ |
168
|
115 |
|
final public function refreshFileContent() |
169
|
|
|
{ |
170
|
|
|
// This function can be called after the initial object was created and the file may have been deleted since the |
171
|
|
|
// creation of the object. |
172
|
115 |
View Code Duplication |
if (!$this->fs->exists($this->filePath)) |
|
|
|
|
173
|
115 |
|
{ |
174
|
1 |
|
throw new FileNotFoundException(null, 0, null, $this->filePath); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// $fileStructure[1] is the YAML |
|
|
|
|
178
|
|
|
// $fileStructure[2] is the amount of new lines after the closing `---` and the beginning of content |
179
|
|
|
// $fileStructure[3] is the body of the document |
180
|
115 |
|
$fileStructure = array(); |
181
|
|
|
|
182
|
115 |
|
$rawFileContents = file_get_contents($this->filePath); |
183
|
115 |
|
preg_match('/---\R(.*?\R)?---(\s+)(.*)/s', $rawFileContents, $fileStructure); |
184
|
|
|
|
185
|
115 |
|
if (count($fileStructure) != 4) |
186
|
115 |
|
{ |
187
|
9 |
|
throw new InvalidSyntaxException('Invalid FrontMatter file', 0, null, $this->getRelativeFilePath()); |
188
|
|
|
} |
189
|
|
|
|
190
|
106 |
|
if (empty(trim($fileStructure[3]))) |
191
|
106 |
|
{ |
192
|
1 |
|
throw new InvalidSyntaxException('FrontMatter files must have a body to render', 0, null, $this->getRelativeFilePath()); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
// The hard coded 1 is the offset used to count the new line used after the first `---` that is not caught in the regex |
196
|
105 |
|
$this->lineOffset = substr_count($fileStructure[1], "\n") + substr_count($fileStructure[2], "\n") + 1; |
197
|
105 |
|
$this->bodyContent = $fileStructure[3]; |
198
|
|
|
|
199
|
105 |
|
if (!empty(trim($fileStructure[1]))) |
200
|
105 |
|
{ |
201
|
89 |
|
$this->frontMatter = Yaml::parse($fileStructure[1], Yaml::PARSE_DATETIME); |
|
|
|
|
202
|
|
|
|
203
|
89 |
|
if (!empty($this->frontMatter) && !is_array($this->frontMatter)) |
204
|
89 |
|
{ |
205
|
1 |
|
throw new ParseException('The evaluated FrontMatter should be an array'); |
206
|
|
|
} |
207
|
88 |
|
} |
208
|
|
|
else |
209
|
|
|
{ |
210
|
18 |
|
$this->frontMatter = array(); |
211
|
|
|
} |
212
|
|
|
|
213
|
104 |
|
$this->frontMatterEvaluated = false; |
214
|
104 |
|
$this->bodyContentEvaluated = false; |
215
|
104 |
|
$this->permalink = null; |
216
|
|
|
|
217
|
104 |
|
$this->findTwigDataDependencies('collections'); |
218
|
104 |
|
$this->findTwigDataDependencies('data'); |
219
|
104 |
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Get all of the references to either DataItems or ContentItems inside of given string. |
223
|
|
|
* |
224
|
|
|
* @param string $filter 'collections' or 'data' |
225
|
|
|
*/ |
226
|
104 |
|
private function findTwigDataDependencies($filter) |
227
|
|
|
{ |
228
|
104 |
|
$regex = '/{[{%](?:.+)?(?:' . $filter . ')(?:\.|\[\')(\w+)(?:\'\])?.+[%}]}/'; |
229
|
104 |
|
$results = array(); |
230
|
|
|
|
231
|
104 |
|
preg_match_all($regex, $this->bodyContent, $results); |
232
|
|
|
|
233
|
104 |
|
$this->dataDependencies[$filter] = array_unique($results[1]); |
234
|
104 |
|
} |
235
|
|
|
|
236
|
|
|
// |
237
|
|
|
// Permalink and redirect functionality |
238
|
|
|
// |
239
|
|
|
|
240
|
39 |
|
final protected function buildPermalink() |
241
|
|
|
{ |
242
|
39 |
|
if (!is_null($this->permalink)) |
243
|
39 |
|
{ |
244
|
8 |
|
return; |
245
|
|
|
} |
246
|
|
|
|
247
|
37 |
|
if (!is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion()) |
248
|
37 |
|
{ |
249
|
|
|
throw new \Exception('The permalink for this item has not been set'); |
250
|
|
|
} |
251
|
|
|
|
252
|
37 |
|
$permalink = (is_array($this->frontMatter) && isset($this->frontMatter['permalink'])) ? |
253
|
37 |
|
$this->frontMatter['permalink'] : $this->getPathPermalink(); |
254
|
|
|
|
255
|
37 |
View Code Duplication |
if (is_array($permalink)) |
|
|
|
|
256
|
37 |
|
{ |
257
|
19 |
|
$this->permalink = $permalink[0]; |
258
|
19 |
|
array_shift($permalink); |
259
|
19 |
|
$this->redirects = $permalink; |
260
|
19 |
|
} |
261
|
|
|
else |
262
|
|
|
{ |
263
|
24 |
|
$this->permalink = $permalink; |
264
|
24 |
|
$this->redirects = array(); |
265
|
|
|
} |
266
|
37 |
|
} |
267
|
|
|
|
268
|
|
|
// |
269
|
|
|
// WritableFrontMatter Implementation |
270
|
|
|
// |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* {@inheritdoc} |
274
|
|
|
*/ |
275
|
4 |
|
final public function evaluateFrontMatter($variables = null) |
276
|
|
|
{ |
277
|
4 |
|
if (!is_null($variables)) |
278
|
4 |
|
{ |
279
|
4 |
|
$this->frontMatter = array_merge($this->frontMatter, $variables); |
280
|
4 |
|
$this->evaluateYaml($this->frontMatter); |
281
|
4 |
|
} |
282
|
4 |
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* {@inheritdoc} |
286
|
|
|
*/ |
287
|
29 |
|
final public function getFrontMatter($evaluateYaml = true) |
288
|
|
|
{ |
289
|
29 |
|
if (is_null($this->frontMatter)) |
290
|
29 |
|
{ |
291
|
|
|
$this->frontMatter = array(); |
292
|
|
|
} |
293
|
29 |
|
elseif (!$this->frontMatterEvaluated && $evaluateYaml) |
294
|
|
|
{ |
295
|
23 |
|
$this->evaluateYaml($this->frontMatter); |
296
|
22 |
|
} |
297
|
|
|
|
298
|
28 |
|
return $this->frontMatter; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* {@inheritdoc} |
303
|
|
|
*/ |
304
|
2 |
|
final public function hasExpandedFrontMatter() |
305
|
|
|
{ |
306
|
2 |
|
return !is_null($this->frontMatterParser) && $this->frontMatterParser->hasExpansion(); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* {@inheritdoc. |
311
|
|
|
*/ |
312
|
|
|
final public function appendFrontMatter(array $frontMatter) |
313
|
|
|
{ |
314
|
|
|
foreach ($frontMatter as $key => $value) |
315
|
|
|
{ |
316
|
|
|
$this->writableFrontMatter[$key] = $value; |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* {@inheritdoc. |
322
|
|
|
*/ |
323
|
|
|
final public function deleteFrontMatter($key) |
324
|
|
|
{ |
325
|
|
|
if (!isset($this->writableFrontMatter[$key])) |
326
|
|
|
{ |
327
|
|
|
return; |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
unset($this->writableFrontMatter[$key]); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
/** |
334
|
|
|
* {@inheritdoc. |
335
|
|
|
*/ |
336
|
2 |
|
final public function setFrontMatter(array $frontMatter) |
337
|
|
|
{ |
338
|
2 |
|
if (!is_array($frontMatter)) |
339
|
2 |
|
{ |
340
|
|
|
throw new \InvalidArgumentException('An array is required for setting the writable FrontMatter'); |
341
|
|
|
} |
342
|
|
|
|
343
|
2 |
|
$this->writableFrontMatter = $frontMatter; |
344
|
2 |
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Evaluate an array of data for FrontMatter variables. This function will modify the array in place. |
348
|
|
|
* |
349
|
|
|
* @param array $yaml An array of data containing FrontMatter variables |
350
|
|
|
* |
351
|
|
|
* @throws YamlVariableUndefinedException A FrontMatter variable used does not exist |
352
|
|
|
*/ |
353
|
27 |
|
private function evaluateYaml(&$yaml) |
354
|
|
|
{ |
355
|
|
|
try |
356
|
|
|
{ |
357
|
27 |
|
$this->frontMatterParser = new Parser($yaml, array( |
358
|
27 |
|
'filename' => $this->getFileName(), |
359
|
27 |
|
'basename' => $this->getName(), |
360
|
27 |
|
)); |
361
|
26 |
|
$this->frontMatterEvaluated = true; |
362
|
|
|
} |
363
|
27 |
|
catch (\Exception $e) |
364
|
|
|
{ |
365
|
1 |
|
throw FileAwareException::castException($e, $this->getRelativeFilePath()); |
366
|
|
|
} |
367
|
26 |
|
} |
368
|
|
|
|
369
|
|
|
// |
370
|
|
|
// ArrayAccess Implementation |
371
|
|
|
// |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* {@inheritdoc} |
375
|
|
|
*/ |
376
|
|
|
public function offsetSet($offset, $value) |
377
|
|
|
{ |
378
|
|
|
if (is_null($offset)) |
379
|
|
|
{ |
380
|
|
|
throw new \InvalidArgumentException('$offset cannot be null'); |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
$this->writableFrontMatter[$offset] = $value; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* {@inheritdoc} |
388
|
|
|
*/ |
389
|
31 |
|
public function offsetExists($offset) |
390
|
|
|
{ |
391
|
31 |
|
if (isset($this->writableFrontMatter[$offset]) || isset($this->frontMatter[$offset])) |
392
|
31 |
|
{ |
393
|
30 |
|
return true; |
394
|
|
|
} |
395
|
|
|
|
396
|
14 |
|
$fxnCall = 'get' . ucfirst($offset); |
397
|
|
|
|
398
|
14 |
|
return method_exists($this, $fxnCall) && in_array($fxnCall, static::$whiteListFunctions); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
/** |
402
|
|
|
* {@inheritdoc} |
403
|
|
|
*/ |
404
|
|
|
public function offsetUnset($offset) |
405
|
|
|
{ |
406
|
|
|
unset($this->writableFrontMatter[$offset]); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* {@inheritdoc} |
411
|
|
|
*/ |
412
|
48 |
|
public function offsetGet($offset) |
413
|
|
|
{ |
414
|
48 |
|
$fxnCall = 'get' . ucfirst($offset); |
415
|
|
|
|
416
|
48 |
|
if (in_array($fxnCall, self::$whiteListFunctions) && method_exists($this, $fxnCall)) |
417
|
48 |
|
{ |
418
|
6 |
|
return call_user_func_array(array($this, $fxnCall), array()); |
419
|
|
|
} |
420
|
|
|
|
421
|
42 |
|
if (isset($this->writableFrontMatter[$offset])) |
422
|
42 |
|
{ |
423
|
|
|
return $this->writableFrontMatter[$offset]; |
424
|
|
|
} |
425
|
|
|
|
426
|
42 |
|
if (isset($this->frontMatter[$offset])) |
427
|
42 |
|
{ |
428
|
41 |
|
return $this->frontMatter[$offset]; |
429
|
|
|
} |
430
|
|
|
|
431
|
5 |
|
return null; |
432
|
|
|
} |
433
|
|
|
} |
434
|
|
|
|
This check examines a number of code elements and verifies that they conform to the given naming conventions.
You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.