@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/Yaml.js +414 -0
- package/YamlDiagnostic.js +126 -0
- package/YamlDocument.js +181 -0
- package/YamlEdit.js +45 -0
- package/YamlFormat.js +309 -0
- package/YamlNode.js +396 -0
- package/YamlVisitor.js +172 -0
- package/index.d.ts +1331 -0
- package/index.js +9 -0
- package/internal/composer/anchors.js +97 -0
- package/internal/composer/block.js +1155 -0
- package/internal/composer/document.js +677 -0
- package/internal/composer/flow.js +400 -0
- package/internal/composer/scalars.js +885 -0
- package/internal/composer/state.js +117 -0
- package/internal/composer/tags.js +88 -0
- package/internal/cst-parser.js +716 -0
- package/internal/diagnostics.js +80 -0
- package/internal/diff.js +56 -0
- package/internal/fold.js +153 -0
- package/internal/lexer.js +959 -0
- package/internal/stringifier.js +797 -0
- package/package.json +47 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -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 };
|