@effected/yaml 0.1.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.
@@ -0,0 +1,885 @@
1
+ import { YamlScalar } from "../../YamlNode.js";
2
+ import { registerAnchor } from "./anchors.js";
3
+ import { lineCol } from "./state.js";
4
+ import { resolveTagHandle } from "./tags.js";
5
+
6
+ //#region src/internal/composer/scalars.ts
7
+ const NULL_RE = /^(?:null|Null|NULL|~)$/;
8
+ const TRUE_RE = /^(?:true|True|TRUE)$/;
9
+ const FALSE_RE = /^(?:false|False|FALSE)$/;
10
+ const INT_RE = /^[-+]?[0-9]+$/;
11
+ const OCT_RE = /^0o[0-7]+$/;
12
+ const HEX_RE = /^0x[\dA-Fa-f]+$/;
13
+ const FLOAT_RE = /^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?$/;
14
+ const INF_RE = /^[-+]?\.(?:inf|Inf|INF)$/;
15
+ const NAN_RE = /^\.(?:nan|NaN|NAN)$/;
16
+ /**
17
+ * Parses an integer string, returning `bigint` when the value exceeds
18
+ * `Number.MAX_SAFE_INTEGER` to avoid silent precision loss.
19
+ */
20
+ function safeParseInt(value, radix) {
21
+ const n = Number.parseInt(value, radix);
22
+ if (Number.isSafeInteger(n)) return n;
23
+ return BigInt(`${radix === 16 ? "0x" : radix === 8 ? "0o" : ""}${value}`);
24
+ }
25
+ /**
26
+ * Classify a plain scalar's numeric form for duplicate-key identity. `1` is an
27
+ * `!!int` and `1.0` an `!!float` even though both resolve to the JS number `1`,
28
+ * so they are distinct mapping keys; `1` and `0x1` are both `!!int 1` and so
29
+ * are the same key. Returns `null` for non-numeric plain text. Uses the same
30
+ * regexes as {@link resolvePlainScalar} so the classification always agrees
31
+ * with how the value was resolved.
32
+ */
33
+ function classifyPlainNumeric(raw) {
34
+ const t = raw.trim();
35
+ if (OCT_RE.test(t) || HEX_RE.test(t) || INT_RE.test(t)) return "int";
36
+ if (INF_RE.test(t) || NAN_RE.test(t) || FLOAT_RE.test(t)) return "float";
37
+ return null;
38
+ }
39
+ function resolvePlainScalar(value) {
40
+ if (value === "" || NULL_RE.test(value)) return null;
41
+ if (TRUE_RE.test(value)) return true;
42
+ if (FALSE_RE.test(value)) return false;
43
+ if (OCT_RE.test(value)) return safeParseInt(value.slice(2), 8);
44
+ if (HEX_RE.test(value)) return safeParseInt(value.slice(2), 16);
45
+ if (INT_RE.test(value)) return safeParseInt(value, 10);
46
+ if (INF_RE.test(value)) return value.startsWith("-") ? -Number.POSITIVE_INFINITY : Number.POSITIVE_INFINITY;
47
+ if (NAN_RE.test(value)) return NaN;
48
+ if (FLOAT_RE.test(value)) {
49
+ const n = Number.parseFloat(value);
50
+ if (!Number.isNaN(n)) return n;
51
+ }
52
+ return value;
53
+ }
54
+ function resolveTaggedScalar(rawValue, tag) {
55
+ switch (tag) {
56
+ case "!!str":
57
+ case "tag:yaml.org,2002:str": return rawValue;
58
+ case "!!int":
59
+ case "tag:yaml.org,2002:int": {
60
+ if (OCT_RE.test(rawValue)) return Number.parseInt(rawValue.slice(2), 8);
61
+ if (HEX_RE.test(rawValue)) return Number.parseInt(rawValue.slice(2), 16);
62
+ const n = Number.parseInt(rawValue, 10);
63
+ return Number.isNaN(n) ? rawValue : n;
64
+ }
65
+ case "!!float":
66
+ case "tag:yaml.org,2002:float": {
67
+ if (INF_RE.test(rawValue)) return rawValue.startsWith("-") ? -Number.POSITIVE_INFINITY : Number.POSITIVE_INFINITY;
68
+ if (NAN_RE.test(rawValue)) return NaN;
69
+ const n = Number.parseFloat(rawValue);
70
+ return Number.isNaN(n) ? rawValue : n;
71
+ }
72
+ case "!!bool":
73
+ case "tag:yaml.org,2002:bool":
74
+ if (TRUE_RE.test(rawValue)) return true;
75
+ if (FALSE_RE.test(rawValue)) return false;
76
+ return rawValue;
77
+ case "!!null":
78
+ case "tag:yaml.org,2002:null": return null;
79
+ default: return rawValue;
80
+ }
81
+ }
82
+ function resolveScalar(rawValue, style, tag, state) {
83
+ if (tag) return resolveTaggedScalar(rawValue, state ? resolveTagHandle(tag, state) : tag);
84
+ if (style !== "plain") return rawValue;
85
+ return resolvePlainScalar(rawValue);
86
+ }
87
+ function getScalarStyle(node) {
88
+ if (node.type === "block-scalar") return node.source.trimStart()[0] === ">" ? "block-folded" : "block-literal";
89
+ const first = node.source[0];
90
+ if (first === "'") return "single-quoted";
91
+ if (first === "\"") return "double-quoted";
92
+ return "plain";
93
+ }
94
+ /**
95
+ * Extracts the chomp indicator from a block scalar's header.
96
+ * Returns "strip" for `-`, "keep" for `+`, "clip" otherwise (default).
97
+ * Returns undefined for non-block scalars.
98
+ */
99
+ function getBlockChomp(node) {
100
+ if (node.type !== "block-scalar") return void 0;
101
+ const src = node.source.trimStart();
102
+ const headerEnd = src.indexOf("\n");
103
+ const header = headerEnd === -1 ? src : src.slice(0, headerEnd);
104
+ if (header.includes("+")) return "keep";
105
+ if (header.includes("-")) return "strip";
106
+ return "clip";
107
+ }
108
+ function getScalarValue(node, fullText) {
109
+ if (node.type === "block-scalar") return decodeBlockScalar(node.source, fullText, node.offset);
110
+ const style = getScalarStyle(node);
111
+ if (style === "single-quoted") return decodeSingleQuoted(node.source);
112
+ if (style === "double-quoted") return decodeDoubleQuoted(node.source);
113
+ return decodePlainScalar(node.source);
114
+ }
115
+ /**
116
+ * YAML 1.2 §6.5 flow line folding for plain scalars.
117
+ * - Bare newline between non-empty lines becomes a space (fold)
118
+ * - Empty line(s) preserved as newline characters
119
+ * - Leading whitespace on continuation lines trimmed
120
+ * - Trailing whitespace before newlines trimmed
121
+ */
122
+ function decodePlainScalar(raw) {
123
+ const trimmed = raw.trim();
124
+ if (!trimmed.includes("\n")) return trimmed;
125
+ return foldFlowLines(trimmed);
126
+ }
127
+ /**
128
+ * Decode single-quoted scalar with flow folding.
129
+ * Only escape: '' → '
130
+ * Bare newlines follow flow folding rules.
131
+ */
132
+ function decodeSingleQuoted(raw) {
133
+ const unescaped = raw.slice(1, -1).replace(/''/g, "'");
134
+ if (!unescaped.includes("\n")) return unescaped;
135
+ return foldFlowLines(unescaped);
136
+ }
137
+ function decodeDoubleQuoted(raw) {
138
+ const inner = raw.slice(1, -1);
139
+ let result = "";
140
+ let significantEnd = 0;
141
+ let i = 0;
142
+ while (i < inner.length) {
143
+ const ch = inner[i];
144
+ if (ch === "\\") {
145
+ i++;
146
+ const esc = inner[i];
147
+ switch (esc) {
148
+ case "\\":
149
+ result += "\\";
150
+ break;
151
+ case "\"":
152
+ result += "\"";
153
+ break;
154
+ case "/":
155
+ result += "/";
156
+ break;
157
+ case "b":
158
+ result += "\b";
159
+ break;
160
+ case "f":
161
+ result += "\f";
162
+ break;
163
+ case "n":
164
+ result += "\n";
165
+ break;
166
+ case "r":
167
+ result += "\r";
168
+ break;
169
+ case "t":
170
+ result += " ";
171
+ break;
172
+ case "0":
173
+ result += "\0";
174
+ break;
175
+ case "a":
176
+ result += "\x07";
177
+ break;
178
+ case "e":
179
+ result += "\x1B";
180
+ break;
181
+ case "v":
182
+ result += "\v";
183
+ break;
184
+ case " ":
185
+ result += " ";
186
+ break;
187
+ case "N":
188
+ result += "…";
189
+ break;
190
+ case "_":
191
+ result += "\xA0";
192
+ break;
193
+ case "L":
194
+ result += "\u2028";
195
+ break;
196
+ case "P":
197
+ result += "\u2029";
198
+ break;
199
+ case "x": {
200
+ const hex = inner.slice(i + 1, i + 3);
201
+ result += String.fromCharCode(Number.parseInt(hex, 16));
202
+ i += 2;
203
+ break;
204
+ }
205
+ case "u": {
206
+ const hex = inner.slice(i + 1, i + 5);
207
+ result += String.fromCodePoint(Number.parseInt(hex, 16));
208
+ i += 4;
209
+ break;
210
+ }
211
+ case "U": {
212
+ const hex = inner.slice(i + 1, i + 9);
213
+ const cp = Number.parseInt(hex, 16);
214
+ result += cp <= 1114111 ? String.fromCodePoint(cp) : "�";
215
+ i += 8;
216
+ break;
217
+ }
218
+ case "\n":
219
+ i++;
220
+ while (i < inner.length && (inner[i] === " " || inner[i] === " ")) i++;
221
+ continue;
222
+ case "\r":
223
+ i++;
224
+ if (i < inner.length && inner[i] === "\n") i++;
225
+ while (i < inner.length && (inner[i] === " " || inner[i] === " ")) i++;
226
+ continue;
227
+ default: result += esc === void 0 ? "\\" : esc;
228
+ }
229
+ significantEnd = result.length;
230
+ i++;
231
+ } else if (ch === "\n" || ch === "\r" && inner[i + 1] === "\n") {
232
+ result = result.slice(0, significantEnd);
233
+ i += ch === "\r" ? 2 : 1;
234
+ while (i < inner.length && (inner[i] === " " || inner[i] === " ")) i++;
235
+ if (i < inner.length && (inner[i] === "\n" || inner[i] === "\r")) while (i < inner.length && (inner[i] === "\n" || inner[i] === "\r")) {
236
+ result += "\n";
237
+ i += inner[i] === "\r" && inner[i + 1] === "\n" ? 2 : 1;
238
+ while (i < inner.length && (inner[i] === " " || inner[i] === " ")) i++;
239
+ }
240
+ else result += " ";
241
+ significantEnd = result.length;
242
+ } else {
243
+ result += ch;
244
+ if (ch !== " " && ch !== " ") significantEnd = result.length;
245
+ i++;
246
+ }
247
+ }
248
+ return result;
249
+ }
250
+ /**
251
+ * Apply YAML 1.2 §6.5 flow line folding to a string.
252
+ * - Split into lines, trim trailing whitespace from each
253
+ * - Newline between non-empty lines becomes a space
254
+ * - Empty line preserved as newline in output
255
+ * - Leading whitespace (indentation) on continuation lines trimmed
256
+ */
257
+ function foldFlowLines(text) {
258
+ const lines = text.split("\n");
259
+ let result = "";
260
+ for (let i = 0; i < lines.length; i++) {
261
+ const line = lines[i] ?? "";
262
+ if (i === 0) {
263
+ result += line.replace(/[ \t]+$/, "");
264
+ continue;
265
+ }
266
+ const isLast = i === lines.length - 1;
267
+ const trimmed = isLast ? line.trimStart() : line.trim();
268
+ if (trimmed === "") if (isLast) {
269
+ if (result.length === 0 || result[result.length - 1] !== "\n") result += " ";
270
+ } else result += "\n";
271
+ else {
272
+ if (result.length > 0 && result[result.length - 1] !== "\n") result += " ";
273
+ result += trimmed;
274
+ }
275
+ }
276
+ return result;
277
+ }
278
+ /**
279
+ * Collect a multi-line plain scalar key from consecutive CST children.
280
+ * Like `collectMultilinePlainScalar`, but for keys: collects plain scalars
281
+ * up until the `:` value separator, merging them with flow line folding.
282
+ * Returns the folded key text and the index after the last consumed child.
283
+ */
284
+ function collectMultilineKey(children, startIdx) {
285
+ const first = children[startIdx];
286
+ if (first?.type !== "flow-scalar") return {
287
+ value: first?.source.trim() ?? "",
288
+ nextIdx: startIdx + 1
289
+ };
290
+ const parts = [first.source.trim()];
291
+ let idx = startIdx + 1;
292
+ while (idx < children.length) {
293
+ const child = children[idx];
294
+ if (!child) break;
295
+ if (child.type === "newline" || child.type === "whitespace" && child.source.trim() === "") {
296
+ idx++;
297
+ continue;
298
+ }
299
+ if (child.type === "whitespace" && (child.source === ":" || child.source === ",")) break;
300
+ if (child.type === "flow-scalar" && getScalarStyle(child) === "plain") {
301
+ parts.push(child.source.trim());
302
+ idx++;
303
+ continue;
304
+ }
305
+ break;
306
+ }
307
+ if (parts.length === 1) return {
308
+ value: parts[0] ?? "",
309
+ nextIdx: idx
310
+ };
311
+ return {
312
+ value: foldFlowLines(parts.join("\n")),
313
+ nextIdx: idx
314
+ };
315
+ }
316
+ /**
317
+ * Extract the trimmed content of the line at `offset` in `text`.
318
+ * Returns the trimmed text and the offset of the next line (or EOF).
319
+ */
320
+ function extractLineContent(text, offset) {
321
+ let lineStart = offset;
322
+ while (lineStart > 0 && text[lineStart - 1] !== "\n") lineStart--;
323
+ let lineEnd = offset;
324
+ while (lineEnd < text.length && text[lineEnd] !== "\n" && text[lineEnd] !== "\r") lineEnd++;
325
+ return {
326
+ lineText: text.slice(lineStart, lineEnd).trim(),
327
+ lineEndOffset: lineEnd
328
+ };
329
+ }
330
+ /**
331
+ * Skip all children whose offset falls on the same line as `lineOffset`.
332
+ * Returns the index of the first child that is past the line end.
333
+ */
334
+ function skipChildrenOnLine(children, startIdx, lineEndOffset) {
335
+ let idx = startIdx;
336
+ while (idx < children.length) {
337
+ const c = children[idx];
338
+ if (!c) break;
339
+ if (c.type === "newline" && c.offset >= lineEndOffset) break;
340
+ if (c.offset > lineEndOffset) break;
341
+ idx++;
342
+ }
343
+ return idx;
344
+ }
345
+ /**
346
+ * Collect a multi-line plain scalar from consecutive CST children.
347
+ * Starting from a plain flow-scalar at `startIdx`, look ahead through
348
+ * newlines and whitespace for more plain flow-scalars that continue the
349
+ * same value. Returns the folded scalar text and the index after the last
350
+ * consumed child.
351
+ *
352
+ * A continuation scalar must:
353
+ * - Be a plain flow-scalar (not quoted)
354
+ * - NOT be followed by a value-sep (`:`) — that makes it a mapping key
355
+ * - Be separated from the previous scalar only by newlines/whitespace
356
+ *
357
+ * When the lexer mis-tokenizes continuation line content as anchors, tags,
358
+ * aliases, directives, or block-seq entries, this function detects such
359
+ * lines and extracts the raw source text as continuation parts (3MYT, FBC9,
360
+ * XLQ9, AB8U).
361
+ *
362
+ * `endOffset` is the source end of the last consumed fragment, so callers
363
+ * can span the composed scalar node across the whole folded value (the
364
+ * sourceMultiline decoration pass then stamps it from the span).
365
+ */
366
+ function collectMultilinePlainScalar(children, startIdx, minContinuationColumn, sourceText) {
367
+ const first = children[startIdx];
368
+ if (first?.type !== "flow-scalar") return {
369
+ value: first?.source.trim() ?? "",
370
+ nextIdx: startIdx + 1,
371
+ partsCount: 1,
372
+ endOffset: first === void 0 ? 0 : first.offset + first.length
373
+ };
374
+ if (getScalarStyle(first) !== "plain") return {
375
+ value: getScalarValue(first),
376
+ nextIdx: startIdx + 1,
377
+ partsCount: 1,
378
+ endOffset: first.offset + first.length
379
+ };
380
+ const parts = [first.source.trim()];
381
+ let endOffset = first.offset + first.length;
382
+ let emptyLines = 0;
383
+ let idx = startIdx + 1;
384
+ let sawNewline = false;
385
+ while (idx < children.length) {
386
+ const child = children[idx];
387
+ if (!child) break;
388
+ if (child.type === "newline") {
389
+ emptyLines++;
390
+ sawNewline = true;
391
+ idx++;
392
+ continue;
393
+ }
394
+ if (child.type === "whitespace") {
395
+ if (child.source === ":" || child.source === "?" || child.source === "-") break;
396
+ idx++;
397
+ continue;
398
+ }
399
+ if (child.type === "comment") break;
400
+ if (child.type === "flow-scalar" && getScalarStyle(child) === "plain") {
401
+ if (hasValueSepAfterInList(children, idx + 1)) break;
402
+ if (hasBlockMapAfterInList(children, idx + 1)) break;
403
+ if (minContinuationColumn !== void 0 && sourceText) {
404
+ if (lineCol(sourceText, child.offset).column < minContinuationColumn) break;
405
+ }
406
+ if (emptyLines > 1) for (let e = 0; e < emptyLines - 1; e++) parts.push("");
407
+ parts.push(child.source.trim());
408
+ endOffset = child.offset + child.length;
409
+ emptyLines = 0;
410
+ sawNewline = false;
411
+ idx++;
412
+ continue;
413
+ }
414
+ if (sawNewline && sourceText && child.type !== "flow-scalar" && child.type !== "block-scalar") {
415
+ const childCol = lineCol(sourceText, child.offset).column;
416
+ const isDirectiveContinuation = child.type === "directive";
417
+ if (minContinuationColumn !== void 0 && !isDirectiveContinuation && childCol < minContinuationColumn) break;
418
+ if (childCol > 0 || isDirectiveContinuation) {
419
+ const { lineText, lineEndOffset } = extractLineContent(sourceText, child.offset);
420
+ if (lineText.length > 0) {
421
+ if (emptyLines > 1) for (let e = 0; e < emptyLines - 1; e++) parts.push("");
422
+ parts.push(lineText);
423
+ let contentEnd = lineEndOffset;
424
+ while (contentEnd > 0 && (sourceText[contentEnd - 1] === " " || sourceText[contentEnd - 1] === " ")) contentEnd--;
425
+ endOffset = contentEnd;
426
+ emptyLines = 0;
427
+ sawNewline = false;
428
+ idx = skipChildrenOnLine(children, idx, lineEndOffset);
429
+ continue;
430
+ }
431
+ }
432
+ }
433
+ break;
434
+ }
435
+ if (parts.length === 1) return {
436
+ value: parts[0] ?? "",
437
+ nextIdx: idx,
438
+ partsCount: 1,
439
+ endOffset
440
+ };
441
+ return {
442
+ value: foldFlowLines(parts.join("\n")),
443
+ nextIdx: idx,
444
+ partsCount: parts.length,
445
+ endOffset
446
+ };
447
+ }
448
+ /**
449
+ * Find the index of the next non-trivia child (skips newline, whitespace, comment).
450
+ * If `stopAtDash` is true, returns null when a `-` indicator is encountered before
451
+ * any significant child (used to avoid merging across sequence entry boundaries).
452
+ */
453
+ function findNextSignificantChild(children, startIdx, stopAtDash = false) {
454
+ for (let j = startIdx; j < children.length; j++) {
455
+ const c = children[j];
456
+ if (!c) continue;
457
+ if (c.type === "newline" || c.type === "comment") continue;
458
+ if (c.type === "whitespace") {
459
+ if (stopAtDash && c.source.trim() === "-") return null;
460
+ continue;
461
+ }
462
+ return j;
463
+ }
464
+ return null;
465
+ }
466
+ /**
467
+ * Check if a value separator (`:`) follows in a CST children list,
468
+ * skipping whitespace and newlines.
469
+ */
470
+ function hasValueSepAfterInList(children, startIdx) {
471
+ return findValueSepOffset(children, startIdx) >= 0;
472
+ }
473
+ /**
474
+ * Check if the next non-trivia child is a block-map (indicating that the
475
+ * preceding scalar is the first key of a nested implicit mapping). Returns
476
+ * false if a sibling `:` value-sep is encountered first, since that means
477
+ * the scalar is a key at the current level (not a nested mapping start).
478
+ */
479
+ function hasBlockMapAfterInList(children, startIdx) {
480
+ for (let j = startIdx; j < children.length; j++) {
481
+ const c = children[j];
482
+ if (!c) continue;
483
+ if (c.type === "newline" || c.type === "comment") continue;
484
+ if (c.type === "whitespace") {
485
+ if (c.source === ":") return false;
486
+ continue;
487
+ }
488
+ return c.type === "block-map";
489
+ }
490
+ return false;
491
+ }
492
+ /** Find the offset of the next ":" value separator in a CST children list, or -1 if none. */
493
+ function findValueSepOffset(children, startIdx) {
494
+ for (let j = startIdx; j < children.length; j++) {
495
+ const c = children[j];
496
+ if (!c) continue;
497
+ if (c.type === "newline" || c.type === "comment") continue;
498
+ if (c.type === "whitespace") {
499
+ if (c.source === ":") return c.offset;
500
+ continue;
501
+ }
502
+ return -1;
503
+ }
504
+ return -1;
505
+ }
506
+ /** Check if a ":" value-sep exists between startIdx (inclusive) and endIdx (exclusive). */
507
+ function hasValueSepBetween(children, startIdx, endIdx) {
508
+ for (let j = startIdx; j < endIdx; j++) {
509
+ const c = children[j];
510
+ if (!c) continue;
511
+ if (c.type === "whitespace" && c.source === ":") return true;
512
+ }
513
+ return false;
514
+ }
515
+ /**
516
+ * Returns true when the first non-trivia child of a block-map CST node is a
517
+ * `:` value separator (i.e., the block map begins with an implicit empty key
518
+ * followed by a value indicator). Used to decide whether a pending anchor/tag
519
+ * belongs to that empty key rather than to the block map itself.
520
+ */
521
+ function blockMapStartsWithValueSep(blockMap) {
522
+ for (const c of blockMap.children ?? []) {
523
+ if (!c) continue;
524
+ if (c.type === "newline" || c.type === "comment") continue;
525
+ if (c.type === "whitespace") {
526
+ if (c.source === ":") return true;
527
+ if (c.source.trim() === "") continue;
528
+ return false;
529
+ }
530
+ return false;
531
+ }
532
+ return false;
533
+ }
534
+ /**
535
+ * Like `hasValueSepAfterInList`, but also skips over plain flow-scalars
536
+ * that appear after a newline. Used to detect multi-line keys:
537
+ * `multi\n line: value` where `:` comes after continuation plain scalars.
538
+ * Only allows skipping plain scalars that were preceded by a newline,
539
+ * preventing false matches across comma-delimited entries on the same line.
540
+ */
541
+ function hasValueSepThroughPlainScalars(children, startIdx) {
542
+ let sawNewline = false;
543
+ for (let j = startIdx; j < children.length; j++) {
544
+ const c = children[j];
545
+ if (!c) continue;
546
+ if (c.type === "newline") {
547
+ sawNewline = true;
548
+ continue;
549
+ }
550
+ if (c.type === "comment") continue;
551
+ if (c.type === "whitespace") {
552
+ if (c.source === ":") return true;
553
+ if (c.source === ",") return false;
554
+ continue;
555
+ }
556
+ if (sawNewline && c.type === "flow-scalar" && getScalarStyle(c) === "plain") continue;
557
+ return false;
558
+ }
559
+ return false;
560
+ }
561
+ /** Find the next non-trivia CST child in a list, returning the node and its index. */
562
+ function findNextContentInList(children, startIdx) {
563
+ for (let j = startIdx; j < children.length; j++) {
564
+ const c = children[j];
565
+ if (!c) continue;
566
+ if (c.type === "whitespace" || c.type === "newline" || c.type === "comment") continue;
567
+ return {
568
+ node: c,
569
+ idx: j
570
+ };
571
+ }
572
+ return null;
573
+ }
574
+ function findFirstContent(children) {
575
+ for (const c of children) {
576
+ if (!c) continue;
577
+ if (c.type === "whitespace" && c.source.trim() === "") continue;
578
+ if (c.type === "newline") continue;
579
+ return c;
580
+ }
581
+ }
582
+ function findLastContent(children) {
583
+ for (let i = children.length - 1; i >= 0; i--) {
584
+ const c = children[i];
585
+ if (!c) continue;
586
+ if (c.type === "whitespace" && c.source.trim() === "") continue;
587
+ if (c.type === "newline") continue;
588
+ return c;
589
+ }
590
+ }
591
+ /**
592
+ * Find the next content child (skipping trivia AND anchor/tag properties),
593
+ * or null. Used at the document level where properties precede content.
594
+ */
595
+ function findNextContentChild(children, startIdx) {
596
+ for (let i = startIdx; i < children.length; i++) {
597
+ const c = children[i];
598
+ if (!c) continue;
599
+ if (c.type === "whitespace" || c.type === "newline" || c.type === "comment" || c.type === "anchor" || c.type === "tag") continue;
600
+ return c;
601
+ }
602
+ return null;
603
+ }
604
+ function indexOfChild(children, target) {
605
+ for (let i = 0; i < children.length; i++) if (children[i] === target) return i;
606
+ return -1;
607
+ }
608
+ /** Check if there's a value separator ":" after startIdx (skipping only whitespace). */
609
+ function hasValueSepAfter(children, startIdx) {
610
+ for (let j = startIdx; j < children.length; j++) {
611
+ const c = children[j];
612
+ if (!c) continue;
613
+ if (c.type === "whitespace" && c.source === ":") return true;
614
+ if (c.type === "whitespace" && c.source !== ":") continue;
615
+ if (c.type === "newline") continue;
616
+ break;
617
+ }
618
+ return false;
619
+ }
620
+ /**
621
+ * Scan backward in the full document text from a block scalar indicator's
622
+ * position to find the parent context indentation level n. This handles:
623
+ * - Same-line ":" (mapping value): n = key's column indent
624
+ * - Same-line "-" (seq entry): n = column of "-"
625
+ * - Own-line (preceded by newline, tag, anchor): scan further back across
626
+ * lines to find the ":" or "-" that introduced this value
627
+ */
628
+ function findParentIndent(fullText, indicatorOffset) {
629
+ let scanBack = indicatorOffset - 1;
630
+ while (scanBack >= 0 && (fullText[scanBack] === " " || fullText[scanBack] === " ")) scanBack--;
631
+ if (scanBack >= 0 && fullText[scanBack] === ":") return findKeyIndent(fullText, scanBack);
632
+ if (scanBack >= 0 && fullText[scanBack] === "-") return findColOnLine(fullText, scanBack);
633
+ while (scanBack >= 0) {
634
+ const ch = fullText[scanBack];
635
+ if (ch === ":") return findKeyIndent(fullText, scanBack);
636
+ if (ch === "-") {
637
+ const afterDash = scanBack + 1;
638
+ if (afterDash >= fullText.length || fullText[afterDash] === " " || fullText[afterDash] === " " || fullText[afterDash] === "\n" || fullText[afterDash] === "\r") return findColOnLine(fullText, scanBack);
639
+ }
640
+ scanBack--;
641
+ }
642
+ return 0;
643
+ }
644
+ /** Find the column of a character on its line. */
645
+ function findColOnLine(text, pos) {
646
+ let lineStart = pos;
647
+ while (lineStart > 0 && text[lineStart - 1] !== "\n" && text[lineStart - 1] !== "\r") lineStart--;
648
+ return pos - lineStart;
649
+ }
650
+ /** Find the key indentation for a mapping ":" at the given position. */
651
+ function findKeyIndent(text, colonPos) {
652
+ let lineStart = colonPos;
653
+ while (lineStart > 0 && text[lineStart - 1] !== "\n" && text[lineStart - 1] !== "\r") lineStart--;
654
+ let spaces = 0;
655
+ while (lineStart + spaces < text.length && text[lineStart + spaces] === " ") spaces++;
656
+ if (lineStart + spaces < text.length && text[lineStart + spaces] === "-") {
657
+ const afterDash = lineStart + spaces + 1;
658
+ if (afterDash < text.length && (text[afterDash] === " " || text[afterDash] === " ")) return spaces + 2;
659
+ }
660
+ return spaces;
661
+ }
662
+ function decodeBlockScalar(raw, fullText, nodeOffset) {
663
+ const firstChar = raw.trimStart()[0];
664
+ const isFolded = firstChar === ">";
665
+ let i = raw.indexOf(firstChar === ">" ? ">" : "|");
666
+ if (i < 0) return "";
667
+ i++;
668
+ let chomp = "clip";
669
+ let explicitIndent = 0;
670
+ for (let hc = 0; hc < 2 && i < raw.length && raw[i] !== "\n" && raw[i] !== "\r"; hc++) {
671
+ const ch = raw[i];
672
+ if (ch === "-") {
673
+ chomp = "strip";
674
+ i++;
675
+ } else if (ch === "+") {
676
+ chomp = "keep";
677
+ i++;
678
+ } else if (ch !== void 0 && ch >= "1" && ch <= "9") {
679
+ explicitIndent = Number.parseInt(ch, 10);
680
+ i++;
681
+ } else break;
682
+ }
683
+ while (i < raw.length && raw[i] !== "\n" && raw[i] !== "\r") i++;
684
+ if (i < raw.length) if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
685
+ else i++;
686
+ let contentIndent = explicitIndent;
687
+ let foundContent = explicitIndent > 0;
688
+ if (explicitIndent > 0 && fullText !== void 0 && nodeOffset !== void 0) {
689
+ contentIndent = findParentIndent(fullText, nodeOffset) + explicitIndent;
690
+ foundContent = true;
691
+ } else if (contentIndent === 0) {
692
+ let scanAhead = i;
693
+ while (scanAhead < raw.length) {
694
+ let spaces = 0;
695
+ while (scanAhead < raw.length && raw[scanAhead] === " ") {
696
+ spaces++;
697
+ scanAhead++;
698
+ }
699
+ if (scanAhead >= raw.length || raw[scanAhead] === "\n" || raw[scanAhead] === "\r") {
700
+ if (scanAhead < raw.length) {
701
+ scanAhead++;
702
+ if (raw[scanAhead - 1] === "\r" && scanAhead < raw.length && raw[scanAhead] === "\n") scanAhead++;
703
+ }
704
+ continue;
705
+ }
706
+ contentIndent = spaces;
707
+ foundContent = true;
708
+ break;
709
+ }
710
+ }
711
+ if (!foundContent) {
712
+ if (chomp === "keep") {
713
+ let count = 0;
714
+ let j = i;
715
+ while (j < raw.length) {
716
+ while (j < raw.length && (raw[j] === " " || raw[j] === " ")) j++;
717
+ if (j >= raw.length) {
718
+ if (count === 0) count = 1;
719
+ break;
720
+ }
721
+ if (raw[j] === "\n") {
722
+ count++;
723
+ j++;
724
+ } else if (raw[j] === "\r") {
725
+ count++;
726
+ j++;
727
+ if (j < raw.length && raw[j] === "\n") j++;
728
+ } else break;
729
+ }
730
+ return "\n".repeat(count);
731
+ }
732
+ return "";
733
+ }
734
+ const lines = [];
735
+ const trailingNewlines = [];
736
+ while (i < raw.length) {
737
+ let spaces = 0;
738
+ while (i < raw.length && raw[i] === " ") {
739
+ spaces++;
740
+ i++;
741
+ }
742
+ if (i >= raw.length || raw[i] === "\n" || raw[i] === "\r") {
743
+ if (spaces > contentIndent) {
744
+ for (const nl of trailingNewlines) lines.push(nl);
745
+ trailingNewlines.length = 0;
746
+ lines.push(" ".repeat(spaces - contentIndent));
747
+ } else trailingNewlines.push("");
748
+ if (i < raw.length) if (raw[i] === "\r" && i + 1 < raw.length && raw[i + 1] === "\n") i += 2;
749
+ else i++;
750
+ continue;
751
+ }
752
+ if (spaces < contentIndent) break;
753
+ for (const _nl of trailingNewlines) lines.push("");
754
+ trailingNewlines.length = 0;
755
+ const extra = " ".repeat(spaces - contentIndent);
756
+ const contentStart = i;
757
+ while (i < raw.length && raw[i] !== "\n" && raw[i] !== "\r") i++;
758
+ lines.push(extra + raw.slice(contentStart, i));
759
+ if (i < raw.length) if (raw[i] === "\r" && i + 1 < raw.length && raw[i + 1] === "\n") i += 2;
760
+ else i++;
761
+ }
762
+ let value;
763
+ if (isFolded) {
764
+ let result = "";
765
+ let prevMoreIndented = false;
766
+ let hadContent = false;
767
+ for (let li = 0; li < lines.length; li++) {
768
+ const ln = lines[li] ?? "";
769
+ const isMoreIndented = ln.length > 0 && (ln[0] === " " || ln[0] === " ");
770
+ if (ln === "") result += "\n";
771
+ else if (!hadContent) {
772
+ result += ln;
773
+ prevMoreIndented = isMoreIndented;
774
+ hadContent = true;
775
+ } else {
776
+ if (result[result.length - 1] === "\n") if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
777
+ else result += ln;
778
+ else if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
779
+ else result += ` ${ln}`;
780
+ prevMoreIndented = isMoreIndented;
781
+ }
782
+ }
783
+ if (hadContent || trailingNewlines.length > 0) {
784
+ if (chomp === "keep") {
785
+ result += "\n";
786
+ for (const _nl of trailingNewlines) result += "\n";
787
+ } else if (chomp !== "strip") result += "\n";
788
+ }
789
+ value = result;
790
+ } else {
791
+ value = lines.join("\n");
792
+ if (lines.length > 0 || trailingNewlines.length > 0) {
793
+ if (chomp === "keep") {
794
+ value += "\n";
795
+ for (const _nl of trailingNewlines) value += "\n";
796
+ } else if (chomp !== "strip") value += "\n";
797
+ }
798
+ }
799
+ return value;
800
+ }
801
+ /**
802
+ * 5LLU, S98Z, W9L4: per YAML 1.2 §8.1.1, leading empty lines preceding the
803
+ * first content line in a block scalar must satisfy l-empty(n,c) — at most
804
+ * n leading spaces, where n is the content indent. When the indent indicator
805
+ * is auto-detected from the first non-empty line, that line establishes n;
806
+ * any preceding empty line with more than n spaces is malformed.
807
+ */
808
+ function validateBlockScalarLeadingEmpties(cst, state) {
809
+ const raw = cst.source;
810
+ let i = 0;
811
+ while (i < raw.length && raw[i] !== "\n" && raw[i] !== "\r") i++;
812
+ if (i < raw.length) if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
813
+ else i++;
814
+ const emptyIndents = [];
815
+ while (i < raw.length) {
816
+ const lineStart = i;
817
+ let spaces = 0;
818
+ while (i < raw.length && raw[i] === " ") {
819
+ spaces++;
820
+ i++;
821
+ }
822
+ if (i >= raw.length || raw[i] === "\n" || raw[i] === "\r") {
823
+ emptyIndents.push({
824
+ indent: spaces,
825
+ offsetInRaw: lineStart
826
+ });
827
+ if (i < raw.length) if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
828
+ else i++;
829
+ continue;
830
+ }
831
+ const contentIndent = spaces;
832
+ for (const empty of emptyIndents) if (empty.indent > contentIndent) {
833
+ const offset = cst.offset + empty.offsetInRaw;
834
+ state.errors.push({
835
+ code: "InvalidIndentation",
836
+ message: "Block scalar leading empty line cannot be more indented than the first content line",
837
+ offset,
838
+ length: empty.indent
839
+ });
840
+ return;
841
+ }
842
+ return;
843
+ }
844
+ }
845
+ function makeScalar(cst, state, meta) {
846
+ const style = getScalarStyle(cst);
847
+ if (style === "block-literal" || style === "block-folded") validateBlockScalarLeadingEmpties(cst, state);
848
+ const rawValue = getScalarValue(cst, state.text);
849
+ const value = resolveScalar(rawValue, style, meta?.tag, state);
850
+ const chomp = getBlockChomp(cst);
851
+ const needsRaw = style === "plain" && typeof value !== "string" && value !== void 0 && shouldPreserveRaw(rawValue, value);
852
+ const scalar = new YamlScalar({
853
+ value,
854
+ style,
855
+ offset: cst.offset,
856
+ length: cst.length,
857
+ ...meta?.tag !== void 0 ? { tag: meta.tag } : {},
858
+ ...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
859
+ ...meta?.comment !== void 0 ? { comment: meta.comment } : {},
860
+ ...chomp !== void 0 ? { chomp } : {},
861
+ ...needsRaw ? { raw: rawValue } : {}
862
+ });
863
+ if (meta?.anchor) registerAnchor(scalar, meta.anchor, state, cst.offset);
864
+ return scalar;
865
+ }
866
+ /**
867
+ * Returns true when the scalar's source representation should be preserved
868
+ * for canonical round-trip — i.e. the source form differs from `String(value)`
869
+ * but resolves to the same value.
870
+ *
871
+ * Special-float values (NaN, +/-Infinity) are excluded: their canonical YAML
872
+ * spelling is the lowercase `.inf` / `.nan` form per spec §10.3, so source
873
+ * variants like `.INF` or `.NaN` should normalize on round-trip rather than
874
+ * preserve.
875
+ */
876
+ function shouldPreserveRaw(rawValue, value) {
877
+ if (typeof value === "number") {
878
+ if (Number.isNaN(value) || !Number.isFinite(value)) return false;
879
+ return rawValue !== String(value);
880
+ }
881
+ return false;
882
+ }
883
+
884
+ //#endregion
885
+ export { blockMapStartsWithValueSep, classifyPlainNumeric, collectMultilineKey, collectMultilinePlainScalar, findFirstContent, findLastContent, findNextContentChild, findNextContentInList, findNextSignificantChild, findValueSepOffset, foldFlowLines, getBlockChomp, getScalarStyle, getScalarValue, hasBlockMapAfterInList, hasValueSepAfter, hasValueSepAfterInList, hasValueSepBetween, hasValueSepThroughPlainScalars, indexOfChild, makeScalar, resolveScalar, shouldPreserveRaw };