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