Passed
Branch master (f496ba)
by stéphane
02:11
created
1
<?php
2
namespace Dallgoot\Yaml;
3
4
use Dallgoot\Yaml\{Types as T, Regex as R};
5
use \SplDoublyLinkedList as DLL;
6
7
class Node
8
{
9
    public $indent = -1;
10
    public $line;
11
    public $type;
12
    /** @var Node|\SplDoublyLinkedList|DLL|null|string */
13
    public $value;
14
    private $_parent;
15
16
    public function __construct($nodeString = null, $line = null)
17
    {
18
        $this->line = $line;
19
        if (is_null($nodeString)) {
20
            $this->type = T::ROOT;
21
        } else {
22
            $this->parse($nodeString);
23
        }
24
    }
25
    public function setParent(Node $node):Node
26
    {
27
        $this->_parent = $node;
28
        return $this;
29
    }
30
31
    public function getParent($indent = null):Node
32
    {
33
        if (is_null($indent)) {
34
             return $this->_parent ?? $this;
35
        }
36
        $cursor = $this;
37
        while ($cursor->indent >= $indent) {
38
            $cursor = $cursor->_parent;
39
        }
40
        return $cursor;
41
    }
42
43
    public function add(Node $child):void
44
    {
45
        $child->setParent($this);
46
        $current = $this->value;
47
        if (in_array($this->type, T::$LITTERALS)) {
48
            $child->type = T::SCALAR;
49
            unset($child->name);
50
        }
51
        if (is_null($current)) {
52
            $this->value = $child;
53
            return;
54
        } elseif ($current instanceof Node) {
55
            $this->value = new DLL();
56
            $this->value->setIteratorMode(DLL::IT_MODE_KEEP);
57
            $this->value->push($current);
58
        }
59
        $this->value->push($child);
60
        //modify type according to child
61
        if ($this->value instanceof DLL && !property_exists($this->value, "type")) {
62
            switch ($child->type) {
63
                case T::KEY:    $this->value->type = T::MAPPING;break;
64
                case T::ITEM:   $this->value->type = T::SEQUENCE;break;
65
                case T::SCALAR: $this->value->type = $this->type;break;
66
            }
67
        }
68
    }
69
70
    public function getDeepestNode():Node
71
    {
72
        $cursor = $this;
73
        while ($cursor->value instanceof Node) {
74
            $cursor = $cursor->value;
75
        }
76
        return $cursor;
77
    }
78
    /**
79
    *  CAUTION : the types assumed here are NOT FINAL : they CAN be adjusted according to parent
80
    */
81
    public function parse(String $nodeString):Node
82
    {
83
        $nodeValue = preg_replace("/^\t+/m", " ", $nodeString);//permissive to tabs but replacement
84
        $this->indent = strspn($nodeValue, ' ');
85
        $nodeValue = ltrim($nodeValue);
86
        if ($nodeValue === '') {
87
            $this->type = T::EMPTY;
88
            $this->indent = 0;
89
        } elseif (substr($nodeValue, 0, 3) === '...') {//TODO: can have something after?
90
            $this->type = T::DOC_END;
91
        } elseif (preg_match(R::KEY, $nodeValue, $matches)) {
92
            $this->_onKey($matches);
93
        } else {//NOTE: can be of another type according to parent
94
            list($this->type, $value) = $this->_define($nodeValue);
95
            is_object($value) ? $this->add($value) : $this->value = $value;
96
        }
97
        return $this;
98
    }
99
100
    /**
101
     *  Set the type and value according to first character
102
     *
103
     * @param      string  $nodeValue  The node value
104
     * @return     array   contains [node->type, node->value]
105
     */
106
    private function _define($nodeValue):array
107
    {
108
        $v = substr($nodeValue, 1);
109
        if (in_array($nodeValue[0], ['"', "'"])) {
110
            $type = R::isProperlyQuoted($nodeValue) ? T::QUOTED : T::PARTIAL;
111
            return [$type, $nodeValue];
112
        }
113
        if (in_array($nodeValue[0], ['{', '[']))      return $this->_onObject($nodeValue);
114
        if (in_array($nodeValue[0], ['!', '&', '*'])) return $this->_onNodeAction($nodeValue);
115
        switch ($nodeValue[0]) {
116
            case '#': return [T::COMMENT, ltrim($v)];
117
            case "-": return $this->_onMinus($nodeValue);
118
            case '%': return [T::DIRECTIVE, ltrim($v)];
119
            case '?': return [T::SET_KEY,   empty($v) ? null : new Node(ltrim($v), $this->line)];
120
            case ':': return [T::SET_VALUE, empty($v) ? null : new Node(ltrim($v), $this->line)];
121
            case '>': return [T::LITTERAL_FOLDED, null];
122
            case '|': return [T::LITTERAL, null];
123
            default:
124
                return [T::SCALAR, $nodeValue];
125
        }
126
    }
127
128
    private function _onKey($matches):void
129
    {
130
        $this->type = T::KEY;
131
        $this->name = trim($matches[1]);
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
132
        $keyValue = isset($matches[2]) ? trim($matches[2]) : null;
133
        if (!empty($keyValue)) {
134
            $n = new Node($keyValue, $this->line);
135
            $hasComment = strpos($keyValue, ' #');
136
            if (!is_bool($hasComment)) {
137
                $tmpNode = new Node(trim(substr($keyValue, 0, $hasComment)), $this->line);
138
                if ($tmpNode->type !== T::PARTIAL) {
139
                    $comment = new Node(trim(substr($keyValue, $hasComment+1)), $this->line);
140
                    $this->add($comment);
141
                    $n = $tmpNode;
142
                }
143
            }
144
            $n->indent = $this->indent + strlen($this->name);
145
            $this->add($n);
146
        }
147
    }
148
149
    private function _onObject($value):array
150
    {
151
        json_decode($value, false, 512, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES);
152
        if (json_last_error() === JSON_ERROR_NONE)  return [T::JSON, $value];
153
        if (preg_match(R::MAPPING, $value))         return [T::MAPPING_SHORT, $value];
154
        if (preg_match(R::SEQUENCE, $value))        return [T::SEQUENCE_SHORT, $value];
155
        return [T::PARTIAL, $value];
156
    }
157
158
    private function _onMinus($nodeValue):array
159
    {
160
        if (substr($nodeValue, 0, 3) === '---') {
161
            $rest = trim(substr($nodeValue, 3));
162
            if (empty($rest)) return [T::DOC_START, null];
163
            $n = new Node($rest, $this->line);
164
            $n->indent = $this->indent + 4;
165
            return [T::DOC_START, $n->setParent($this)];
166
        }
167
        if (preg_match(R::ITEM, $nodeValue, $matches)) {
168
            if (isset($matches[1]) && !empty(trim($matches[1]))) {
169
                $n = new Node(trim($matches[1]), $this->line);
170
                return [T::ITEM, $n->setParent($this)];
171
            }
172
            return [T::ITEM, null];
173
        }
174
        return [T::SCALAR, $nodeValue];
175
    }
176
177
    private function _onNodeAction($nodeValue):array
178
    {
179
        // TODO: handle tags like  <tag:clarkevans.com,2002:invoice>
180
        $v = substr($nodeValue, 1);
181
        $type = ['!' => T::TAG, '&' => T::REF_DEF, '*' => T::REF_CALL][$nodeValue[0]];
182
        $pos = strpos($v, ' ');
183
        $this->name = is_bool($pos) ? $v : strstr($v, ' ', true);
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
184
        $n = is_bool($pos) ? null : (new Node(trim(substr($nodeValue, $pos+1)), $this->line))->setParent($this);
185
        return [$type, $n];
186
    }
187
188
    public function getPhpValue()
189
    {
190
        $v = $this->value;
191
        if (is_null($v)) return null;
192
        switch ($this->type) {
193
            case T::JSON:   return json_decode($v, false, 512, JSON_PARTIAL_OUTPUT_ON_ERROR);
194
            case T::QUOTED: return substr($v, 1, -1);
195
            case T::RAW:    return strval($v);
196
            case T::REF_CALL://fall through
197
            case T::SCALAR: return $this->getScalar($v);
198
            case T::MAPPING_SHORT:  return $this->getShortMapping(substr($this->value, 1, -1));
199
            //TODO : that's not robust enough, improve it
200
            case T::SEQUENCE_SHORT:
201
                $f = function ($e) { return self::getScalar(trim($e));};
0 ignored issues
show
Bug Best Practice introduced by
The method Dallgoot\Yaml\Node::getScalar() is not static, but was called statically. ( Ignorable by Annotation )

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

201
                $f = function ($e) { return self::/** @scrutinizer ignore-call */ getScalar(trim($e));};
Loading history...
202
                return array_map($f, explode(",", substr($this->value, 1, -1)));
203
            default:
204
                trigger_error("Error can not get PHP type for ".T::getName($this->type), E_USER_WARNING);
205
                return null;
206
        }
207
    }
208
209
    private function getScalar($v)
210
    {
211
        $types = ['yes'   => true,
212
                  'no'    => false,
213
                  'true'  => true,
214
                  'false' => false,
215
                  'null'  => null,
216
                  '.inf'  => INF,
217
                  '-.inf' => -INF,
218
                  '.nan'  => NAN
219
        ];
220
        if (in_array(strtolower($v), array_keys($types))) return $types[strtolower($v)];
221
        if (R::isDate($v))   return date_create($v);
222
        if (R::isNumber($v)) return $this->getNumber($v);
223
        return strval($v);
224
    }
225
226
    private function getNumber($v)
227
    {
228
        if (preg_match("/^(0o\d+)$/i", $v))      return intval(base_convert($v, 8, 10));
229
        if (preg_match("/^(0x[\da-f]+)$/i", $v)) return intval(base_convert($v, 16, 10));
230
        // if preg_match("/^([\d.]+e[-+]\d{1,2})$/", $v)://fall through
231
        // if preg_match("/^([-+]?(?:\d+|\d*.\d+))$/", $v):
232
            return is_bool(strpos($v, '.')) ? intval($v) : floatval($v);
233
    }
234
235
    //TODO : that's not robust enough, improve it
236
    private function getShortMapping($mappingString):object
237
    {
238
        $out = new \StdClass();
239
        foreach (explode(',', $mappingString) as $value) {
240
            list($keyName, $keyValue) = explode(':', $value);
241
            $out->{trim($keyName)} = $this->getScalar(trim($keyValue));
242
        }
243
        return $out;
244
    }
245
246
    public function __debugInfo():array
247
    {
248
        $out = ['line'  => $this->line,
249
                'indent'=> $this->indent,
250
                'type'  => T::getName($this->type),
251
                'value' => $this->value];
252
        property_exists($this, 'name') ? $out['type'] .= "($this->name)" : null;
253
        return $out;
254
    }
255
}
256