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\Document; |
9
|
|
|
|
10
|
|
|
use allejo\stakx\Exception\FileAwareException; |
11
|
|
|
use allejo\stakx\Exception\InvalidSyntaxException; |
12
|
|
|
use allejo\stakx\FrontMatter\Exception\YamlVariableUndefinedException; |
13
|
|
|
use allejo\stakx\FrontMatter\FrontMatterParser; |
14
|
|
|
use Symfony\Component\Yaml\Exception\ParseException; |
15
|
|
|
use Symfony\Component\Yaml\Yaml; |
16
|
|
|
|
17
|
|
|
abstract class FrontMatterDocument extends ReadableDocument implements \IteratorAggregate, \ArrayAccess |
18
|
|
|
{ |
19
|
|
|
const TEMPLATE = "---\n%s\n---\n\n%s"; |
20
|
|
|
|
21
|
|
|
/** @var array Functions that are white listed and can be called from templates. */ |
22
|
|
|
public static $whiteListedFunctions = [ |
23
|
|
|
'getPermalink', 'getRedirects', 'getTargetFile', 'getContent', |
24
|
|
|
'getFilename', 'getBasename', 'getExtension', 'isDraft', |
25
|
|
|
]; |
26
|
|
|
|
27
|
|
|
/** @var array FrontMatter keys that will be defined internally and cannot be overridden by users. */ |
28
|
|
|
protected $specialFrontMatter = [ |
29
|
|
|
'filePath' => null, |
30
|
|
|
]; |
31
|
|
|
|
32
|
|
|
protected $frontMatterEvaluated = false; |
33
|
|
|
protected $bodyContentEvaluated = false; |
34
|
|
|
|
35
|
|
|
/** @var FrontMatterParser */ |
36
|
|
|
protected $frontMatterParser; |
37
|
|
|
|
38
|
|
|
/** @var array FrontMatter that is read from user documents. */ |
39
|
|
|
protected $frontMatter = []; |
40
|
|
|
|
41
|
|
|
/** @var int The number of lines that Twig template errors should offset. */ |
42
|
|
|
protected $lineOffset = 0; |
43
|
|
|
|
44
|
|
|
/// |
45
|
|
|
// Getter functions |
46
|
|
|
/// |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* {@inheritdoc} |
50
|
|
|
*/ |
51
|
19 |
|
public function getIterator() |
52
|
|
|
{ |
53
|
19 |
|
return (new \ArrayIterator($this->frontMatter)); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Get the number of lines that are taken up by FrontMatter and whitespace. |
58
|
|
|
* |
59
|
|
|
* @return int |
60
|
|
|
*/ |
61
|
1 |
|
public function getLineOffset() |
62
|
|
|
{ |
63
|
1 |
|
return $this->lineOffset; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Get whether or not this document is a draft. |
68
|
|
|
* |
69
|
|
|
* @return bool |
70
|
|
|
*/ |
71
|
4 |
|
public function isDraft() |
72
|
|
|
{ |
73
|
4 |
|
return (isset($this->frontMatter['draft']) && $this->frontMatter['draft'] === true); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/// |
77
|
|
|
// FrontMatter functionality |
78
|
|
|
/// |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* {@inheritdoc} |
82
|
|
|
*/ |
83
|
119 |
|
public function readContent() |
84
|
|
|
{ |
85
|
|
|
// $fileStructure[1] is the YAML |
|
|
|
|
86
|
|
|
// $fileStructure[2] is the amount of new lines after the closing `---` and the beginning of content |
87
|
|
|
// $fileStructure[3] is the body of the document |
88
|
119 |
|
$fileStructure = array(); |
89
|
|
|
|
90
|
119 |
|
$rawFileContents = $this->file->getContents(); |
91
|
119 |
|
preg_match('/---\R(.*?\R)?---(\s+)(.*)/s', $rawFileContents, $fileStructure); |
92
|
|
|
|
93
|
119 |
|
if (count($fileStructure) != 4) |
94
|
119 |
|
{ |
95
|
9 |
|
throw new InvalidSyntaxException('Invalid FrontMatter file', 0, null, $this->getRelativeFilePath()); |
96
|
|
|
} |
97
|
|
|
|
98
|
110 |
|
if (empty(trim($fileStructure[3]))) |
99
|
110 |
|
{ |
100
|
1 |
|
throw new InvalidSyntaxException('FrontMatter files must have a body to render', 0, null, $this->getRelativeFilePath()); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
// The hard coded 1 is the offset used to count the new line used after the first `---` that is not caught in the regex |
104
|
109 |
|
$this->lineOffset = substr_count($fileStructure[1], "\n") + substr_count($fileStructure[2], "\n") + 1; |
105
|
109 |
|
$this->bodyContent = $fileStructure[3]; |
106
|
|
|
|
107
|
109 |
|
if (!empty(trim($fileStructure[1]))) |
108
|
109 |
|
{ |
109
|
92 |
|
$this->frontMatter = Yaml::parse($fileStructure[1], Yaml::PARSE_DATETIME); |
110
|
|
|
|
111
|
92 |
|
if (!empty($this->frontMatter) && !is_array($this->frontMatter)) |
112
|
92 |
|
{ |
113
|
1 |
|
throw new ParseException('The evaluated FrontMatter should be an array'); |
114
|
|
|
} |
115
|
91 |
|
} |
116
|
|
|
else |
117
|
|
|
{ |
118
|
20 |
|
$this->frontMatter = array(); |
119
|
|
|
} |
120
|
|
|
|
121
|
108 |
|
$this->frontMatterEvaluated = false; |
122
|
108 |
|
$this->bodyContentEvaluated = false; |
123
|
108 |
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Get the FrontMatter for this document. |
127
|
|
|
* |
128
|
|
|
* @param bool $evaluateYaml Whether or not to evaluate any variables. |
129
|
|
|
* |
130
|
|
|
* @return array |
131
|
|
|
*/ |
132
|
30 |
|
final public function getFrontMatter($evaluateYaml = true) |
133
|
|
|
{ |
134
|
30 |
|
if ($this->frontMatter === null) |
135
|
30 |
|
{ |
136
|
|
|
$this->frontMatter = []; |
137
|
|
|
} |
138
|
30 |
|
elseif (!$this->frontMatterEvaluated && $evaluateYaml) |
139
|
|
|
{ |
140
|
24 |
|
$this->evaluateYaml($this->frontMatter); |
141
|
23 |
|
} |
142
|
|
|
|
143
|
29 |
|
return $this->frontMatter; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Evaluate the FrontMatter in this object by merging a custom array of data. |
148
|
|
|
* |
149
|
|
|
* @param array|null $variables An array of YAML variables to use in evaluating the `$permalink` value |
150
|
|
|
*/ |
151
|
9 |
|
final public function evaluateFrontMatter(array $variables = null) |
152
|
|
|
{ |
153
|
9 |
|
if ($variables !== null) |
154
|
9 |
|
{ |
155
|
9 |
|
$this->frontMatter = array_merge($this->frontMatter, $variables); |
156
|
9 |
|
$this->evaluateYaml($this->frontMatter); |
157
|
9 |
|
} |
158
|
9 |
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Returns true when the evaluated Front Matter has expanded values embeded. |
162
|
|
|
* |
163
|
|
|
* @return bool |
164
|
|
|
*/ |
165
|
2 |
|
final public function hasExpandedFrontMatter() |
166
|
|
|
{ |
167
|
2 |
|
return ($this->frontMatterParser !== null && $this->frontMatterParser->hasExpansion()); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Evaluate an array of data for FrontMatter variables. This function will modify the array in place. |
172
|
|
|
* |
173
|
|
|
* @param array $yaml An array of data containing FrontMatter variables |
174
|
|
|
* |
175
|
|
|
* @see $specialFrontMatter |
176
|
|
|
* |
177
|
|
|
* @throws YamlVariableUndefinedException A FrontMatter variable used does not exist |
178
|
|
|
*/ |
179
|
31 |
|
private function evaluateYaml(&$yaml) |
180
|
|
|
{ |
181
|
|
|
try |
182
|
|
|
{ |
183
|
|
|
// The second parameter for this parser must match the $specialFrontMatter structure |
184
|
31 |
|
$this->frontMatterParser = new FrontMatterParser($yaml, [ |
185
|
31 |
|
'filePath' => $this->getRelativeFilePath(), |
186
|
31 |
|
]); |
187
|
31 |
|
$this->frontMatterParser->parse(); |
188
|
30 |
|
$this->frontMatterEvaluated = true; |
189
|
|
|
} |
190
|
31 |
|
catch (\Exception $e) |
191
|
|
|
{ |
192
|
1 |
|
throw FileAwareException::castException($e, $this->getRelativeFilePath()); |
193
|
|
|
} |
194
|
30 |
|
} |
195
|
|
|
|
196
|
|
|
/// |
197
|
|
|
// ArrayAccess Implementation |
198
|
|
|
/// |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* {@inheritdoc} |
202
|
|
|
*/ |
203
|
|
|
public function offsetSet($offset, $value) |
204
|
|
|
{ |
205
|
|
|
throw new \LogicException('FrontMatter is read-only.'); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* {@inheritdoc} |
210
|
|
|
*/ |
211
|
34 |
|
public function offsetExists($offset) |
212
|
|
|
{ |
213
|
34 |
|
if (isset($this->frontMatter[$offset]) || isset($this->specialFrontMatter[$offset])) |
214
|
34 |
|
{ |
215
|
33 |
|
return true; |
216
|
|
|
} |
217
|
|
|
|
218
|
14 |
|
$fxnCall = 'get' . ucfirst($offset); |
219
|
|
|
|
220
|
14 |
|
return method_exists($this, $fxnCall) && in_array($fxnCall, static::$whiteListedFunctions); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* {@inheritdoc} |
225
|
|
|
*/ |
226
|
|
|
public function offsetUnset($offset) |
227
|
|
|
{ |
228
|
|
|
throw new \LogicException('FrontMatter is read-only.'); |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* {@inheritdoc} |
233
|
|
|
*/ |
234
|
47 |
|
public function offsetGet($offset) |
235
|
|
|
{ |
236
|
47 |
|
if (isset($this->specialFrontMatter[$offset])) |
237
|
47 |
|
{ |
238
|
|
|
return $this->specialFrontMatter[$offset]; |
239
|
|
|
} |
240
|
|
|
|
241
|
47 |
|
$fxnCall = 'get' . ucfirst($offset); |
242
|
|
|
|
243
|
47 |
|
if (in_array($fxnCall, self::$whiteListedFunctions) && method_exists($this, $fxnCall)) |
244
|
47 |
|
{ |
245
|
6 |
|
return call_user_func_array([$this, $fxnCall], []); |
246
|
|
|
} |
247
|
|
|
|
248
|
41 |
|
if (isset($this->frontMatter[$offset])) |
249
|
41 |
|
{ |
250
|
40 |
|
return $this->frontMatter[$offset]; |
251
|
|
|
} |
252
|
|
|
|
253
|
2 |
|
return null; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.