1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Dallgoot\Yaml; |
4
|
|
|
|
5
|
|
|
use Dallgoot\Yaml\{Yaml as Y, Regex as R}; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Constructs the result (YamlObject or array) according to every Node and respecting value |
9
|
|
|
* |
10
|
|
|
* @author Stéphane Rebai <[email protected]> |
11
|
|
|
* @license Apache 2.0 |
12
|
|
|
* @link TODO : url to specific online doc |
13
|
|
|
*/ |
14
|
|
|
final class Builder |
15
|
|
|
{ |
16
|
|
|
private static $_root; |
|
|
|
|
17
|
|
|
private static $_debug; |
|
|
|
|
18
|
|
|
|
19
|
|
|
const ERROR_NO_KEYNAME = self::class.": key has NO IDENTIFIER on line %d"; |
20
|
|
|
const INVALID_DOCUMENT = self::class.": DOCUMENT %d can NOT be a mapping AND a sequence"; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Builds a file. check multiple documents & split if more than one documents |
24
|
|
|
* |
25
|
|
|
* @param Node $_root The root node : Node with Node->type === YAML::ROOT |
26
|
|
|
* @param int $_debug the level of debugging requested |
27
|
|
|
* |
28
|
|
|
* @return array|YamlObject list of documents or just one. |
29
|
|
|
* @todo implement splitting on YAML::DOC_END also |
30
|
|
|
*/ |
31
|
|
|
public static function buildContent(Node $_root, int $_debug) |
|
|
|
|
32
|
|
|
{ |
33
|
|
|
self::$_debug = $_debug; |
|
|
|
|
34
|
|
|
$totalDocStart = 0; |
35
|
|
|
$documents = []; |
36
|
|
|
if ($_root->value instanceof Node) { |
|
|
|
|
37
|
|
|
$q = new NodeList; |
38
|
|
|
$q->push($_root->value); |
|
|
|
|
39
|
|
|
self::$_root = new YamlObject; |
40
|
|
|
$tmp = self::buildNodeList($q, self::$_root); |
41
|
|
|
return $tmp; |
42
|
|
|
} |
43
|
|
|
$_root->value instanceof NodeList && $_root->value->setIteratorMode(NodeList::IT_MODE_DELETE); |
|
|
|
|
44
|
|
|
foreach ($_root->value as $child) { |
|
|
|
|
45
|
|
|
if ($child->type & Y::DOC_START) $totalDocStart++; |
46
|
|
|
//if 0 or 1 DOC_START = we are still in first document |
47
|
|
|
$currentDoc = $totalDocStart > 1 ? $totalDocStart - 1 : 0; |
48
|
|
|
if (!isset($documents[$currentDoc])) $documents[$currentDoc] = new NodeList(); |
49
|
|
|
$documents[$currentDoc]->push($child); |
50
|
|
|
} |
51
|
|
|
$content = []; |
52
|
|
|
foreach ($documents as $num => $list) { |
53
|
|
|
try { |
54
|
|
|
self::$_root = new YamlObject; |
55
|
|
|
$content[] = self::buildNodeList($list, self::$_root); |
56
|
|
|
} catch (\Exception $e) { |
57
|
|
|
throw new \ParseError(sprintf(self::INVALID_DOCUMENT, $num)); |
|
|
|
|
58
|
|
|
} |
59
|
|
|
} |
60
|
|
|
return count($content) === 1 ? $content[0] : $content; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Generic function to distinguish between Node and NodeList |
65
|
|
|
* |
66
|
|
|
* @param Node|NodeList $node The node. |
|
|
|
|
67
|
|
|
* @param mixed $parent The parent |
68
|
|
|
* |
69
|
|
|
* @return mixed ( description_of_the_return_value ) |
70
|
|
|
*/ |
71
|
|
|
private static function build(object $node, &$parent = null) |
72
|
|
|
{ |
73
|
|
|
if ($node instanceof NodeList) return self::buildNodeList($node, $parent); |
74
|
|
|
return self::buildNode($node, $parent); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Builds a node list. |
79
|
|
|
* |
80
|
|
|
* @param NodeList $node The node |
81
|
|
|
* @param mixed $parent The parent |
82
|
|
|
* |
83
|
|
|
* @return mixed The parent (object|array) or a string representing the NodeList. |
84
|
|
|
*/ |
85
|
|
|
private static function buildNodeList(NodeList $node, &$parent=null) |
86
|
|
|
{ |
87
|
|
|
$node->forceType(); |
88
|
|
|
if ($node->type & (Y::RAW | Y::LITTERALS)) { |
89
|
|
|
return self::buildLitteral($node, (int) $node->type); |
90
|
|
|
} |
91
|
|
|
$action = function ($child, &$parent, &$out) { |
92
|
|
|
self::build($child, $out); |
93
|
|
|
}; |
94
|
|
|
if ($node->type & (Y::COMPACT_MAPPING|Y::MAPPING|Y::SET)) { |
95
|
|
|
$out = $parent ?? new \StdClass; |
96
|
|
|
} elseif ($node->type & (Y::COMPACT_SEQUENCE|Y::SEQUENCE)) { |
97
|
|
|
$out = $parent ?? []; |
98
|
|
|
} else { |
99
|
|
|
$out = ''; |
100
|
|
|
$action = function ($child, &$parent, &$out) { |
101
|
|
|
if ($child->type & (Y::SCALAR|Y::QUOTED)) { |
102
|
|
|
if ($parent) { |
103
|
|
|
$parent->setText(self::build($child)); |
104
|
|
|
} else { |
105
|
|
|
$out .= self::build($child); |
106
|
|
|
} |
107
|
|
|
} |
108
|
|
|
}; |
109
|
|
|
} |
110
|
|
|
foreach ($node as $child) { |
111
|
|
|
$action($child, $parent, $out); |
112
|
|
|
} |
113
|
|
|
if ($node->type & (Y::COMPACT_SEQUENCE|Y::COMPACT_MAPPING)) { |
114
|
|
|
$out = new Compact($out); |
115
|
|
|
} |
116
|
|
|
return is_null($out) ? $parent : $out; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Builds a node. |
121
|
|
|
* |
122
|
|
|
* @param Node $node The node of any Node->type |
123
|
|
|
* @param mixed $parent The parent |
124
|
|
|
* |
125
|
|
|
* @return mixed The node value as scalar, array or object or null to otherwise. |
126
|
|
|
*/ |
127
|
|
|
private static function buildNode(Node $node, &$parent) |
128
|
|
|
{ |
129
|
|
|
extract((array) $node, EXTR_REFS); |
|
|
|
|
130
|
|
|
if ($type & (Y::REF_DEF | Y::REF_CALL)) { |
131
|
|
|
if (is_object($value)) { |
132
|
|
|
$tmp = self::build($value, $parent) ?? $parent; |
133
|
|
|
} else { |
134
|
|
|
$tmp = Node2PHP::get($node); |
135
|
|
|
} |
136
|
|
|
if ($type === Y::REF_DEF) self::$_root->addReference($identifier, $tmp); |
137
|
|
|
return self::$_root->getReference($identifier); |
138
|
|
|
} |
139
|
|
|
if ($type & (Y::COMPACT_MAPPING|Y::COMPACT_SEQUENCE)) { |
140
|
|
|
return self::buildNodeList($node->value, $parent); |
|
|
|
|
141
|
|
|
} |
142
|
|
|
if ($type & Y::COMMENT) self::$_root->addComment($node->line, $node->value); |
143
|
|
|
$typesActions = [Y::DIRECTIVE => 'buildDirective', |
144
|
|
|
Y::ITEM => 'buildItem', |
145
|
|
|
Y::KEY => 'buildKey', |
146
|
|
|
Y::SET_KEY => 'buildSetKey', |
147
|
|
|
Y::SET_VALUE => 'buildSetValue', |
148
|
|
|
Y::TAG => 'buildTag', |
149
|
|
|
]; |
150
|
|
|
if (isset($typesActions[$type])) { |
151
|
|
|
return self::{$typesActions[$type]}($node, $parent); |
152
|
|
|
} |
153
|
|
|
return is_object($value) ? self::build($value, $parent) : Node2PHP::get($node); |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Builds a key and set the property + value to the given parent |
158
|
|
|
* |
159
|
|
|
* @param Node $node The node with type YAML::KEY |
160
|
|
|
* @param object|array $parent The parent |
|
|
|
|
161
|
|
|
* |
162
|
|
|
* @throws \ParseError if Key has no name(identifier) Note: empty string is allowed |
163
|
|
|
* @return null |
164
|
|
|
*/ |
165
|
|
|
private static function buildKey(Node $node, &$parent=null) |
166
|
|
|
{ |
167
|
|
|
extract((array) $node, EXTR_REFS); |
|
|
|
|
168
|
|
|
if (is_null($identifier)) { |
169
|
|
|
throw new \ParseError(sprintf(self::ERROR_NO_KEYNAME, $line)); |
|
|
|
|
170
|
|
|
} else { |
171
|
|
|
if ($value instanceof Node) { |
172
|
|
|
if ($value->type & (Y::ITEM|Y::KEY)) { |
173
|
|
|
$list = new NodeList(); |
174
|
|
|
$list->push($value); |
175
|
|
|
$list->type = $value->type & Y::ITEM ? Y::SEQUENCE : Y::MAPPING; |
176
|
|
|
$value = $list; |
177
|
|
|
} else { |
178
|
|
|
$result = self::build($value); |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
if ($value instanceof NodeList) { |
182
|
|
|
$childTypes = $value->getTypes(); |
183
|
|
|
if (is_null($value->type) && $childTypes & Y::SCALAR && !($childTypes & Y::COMMENT)) { |
184
|
|
|
$result = self::buildLitteral($value, Y::LITT_FOLDED); |
185
|
|
|
} else { |
186
|
|
|
$result = self::buildNodeList($value); |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
if (is_null($parent)) { |
190
|
|
|
return $result; |
191
|
|
|
} else { |
192
|
|
|
if (is_array($parent)) { |
193
|
|
|
$parent[$identifier] = $result; |
194
|
|
|
} else { |
195
|
|
|
$parent->{$identifier} = $result; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Builds an item. Adds the item value to the parent array|Iterator |
203
|
|
|
* |
204
|
|
|
* @param Node $node The node with type YAML::ITEM |
205
|
|
|
* @param array|\Iterator $parent The parent |
206
|
|
|
* |
207
|
|
|
* @throws \Exception if parent is another type than array or object Iterator |
208
|
|
|
* @return null |
209
|
|
|
*/ |
210
|
|
|
private static function buildItem(Node $node, &$parent) |
211
|
|
|
{ |
212
|
|
|
extract((array) $node, EXTR_REFS);//var_dump(__METHOD__); |
|
|
|
|
213
|
|
|
if (!is_array($parent) && !($parent instanceof \ArrayIterator)) { |
214
|
|
|
throw new \Exception("parent must be an Iterable not ".(is_object($parent) ? get_class($parent) : gettype($parent)), 1); |
215
|
|
|
} |
216
|
|
|
$ref = $parent instanceof \ArrayIterator ? $parent->getArrayCopy() : $parent; |
217
|
|
|
$numKeys = array_filter(array_keys($ref), 'is_int'); |
218
|
|
|
$key = count($numKeys) > 0 ? max($numKeys) + 1 : 0; |
219
|
|
|
if ($value instanceof Node) { |
220
|
|
|
if($value->type & Y::KEY) { |
221
|
|
|
self::buildKey($node->value, $parent); |
|
|
|
|
222
|
|
|
return; |
223
|
|
|
} elseif ($value->type & Y::ITEM) { |
224
|
|
|
$list = new NodeList(); |
225
|
|
|
$list->push($value); |
226
|
|
|
$list->type = Y::SEQUENCE; |
227
|
|
|
$result = self::buildNodeList($list); |
228
|
|
|
} else { |
229
|
|
|
$result = self::build($value); |
230
|
|
|
} |
231
|
|
|
} elseif ($value instanceof NodeList) { |
232
|
|
|
$result = self::buildNodeList($value); |
233
|
|
|
} |
234
|
|
|
$parent[$key] = $result; |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Builds a litteral (folded or not) or any NodeList that has YAML::RAW type (like a multiline value) |
240
|
|
|
* |
241
|
|
|
* @param NodeList $children The children |
|
|
|
|
242
|
|
|
* @param integer $type The type |
243
|
|
|
* |
244
|
|
|
* @return string The litteral. |
245
|
|
|
* @todo : Example 6.1. Indentation Spaces spaces must be considered as content |
246
|
|
|
*/ |
247
|
|
|
private static function buildLitteral(NodeList $list, int $type):string |
248
|
|
|
{ |
249
|
|
|
$list->rewind(); |
250
|
|
|
$refIndent = $list->current()->indent; |
251
|
|
|
//remove trailing blank |
252
|
|
|
while ($list->top()->type & Y::BLANK) { |
253
|
|
|
$list->pop(); |
254
|
|
|
} |
255
|
|
|
$result = ''; |
256
|
|
|
$separator = ''; |
257
|
|
|
if ($type & Y::LITT) $separator = "\n"; |
258
|
|
|
if ($type & Y::LITT_FOLDED) $separator = ' '; |
259
|
|
|
foreach ($list as $child) { |
260
|
|
|
if ($child->value instanceof NodeList) { |
261
|
|
|
$result .= self::buildLitteral($child->value, $type).$separator; |
262
|
|
|
} else { |
263
|
|
|
$val = $child->type & (Y::SCALAR|Y::BLANK) ? $child->value : substr($child->raw, $refIndent); |
264
|
|
|
if ($type & Y::LITT_FOLDED && ($child->indent > $refIndent || ($child->type & Y::BLANK))) { |
265
|
|
|
if ($result[-1] === $separator) |
266
|
|
|
$result[-1] = "\n"; |
267
|
|
|
if ($result[-1] === "\n") |
268
|
|
|
$result .= $val; |
269
|
|
|
continue; |
270
|
|
|
} |
271
|
|
|
$result .= $val.$separator; |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
return rtrim($result); |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Builds a set key. |
279
|
|
|
* |
280
|
|
|
* @param Node $node The node of type YAML::SET_KEY. |
281
|
|
|
* @param object $parent The parent |
282
|
|
|
* |
283
|
|
|
* @throws \Exception if a problem occurs during serialisation (json format) of the key |
284
|
|
|
*/ |
285
|
|
|
private function buildSetKey(Node $node, &$parent) |
|
|
|
|
286
|
|
|
{ |
287
|
|
|
$built = is_object($node->value) ? self::build($node->value) : null; |
|
|
|
|
288
|
|
|
$stringKey = is_string($built) && Regex::isProperlyQuoted($built) ? trim($built, '\'" '): $built; |
289
|
|
|
$key = json_encode($stringKey, JSON_PARTIAL_OUTPUT_ON_ERROR|JSON_UNESCAPED_SLASHES); |
290
|
|
|
// if (empty($key)) throw new \Exception("Cant serialize complex key: ".var_export($node->value, true), 1); |
291
|
|
|
$parent->{trim($key, '\'" ')} = null; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Builds a set value. |
296
|
|
|
* |
297
|
|
|
* @param Node $node The node of type YAML::SET_VALUE |
298
|
|
|
* @param object $parent The parent (the document object or any previous object created through a mapping key) |
299
|
|
|
*/ |
300
|
|
|
private function buildSetValue(Node $node, &$parent) |
|
|
|
|
301
|
|
|
{ |
302
|
|
|
$prop = array_keys(get_object_vars($parent)); |
303
|
|
|
$key = end($prop); |
304
|
|
|
if ($node->value->type & (Y::ITEM|Y::KEY|Y::SEQUENCE|Y::MAPPING)) { |
305
|
|
|
$p = $node->value->type === Y::ITEM ? [] : new \StdClass; |
306
|
|
|
self::build($node->value, $p); |
|
|
|
|
307
|
|
|
} else { |
308
|
|
|
$p = self::build($node->value, $parent->{$key}); |
|
|
|
|
309
|
|
|
} |
310
|
|
|
$parent->{$key} = $p; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Builds a tag and its value (also built) and encapsulates them in a Tag object. |
315
|
|
|
* |
316
|
|
|
* @param Node $node The node of type YAML::TAG |
317
|
|
|
* @param mixed $parent The parent |
318
|
|
|
* |
319
|
|
|
* @return Tag|null The tag object of class Dallgoot\Yaml\Tag. |
320
|
|
|
*/ |
321
|
|
|
private static function buildTag(Node $node, &$parent) |
322
|
|
|
{ |
323
|
|
|
$name = (string) $node->identifier; |
324
|
|
|
if ($parent === self::$_root && empty($node->value)) { |
325
|
|
|
$parent->addTag($name); |
326
|
|
|
return; |
327
|
|
|
} |
328
|
|
|
$target = $node->value; |
329
|
|
|
if ($node->value instanceof Node) { |
330
|
|
|
if ($node->value->type & (Y::KEY|Y::ITEM)) { |
331
|
|
|
if (is_null($parent)) { |
332
|
|
|
$target = new NodeList; |
333
|
|
|
$target->push($node->value); |
334
|
|
|
$target->type = $node->value->type & Y::KEY ? Y::MAPPING : Y::SEQUENCE; |
335
|
|
|
} else { |
336
|
|
|
$node->value->type & Y::KEY ? self::buildKey($node->value, $parent) : self::buildItem($node->value, $parent); |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
//TODO: have somewhere a list of common tags and their treatment |
341
|
|
|
// if (in_array($node->identifier, ['!binary', '!str'])) { |
342
|
|
|
// $target->type = Y::RAW; |
343
|
|
|
// } |
344
|
|
|
return new Tag($name, is_object($target) ? self::build($target) : null); |
|
|
|
|
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* Builds a directive. NOT IMPLEMENTED YET |
349
|
|
|
* |
350
|
|
|
* @param Node $node The node |
351
|
|
|
* @param mixed $parent The parent |
352
|
|
|
* @todo implement if requested |
353
|
|
|
*/ |
354
|
|
|
private function buildDirective(Node $node, $parent) |
|
|
|
|
355
|
|
|
{ |
356
|
|
|
// TODO : implement |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
This check examines a number of code elements and verifies that they conform to the given naming conventions.
You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.