Completed
Pull Request — master (#41)
by Vladimir
04:25
created

FrontMatterParser::getFrontMatterVariables()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 10
ccs 4
cts 4
cp 1
crap 1
rs 9.4285
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\Utilities\ArrayUtilities;
11
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 40
    public function __construct(&$rawFrontMatter)
65
    {
66 40
        $this->expansionUsed = false;
67 40
        $this->nestingLevel = 0;
68 40
        $this->yamlKeys = array();
69
70 40
        $this->frontMatter = &$rawFrontMatter;
71
72 40
        $this->handleSpecialFrontMatter();
73 40
        $this->evaluateBlock($this->frontMatter);
74 35
    }
75
76
    /**
77
     * True if any fields were expanded in the Front Matter block.
78
     *
79
     * @return bool
80
     */
81 18
    public function hasExpansion()
82
    {
83 18
        return $this->expansionUsed;
84
    }
85
86
    //
87
    // Special FrontMatter fields
88
    //
89
90
    /**
91
     * Special treatment for some FrontMatter variables.
92
     */
93 40
    private function handleSpecialFrontMatter()
94
    {
95 40
        $this->handleDateField();
96 40
    }
97
98
    /**
99
     * Special treatment for the `date` field in FrontMatter that creates three new variables: year, month, day.
100
     */
101 40
    private function handleDateField()
102
    {
103 40
        if (!isset($this->frontMatter['date']))
104 40
        {
105 33
            return;
106
        }
107
108 7
        $date = &$this->frontMatter['date'];
109 7
        $itemDate = $this->guessDateTime($date);
110
111 7
        if (!$itemDate === false)
112 7
        {
113 7
            $this->frontMatter['date'] = $itemDate->format('U');
114 7
            $this->frontMatter['year'] = $itemDate->format('Y');
115 7
            $this->frontMatter['month'] = $itemDate->format('m');
116 7
            $this->frontMatter['day'] = $itemDate->format('d');
117 7
        }
118 7
    }
119
120
    //
121
    // Evaluation
122
    //
123
124
    /**
125
     * Evaluate an array as Front Matter.
126
     *
127
     * @param array $yaml
128
     */
129 40
    private function evaluateBlock(&$yaml)
130
    {
131 40
        ++$this->nestingLevel;
132
133 40
        foreach ($yaml as $key => &$value)
134
        {
135 39
            $this->yamlKeys[$this->nestingLevel] = $key;
136 39
            $keys = implode('.', $this->yamlKeys);
137
138 39
            if (in_array($key, self::$expandableFields, true))
139 39
            {
140 21
                $value = $this->evaluateExpandableField($keys, $value);
141 20
            }
142 32
            elseif (is_array($value))
143
            {
144 13
                $this->evaluateBlock($value);
145 13
            }
146 32
            elseif (is_string($value))
147
            {
148 31
                $value = $this->evaluateBasicType($keys, $value);
149 28
            }
150 11
            elseif ($value instanceof \DateTime)
151
            {
152 4
                $value = $this->castDateTimeTimezone($value->format('U'));
153 4
            }
154 38
        }
155
156 37
        --$this->nestingLevel;
157 37
        $this->yamlKeys = array();
158 37
    }
159
160
    /**
161
     * Evaluate an expandable field.
162
     *
163
     * @param string $key
164
     * @param string $fmStatement
165
     *
166
     * @return array
167
     */
168 21
    private function evaluateExpandableField($key, $fmStatement)
169
    {
170 21
        if (!is_array($fmStatement))
171 21
        {
172 20
            $fmStatement = array($fmStatement);
173 20
        }
174
175 21
        $wip = array();
176
177 21
        foreach ($fmStatement as $statement)
178
        {
179 21
            $value = $this->evaluateBasicType($key, $statement, true);
180
181
            // Only continue expansion if there are Front Matter variables remain in the string, this means there'll be
182
            // Front Matter variables referencing arrays
183 21
            $expandingVars = $this->getFrontMatterVariables($value);
184 21
            if (!empty($expandingVars))
185 21
            {
186 4
                $value = $this->evaluateArrayType($key, $value, $expandingVars);
187 3
            }
188
189 20
            $wip[] = $value;
190 20
        }
191
192 20
        return $wip;
193
    }
194
195
    /**
196
     * Convert a string or an array into an array of ExpandedValue objects created through "value expansion".
197
     *
198
     * @param string $frontMatterKey     The current hierarchy of the Front Matter keys being used
199
     * @param string $expandableValue    The Front Matter value that will be expanded
200
     * @param array  $arrayVariableNames The Front Matter variable names that reference arrays
201
     *
202
     * @throws YamlUnsupportedVariableException If a multidimensional array is given for value expansion
203
     *
204
     * @return array
205
     */
206 4
    private function evaluateArrayType($frontMatterKey, $expandableValue, $arrayVariableNames)
207
    {
208 4
        if (!is_array($expandableValue))
209 4
        {
210 4
            $expandableValue = array($expandableValue);
211 4
        }
212
213 4
        $this->expansionUsed = true;
214
215 4
        foreach ($arrayVariableNames as $variable)
216
        {
217 4
            if (ArrayUtilities::is_multidimensional($this->frontMatter[$variable]))
218 4
            {
219 1
                throw new YamlUnsupportedVariableException("Yaml array expansion is not supported with multidimensional arrays with `$variable` for key `$frontMatterKey`");
220
            }
221
222 3
            $wip = array();
223
224 3
            foreach ($expandableValue as &$statement)
225
            {
226 3
                foreach ($this->frontMatter[$variable] as $value)
227
                {
228 3
                    $evaluatedValue = ($statement instanceof ExpandedValue) ? clone $statement : new ExpandedValue($statement);
229 3
                    $evaluatedValue->setEvaluated(str_replace('%' . $variable, $value, $evaluatedValue->getEvaluated()));
230 3
                    $evaluatedValue->setIterator($variable, $value);
231
232 3
                    $wip[] = $evaluatedValue;
233 3
                }
234 3
            }
235
236 3
            $expandableValue = $wip;
237 3
        }
238
239 3
        return $expandableValue;
240
    }
241
242
    /**
243
     * Evaluate an string for FrontMatter variables and replace them with the corresponding values.
244
     *
245
     * @param string $key          The key of the Front Matter value
246
     * @param string $string       The string that will be evaluated
247
     * @param bool   $ignoreArrays When set to true, an exception won't be thrown when an array is found with the
248
     *                             interpolation
249
     *
250
     * @throws YamlUnsupportedVariableException A FrontMatter variable is not an int, float, or string
251
     *
252
     * @return string The final string with variables evaluated
253
     */
254 39
    private function evaluateBasicType($key, $string, $ignoreArrays = false)
255
    {
256 39
        $variables = $this->getFrontMatterVariables($string);
257
258 39
        foreach ($variables as $variable)
259
        {
260 22
            $value = $this->getVariableValue($key, $variable);
261
262 20
            if (is_array($value) || is_bool($value))
263 20
            {
264
                if ($ignoreArrays)
265 6
                {
266 4
                    continue;
267
                }
268
269 2
                throw new YamlUnsupportedVariableException("Yaml variable `$variable` for `$key` is not a supported data type.");
270
            }
271
272 14
            $string = str_replace('%' . $variable, $value, $string);
273 36
        }
274
275 36
        return $string;
276
    }
277
278
    //
279
    // Variable management
280
    //
281
282
    /**
283
     * Get an array of FrontMatter variables in the specified string that need to be interpolated.
284
     *
285
     * @param string $string
286
     *
287
     * @return string[]
288
     */
289 39
    private function getFrontMatterVariables($string)
290
    {
291 39
        $variables = array();
292
293 39
        preg_match_all(self::VARIABLE_DEF, $string, $variables);
294
295
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
296
        // matching result individually.
297 39
        return $variables[1];
298
    }
299
300
    /**
301
     * Get the value of a FM variable or throw an exception.
302
     *
303
     * @param string $key
304
     * @param string $varName
305
     *
306
     * @throws YamlVariableUndefinedException
307
     *
308
     * @return mixed
309
     */
310 22
    private function getVariableValue($key, $varName)
311
    {
312 22
        if (!isset($this->frontMatter[$varName]))
313 22
        {
314 2
            throw new YamlVariableUndefinedException("Yaml variable `$varName` is not defined for: $key");
315
        }
316
317 20
        return $this->frontMatter[$varName];
318
    }
319
320
    //
321
    // Utility functions
322
    //
323
324
    /**
325
     * @param string $epochTime
326
     *
327
     * @return bool|\DateTime
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use \DateTime|false.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
328
     */
329 6
    private function castDateTimeTimezone($epochTime)
330
    {
331 6
        $timezone = new \DateTimeZone(date_default_timezone_get());
332 6
        $value = \DateTime::createFromFormat('U', $epochTime);
333 6
        $value->setTimezone($timezone);
334
335 6
        return $value;
336
    }
337
338
    /**
339
     * @param $guess
340
     *
341
     * @return bool|\DateTime
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use \DateTime|false.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
342
     */
343 7
    private function guessDateTime($guess)
344
    {
345 7
        if ($guess instanceof \DateTime)
346 7
        {
347 1
            return $guess;
348
        }
349 6
        elseif (is_numeric($guess))
350
        {
351 2
            return $this->castDateTimeTimezone($guess);
352
        }
353
354
        try
355
        {
356 4
            return new \DateTime($guess);
357
        }
358
        catch (\Exception $e)
359
        {
360
            return false;
361
        }
362
    }
363
}
364