Completed
Push — master ( 569899...5bb983 )
by stéphane
02:08
created

Node::onHyphen()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 16
nc 5
nop 1
dl 0
loc 20
rs 9.1111
c 0
b 0
f 0
1
<?php
2
3
namespace Dallgoot\Yaml;
4
5
use Dallgoot\Yaml\{Yaml as Y, Regex as R};
6
7
/**
8
 *
9
 * @author  Stéphane Rebai <[email protected]>
10
 * @license Apache 2.0
11
 * @link    TODO : url to specific online doc
12
 */
13
final class Node
14
{
15
    /** @var int */
16
    public $indent = -1;
17
    /** @var int */
18
    public $line;
19
    /** @var int */
20
    public $type;
21
    /** @var null|string|boolean */
22
    public $identifier;
23
    /** @var Node|NodeList|null|string */
24
    public $value = null;
25
    /** @var null|string */
26
    public $raw;
27
28
    /** @var null|Node */
29
    private $parent;
30
31
    /**
32
     * Create the Node object and parses $nodeString IF not null (else assume a root type Node)
33
     *
34
     * @param string|null $nodeString The node string
35
     * @param int|null    $line       The line
36
     */
37
    public function __construct($nodeString = null, $line = 0)
38
    {
39
        $this->line = (int) $line;
40
        if (is_null($nodeString)) {
41
            $this->type = Y::ROOT;
42
        } else {
43
            $this->raw = $nodeString;
44
            $this->parse($nodeString);
45
        }
46
    }
47
48
    /**
49
     * Sets the parent of the current Node
50
     *
51
     * @param Node $node The node
52
     *
53
     * @return Node|self The currentNode
54
     */
55
    public function setParent(Node $node):Node
56
    {
57
        $this->parent = $node;
58
        return $this;
59
    }
60
61
    /**
62
     * Gets the ancestor with specified $indent or the direct $parent OR the current Node itself
63
     *
64
     * @param int|null $indent The indent
65
     * @param int $type  first ancestor of this YAML::type is returned
66
     *
67
     * @return Node|self   The parent.
68
     */
69
    public function getParent(int $indent = null, $type = 0):Node
70
    {
71
        if ($this->type === Y::ROOT) return $this;
72
        if (!is_int($indent)) return $this->parent ?? $this;
73
        $cursor = $this;
74
        while ($cursor instanceof Node && $cursor->indent >= $indent) {
75
            if ($cursor->indent === $indent && $cursor->type !== $type) {
76
                $cursor = $cursor->parent ?? $cursor;
77
                break;
78
            }
79
            $cursor = $cursor->parent;
80
        }
81
        return $cursor;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $cursor could return the type null which is incompatible with the type-hinted return Dallgoot\Yaml\Node. Consider adding an additional type-check to rule them out.
Loading history...
82
    }
83
84
    /**
85
     * Set the value for the current Node :
86
     * - if value is null , then value = $child (Node)
87
     * - if value is Node, then value is a NodeList with (previous value AND $child)
88
     * - if value is a NodeList, push $child into and set NodeList type accordingly
89
     *
90
     * @param Node $child The child
91
     * @todo  refine the conditions when Y::LITTERALS
92
     */
93
    public function add(Node $child)
94
    {
95
        if ($this->type & (Y::SCALAR|Y::QUOTED)) {
96
            $this->getParent()->add($child);
97
            return;
98
        }
99
        $child->setParent($this);
100
        $current = $this->value;
101
        if (is_null($current)) {
102
            $this->value = $child;
103
            return;
104
        }elseif ($current instanceof Node) {
105
            $this->value = new NodeList();
106
            if ($current->type & Y::LITTERALS) {
107
                $this->value->type = $current->type;
108
            } else {
109
                $this->value->push($current);
110
            }
111
        }
112
        // if (is_null($this->value->type)) {
113
        //     $this->adjustValueType($child);
114
        // }
115
        $this->value->push($child);
0 ignored issues
show
Bug introduced by
The method push() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
        $this->value->/** @scrutinizer ignore-call */ 
116
                      push($child);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method push() does not exist on Dallgoot\Yaml\Node. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
        $this->value->/** @scrutinizer ignore-call */ 
116
                      push($child);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
116
    }
117
118
    //modify value type according to child
119
    // private function adjustValueType(Node $child)
120
    // {
121
    //     if ($child->type & Y::SET_KEY)   $this->value->type = Y::SET;
122
    //     if ($child->type & Y::KEY)       $this->value->type = Y::MAPPING;
123
    //     if ($child->type & Y::ITEM)      $this->value->type = Y::SEQUENCE;
124
    // }
125
126
    /**
127
     * Gets the deepest node.
128
     *
129
     * @return Node|self  The deepest node.
130
     */
131
    public function getDeepestNode():Node
132
    {
133
        $cursor = $this;
134
        while ($cursor->value instanceof Node || $cursor->value instanceof NodeList) {
135
            if ($cursor->value instanceof NodeList) {
136
                if ($cursor->value->count() === 1) {
137
                    $cursor = $cursor->value->OffsetGet(0);
138
                } else {
139
                    $cursor = $cursor;
140
                    break;
141
                }
142
            } else {
143
                $cursor = $cursor->value;
144
            }
145
        }
146
        return $cursor;
147
    }
148
149
    /**
150
     * Parses the string (assumed to be a line from a valid YAML)
151
     *
152
     * @param string $nodeString The node string
153
     *
154
     * @return Node|self
155
     */
156
    public function parse(string $nodeString):Node
157
    {
158
        $nodeValue = preg_replace("/^\t+/m", " ", $nodeString); //permissive to tabs but replacement
159
        $this->indent = strspn($nodeValue, ' ');
160
        $nodeValue = ltrim($nodeValue);
161
        if ($nodeValue === '') {
162
            $this->type = Y::BLANK;
163
        } elseif (substr($nodeValue, 0, 3) === '...') {//TODO: can have something on same line ?
164
            $this->type = Y::DOC_END;
165
        } elseif (preg_match(R::KEY, $nodeValue, $matches)) {
166
            $this->onKey($matches);
167
        } else {
168
            $this->identify($nodeValue);
169
        }
170
        return $this;
171
    }
172
173
    /**
174
     *  Set the type and value according to first character
175
     *
176
     * @param string $nodeValue The node value
177
     */
178
    private function identify($nodeValue)
179
    {
180
        $v = ltrim(substr($nodeValue, 1));
181
        $first = $nodeValue[0];
182
        if ($first === "-")                        $this->onHyphen($nodeValue);
183
        elseif (in_array($first, ['"', "'"]))      $this->onQuoted($nodeValue);
184
        elseif (in_array($first, ['{', '[']))      $this->onCompact($nodeValue);
185
        elseif (in_array($first, ['?', ':']))      $this->onSetElement($nodeValue);
186
        elseif (in_array($first, ['!', '&', '*'])) $this->onNodeAction($nodeValue);
187
        else {
188
            $characters = [ '#' =>  [Y::COMMENT, $v],
189
                            '%' =>  [Y::DIRECTIVE, $v],
190
                            '>' =>  [Y::LITT_FOLDED, null],
191
                            '|' =>  [Y::LITT, null]
192
                            ];
193
            if (isset($characters[$first])) {
194
                $this->type  = $characters[$first][0];
195
                $this->value = $characters[$first][1];
196
            } else {
197
                $this->type  = Y::SCALAR;
198
                $this->value = $nodeValue;
199
            }
200
        }
201
    }
202
203
    private function onQuoted($nodeValue)
204
    {
205
        $this->type = R::isProperlyQuoted($nodeValue) ? Y::QUOTED : Y::PARTIAL;
206
        $this->value = $nodeValue;
207
    }
208
209
    private function onSetElement($nodeValue)
210
    {
211
        $this->type = $nodeValue[0] === '?' ? Y::SET_KEY : Y::SET_VALUE;
212
        $v = trim(substr($nodeValue, 1));
213
        if (!empty($v)) {
214
            $this->value = new NodeList;
215
            $this->add(new Node($v, $this->line));
216
        }
217
    }
218
219
    /**
220
     * Process when a "key: value" syntax is found in the parsed string
221
     * Note : key is match 1, value is match 2 as per regex from R::KEY
222
     *
223
     * @param array $matches The matches provided by 'preg_match' function in Node::parse
224
     */
225
    private function onKey(array $matches)
226
    {
227
        $this->type = Y::KEY;
228
        $this->identifier = trim($matches[1], '"\' ');
229
        $value = isset($matches[2]) ? trim($matches[2]) : null;
230
        if (!empty($value)) {
231
            $hasComment = strpos($value, ' #');
232
            if (is_bool($hasComment)) {
1 ignored issue
show
introduced by
The condition is_bool($hasComment) is always false.
Loading history...
233
                $n = new Node($value, $this->line);
234
            } else {
235
                $n = new Node(trim(substr($value, 0, $hasComment)), $this->line);
236
                if ($n->type !== Y::PARTIAL) {
237
                    $comment = new Node(trim(substr($value, $hasComment + 1)), $this->line);
238
                    $comment->identifier = true; //to specify it is NOT a fullline comment
239
                    $this->add($comment);
240
                }
241
            }
242
            $n->indent = $this->indent + strlen($this->identifier);
243
            $this->add($n);
244
        }
245
    }
246
247
    /**
248
     * Determines the correct type and value when a compact object/array syntax is found
249
     *
250
     * @param string $value The value assumed to start with { or [ or characters
251
     *
252
     * @see Node::identify
253
     */
254
    private function onCompact($value)
255
    {
256
        $this->value = json_decode($value, false, 512, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
257
        if (json_last_error() === JSON_ERROR_NONE){
258
            $this->type = Y::JSON;
259
            return;
260
        }
261
        $this->value = new NodeList();
262
        if (preg_match(R::MAPPING, $value)){
263
            $this->type = Y::COMPACT_MAPPING;
264
            $this->value->type = Y::COMPACT_MAPPING;
265
            preg_match_all(R::MAPPING_VALUES, trim(substr($value, 1,-1)), $matches);
266
            foreach ($matches['k'] as $index => $property) {
267
                $n = new Node('', $this->line);
268
                $n->type = Y::KEY;
269
                $n->identifier = trim($property, '"\' ');//TODO : maybe check for proper quoting first ?
270
                $n->value = new Node($matches['v'][$index], $this->line);
271
                $this->value->push($n);
272
            }
273
            return;
274
        }
275
        if (preg_match(R::SEQUENCE, $value)){
276
            $this->type = Y::COMPACT_SEQUENCE;
277
            $this->value->type = Y::COMPACT_SEQUENCE;
278
            $count = preg_match_all(R::SEQUENCE_VALUES, trim(substr($value, 1,-1)), $matches);
0 ignored issues
show
Unused Code introduced by
The assignment to $count is dead and can be removed.
Loading history...
279
            foreach ($matches['item'] as $key => $item) {
280
                $i = new Node('', $this->line);
281
                $i->type = Y::ITEM;
282
                $i->add(new Node($item, $this->line));
283
                $this->value->push($i);
284
            }
285
            return;
286
        }
287
        $this->value = $value;
288
        $this->type  = Y::PARTIAL;
289
    }
290
291
    /**
292
     * Determines type and value when an hyphen "-" is found
293
     *
294
     * @param string $nodeValue The node value
295
     *
296
     * @see Node::identify
297
     */
298
    private function onHyphen($nodeValue)
299
    {
300
        if (substr($nodeValue, 0, 3) === '---') {
301
            $this->type = Y::DOC_START;
302
            $rest = trim(substr($nodeValue, 3));
303
            if (!empty($rest)) {
304
                $n = new Node($rest, $this->line);
305
                $n->indent = $this->indent + 4;
306
                $this->value = $n->setParent($this);
307
            }
308
        } elseif (preg_match(R::ITEM, $nodeValue, $matches)) {
309
            $this->type = Y::ITEM;
310
            if (isset($matches[1]) && !empty(trim($matches[1]))) {
311
                $n = new Node(trim($matches[1]), $this->line);
312
                $n->indent = $this->indent + 2;
313
                $this->value = $n->setParent($this);
314
            }
315
        } else {
316
            $this->type  = Y::SCALAR;
317
            $this->value = $nodeValue;
318
        }
319
    }
320
321
    /**
322
     * Determines the type and value according to $nodeValue when one of these characters is found : !,&,*
323
     *
324
     * @param string $nodeValue The node value
325
     *
326
     * @see  Node::identify
327
     * @todo handle tags like  <tag:clarkevans.com,2002:invoice>
328
     */
329
    private function onNodeAction($nodeValue)
330
    {
331
        $v = substr($nodeValue, 1);
332
        $this->type = ['!' => Y::TAG, '&' => Y::REF_DEF, '*' => Y::REF_CALL][$nodeValue[0]];
333
        $this->identifier = $v;
334
        $pos = strpos($v, ' ');
335
        if ($this->type & (Y::TAG|Y::REF_DEF) && is_int($pos)) {
336
            $this->identifier = strstr($v, ' ', true);
337
            $value = trim(substr($nodeValue, $pos + 1));
338
            $value = R::isProperlyQuoted($value) ? trim($value, "\"'") : $value;
339
            $this->add((new Node($value, $this->line))->setParent($this));
340
        }
341
    }
342
343
    /**
344
     * PHP internal function for debugging purpose : simplify output provided by 'var_dump'
345
     *
346
     * @return array  the Node properties and respective values displayed by 'var_dump'
347
     */
348
    public function __debugInfo():array
349
    {
350
        return ['line'  => $this->line,
351
                'indent'=> $this->indent,
352
                'type'  => Y::getName($this->type).($this->identifier ? "($this->identifier)" : ''),
353
                'value' => $this->value];
354
    }
355
}
356