Completed
Pull Request — master (#41)
by Vladimir
02:46
created

FrontMatterParser::guessDateTime()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.0466

Importance

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