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