1 | <?php |
||
14 | class Parser |
||
15 | { |
||
16 | /** |
||
17 | * The RegEx used to identify Front Matter variables. |
||
18 | */ |
||
19 | const VARIABLE_DEF = '/(?<!\\\\)%([a-zA-Z]+)/'; |
||
20 | |||
21 | /** |
||
22 | * A list of special fields in the Front Matter that will support expansion. |
||
23 | * |
||
24 | * @var string[] |
||
25 | */ |
||
26 | private static $expandableFields = array('permalink'); |
||
27 | |||
28 | /** |
||
29 | * Whether or not an field was expanded into several values. |
||
30 | * |
||
31 | * Only fields specified in $expandableFields will cause this value to be set to true |
||
32 | * |
||
33 | * @var bool |
||
34 | */ |
||
35 | private $expansionUsed; |
||
36 | |||
37 | /** |
||
38 | * The current depth of the recursion for evaluating nested arrays in the Front Matter. |
||
39 | * |
||
40 | * @var int |
||
41 | */ |
||
42 | private $nestingLevel; |
||
43 | |||
44 | /** |
||
45 | * Special FrontMatter keys that are defined manually. |
||
46 | * |
||
47 | * @var array |
||
48 | */ |
||
49 | private $specialKeys; |
||
50 | |||
51 | /** |
||
52 | * The current hierarchy of the keys that are being evaluated. |
||
53 | * |
||
54 | * Since arrays can be nested, we'll keep track of the keys up until the current depth. This information is used for |
||
55 | * error reporting |
||
56 | * |
||
57 | * @var array |
||
58 | */ |
||
59 | private $yamlKeys; |
||
60 | |||
61 | /** |
||
62 | * The entire Front Matter block; evaluation will happen in place. |
||
63 | * |
||
64 | * @var array |
||
65 | */ |
||
66 | private $frontMatter; |
||
67 | |||
68 | 47 | public function __construct(array &$rawFrontMatter, array $specialKeys = array()) |
|
80 | |||
81 | /** |
||
82 | * True if any fields were expanded in the FrontMatter block. |
||
83 | * |
||
84 | * @return bool |
||
85 | */ |
||
86 | 20 | public function hasExpansion() |
|
90 | |||
91 | // |
||
92 | // Special FrontMatter fields |
||
93 | // |
||
94 | |||
95 | /** |
||
96 | * Special treatment for some FrontMatter variables. |
||
97 | */ |
||
98 | 47 | private function handleSpecialFrontMatter() |
|
103 | |||
104 | /** |
||
105 | * Merge in the special keys with the existing FrontMatter. |
||
106 | */ |
||
107 | 47 | private function handleSpecialKeys() |
|
111 | |||
112 | /** |
||
113 | * Special treatment for the `date` field in FrontMatter that creates three new variables: year, month, day. |
||
114 | */ |
||
115 | 47 | private function handleDateField() |
|
116 | { |
||
117 | 47 | if (!isset($this->frontMatter['date'])) |
|
118 | { |
||
119 | 40 | return; |
|
120 | } |
||
121 | |||
122 | 7 | $date = &$this->frontMatter['date']; |
|
123 | 7 | $itemDate = $this->guessDateTime($date); |
|
124 | |||
125 | 7 | if (!$itemDate === false) |
|
126 | { |
||
127 | 7 | $this->frontMatter['date'] = $itemDate->format('U'); |
|
128 | 7 | $this->frontMatter['year'] = $itemDate->format('Y'); |
|
129 | 7 | $this->frontMatter['month'] = $itemDate->format('m'); |
|
130 | 7 | $this->frontMatter['day'] = $itemDate->format('d'); |
|
131 | } |
||
132 | 7 | } |
|
133 | |||
134 | // |
||
135 | // Evaluation |
||
136 | // |
||
137 | |||
138 | /** |
||
139 | * Evaluate an array as Front Matter. |
||
140 | * |
||
141 | * @param array $yaml |
||
142 | */ |
||
143 | 47 | private function evaluateBlock(&$yaml) |
|
144 | { |
||
145 | 47 | ++$this->nestingLevel; |
|
146 | |||
147 | 47 | foreach ($yaml as $key => &$value) |
|
148 | { |
||
149 | 47 | $this->yamlKeys[$this->nestingLevel] = $key; |
|
150 | 47 | $keys = implode('.', $this->yamlKeys); |
|
151 | |||
152 | 47 | if (in_array($key, self::$expandableFields, true)) |
|
153 | { |
||
154 | 25 | $value = $this->evaluateExpandableField($keys, $value); |
|
155 | } |
||
156 | 47 | elseif (is_array($value)) |
|
157 | { |
||
158 | 17 | $this->evaluateBlock($value); |
|
159 | } |
||
160 | 47 | elseif (is_string($value)) |
|
161 | { |
||
162 | 47 | $value = $this->evaluateBasicType($keys, $value); |
|
163 | } |
||
164 | elseif ($value instanceof \DateTime) |
||
165 | { |
||
166 | 45 | $value = $this->castDateTimeTimezone($value->format('U')); |
|
167 | } |
||
168 | } |
||
169 | |||
170 | 44 | --$this->nestingLevel; |
|
171 | 44 | $this->yamlKeys = array(); |
|
172 | 44 | } |
|
173 | |||
174 | /** |
||
175 | * Evaluate an expandable field. |
||
176 | * |
||
177 | * @param string $key |
||
178 | * @param string $fmStatement |
||
179 | * |
||
180 | * @return array |
||
181 | */ |
||
182 | 25 | private function evaluateExpandableField($key, $fmStatement) |
|
183 | { |
||
184 | 25 | if (!is_array($fmStatement)) |
|
185 | { |
||
186 | 24 | $fmStatement = array($fmStatement); |
|
|
|||
187 | } |
||
188 | |||
189 | 25 | $wip = array(); |
|
190 | |||
191 | 25 | foreach ($fmStatement as $statement) |
|
192 | { |
||
193 | 25 | $value = $this->evaluateBasicType($key, $statement, true); |
|
194 | |||
195 | // Only continue expansion if there are Front Matter variables remain in the string, this means there'll be |
||
196 | // Front Matter variables referencing arrays |
||
197 | 25 | $expandingVars = $this->getFrontMatterVariables($value); |
|
198 | 25 | if (!empty($expandingVars)) |
|
199 | { |
||
200 | 6 | $value = $this->evaluateArrayType($key, $value, $expandingVars); |
|
201 | } |
||
202 | |||
203 | 24 | $wip[] = $value; |
|
204 | } |
||
205 | |||
206 | 24 | return $wip; |
|
207 | } |
||
208 | |||
209 | /** |
||
210 | * Convert a string or an array into an array of ExpandedValue objects created through "value expansion". |
||
211 | * |
||
212 | * @param string $frontMatterKey The current hierarchy of the Front Matter keys being used |
||
213 | * @param string $expandableValue The Front Matter value that will be expanded |
||
214 | * @param string[] $arrayVariableNames The Front Matter variable names that reference arrays |
||
215 | * |
||
216 | * @throws YamlUnsupportedVariableException If a multidimensional array is given for value expansion |
||
217 | * |
||
218 | * @return array |
||
219 | */ |
||
220 | 6 | private function evaluateArrayType($frontMatterKey, $expandableValue, $arrayVariableNames) |
|
221 | { |
||
222 | 6 | if (!is_array($expandableValue)) |
|
223 | { |
||
224 | 6 | $expandableValue = array($expandableValue); |
|
225 | } |
||
226 | |||
227 | 6 | $this->expansionUsed = true; |
|
228 | |||
229 | 6 | foreach ($arrayVariableNames as $variable) |
|
230 | { |
||
231 | 6 | if (ArrayUtilities::is_multidimensional($this->frontMatter[$variable])) |
|
232 | { |
||
233 | 1 | throw new YamlUnsupportedVariableException("Yaml array expansion is not supported with multidimensional arrays with `$variable` for key `$frontMatterKey`"); |
|
234 | } |
||
235 | |||
236 | 5 | $wip = array(); |
|
237 | |||
238 | 5 | foreach ($expandableValue as &$statement) |
|
239 | { |
||
240 | 5 | foreach ($this->frontMatter[$variable] as $value) |
|
241 | { |
||
242 | 5 | $evaluatedValue = ($statement instanceof ExpandedValue) ? clone $statement : new ExpandedValue($statement); |
|
243 | 5 | $evaluatedValue->setEvaluated(str_replace('%' . $variable, $value, $evaluatedValue->getEvaluated())); |
|
244 | 5 | $evaluatedValue->setIterator($variable, $value); |
|
245 | |||
246 | 5 | $wip[] = $evaluatedValue; |
|
247 | } |
||
248 | } |
||
249 | |||
250 | 5 | $expandableValue = $wip; |
|
251 | } |
||
252 | |||
253 | 5 | return $expandableValue; |
|
254 | } |
||
255 | |||
256 | /** |
||
257 | * Evaluate an string for FrontMatter variables and replace them with the corresponding values. |
||
258 | * |
||
259 | * @param string $key The key of the Front Matter value |
||
260 | * @param string $string The string that will be evaluated |
||
261 | * @param bool $ignoreArrays When set to true, an exception won't be thrown when an array is found with the |
||
262 | * interpolation |
||
263 | * |
||
264 | * @throws YamlUnsupportedVariableException A FrontMatter variable is not an int, float, or string |
||
265 | * |
||
266 | * @return string The final string with variables evaluated |
||
267 | */ |
||
268 | 47 | private function evaluateBasicType($key, $string, $ignoreArrays = false) |
|
269 | { |
||
270 | 47 | $variables = $this->getFrontMatterVariables($string); |
|
271 | |||
272 | 47 | foreach ($variables as $variable) |
|
273 | { |
||
274 | 28 | $value = $this->getVariableValue($key, $variable); |
|
275 | |||
276 | 26 | if (is_array($value) || is_bool($value)) |
|
277 | { |
||
278 | 8 | if ($ignoreArrays) |
|
279 | { |
||
280 | 6 | continue; |
|
281 | } |
||
282 | |||
283 | 2 | throw new YamlUnsupportedVariableException("Yaml variable `$variable` for `$key` is not a supported data type."); |
|
284 | } |
||
285 | |||
286 | 18 | $string = str_replace('%' . $variable, $value, $string); |
|
287 | } |
||
288 | |||
289 | 44 | return $string; |
|
290 | } |
||
291 | |||
292 | // |
||
293 | // Variable management |
||
294 | // |
||
295 | |||
296 | /** |
||
297 | * Get an array of FrontMatter variables in the specified string that need to be interpolated. |
||
298 | * |
||
299 | * @param string $string |
||
300 | * |
||
301 | * @return string[] |
||
302 | */ |
||
303 | 47 | private function getFrontMatterVariables($string) |
|
313 | |||
314 | /** |
||
315 | * Get the value of a FM variable or throw an exception. |
||
316 | * |
||
317 | * @param string $key |
||
318 | * @param string $varName |
||
319 | * |
||
320 | * @throws YamlVariableUndefinedException |
||
321 | * |
||
322 | * @return mixed |
||
323 | */ |
||
324 | 28 | private function getVariableValue($key, $varName) |
|
325 | { |
||
326 | 28 | if (!isset($this->frontMatter[$varName])) |
|
327 | { |
||
328 | 2 | throw new YamlVariableUndefinedException("Yaml variable `$varName` is not defined for: $key"); |
|
329 | } |
||
330 | |||
331 | 26 | return $this->frontMatter[$varName]; |
|
332 | } |
||
333 | |||
334 | // |
||
335 | // Utility functions |
||
336 | // |
||
337 | |||
338 | /** |
||
339 | * @param string $epochTime |
||
340 | * |
||
341 | * @return bool|\DateTime |
||
342 | */ |
||
343 | 6 | private function castDateTimeTimezone($epochTime) |
|
351 | |||
352 | /** |
||
353 | * @param $guess |
||
354 | * |
||
355 | * @return bool|\DateTime |
||
356 | */ |
||
357 | 7 | private function guessDateTime($guess) |
|
377 | } |
||
378 |