Completed
Push — master ( 51b3db...ff05e7 )
by Vladimir
13s
created

FrontMatterParser::evaluateExpandableField()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 26
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 11
c 0
b 0
f 0
nc 6
nop 2
dl 0
loc 26
ccs 16
cts 16
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 16
    public function __construct(&$rawFrontMatter)
65
    {
66 16
        $this->expansionUsed = false;
67 16
        $this->nestingLevel = 0;
68 16
        $this->yamlKeys = array();
69
70 16
        $this->frontMatter = &$rawFrontMatter;
71
72 16
        $this->evaluateBlock($this->frontMatter);
73 11
    }
74
75
    /**
76
     * True if any fields were expanded in the Front Matter block
77
     *
78
     * @return bool
79
     */
80 6
    public function hasExpansion ()
81
    {
82 6
        return $this->expansionUsed;
83
    }
84
85
    /**
86
     * Evaluate an array as Front Matter
87
     *
88
     * @param array $yaml
89
     */
90 16
    private function evaluateBlock (&$yaml)
91
    {
92 16
        $this->nestingLevel++;
93
94 16
        foreach ($yaml as $key => &$value)
95
        {
96 16
            $this->yamlKeys[$this->nestingLevel] = $key;
97 16
            $keys = implode('.', $this->yamlKeys);
98
99 16
            if (in_array($key, self::$expandableFields, true))
100 16
            {
101 6
                $value = $this->evaluateExpandableField($keys, $value);
102 5
            }
103 16
            else if (is_array($value))
104 16
            {
105 6
                $this->evaluateBlock($value);
106 6
            }
107
            else
108
            {
109 16
                $value = $this->evaluateBasicType($keys, $value);
110
            }
111 14
        }
112
113 13
        $this->nestingLevel--;
114 13
        $this->yamlKeys = array();
115 13
    }
116
117
    /**
118
     * Evaluate an expandable field
119
     *
120
     * @param  string $key
121
     * @param  string $fmStatement
122
     *
123
     * @return array
124
     */
125 6
    private function evaluateExpandableField ($key, $fmStatement)
126
    {
127 6
        if (!is_array($fmStatement))
128 6
        {
129 5
            $fmStatement = array($fmStatement);
130 5
        }
131
132 6
        $wip = array();
133
134 6
        foreach ($fmStatement as $statement)
135
        {
136 6
            $value = $this->evaluateBasicType($key, $statement, true);
137
138
            // Only continue expansion if there are Front Matter variables remain in the string, this means there'll be
139
            // Front Matter variables referencing arrays
140 6
            $expandingVars = $this->getFrontMatterVariables($value);
141 6
            if (!empty($expandingVars))
142 6
            {
143 4
                $value = $this->evaluateArrayType($key, $value, $expandingVars);
144 3
            }
145
146 5
            $wip[] = $value;
147 5
        }
148
149 5
        return $wip;
150
    }
151
152
    /**
153
     * Convert a string or an array into an array of ExpandedValue objects created through "value expansion"
154
     *
155
     * @param  string $frontMatterKey     The current hierarchy of the Front Matter keys being used
156
     * @param  string $expandableValue    The Front Matter value that will be expanded
157
     * @param  array  $arrayVariableNames The Front Matter variable names that reference arrays
158
     *
159
     * @return array
160
     *
161
     * @throws YamlUnsupportedVariableException If a multidimensional array is given for value expansion
162
     */
163 4
    private function evaluateArrayType ($frontMatterKey, $expandableValue, $arrayVariableNames)
164
    {
165 4
        if (!is_array($expandableValue))
166 4
        {
167 4
            $expandableValue = array($expandableValue);
168 4
        }
169
170 4
        $this->expansionUsed = true;
171
172 4
        foreach ($arrayVariableNames as $variable)
173
        {
174 4
            if (ArrayUtilities::is_multidimensional($this->frontMatter[$variable]))
175 4
            {
176 1
                throw new YamlUnsupportedVariableException("Yaml array expansion is not supported with multidimensional arrays with `$variable` for key `$frontMatterKey`");
177
            }
178
179 3
            $wip = array();
180
181 3
            foreach ($expandableValue as &$statement)
182
            {
183 3
                foreach ($this->frontMatter[$variable] as $value)
184
                {
185 3
                    $evaluatedValue = ($statement instanceof ExpandedValue) ? clone($statement) : new ExpandedValue($statement);
186 3
                    $evaluatedValue->setEvaluated(str_replace('%' . $variable, $value, $evaluatedValue->getEvaluated()));
187 3
                    $evaluatedValue->setIterator($variable, $value);
188
189 3
                    $wip[] = $evaluatedValue;
190 3
                }
191 3
            }
192
193 3
            $expandableValue = $wip;
194 3
        }
195
196 3
        return $expandableValue;
197
    }
198
199
    /**
200
     * Evaluate an string for FrontMatter variables and replace them with the corresponding values
201
     *
202
     * @param  string $key          The key of the Front Matter value
203
     * @param  string $string       The string that will be evaluated
204
     * @param  bool   $ignoreArrays When set to true, an exception won't be thrown when an array is found with the
205
     *                              interpolation
206
     *
207
     * @return string The final string with variables evaluated
208
     *
209
     * @throws YamlUnsupportedVariableException A FrontMatter variable is not an int, float, or string
210
     */
211 16
    private function evaluateBasicType ($key, $string, $ignoreArrays = false)
212
    {
213 16
        $variables = $this->getFrontMatterVariables($string);
214
215 16
        foreach ($variables as $variable)
216
        {
217 15
            $value = $this->getVariableValue($key, $variable);
218
219 13
            if (is_array($value) || is_bool($value))
220 13
            {
221 6
                if ($ignoreArrays) { continue; }
222
223 2
                throw new YamlUnsupportedVariableException("Yaml variable `$variable` for `$key` is not a supported data type.");
224
            }
225
226 7
            $string = str_replace('%' . $variable, $value, $string);
227 14
        }
228
229 14
        return $string;
230
    }
231
232
    /**
233
     * Get an array of FrontMatter variables in the specified string that need to be interpolated
234
     *
235
     * @param  string $string
236
     *
237
     * @return string[]
238
     */
239 16
    private function getFrontMatterVariables ($string)
240
    {
241 16
        $variables = array();
242
243 16
        preg_match_all(self::VARIABLE_DEF, $string, $variables);
244
245
        // Default behavior causes $variables[0] is the entire string that was matched. $variables[1] will be each
246
        // matching result individually.
247 16
        return $variables[1];
248
    }
249
250
    /**
251
     * Get the value of a FM variable or throw an exception
252
     *
253
     * @param  string $key
254
     * @param  string $varName
255
     *
256
     * @return mixed
257
     * @throws YamlVariableUndefinedException
258
     */
259 15
    private function getVariableValue ($key, $varName)
260
    {
261 15
        if (!isset($this->frontMatter[$varName]))
262 15
        {
263 2
            throw new YamlVariableUndefinedException("Yaml variable `$varName` is not defined for: $key");
264
        }
265
266 13
        return $this->frontMatter[$varName];
267
    }
268
}