@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,797 @@
1
+ import { YamlAlias, YamlMap, YamlPair, YamlScalar, YamlSeq } from "../YamlNode.js";
2
+ import { hasInteriorTrailingWhitespace, hasNewlineSpacesTab, isControlChar, renderBlockFolded, renderBlockLiteral, renderSingleQuotedMultiline } from "./fold.js";
3
+
4
+ //#region src/internal/stringifier.ts
5
+ /**
6
+ * Thrown by the stringifier on its failure paths (circular references). The
7
+ * facade catches this and materializes the public stringify error.
8
+ */
9
+ var StringifyFailure = class extends Error {
10
+ reason;
11
+ constructor(reason) {
12
+ super(reason);
13
+ this.name = "StringifyFailure";
14
+ this.reason = reason;
15
+ }
16
+ };
17
+ /**
18
+ * Thrown by the value-stringifier trio when its mutual recursion exceeds
19
+ * {@link MAX_NESTING_DEPTH}. A deeply-nested acyclic value (e.g. a 50 000-deep
20
+ * array) would otherwise overflow the call stack as an unhandled `RangeError`
21
+ * defect; the facade catches this and materializes a fatal
22
+ * `YamlStringifyError` (a `NestingDepthExceeded` diagnostic), keeping the
23
+ * failure on the typed error channel — the stringify mirror of the composer's
24
+ * nesting-depth guard.
25
+ */
26
+ var StringifyDepthExceeded = class extends Error {
27
+ constructor() {
28
+ super(`Nesting depth exceeded maximum of ${256}`);
29
+ this.name = "StringifyDepthExceeded";
30
+ }
31
+ };
32
+ const NULL_RE = /^(?:null|Null|NULL|~)$/;
33
+ const TRUE_RE = /^(?:true|True|TRUE)$/;
34
+ const FALSE_RE = /^(?:false|False|FALSE)$/;
35
+ const INT_RE = /^[-+]?[0-9]+$/;
36
+ const OCT_RE = /^0o[0-7]+$/;
37
+ const HEX_RE = /^0x[\dA-Fa-f]+$/;
38
+ const FLOAT_RE = /^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)(?:[eE][-+]?[0-9]+)?$/;
39
+ const INF_RE = /^[-+]?\.(?:inf|Inf|INF)$/;
40
+ const NAN_RE = /^\.(?:nan|NaN|NAN)$/;
41
+ /**
42
+ * YAML indicator characters that require quoting when appearing in plain scalars.
43
+ */
44
+ const INDICATOR_CHARS = /* @__PURE__ */ new Set([
45
+ ":",
46
+ "#",
47
+ "{",
48
+ "}",
49
+ "[",
50
+ "]",
51
+ ",",
52
+ "&",
53
+ "*",
54
+ "?",
55
+ "|",
56
+ "-",
57
+ "<",
58
+ ">",
59
+ "=",
60
+ "!",
61
+ "%",
62
+ "@",
63
+ "`",
64
+ "\"",
65
+ "'"
66
+ ]);
67
+ /**
68
+ * Returns true if a string value would be mis-resolved as a non-string YAML type.
69
+ *
70
+ * Tests against all YAML 1.2 Core Schema type patterns (null, bool, int, float,
71
+ * inf, nan). Any string matching these patterns must be quoted to preserve its
72
+ * string identity during a parse round-trip.
73
+ */
74
+ function wouldBeResolved(s) {
75
+ if (s === "") return true;
76
+ if (NULL_RE.test(s)) return true;
77
+ if (TRUE_RE.test(s)) return true;
78
+ if (FALSE_RE.test(s)) return true;
79
+ if (OCT_RE.test(s)) return true;
80
+ if (HEX_RE.test(s)) return true;
81
+ if (INT_RE.test(s)) return true;
82
+ if (INF_RE.test(s)) return true;
83
+ if (NAN_RE.test(s)) return true;
84
+ if (FLOAT_RE.test(s)) return true;
85
+ return false;
86
+ }
87
+ /**
88
+ * Returns true if a string requires quoting to be safely represented as a plain scalar.
89
+ *
90
+ * Checks multiple conditions beyond type-conflict detection: empty strings,
91
+ * embedded newlines, leading indicator characters or whitespace, and inline
92
+ * comment/mapping-value patterns (`: `, ` #`). This is the single gate that
93
+ * decides whether a plain scalar is safe or must be wrapped in quotes.
94
+ */
95
+ function requiresQuoting(s, ignoreType = false) {
96
+ if (s === "") return true;
97
+ if (s.includes("\n")) return true;
98
+ if (!ignoreType && wouldBeResolved(s)) return true;
99
+ const first = s[0];
100
+ if (first === " " || first === " ") return true;
101
+ if (first !== void 0 && INDICATOR_CHARS.has(first)) if (first === ":" || first === "?" || first === "-") {
102
+ const second = s[1];
103
+ if (s.length === 1 || second === " " || second === " ") return true;
104
+ } else return true;
105
+ if (s.startsWith("---") || s.startsWith("...")) return true;
106
+ if (s.includes(": ") || s.includes(": ") || s.endsWith(":")) return true;
107
+ if (s.includes(" #") || s.includes(" #")) return true;
108
+ const last = s[s.length - 1];
109
+ if (last === " " || last === " ") return true;
110
+ for (let i = 0; i < s.length; i++) if (isControlChar(s.charCodeAt(i))) return true;
111
+ return false;
112
+ }
113
+ /**
114
+ * Returns true if the string contains any non-ASCII character (code point \> 0x7E).
115
+ */
116
+ function hasNonAscii(s) {
117
+ for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 126) return true;
118
+ return false;
119
+ }
120
+ /**
121
+ * Renders a string scalar using double-quote style.
122
+ *
123
+ * When `canonical` is true, non-ASCII characters are escaped as `\uXXXX`
124
+ * (or `\UXXXXXXXX` for supplementary plane) and C0 control characters use
125
+ * named escapes where YAML 1.2 defines them (`\b`, `\0`, `\a`, `\v`, `\e`).
126
+ */
127
+ function renderDoubleQuoted(s, canonical = false) {
128
+ let escaped = s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
129
+ let result = "";
130
+ for (let i = 0; i < escaped.length; i++) {
131
+ const code = escaped.charCodeAt(i);
132
+ if (isControlChar(code)) if (code === 0) result += "\\0";
133
+ else if (code === 7) result += "\\a";
134
+ else if (code === 8) result += "\\b";
135
+ else if (code === 11) result += "\\v";
136
+ else if (code === 12) result += "\\f";
137
+ else if (code === 27) result += "\\e";
138
+ else result += `\\x${code.toString(16).padStart(2, "0")}`;
139
+ else if (canonical && code > 126) {
140
+ if (code >= 55296 && code <= 56319 && i + 1 < escaped.length) {
141
+ const low = escaped.charCodeAt(i + 1);
142
+ if (low >= 56320 && low <= 57343) {
143
+ const cp = (code - 55296) * 1024 + (low - 56320) + 65536;
144
+ result += `\\U${cp.toString(16).toUpperCase().padStart(8, "0")}`;
145
+ i++;
146
+ continue;
147
+ }
148
+ }
149
+ result += `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
150
+ } else result += escaped[i];
151
+ }
152
+ escaped = result;
153
+ return `"${escaped}"`;
154
+ }
155
+ /**
156
+ * Renders a string scalar using single-quote style.
157
+ */
158
+ function renderSingleQuoted(s) {
159
+ return `'${s.replace(/'/g, "''")}'`;
160
+ }
161
+ /**
162
+ * Renders a string value as a YAML scalar using the requested style.
163
+ * Falls back to double-quoted if the requested style is unsafe for the value.
164
+ *
165
+ * Multi-line strings are routed to block styles regardless of the requested
166
+ * style (except double-quoted). For single-line strings, plain style delegates
167
+ * to {@link requiresQuoting} and falls back to double-quoted when the value
168
+ * would be ambiguous. Block literal and block folded styles are always
169
+ * accepted for single-line strings even though the output is unusual.
170
+ */
171
+ function renderString(s, style, indent, ignoreType = false, canonical = false, explicitChomp, parentPosition) {
172
+ if (s.includes("\n")) {
173
+ let hasControl = false;
174
+ for (let i = 0; i < s.length; i++) {
175
+ const code = s.charCodeAt(i);
176
+ if (isControlChar(code) || code === 13) {
177
+ hasControl = true;
178
+ break;
179
+ }
180
+ }
181
+ if (hasControl) return renderDoubleQuoted(s, canonical);
182
+ if (/^[\n ]*$/.test(s) && style !== "block-literal" && style !== "block-folded") return renderDoubleQuoted(s, canonical);
183
+ if (canonical && (style === "block-literal" || style === "block-folded")) {
184
+ if (hasInteriorTrailingWhitespace(s)) return renderDoubleQuoted(s, canonical);
185
+ if (hasNewlineSpacesTab(s)) return renderDoubleQuoted(s, canonical);
186
+ }
187
+ if (style === "block-literal") return renderBlockLiteral(s, indent, explicitChomp, parentPosition);
188
+ if (style === "block-folded") return renderBlockFolded(s, indent);
189
+ if (style === "plain" || style === "single-quoted") {
190
+ if (canonical) {
191
+ const sq = renderSingleQuotedMultiline(s, indent);
192
+ if (sq !== null) return sq;
193
+ }
194
+ return renderBlockLiteral(s, indent, explicitChomp, parentPosition);
195
+ }
196
+ return renderDoubleQuoted(s, canonical);
197
+ }
198
+ if (canonical && hasNonAscii(s)) return renderDoubleQuoted(s, true);
199
+ if (s === "" && (style === "block-literal" || style === "block-folded")) return renderDoubleQuoted(s, canonical);
200
+ switch (style) {
201
+ case "plain":
202
+ if (requiresQuoting(s, ignoreType)) {
203
+ if (s.includes(" ") || s.includes("\r")) return renderDoubleQuoted(s, canonical);
204
+ for (let i = 0; i < s.length; i++) if (isControlChar(s.charCodeAt(i))) return renderDoubleQuoted(s, canonical);
205
+ return renderSingleQuoted(s);
206
+ }
207
+ return s;
208
+ case "single-quoted": return renderSingleQuoted(s);
209
+ case "double-quoted": return renderDoubleQuoted(s, canonical);
210
+ case "block-literal": return renderBlockLiteral(s, indent, explicitChomp, parentPosition);
211
+ case "block-folded": return renderBlockFolded(s, indent);
212
+ }
213
+ }
214
+ /**
215
+ * Returns true if the rendered text ends with an open-ended block scalar
216
+ * (`|+` or `>+` keep-chomp). Such scalars consume any trailing blank lines
217
+ * up to the next document marker, so an explicit `...` is required for the
218
+ * reader to know where the value ends.
219
+ *
220
+ * Detects the most recent `|` or `>` indicator on a header line (matching
221
+ * the form `|<digits>?<chomp>?$` after optional indent and node prefixes)
222
+ * and returns true when the chomp indicator is `+`.
223
+ */
224
+ function endsWithKeepChomp(rendered) {
225
+ const match = rendered.match(/[|>][1-9]?[+-]?$|[|>][1-9]?[+-]?(?=\n)/g);
226
+ if (!match) return false;
227
+ return match[match.length - 1].includes("+");
228
+ }
229
+ /**
230
+ * Renders a number value as a YAML scalar string.
231
+ *
232
+ * Maps JavaScript special number values to their YAML 1.2 Core Schema
233
+ * equivalents: `NaN` becomes `.nan`, positive infinity becomes `.inf`,
234
+ * and negative infinity becomes `-.inf`. All other numbers use
235
+ * `String(n)` which produces valid YAML integer or float literals.
236
+ */
237
+ function renderNumber(n) {
238
+ if (Number.isNaN(n)) return ".nan";
239
+ if (n === Number.POSITIVE_INFINITY) return ".inf";
240
+ if (n === Number.NEGATIVE_INFINITY) return "-.inf";
241
+ return String(n);
242
+ }
243
+ /**
244
+ * Detects circular references by tracking the object ancestor chain.
245
+ */
246
+ function detectCircular(value, seen) {
247
+ if (value !== null && typeof value === "object") {
248
+ if (seen.has(value)) throw new StringifyFailure("Circular reference detected");
249
+ }
250
+ }
251
+ /** Resolves optional stringify options into a fully-defaulted context. */
252
+ function createContext(options) {
253
+ return {
254
+ indent: options?.indent ?? 2,
255
+ lineWidth: options?.lineWidth ?? 80,
256
+ defaultScalarStyle: options?.defaultScalarStyle ?? "plain",
257
+ defaultCollectionStyle: options?.defaultCollectionStyle ?? "block",
258
+ sortKeys: options?.sortKeys ?? false,
259
+ forceDefaultStyles: options?.forceDefaultStyles ?? false,
260
+ seen: /* @__PURE__ */ new Set()
261
+ };
262
+ }
263
+ /**
264
+ * Recursively stringifies a JavaScript value into YAML lines.
265
+ *
266
+ * Returns an array of lines with NO leading indentation — the caller is
267
+ * responsible for prepending the appropriate indentation prefix to each line.
268
+ * This avoids double-indentation when embedding nested collections.
269
+ */
270
+ function stringifyLines(value, ctx, depth) {
271
+ if (depth > 256) throw new StringifyDepthExceeded();
272
+ if (value === null || value === void 0) return ["null"];
273
+ if (typeof value === "boolean") return [value ? "true" : "false"];
274
+ if (typeof value === "number") return [renderNumber(value)];
275
+ if (typeof value === "bigint") return [value.toString()];
276
+ if (typeof value === "string") return renderString(value, ctx.defaultScalarStyle, " ".repeat(ctx.indent), false, ctx.forceDefaultStyles).split("\n");
277
+ if (Array.isArray(value)) {
278
+ detectCircular(value, ctx.seen);
279
+ ctx.seen.add(value);
280
+ try {
281
+ return stringifyArrayLines(value, ctx, depth);
282
+ } finally {
283
+ ctx.seen.delete(value);
284
+ }
285
+ }
286
+ if (typeof value === "object" && value !== null) {
287
+ detectCircular(value, ctx.seen);
288
+ ctx.seen.add(value);
289
+ try {
290
+ return stringifyObjectLines(value, ctx, depth);
291
+ } finally {
292
+ ctx.seen.delete(value);
293
+ }
294
+ }
295
+ return [renderDoubleQuoted(String(value))];
296
+ }
297
+ /**
298
+ * Stringifies a JavaScript array into YAML sequence lines (no leading indent).
299
+ */
300
+ function stringifyArrayLines(arr, ctx, depth) {
301
+ if (arr.length === 0) return ["[]"];
302
+ if (ctx.defaultCollectionStyle === "flow") return [`[${arr.map((item) => stringifyLines(item, ctx, depth + 1).join(" ")).join(", ")}]`];
303
+ const pad = " ".repeat(ctx.indent);
304
+ const lines = [];
305
+ for (const item of arr) {
306
+ const itemLines = stringifyLines(item, ctx, depth + 1);
307
+ if (itemLines.length === 1) lines.push(`- ${itemLines[0]}`);
308
+ else {
309
+ const first = itemLines[0];
310
+ if (first.startsWith("|") || first.startsWith(">")) {
311
+ lines.push(`- ${first}`);
312
+ for (let i = 1; i < itemLines.length; i++) lines.push(itemLines[i]);
313
+ } else {
314
+ lines.push(`- ${itemLines[0]}`);
315
+ for (let i = 1; i < itemLines.length; i++) lines.push(`${pad}${itemLines[i]}`);
316
+ }
317
+ }
318
+ }
319
+ return lines;
320
+ }
321
+ /**
322
+ * Returns true when a value is a non-empty block collection (object or array
323
+ * with block style). Such values must never be placed inline after a key colon
324
+ * in a block mapping — they must always start on the next line.
325
+ */
326
+ function isBlockCollection(value, ctx) {
327
+ if (ctx.defaultCollectionStyle === "flow") return false;
328
+ if (Array.isArray(value) && value.length > 0) return true;
329
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0) return true;
330
+ return false;
331
+ }
332
+ /**
333
+ * Stringifies a JavaScript object into YAML mapping lines (no leading indent).
334
+ */
335
+ function stringifyObjectLines(obj, ctx, depth) {
336
+ const keys = Object.keys(obj);
337
+ if (keys.length === 0) return ["{}"];
338
+ if (ctx.sortKeys) keys.sort();
339
+ if (ctx.defaultCollectionStyle === "flow") return [`{${keys.map((k) => {
340
+ return `${renderString(k, "plain", "")}: ${stringifyLines(obj[k], ctx, depth + 1).join(" ")}`;
341
+ }).join(", ")}}`];
342
+ const renderKey = (k) => k.includes("\n") ? renderDoubleQuoted(k, ctx.forceDefaultStyles) : renderString(k, "plain", "");
343
+ const pad = " ".repeat(ctx.indent);
344
+ const lines = [];
345
+ for (const k of keys) {
346
+ const keyStr = renderKey(k);
347
+ const val = obj[k];
348
+ const valLines = stringifyLines(val, ctx, depth + 1);
349
+ if (valLines.length === 1 && !isBlockCollection(val, ctx)) lines.push(`${keyStr}: ${valLines[0]}`);
350
+ else {
351
+ const first = valLines[0];
352
+ if (first.startsWith("|") || first.startsWith(">")) {
353
+ lines.push(`${keyStr}: ${first}`);
354
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
355
+ } else if (Array.isArray(val) && val.length > 0) {
356
+ lines.push(`${keyStr}:`);
357
+ for (const vl of valLines) lines.push(vl);
358
+ } else {
359
+ lines.push(`${keyStr}:`);
360
+ for (const vl of valLines) lines.push(`${pad}${vl}`);
361
+ }
362
+ }
363
+ }
364
+ return lines;
365
+ }
366
+ /** Standard YAML 1.2 secondary tag prefix. */
367
+ const YAML_TAG_PREFIX = "tag:yaml.org,2002:";
368
+ /**
369
+ * Build a tag resolution map from document directives.
370
+ * Maps tag handles (e.g., "!!", "!e!") to their URI prefixes.
371
+ */
372
+ function buildTagMap(directives) {
373
+ const map = /* @__PURE__ */ new Map();
374
+ for (const d of directives) if (d.name === "TAG" && d.parameters.length >= 2) map.set(d.parameters[0], d.parameters[1]);
375
+ return map;
376
+ }
377
+ /**
378
+ * Normalize a tag for canonical output.
379
+ *
380
+ * - Resolves custom handles using the tag map from directives
381
+ * - Abbreviates `tag:yaml.org,2002:XXX` URIs to `!!XXX`
382
+ * - Simplifies verbatim `!<!XXX>` to `!XXX`
383
+ * - Expands non-standard `!!` redefinitions to verbatim form
384
+ */
385
+ function normalizeTag(tag, tagMap) {
386
+ if (tag.startsWith("!<") && tag.endsWith(">")) {
387
+ const uri = tag.slice(2, -1);
388
+ if (uri.startsWith(YAML_TAG_PREFIX)) return `!!${uri.slice(18)}`;
389
+ if (uri.startsWith("!")) return uri;
390
+ return tag;
391
+ }
392
+ if (tag.startsWith("!!")) {
393
+ const customPrefix = tagMap.get("!!");
394
+ if (customPrefix && customPrefix !== YAML_TAG_PREFIX) return `!<${customPrefix}${tag.slice(2)}>`;
395
+ return tag;
396
+ }
397
+ const namedMatch = tag.match(/^(![\w-]*!)(.*)$/);
398
+ if (namedMatch) {
399
+ const handle = namedMatch[1];
400
+ const suffix = namedMatch[2] ?? "";
401
+ const prefix = tagMap.get(handle);
402
+ if (prefix) {
403
+ const uri = prefix + suffix;
404
+ if (uri.startsWith(YAML_TAG_PREFIX)) return `!!${uri.slice(18)}`;
405
+ return `!<${uri}>`;
406
+ }
407
+ }
408
+ if (tag.startsWith("!") && tag.length > 1 && !tag.startsWith("!!")) {
409
+ const prefix = tagMap.get("!");
410
+ if (prefix) {
411
+ const uri = prefix + tag.slice(1);
412
+ if (uri.startsWith(YAML_TAG_PREFIX)) return `!!${uri.slice(18)}`;
413
+ return `!<${uri}>`;
414
+ }
415
+ }
416
+ return tag;
417
+ }
418
+ /**
419
+ * Recursively normalizes tags on all AST nodes using document directives.
420
+ */
421
+ function normalizeNodeTags(node, tagMap) {
422
+ if (node instanceof YamlScalar) return new YamlScalar({
423
+ value: node.value,
424
+ style: node.style,
425
+ ...node.tag ? { tag: normalizeTag(node.tag, tagMap) } : {},
426
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
427
+ ...node.comment !== void 0 ? { comment: node.comment } : {},
428
+ ...node.chomp !== void 0 ? { chomp: node.chomp } : {},
429
+ ...node.raw !== void 0 ? { raw: node.raw } : {},
430
+ ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
431
+ offset: node.offset,
432
+ length: node.length
433
+ });
434
+ if (node instanceof YamlMap) return new YamlMap({
435
+ items: node.items.map((pair) => new YamlPair({
436
+ key: normalizeNodeTags(pair.key, tagMap),
437
+ value: pair.value ? normalizeNodeTags(pair.value, tagMap) : null,
438
+ ...pair.comment !== void 0 ? { comment: pair.comment } : {}
439
+ })),
440
+ style: node.style,
441
+ ...node.tag ? { tag: normalizeTag(node.tag, tagMap) } : {},
442
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
443
+ ...node.comment !== void 0 ? { comment: node.comment } : {},
444
+ ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
445
+ offset: node.offset,
446
+ length: node.length
447
+ });
448
+ if (node instanceof YamlSeq) return new YamlSeq({
449
+ items: node.items.map((item) => normalizeNodeTags(item, tagMap)),
450
+ style: node.style,
451
+ ...node.tag ? { tag: normalizeTag(node.tag, tagMap) } : {},
452
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
453
+ ...node.comment !== void 0 ? { comment: node.comment } : {},
454
+ ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
455
+ offset: node.offset,
456
+ length: node.length
457
+ });
458
+ return node;
459
+ }
460
+ /**
461
+ * Recursively strips all comment fields from AST nodes.
462
+ * Used when forceDefaultStyles is true to produce canonical output.
463
+ */
464
+ function stripNodeComments(node) {
465
+ if (node instanceof YamlScalar) return new YamlScalar({
466
+ value: node.value,
467
+ style: node.style,
468
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
469
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
470
+ ...node.chomp !== void 0 ? { chomp: node.chomp } : {},
471
+ ...node.raw !== void 0 ? { raw: node.raw } : {},
472
+ ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
473
+ offset: node.offset,
474
+ length: node.length
475
+ });
476
+ if (node instanceof YamlMap) return new YamlMap({
477
+ items: node.items.map((pair) => new YamlPair({
478
+ key: stripNodeComments(pair.key),
479
+ value: pair.value ? stripNodeComments(pair.value) : null
480
+ })),
481
+ style: node.style,
482
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
483
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
484
+ ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
485
+ offset: node.offset,
486
+ length: node.length
487
+ });
488
+ if (node instanceof YamlSeq) return new YamlSeq({
489
+ items: node.items.map(stripNodeComments),
490
+ style: node.style,
491
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
492
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
493
+ ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
494
+ offset: node.offset,
495
+ length: node.length
496
+ });
497
+ return node;
498
+ }
499
+ /**
500
+ * Stringifies a YAML AST node into lines (no leading indent), respecting
501
+ * style metadata from the node.
502
+ */
503
+ function stringifyNodeLines(node, ctx, depth) {
504
+ if (depth > 256) throw new StringifyDepthExceeded();
505
+ if (node instanceof YamlScalar) return stringifyScalarNodeLines(node, ctx);
506
+ if (node instanceof YamlMap) return stringifyMapNodeLines(node, ctx, depth);
507
+ if (node instanceof YamlSeq) return stringifySeqNodeLines(node, ctx, depth);
508
+ if (node instanceof YamlAlias) return [`*${node.name}`];
509
+ return ["null"];
510
+ }
511
+ /**
512
+ * Stringifies a YamlScalar node into lines, using the node's style metadata.
513
+ */
514
+ function stringifyScalarNodeLines(node, ctx) {
515
+ const style = node.style ?? ctx.defaultScalarStyle;
516
+ const val = node.value;
517
+ const isEmpty = node.length === 0 && (val === null || val === void 0 || val === "");
518
+ if (isEmpty && (node.tag || node.anchor)) {
519
+ const parts = [];
520
+ if (node.tag) parts.push(node.tag);
521
+ if (node.anchor) parts.push(`&${node.anchor}`);
522
+ return [parts.join(" ")];
523
+ }
524
+ let lines;
525
+ if (val === null || val === void 0) if (isEmpty) lines = [""];
526
+ else lines = ["null"];
527
+ else if (typeof val === "boolean") lines = [val ? "true" : "false"];
528
+ else if (typeof val === "number") lines = [node.raw !== void 0 ? node.raw : renderNumber(val)];
529
+ else if (typeof val === "string") lines = renderString(val, style, " ".repeat(ctx.indent), !!node.tag, ctx.forceDefaultStyles, node.chomp, ctx.parentPosition).split("\n");
530
+ else lines = [renderDoubleQuoted(String(val))];
531
+ if (node.tag) lines[0] = `${node.tag} ${lines[0]}`;
532
+ if (node.anchor) lines[0] = `&${node.anchor} ${lines[0]}`;
533
+ return lines;
534
+ }
535
+ /**
536
+ * Stringifies a YamlMap node into lines, using the node's collection style.
537
+ */
538
+ function stringifyMapNodeLines(node, ctx, depth) {
539
+ const style = ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : node.style ?? ctx.defaultCollectionStyle;
540
+ let items = [...node.items];
541
+ if (ctx.sortKeys) items = items.sort((a, b) => {
542
+ const ka = a.key instanceof YamlScalar ? String(a.key.value) : "";
543
+ const kb = b.key instanceof YamlScalar ? String(b.key.value) : "";
544
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
545
+ });
546
+ if (items.length === 0) {
547
+ let line = "{}";
548
+ const emptyPrefix = buildMetadataPrefix(node.tag, node.anchor);
549
+ if (emptyPrefix) line = `${emptyPrefix} ${line}`;
550
+ return [line];
551
+ }
552
+ if (style === "flow") {
553
+ let line = `{${items.map((pair) => {
554
+ return `${pair.key ? stringifyNodeLines(pair.key, ctx, depth + 1).join(" ") : "null"}: ${pair.value ? stringifyNodeLines(pair.value, ctx, depth + 1).join(" ") : "null"}`;
555
+ }).join(", ")}}`;
556
+ const flowPrefix = buildMetadataPrefix(node.tag, node.anchor);
557
+ if (flowPrefix) line = `${flowPrefix} ${line}`;
558
+ return [line];
559
+ }
560
+ const pad = " ".repeat(ctx.indent);
561
+ const lines = [];
562
+ for (const pair of items) {
563
+ const keyIsScalarWithNewline = pair.key instanceof YamlScalar && (typeof pair.key.value === "string" && pair.key.value.includes("\n") || pair.key.style === "block-literal" || pair.key.style === "block-folded");
564
+ if ((pair.key instanceof YamlMap || pair.key instanceof YamlSeq) && pair.key.items.length > 0 || keyIsScalarWithNewline) {
565
+ const keyLines = stringifyNodeLines(pair.key, ctx, depth + 1);
566
+ lines.push(`? ${keyLines[0]}`);
567
+ const firstTokens = keyLines[0].trim().split(/\s+/).filter(Boolean);
568
+ const firstIsMetaOnly = firstTokens.length > 0 && firstTokens.every((t) => t.startsWith("&") || t.startsWith("!"));
569
+ const keyIsBlockScalar = pair.key instanceof YamlScalar && (pair.key.style === "block-literal" || pair.key.style === "block-folded");
570
+ const contPad = firstIsMetaOnly || keyIsBlockScalar ? "" : pad;
571
+ for (let k = 1; k < keyLines.length; k++) lines.push(`${contPad}${keyLines[k]}`);
572
+ const valNode = pair.value;
573
+ if (!valNode) lines.push(":");
574
+ else {
575
+ const valLines = stringifyNodeLines(valNode, ctx, depth + 1);
576
+ if (valLines.length === 1) lines.push(`: ${valLines[0]}`);
577
+ else {
578
+ const first = valLines[0];
579
+ const firstStripped = stripScalarMetadataPrefix(first);
580
+ const isBlockScalarHeader = firstStripped.startsWith("|") || firstStripped.startsWith(">");
581
+ const isInlineQuoted = valNode instanceof YamlScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""));
582
+ if (isBlockScalarHeader || isInlineQuoted) {
583
+ lines.push(`: ${first}`);
584
+ for (let v = 1; v < valLines.length; v++) lines.push(valLines[v]);
585
+ } else if (first.startsWith("-")) {
586
+ lines.push(`: ${first}`);
587
+ for (let v = 1; v < valLines.length; v++) lines.push(`${pad}${valLines[v]}`);
588
+ } else if (valNode instanceof YamlMap && valNode.items.length > 0 && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block") {
589
+ lines.push(`: ${first}`);
590
+ for (let v = 1; v < valLines.length; v++) lines.push(`${pad}${valLines[v]}`);
591
+ } else {
592
+ lines.push(":");
593
+ for (const vl of valLines) lines.push(`${pad}${vl}`);
594
+ }
595
+ }
596
+ }
597
+ continue;
598
+ }
599
+ let resolvedKeyStr;
600
+ if (ctx.forceDefaultStyles && node.style === "flow" && node.sourceMultiline !== true && pair.key instanceof YamlScalar && (pair.key.style === "single-quoted" || pair.key.style === "double-quoted") && typeof pair.key.value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(pair.key.value)) resolvedKeyStr = pair.key.value;
601
+ else resolvedKeyStr = pair.key ? stringifyNodeLines(pair.key, ctx, depth + 1).join(" ") : "null";
602
+ const keyStr = resolvedKeyStr;
603
+ const keyIsAnchoredOrTaggedEmpty = pair.key instanceof YamlScalar && pair.key.length === 0 && (pair.key.value === null || pair.key.value === void 0 || pair.key.value === "") && (pair.key.anchor !== void 0 || pair.key.tag !== void 0);
604
+ const sep = pair.key instanceof YamlAlias || keyIsAnchoredOrTaggedEmpty ? " :" : ":";
605
+ const valNode = pair.value;
606
+ if (!valNode) {
607
+ const isPlainKey = pair.key instanceof YamlScalar && pair.key.style === "plain";
608
+ const keyIsNonEmpty = pair.key instanceof YamlScalar && pair.key.length > 0;
609
+ const isRootFlowMap = ctx.parentPosition === void 0 && node.style === "flow" && node.sourceMultiline === true;
610
+ if (ctx.forceDefaultStyles && isRootFlowMap && isPlainKey && keyIsNonEmpty) lines.push(`${keyStr}${sep} null`);
611
+ else lines.push(`${keyStr}${sep}`);
612
+ continue;
613
+ }
614
+ const valLines = stringifyNodeLines(valNode, {
615
+ ...ctx,
616
+ parentPosition: "block-map-value"
617
+ }, depth + 1);
618
+ if (valNode instanceof YamlSeq && valNode.items.length > 0 && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block") {
619
+ const seqMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
620
+ const startIdx = seqMeta ? 1 : 0;
621
+ lines.push(seqMeta ? `${keyStr}${sep} ${seqMeta}` : `${keyStr}${sep}`);
622
+ for (let i = startIdx; i < valLines.length; i++) lines.push(valLines[i]);
623
+ } else if (valNode instanceof YamlMap && valNode.items.length > 0 && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block") {
624
+ const mapMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
625
+ const startIdx = mapMeta ? 1 : 0;
626
+ lines.push(mapMeta ? `${keyStr}${sep} ${mapMeta}` : `${keyStr}${sep}`);
627
+ for (let i = startIdx; i < valLines.length; i++) lines.push(`${pad}${valLines[i]}`);
628
+ } else if (valLines.length === 1) lines.push(valLines[0] === "" ? `${keyStr}${sep}` : `${keyStr}${sep} ${valLines[0]}`);
629
+ else {
630
+ const first = valLines[0];
631
+ const firstStripped = stripScalarMetadataPrefix(first);
632
+ const isBlockScalarHeader = firstStripped.startsWith("|") || firstStripped.startsWith(">");
633
+ const isInlineQuoted = valNode instanceof YamlScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""));
634
+ if (isBlockScalarHeader || isInlineQuoted) {
635
+ lines.push(`${keyStr}${sep} ${first}`);
636
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
637
+ } else {
638
+ const mapMeta = valNode instanceof YamlMap && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block" ? buildMetadataPrefix(valNode.tag, valNode.anchor) : void 0;
639
+ if (mapMeta) {
640
+ lines.push(`${keyStr}${sep} ${mapMeta}`);
641
+ for (let i = 1; i < valLines.length; i++) lines.push(`${pad}${valLines[i]}`);
642
+ } else {
643
+ lines.push(`${keyStr}${sep}`);
644
+ for (const vl of valLines) lines.push(`${pad}${vl}`);
645
+ }
646
+ }
647
+ }
648
+ }
649
+ const prefix = buildMetadataPrefix(node.tag, node.anchor);
650
+ if (prefix) lines.unshift(prefix);
651
+ return lines;
652
+ }
653
+ /**
654
+ * Builds a metadata prefix string from tag and anchor.
655
+ * Returns the combined prefix or undefined if neither is present.
656
+ */
657
+ function buildMetadataPrefix(tag, anchor) {
658
+ if (!tag && !anchor) return void 0;
659
+ const parts = [];
660
+ if (anchor) parts.push(`&${anchor}`);
661
+ if (tag) parts.push(tag);
662
+ return parts.join(" ");
663
+ }
664
+ /**
665
+ * Strips a leading run of `&anchor`/`!tag` metadata tokens (each followed by
666
+ * whitespace) from a rendered scalar line and returns the remainder. Used to
667
+ * detect quoted/block scalar style after the optional metadata prefix that
668
+ * `stringifyScalarNodeLines` may prepend to the first output line.
669
+ */
670
+ function stripScalarMetadataPrefix(line) {
671
+ let i = 0;
672
+ while (i < line.length) {
673
+ const ch = line[i];
674
+ if (ch !== "&" && ch !== "!") break;
675
+ let j = i + 1;
676
+ while (j < line.length && line[j] !== " " && line[j] !== " ") j++;
677
+ if (j >= line.length || j === i + 1) break;
678
+ while (j < line.length && (line[j] === " " || line[j] === " ")) j++;
679
+ i = j;
680
+ }
681
+ return line.slice(i);
682
+ }
683
+ /**
684
+ * Stringifies a YamlSeq node into lines, using the node's collection style.
685
+ */
686
+ function stringifySeqNodeLines(node, ctx, depth) {
687
+ const style = ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : node.style ?? ctx.defaultCollectionStyle;
688
+ const items = [...node.items];
689
+ if (items.length === 0) {
690
+ let line = "[]";
691
+ const emptyPrefix = buildMetadataPrefix(node.tag, node.anchor);
692
+ if (emptyPrefix) line = `${emptyPrefix} ${line}`;
693
+ return [line];
694
+ }
695
+ if (style === "flow") {
696
+ let line = `[${items.map((item) => stringifyNodeLines(item, ctx, depth + 1).join(" ")).join(", ")}]`;
697
+ const flowPrefix = buildMetadataPrefix(node.tag, node.anchor);
698
+ if (flowPrefix) line = `${flowPrefix} ${line}`;
699
+ return [line];
700
+ }
701
+ const pad = " ".repeat(ctx.indent);
702
+ const lines = [];
703
+ const itemCtx = {
704
+ ...ctx,
705
+ parentPosition: "block-seq-item"
706
+ };
707
+ for (const item of items) {
708
+ const itemLines = stringifyNodeLines(item, itemCtx, depth + 1);
709
+ if (itemLines.length === 1) lines.push(itemLines[0] === "" ? "-" : `- ${itemLines[0]}`);
710
+ else {
711
+ const first = itemLines[0];
712
+ const firstStripped = stripScalarMetadataPrefix(first);
713
+ const itemIsScalar = item instanceof YamlScalar;
714
+ if (firstStripped.startsWith("|") || firstStripped.startsWith(">") || itemIsScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""))) {
715
+ lines.push(`- ${first}`);
716
+ for (let i = 1; i < itemLines.length; i++) lines.push(itemLines[i]);
717
+ } else {
718
+ lines.push(first === "" ? "-" : `- ${first}`);
719
+ for (let i = 1; i < itemLines.length; i++) lines.push(`${pad}${itemLines[i]}`);
720
+ }
721
+ }
722
+ }
723
+ const prefix = buildMetadataPrefix(node.tag, node.anchor);
724
+ if (prefix) lines.unshift(prefix);
725
+ return lines;
726
+ }
727
+ /**
728
+ * Converts a JavaScript value into a YAML text string.
729
+ *
730
+ * Handles all primitive types, arrays, and plain objects. Special numbers
731
+ * (`Infinity`, `-Infinity`, `NaN`) are rendered as `.inf`, `-.inf`, and
732
+ * `.nan` respectively. Circular references throw {@link StringifyFailure}.
733
+ */
734
+ function stringifyValue(value, options) {
735
+ const result = stringifyLines(value, createContext(options), 0).join("\n");
736
+ return options?.finalNewline ?? true ? `${result}\n` : result;
737
+ }
738
+ /**
739
+ * Converts a composed YAML document AST into a YAML text string, preserving
740
+ * the style metadata encoded in each AST node.
741
+ *
742
+ * Scalar nodes use their `style` field to control rendering; collection
743
+ * nodes use their `style` field (`"block"` or `"flow"`). Nodes without an
744
+ * explicit style fall back to the defaults in `options`.
745
+ */
746
+ function stringifyDocument(doc, options) {
747
+ const ctx = createContext(options);
748
+ const finalNewline = options?.finalNewline ?? true;
749
+ let contents = doc.contents;
750
+ const docComment = ctx.forceDefaultStyles ? void 0 : doc.comment;
751
+ if (ctx.forceDefaultStyles && contents) {
752
+ contents = stripNodeComments(contents);
753
+ const tagMap = buildTagMap(doc.directives);
754
+ contents = normalizeNodeTags(contents, tagMap);
755
+ }
756
+ if (contents === null) {
757
+ if (ctx.forceDefaultStyles) {
758
+ if (doc.hasDocumentStart) return `---\n${doc.hasDocumentEnd ? "...\n" : ""}`;
759
+ return "";
760
+ }
761
+ return finalNewline ? "null\n" : "null";
762
+ }
763
+ const result = stringifyNodeLines(contents, ctx, 0).join("\n");
764
+ const body = finalNewline ? `${result}\n` : result;
765
+ const needsTerminatorForKeepChomp = ctx.forceDefaultStyles && endsWithKeepChomp(result);
766
+ const needsTerminatorForAnchoredPlainScalar = ctx.forceDefaultStyles && doc.hasDocumentStart && contents instanceof YamlScalar && contents.style === "plain" && contents.anchor !== void 0 && !contents.tag;
767
+ const looksLikeDirectiveContinuation = contents instanceof YamlScalar && typeof contents.value === "string" && / %[A-Z]/.test(contents.value);
768
+ const needsTerminatorForMultilinePlainScalar = ctx.forceDefaultStyles && doc.hasDocumentStart && contents instanceof YamlScalar && contents.style === "plain" && contents.sourceMultiline === true && looksLikeDirectiveContinuation;
769
+ const needsTerminatorForDocStartTab = ctx.forceDefaultStyles && doc.hasDocumentStartTab === true;
770
+ const docEnd = doc.hasDocumentEnd || needsTerminatorForKeepChomp || needsTerminatorForAnchoredPlainScalar || needsTerminatorForMultilinePlainScalar || needsTerminatorForDocStartTab ? "...\n" : "";
771
+ if (doc.hasDocumentStart) {
772
+ const rootTag = contents && "tag" in contents ? contents.tag : void 0;
773
+ const rootAnchor = contents && "anchor" in contents ? contents.anchor : void 0;
774
+ const isCollection = contents instanceof YamlMap || contents instanceof YamlSeq;
775
+ const isScalar = contents instanceof YamlScalar;
776
+ if (rootTag || rootAnchor) {
777
+ const metaParts = [];
778
+ if (rootAnchor) metaParts.push(`&${rootAnchor}`);
779
+ if (rootTag) metaParts.push(rootTag);
780
+ const metaStr = metaParts.join(" ");
781
+ let bodyClean = body;
782
+ const metaLinePrefix = `${metaStr}\n`;
783
+ const metaInlinePrefix = `${metaStr} `;
784
+ if (bodyClean.startsWith(metaLinePrefix)) bodyClean = bodyClean.slice(metaLinePrefix.length);
785
+ else if (bodyClean.startsWith(metaInlinePrefix)) bodyClean = bodyClean.slice(metaInlinePrefix.length);
786
+ const docStart = `--- ${metaStr}`;
787
+ const sep = isCollection ? "\n" : " ";
788
+ return docComment ? `# ${docComment}\n${docStart}${sep}${bodyClean}${docEnd}` : `${docStart}${sep}${bodyClean}${docEnd}`;
789
+ }
790
+ if (isScalar) return docComment ? `# ${docComment}\n--- ${body}${docEnd}` : `--- ${body}${docEnd}`;
791
+ return docComment ? `# ${docComment}\n---\n${body}${docEnd}` : `---\n${body}${docEnd}`;
792
+ }
793
+ return docComment ? `# ${docComment}\n${body}${docEnd}` : `${body}${docEnd}`;
794
+ }
795
+
796
+ //#endregion
797
+ export { StringifyDepthExceeded, StringifyFailure, stringifyDocument, stringifyValue, stripNodeComments };