Completed
Push — master ( e4bfce...b9fe45 )
by Vladimir
02:19
created

FrontMatterParser::evaluateBlock()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 13
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 26
ccs 18
cts 18
cp 1
crap 4
rs 8.5806
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 22
    public function __construct(&$rawFrontMatter)
65
    {
66 22
        $this->expansionUsed = false;
67 22
        $this->nestingLevel = 0;
68 22
        $this->yamlKeys = array();
69
70 22
        $this->frontMatter = &$rawFrontMatter;
71
72 22
        $this->handleSpecialFrontMatter();
73 22
        $this->evaluateBlock($this->frontMatter);
74 17
    }
75
76
    /**
77
     * True if any fields were expanded in the Front Matter block
78
     *
79
     * @return bool
80
     */
81 6
    public function hasExpansion ()
82
    {
83 6
        return $this->expansionUsed;
84
    }
85
86
    //
87
    // Special FrontMatter fields
88
    //
89
90
    /**
91
     * Special treatment for some FrontMatter variables
92
     */
93 22
    private function handleSpecialFrontMatter ()
94
    {
95 22
        $this->handleDateField();
96 22
    }
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 6
        {
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 2
        }
116
        else
117
        {
118
            try
119
            {
120 4
                $itemDate = new \DateTime($date);
121
            }
122 4
            catch (\Exception $e) { return; }
123
        }
124
125 6
        if (!$itemDate === false)
126 6
        {
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 6
        }
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 21
            {
154 6
                $value = $this->evaluateExpandableField($keys, $value);
155 5
            }
156 21
            else if (is_array($value))
157 21
            {
158 6
                $this->evaluateBlock($value);
159 6
            }
160
            else
161
            {
162 21
                $value = $this->evaluateBasicType($keys, $value);
163
            }
164 20
        }
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 6
        {
182 5
            $fmStatement = array($fmStatement);
183 5
        }
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 6
            {
196 4
                $value = $this->evaluateArrayType($key, $value, $expandingVars);
197 3
            }
198
199 5
            $wip[] = $value;
200 5
        }
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 4
        {
220 4
            $expandableValue = array($expandableValue);
221 4
        }
222
223 4
        $this->expansionUsed = true;
224
225 4
        foreach ($arrayVariableNames as $variable)
226
        {
227 4
            if (ArrayUtilities::is_multidimensional($this->frontMatter[$variable]))
228 4
            {
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 3
                }
244 3
            }
245
246 3
            $expandableValue = $wip;
247 3
        }
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 13
            {
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 19
        }
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)
297
    {
298 21
        $variables = array();
299
300 21
        preg_match_all(self::VARIABLE_DEF, $string, $variables);
301
302
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
303
        // matching result individually.
304 21
        return $variables[1];
305
    }
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)
317
    {
318 15
        if (!isset($this->frontMatter[$varName]))
319 15
        {
320 2
            throw new YamlVariableUndefinedException("Yaml variable `$varName` is not defined for: $key");
321
        }
322
323 13
        return $this->frontMatter[$varName];
324
    }
325
}