@kirigami/php-prepros 1.9.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +502 -21
- package/README.md +1988 -1817
- package/index.d.ts +62 -26
- package/index.js +1 -1
- package/package.json +59 -59
- package/src/imagebatch.php +68 -67
- package/src/libraries/aliases.inc.php +879 -879
- package/src/libraries/curl.class.php +7 -7
- package/src/libraries/ld.class.php +803 -804
- package/src/libraries/md-legacy.class.php +1257 -0
- package/src/libraries/md.class.php +51 -1249
- package/src/libraries/meta.class.php +496 -506
- package/src/libraries/{normalizer.class.php → normalizer-legacy.class.php} +11 -1
- package/src/libraries/prepros.class.php +85 -8
- package/src/libraries/schema-legacy.class.php +549 -0
- package/src/libraries/schema.class.php +99 -472
- package/src/libraries/yaml-legacy.class.php +598 -0
- package/src/libraries/yaml.class.php +41 -406
- package/src/prepros.js +152 -48
- package/src/prepros.php +4 -5
- package/src/runenv.php +2 -2
- package/src/utils.inc.php +12 -2
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* YAML_LEGACY — lightweight, fully static, pure-PHP YAML parser.
|
|
5
|
+
*
|
|
6
|
+
* Retired in favor of YAML:: (native `yaml`/libyaml extension, ~7x faster —
|
|
7
|
+
* see docs/STATUS.md and docs/DECISIONS.md in the repo root). Kept here as a
|
|
8
|
+
* reference implementation and an easy rollback path; not autoloaded or used
|
|
9
|
+
* anywhere by default. Known to mis-parse compact nested sequences
|
|
10
|
+
* (`- - item`) — see docs/BUGS.md before reviving it.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* $data = YAML_LEGACY::parse($yamlString); // mappings → stdClass (default)
|
|
14
|
+
* $data = YAML_LEGACY::parse($yamlString, true); // mappings → associative array
|
|
15
|
+
* $data = YAML_LEGACY::parseFile('/path/to/config.yaml');
|
|
16
|
+
*
|
|
17
|
+
* Supports:
|
|
18
|
+
* - Typed scalars (string, int, float, bool, null)
|
|
19
|
+
* - Single and double quotes (with escape sequences)
|
|
20
|
+
* - Multi-line blocks (| literal and > folded, with chomping -, +)
|
|
21
|
+
* - Multi-line plain scalars (continuation with no indicator)
|
|
22
|
+
* - Nested mappings and sequences
|
|
23
|
+
* - Inline collections [a, b] and {k: v}
|
|
24
|
+
* - Comments (#)
|
|
25
|
+
* - Multiple documents separated by ---
|
|
26
|
+
*/
|
|
27
|
+
class YAML_LEGACY
|
|
28
|
+
{
|
|
29
|
+
private function __construct() {}
|
|
30
|
+
|
|
31
|
+
// -------------------------------------------------------------------------
|
|
32
|
+
// Public entry points
|
|
33
|
+
// -------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
public static function parse(string $yaml, bool $assoc = false): mixed
|
|
36
|
+
{
|
|
37
|
+
$yaml = str_replace(["\r\n", "\r"], "\n", $yaml);
|
|
38
|
+
$lines = explode("\n", $yaml);
|
|
39
|
+
$pos = 0;
|
|
40
|
+
|
|
41
|
+
$documents = [];
|
|
42
|
+
|
|
43
|
+
while ($pos < count($lines)) {
|
|
44
|
+
$line = $lines[$pos];
|
|
45
|
+
if (preg_match('/^---/', $line)) { $pos++; continue; }
|
|
46
|
+
if (preg_match('/^\.\.\./', $line)) { $pos++; break; }
|
|
47
|
+
|
|
48
|
+
$doc = self::parseBlock($lines, $pos, 0, $assoc);
|
|
49
|
+
$documents[] = $doc;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return count($documents) === 1 ? $documents[0] : $documents;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public static function parseFile(string $path, bool $assoc = false): mixed
|
|
56
|
+
{
|
|
57
|
+
if (!is_readable($path)) {
|
|
58
|
+
throw new \RuntimeException("Cannot read file: $path");
|
|
59
|
+
}
|
|
60
|
+
return self::parse(file_get_contents($path), $assoc);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Loads a YAML or JSON file, then walks the result recursively and replaces
|
|
65
|
+
* any string value that matches a relative path to an existing YAML/JSON
|
|
66
|
+
* file with that file's deserialized content.
|
|
67
|
+
*
|
|
68
|
+
* Each included file is itself resolved relative to its own directory, and
|
|
69
|
+
* so on (recursive).
|
|
70
|
+
*
|
|
71
|
+
* If the string doesn't end in .yml/.yaml/.json, or the resolved file
|
|
72
|
+
* doesn't exist, the value is kept as-is.
|
|
73
|
+
*
|
|
74
|
+
* Circular references (e.g. A → B → A) throw a RuntimeException.
|
|
75
|
+
*
|
|
76
|
+
* Usage:
|
|
77
|
+
* $data = YAML_LEGACY::loadFile('/path/to/config.yaml');
|
|
78
|
+
* $data = YAML_LEGACY::loadFile('/path/to/config.yaml', true); // assoc arrays
|
|
79
|
+
*
|
|
80
|
+
* @param string $path Path to the root file (YAML or JSON).
|
|
81
|
+
* @param bool $assoc true → mappings as arrays, false → stdClass.
|
|
82
|
+
* @return mixed
|
|
83
|
+
*/
|
|
84
|
+
public static function loadFile(string $path, bool $assoc = false): mixed
|
|
85
|
+
{
|
|
86
|
+
$absolute = realpath($path);
|
|
87
|
+
if ($absolute === false || !is_readable($absolute)) {
|
|
88
|
+
throw new \RuntimeException("Cannot read file: $path");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return self::loadFileRecursive($absolute, $assoc, []);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// -------------------------------------------------------------------------
|
|
95
|
+
// Private methods for loadFile
|
|
96
|
+
// -------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Loads and resolves a file, propagating the list of ancestors to detect
|
|
100
|
+
* cycles.
|
|
101
|
+
*
|
|
102
|
+
* @param string $absolute Canonical absolute path of the file to load.
|
|
103
|
+
* @param bool $assoc
|
|
104
|
+
* @param string[] $ancestors Absolute paths of the files currently being processed.
|
|
105
|
+
* @return mixed
|
|
106
|
+
*/
|
|
107
|
+
private static function loadFileRecursive(string $absolute, bool $assoc, array $ancestors): mixed
|
|
108
|
+
{
|
|
109
|
+
if (in_array($absolute, $ancestors, true)) {
|
|
110
|
+
throw new \RuntimeException(
|
|
111
|
+
"Circular reference detected: " . implode(' → ', $ancestors) . " → $absolute"
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$ext = strtolower(pathinfo($absolute, PATHINFO_EXTENSION));
|
|
116
|
+
$raw = file_get_contents($absolute);
|
|
117
|
+
$dir = dirname($absolute);
|
|
118
|
+
|
|
119
|
+
if ($ext === 'json') {
|
|
120
|
+
$data = json_decode($raw, $assoc, 512, JSON_THROW_ON_ERROR);
|
|
121
|
+
} else {
|
|
122
|
+
// .yml, .yaml or any other extension treated as YAML
|
|
123
|
+
$data = self::parse($raw, $assoc);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return self::resolveNode($data, $dir, $assoc, [...$ancestors, $absolute]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Recursively walks a PHP value (stdClass object, array, string, scalar)
|
|
131
|
+
* and resolves references to external files.
|
|
132
|
+
*
|
|
133
|
+
* @param mixed $node
|
|
134
|
+
* @param string $dir Directory of the file that contains this node.
|
|
135
|
+
* @param bool $assoc
|
|
136
|
+
* @param string[] $ancestors
|
|
137
|
+
* @return mixed
|
|
138
|
+
*/
|
|
139
|
+
private static function resolveNode(mixed $node, string $dir, bool $assoc, array $ancestors): mixed
|
|
140
|
+
{
|
|
141
|
+
if (is_string($node)) {
|
|
142
|
+
return self::resolveString($node, $dir, $assoc, $ancestors);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (is_array($node)) {
|
|
146
|
+
foreach ($node as $key => $value) {
|
|
147
|
+
$node[$key] = self::resolveNode($value, $dir, $assoc, $ancestors);
|
|
148
|
+
}
|
|
149
|
+
return $node;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if ($node instanceof \stdClass) {
|
|
153
|
+
foreach ($node as $key => $value) {
|
|
154
|
+
$node->$key = self::resolveNode($value, $dir, $assoc, $ancestors);
|
|
155
|
+
}
|
|
156
|
+
return $node;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// int, float, bool, null → returned as-is
|
|
160
|
+
return $node;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* If the string points to an existing YAML/JSON file (path relative to the
|
|
165
|
+
* $dir directory), loads that file recursively. Otherwise returns the
|
|
166
|
+
* original string.
|
|
167
|
+
*
|
|
168
|
+
* @param string $str
|
|
169
|
+
* @param string $dir
|
|
170
|
+
* @param bool $assoc
|
|
171
|
+
* @param string[] $ancestors
|
|
172
|
+
* @return mixed
|
|
173
|
+
*/
|
|
174
|
+
private static function resolveString(string $str, string $dir, bool $assoc, array $ancestors): mixed
|
|
175
|
+
{
|
|
176
|
+
$trimmed = trim($str);
|
|
177
|
+
|
|
178
|
+
// Quick filter on the extension
|
|
179
|
+
if (!preg_match('/\.(ya?ml|json)$/i', $trimmed)) {
|
|
180
|
+
return $str;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Resolve the path relative to the parent file's directory
|
|
184
|
+
$candidate = $dir . DIRECTORY_SEPARATOR . $trimmed;
|
|
185
|
+
$absolute = realpath($candidate);
|
|
186
|
+
|
|
187
|
+
if ($absolute === false || !is_readable($absolute)) {
|
|
188
|
+
return $str; // File not found → plain string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return self::loadFileRecursive($absolute, $assoc, $ancestors);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// -------------------------------------------------------------------------
|
|
195
|
+
// Recursive parsing
|
|
196
|
+
// -------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
private static function parseBlock(array $lines, int &$pos, int $indent, bool $assoc = false): mixed
|
|
199
|
+
{
|
|
200
|
+
self::skipEmptyAndComments($lines, $pos);
|
|
201
|
+
|
|
202
|
+
if ($pos >= count($lines)) return null;
|
|
203
|
+
|
|
204
|
+
$line = $lines[$pos];
|
|
205
|
+
$lineIndent = self::getIndent($line);
|
|
206
|
+
$trimmed = ltrim($line);
|
|
207
|
+
|
|
208
|
+
if (str_starts_with($trimmed, '- ') || $trimmed === '-') {
|
|
209
|
+
return self::parseSequence($lines, $pos, $lineIndent, $assoc);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (self::isMapping($trimmed)) {
|
|
213
|
+
return self::parseMapping($lines, $pos, $lineIndent, $assoc);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private static function parseMapping(array $lines, int &$pos, int $indent, bool $assoc = false): array|object
|
|
220
|
+
{
|
|
221
|
+
$result = [];
|
|
222
|
+
|
|
223
|
+
while ($pos < count($lines)) {
|
|
224
|
+
self::skipEmptyAndComments($lines, $pos);
|
|
225
|
+
if ($pos >= count($lines)) break;
|
|
226
|
+
|
|
227
|
+
$line = $lines[$pos];
|
|
228
|
+
$lineIndent = self::getIndent($line);
|
|
229
|
+
$trimmed = ltrim($line);
|
|
230
|
+
|
|
231
|
+
if ($lineIndent < $indent) break;
|
|
232
|
+
if ($lineIndent > $indent) break;
|
|
233
|
+
if (preg_match('/^(---|\.\.\.)\s*$/', $trimmed)) break;
|
|
234
|
+
if (!self::isMapping($trimmed)) break;
|
|
235
|
+
|
|
236
|
+
[$key, $rest] = self::splitKeyValue($trimmed);
|
|
237
|
+
$pos++;
|
|
238
|
+
|
|
239
|
+
if ($rest === null) {
|
|
240
|
+
// Value on the following lines: sub-block or multi-line plain scalar
|
|
241
|
+
self::skipEmptyAndComments($lines, $pos);
|
|
242
|
+
if ($pos < count($lines)) {
|
|
243
|
+
$nextIndent = self::getIndent($lines[$pos]);
|
|
244
|
+
if ($nextIndent > $indent) {
|
|
245
|
+
$nextTrimmed = ltrim($lines[$pos]);
|
|
246
|
+
// Plain scalar if it's neither a mapping nor a sequence
|
|
247
|
+
if (!self::isMapping($nextTrimmed) && !str_starts_with($nextTrimmed, '- ')) {
|
|
248
|
+
$result[$key] = self::parseScalar(
|
|
249
|
+
self::collectPlainScalar($lines, $pos, $indent, '')
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
$result[$key] = self::parseBlock($lines, $pos, $nextIndent, $assoc);
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
$result[$key] = null;
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
$result[$key] = null;
|
|
259
|
+
}
|
|
260
|
+
} elseif ($rest === '|' || $rest === '|-' || $rest === '|+') {
|
|
261
|
+
$result[$key] = self::parseLiteralBlock($lines, $pos, $indent, $rest);
|
|
262
|
+
} elseif ($rest === '>' || $rest === '>-' || $rest === '>+') {
|
|
263
|
+
$result[$key] = self::parseFoldedBlock($lines, $pos, $indent, $rest);
|
|
264
|
+
} elseif ($rest !== '' && ($rest[0] === '[' || $rest[0] === '{')) {
|
|
265
|
+
$result[$key] = self::parseInlineCollection($rest, $assoc);
|
|
266
|
+
} else {
|
|
267
|
+
// Inline scalar, may be followed by continuation lines
|
|
268
|
+
$result[$key] = self::parseScalar(
|
|
269
|
+
self::collectPlainScalar($lines, $pos, $indent, $rest)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return $assoc ? $result : (object) $result;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private static function parseSequence(array $lines, int &$pos, int $indent, bool $assoc = false): array
|
|
278
|
+
{
|
|
279
|
+
$result = [];
|
|
280
|
+
|
|
281
|
+
while ($pos < count($lines)) {
|
|
282
|
+
self::skipEmptyAndComments($lines, $pos);
|
|
283
|
+
if ($pos >= count($lines)) break;
|
|
284
|
+
|
|
285
|
+
$line = $lines[$pos];
|
|
286
|
+
$lineIndent = self::getIndent($line);
|
|
287
|
+
$trimmed = ltrim($line);
|
|
288
|
+
|
|
289
|
+
if ($lineIndent < $indent) break;
|
|
290
|
+
if ($lineIndent > $indent) break;
|
|
291
|
+
if (preg_match('/^(---|\.\.\.)\s*$/', $trimmed)) break;
|
|
292
|
+
if (!str_starts_with($trimmed, '- ') && $trimmed !== '-') break;
|
|
293
|
+
|
|
294
|
+
$itemContent = $trimmed === '-' ? '' : substr($trimmed, 2);
|
|
295
|
+
$pos++;
|
|
296
|
+
|
|
297
|
+
if ($itemContent === '') {
|
|
298
|
+
self::skipEmptyAndComments($lines, $pos);
|
|
299
|
+
if ($pos < count($lines)) {
|
|
300
|
+
$nextIndent = self::getIndent($lines[$pos]);
|
|
301
|
+
$result[] = $nextIndent > $indent
|
|
302
|
+
? self::parseBlock($lines, $pos, $nextIndent, $assoc)
|
|
303
|
+
: null;
|
|
304
|
+
} else {
|
|
305
|
+
$result[] = null;
|
|
306
|
+
}
|
|
307
|
+
} elseif (self::isMapping($itemContent)) {
|
|
308
|
+
// Inline mapping inside the sequence:
|
|
309
|
+
// Rebuild a virtual array of lines by prefixing the first key
|
|
310
|
+
// with fakeIndent, then delegate entirely to parseMapping to get
|
|
311
|
+
// all of its logic (|, >, plain scalars, sub-blocks…).
|
|
312
|
+
$fakeIndent = $lineIndent + 2;
|
|
313
|
+
$fakePrefix = str_repeat(' ', $fakeIndent);
|
|
314
|
+
$virtualLines = array_merge(
|
|
315
|
+
[$fakePrefix . $itemContent],
|
|
316
|
+
array_slice($lines, $pos)
|
|
317
|
+
);
|
|
318
|
+
$vPos = 0;
|
|
319
|
+
$itemMap = self::parseMapping($virtualLines, $vPos, $fakeIndent, $assoc);
|
|
320
|
+
$pos += max(0, $vPos - 1);
|
|
321
|
+
$result[] = $itemMap;
|
|
322
|
+
} elseif ($itemContent[0] === '[' || $itemContent[0] === '{') {
|
|
323
|
+
$result[] = self::parseInlineCollection($itemContent, $assoc);
|
|
324
|
+
} else {
|
|
325
|
+
$result[] = self::parseScalar($itemContent);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return $result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// -------------------------------------------------------------------------
|
|
333
|
+
// Multi-line plain scalar
|
|
334
|
+
// -------------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Collects a scalar that may continue on lines more indented than $parentIndent.
|
|
338
|
+
* Continuation lines are joined with a space (implicit folding).
|
|
339
|
+
*/
|
|
340
|
+
private static function collectPlainScalar(array $lines, int &$pos, int $parentIndent, string $first): string
|
|
341
|
+
{
|
|
342
|
+
$parts = $first !== '' ? [trim($first)] : [];
|
|
343
|
+
|
|
344
|
+
while ($pos < count($lines)) {
|
|
345
|
+
$raw = $lines[$pos];
|
|
346
|
+
$trimmed = trim($raw);
|
|
347
|
+
|
|
348
|
+
// Blank line: end of the scalar
|
|
349
|
+
if ($trimmed === '') break;
|
|
350
|
+
|
|
351
|
+
$lineIndent = self::getIndent($raw);
|
|
352
|
+
|
|
353
|
+
// Back to the parent indentation or less: end
|
|
354
|
+
if ($lineIndent <= $parentIndent) break;
|
|
355
|
+
|
|
356
|
+
// Comment alone on the line: end
|
|
357
|
+
if (str_starts_with($trimmed, '#')) break;
|
|
358
|
+
|
|
359
|
+
// It's a mapping or a sequence: end
|
|
360
|
+
if (self::isMapping($trimmed) || str_starts_with($trimmed, '- ')) break;
|
|
361
|
+
|
|
362
|
+
$parts[] = $trimmed;
|
|
363
|
+
$pos++;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return implode(' ', $parts);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// -------------------------------------------------------------------------
|
|
370
|
+
// Multi-line blocks
|
|
371
|
+
// -------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
private static function parseLiteralBlock(array $lines, int &$pos, int $parentIndent, string $indicator): string
|
|
374
|
+
{
|
|
375
|
+
$blockLines = [];
|
|
376
|
+
$blockIndent = null;
|
|
377
|
+
$chomping = self::getChomping($indicator);
|
|
378
|
+
|
|
379
|
+
while ($pos < count($lines)) {
|
|
380
|
+
$raw = $lines[$pos];
|
|
381
|
+
|
|
382
|
+
if (trim($raw) === '') {
|
|
383
|
+
$blockLines[] = '';
|
|
384
|
+
$pos++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
$lineIndent = self::getIndent($raw);
|
|
389
|
+
if ($blockIndent === null) {
|
|
390
|
+
if ($lineIndent <= $parentIndent) break;
|
|
391
|
+
$blockIndent = $lineIndent;
|
|
392
|
+
}
|
|
393
|
+
if ($lineIndent < $blockIndent) break;
|
|
394
|
+
|
|
395
|
+
$blockLines[] = substr($raw, $blockIndent);
|
|
396
|
+
$pos++;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return self::applyChomping(implode("\n", $blockLines), $chomping);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private static function parseFoldedBlock(array $lines, int &$pos, int $parentIndent, string $indicator): string
|
|
403
|
+
{
|
|
404
|
+
$blockLines = [];
|
|
405
|
+
$blockIndent = null;
|
|
406
|
+
$chomping = self::getChomping($indicator);
|
|
407
|
+
|
|
408
|
+
while ($pos < count($lines)) {
|
|
409
|
+
$raw = $lines[$pos];
|
|
410
|
+
|
|
411
|
+
if (trim($raw) === '') {
|
|
412
|
+
$blockLines[] = '';
|
|
413
|
+
$pos++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
$lineIndent = self::getIndent($raw);
|
|
418
|
+
if ($blockIndent === null) {
|
|
419
|
+
if ($lineIndent <= $parentIndent) break;
|
|
420
|
+
$blockIndent = $lineIndent;
|
|
421
|
+
}
|
|
422
|
+
if ($lineIndent < $blockIndent) break;
|
|
423
|
+
|
|
424
|
+
$blockLines[] = rtrim(substr($raw, $blockIndent));
|
|
425
|
+
$pos++;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
$folded = '';
|
|
429
|
+
$count = count($blockLines);
|
|
430
|
+
for ($i = 0; $i < $count; $i++) {
|
|
431
|
+
if ($blockLines[$i] === '') {
|
|
432
|
+
$folded .= "\n";
|
|
433
|
+
} elseif ($i < $count - 1 && $blockLines[$i + 1] !== '') {
|
|
434
|
+
$folded .= $blockLines[$i] . ' ';
|
|
435
|
+
} else {
|
|
436
|
+
$folded .= $blockLines[$i];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return self::applyChomping(rtrim($folded, ' '), $chomping);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// -------------------------------------------------------------------------
|
|
444
|
+
// Inline collections [a, b] {k: v}
|
|
445
|
+
// -------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
private static function parseInlineCollection(string $raw, bool $assoc = false): mixed
|
|
448
|
+
{
|
|
449
|
+
$raw = trim($raw);
|
|
450
|
+
if ($raw[0] === '[') return self::parseInlineSequence($raw, $assoc);
|
|
451
|
+
if ($raw[0] === '{') return self::parseInlineMapping($raw, $assoc);
|
|
452
|
+
return self::parseScalar($raw);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private static function parseInlineSequence(string $raw, bool $assoc = false): array
|
|
456
|
+
{
|
|
457
|
+
$inner = trim(substr($raw, 1, strrpos($raw, ']') - 1));
|
|
458
|
+
if ($inner === '') return [];
|
|
459
|
+
|
|
460
|
+
return array_map(
|
|
461
|
+
fn($item) => self::parseInlineCollection(trim($item), $assoc),
|
|
462
|
+
self::splitInline($inner)
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private static function parseInlineMapping(string $raw, bool $assoc = false): array|object
|
|
467
|
+
{
|
|
468
|
+
$inner = trim(substr($raw, 1, strrpos($raw, '}') - 1));
|
|
469
|
+
if ($inner === '') return $assoc ? [] : new \stdClass();
|
|
470
|
+
|
|
471
|
+
$result = [];
|
|
472
|
+
foreach (self::splitInline($inner) as $pair) {
|
|
473
|
+
$colonPos = strpos($pair, ':');
|
|
474
|
+
if ($colonPos === false) continue;
|
|
475
|
+
$k = trim(substr($pair, 0, $colonPos));
|
|
476
|
+
$v = trim(substr($pair, $colonPos + 1));
|
|
477
|
+
$result[$k] = self::parseInlineCollection($v, $assoc);
|
|
478
|
+
}
|
|
479
|
+
return $assoc ? $result : (object) $result;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private static function splitInline(string $str): array
|
|
483
|
+
{
|
|
484
|
+
$parts = [];
|
|
485
|
+
$depth = 0;
|
|
486
|
+
$current = '';
|
|
487
|
+
$inSingle = false;
|
|
488
|
+
$inDouble = false;
|
|
489
|
+
|
|
490
|
+
for ($i = 0, $len = strlen($str); $i < $len; $i++) {
|
|
491
|
+
$c = $str[$i];
|
|
492
|
+
|
|
493
|
+
if ($c === "'" && !$inDouble) { $inSingle = !$inSingle; $current .= $c; continue; }
|
|
494
|
+
if ($c === '"' && !$inSingle) { $inDouble = !$inDouble; $current .= $c; continue; }
|
|
495
|
+
if ($inSingle || $inDouble) { $current .= $c; continue; }
|
|
496
|
+
|
|
497
|
+
if ($c === '[' || $c === '{') { $depth++; $current .= $c; continue; }
|
|
498
|
+
if ($c === ']' || $c === '}') { $depth--; $current .= $c; continue; }
|
|
499
|
+
|
|
500
|
+
if ($c === ',' && $depth === 0) { $parts[] = $current; $current = ''; continue; }
|
|
501
|
+
$current .= $c;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if ($current !== '') $parts[] = $current;
|
|
505
|
+
return $parts;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// -------------------------------------------------------------------------
|
|
509
|
+
// Scalars
|
|
510
|
+
// -------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
private static function parseScalar(string $value): mixed
|
|
513
|
+
{
|
|
514
|
+
$value = trim($value);
|
|
515
|
+
|
|
516
|
+
if (!preg_match('/^[\'"]/', $value)) {
|
|
517
|
+
$value = rtrim(preg_replace('/(^|\s)#.*$/', '', $value));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if ($value === '') return null;
|
|
521
|
+
|
|
522
|
+
if (str_starts_with($value, '"') && str_ends_with($value, '"') && strlen($value) >= 2) {
|
|
523
|
+
return stripcslashes(substr($value, 1, -1));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (str_starts_with($value, "'") && str_ends_with($value, "'") && strlen($value) >= 2) {
|
|
527
|
+
return str_replace("''", "'", substr($value, 1, -1));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (in_array(strtolower($value), ['~', 'null'], true)) return null;
|
|
531
|
+
if (in_array(strtolower($value), ['true', 'yes', 'on'], true)) return true;
|
|
532
|
+
if (in_array(strtolower($value), ['false', 'no', 'off'], true)) return false;
|
|
533
|
+
if (preg_match('/^-?\d+$/', $value)) return (int) $value;
|
|
534
|
+
if (preg_match('/^-?\d+\.\d*([eE][+-]?\d+)?$/', $value)) return (float) $value;
|
|
535
|
+
if (in_array(strtolower($value), ['.inf', '+.inf'], true)) return INF;
|
|
536
|
+
if (strtolower($value) === '-.inf') return -INF;
|
|
537
|
+
if (strtolower($value) === '.nan') return NAN;
|
|
538
|
+
|
|
539
|
+
return $value;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// -------------------------------------------------------------------------
|
|
543
|
+
// Helpers
|
|
544
|
+
// -------------------------------------------------------------------------
|
|
545
|
+
|
|
546
|
+
private static function getIndent(string $line): int
|
|
547
|
+
{
|
|
548
|
+
return strlen($line) - strlen(ltrim($line));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private static function isMapping(string $trimmed): bool
|
|
552
|
+
{
|
|
553
|
+
return (bool) preg_match('/^(?:"[^"]*"|\'[^\']*\'|[^:\'"\[\{]+):\s?/', $trimmed);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private static function splitKeyValue(string $line): array
|
|
557
|
+
{
|
|
558
|
+
if (preg_match('/^("(?:[^"\\\\]|\\\\.)*"|\'(?:[^\']|\'\')*\'|[^:]+?):\s*(.*)$/', $line, $m)) {
|
|
559
|
+
$key = self::parseScalar($m[1]);
|
|
560
|
+
$val = trim($m[2]) !== '' ? trim($m[2]) : null;
|
|
561
|
+
return [$key, $val];
|
|
562
|
+
}
|
|
563
|
+
return [$line, null];
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private static function skipEmptyAndComments(array $lines, int &$pos): void
|
|
567
|
+
{
|
|
568
|
+
while ($pos < count($lines)) {
|
|
569
|
+
$t = trim($lines[$pos]);
|
|
570
|
+
if ($t === '' || str_starts_with($t, '#')) $pos++;
|
|
571
|
+
else break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private static function getChomping(string $indicator): string
|
|
576
|
+
{
|
|
577
|
+
if (str_ends_with($indicator, '-')) return 'strip';
|
|
578
|
+
if (str_ends_with($indicator, '+')) return 'keep';
|
|
579
|
+
return 'clip';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private static function applyChomping(string $text, string $chomping): string
|
|
583
|
+
{
|
|
584
|
+
// Normalise chomping to match the behaviour observed from the
|
|
585
|
+
// native-backed YAML:: parser (libyaml). Historically the legacy
|
|
586
|
+
// parser preserved all trailing newlines for '+' (keep), while the
|
|
587
|
+
// native parser returns a single trailing newline. To avoid diffs
|
|
588
|
+
// between YAML_LEGACY:: and YAML:: users relying on the latter by
|
|
589
|
+
// default, normalise 'keep' to produce the same single trailing
|
|
590
|
+
// newline as the default (clip) behaviour.
|
|
591
|
+
return match ($chomping) {
|
|
592
|
+
'strip' => rtrim($text, "\n"),
|
|
593
|
+
// Keep '+' -> normalise to a single trailing newline (clip-like)
|
|
594
|
+
'keep' => rtrim($text, "\n") . "\n",
|
|
595
|
+
default => rtrim($text, "\n") . "\n",
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|