@effected/yaml 0.2.0 → 0.3.1
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/Yaml.js +168 -35
- package/YamlDocument.js +11 -0
- package/index.d.ts +89 -2
- package/internal/fold.js +86 -1
- package/internal/stringifier.js +12 -8
- package/package.json +1 -1
package/Yaml.js
CHANGED
|
@@ -4,7 +4,7 @@ import { composeAllDocuments, composeFirstDocument } from "./internal/composer/d
|
|
|
4
4
|
import { isFatalCode } from "./internal/diagnostics.js";
|
|
5
5
|
import { StringifyDepthExceeded, StringifyFailure, stringifyValue } from "./internal/stringifier.js";
|
|
6
6
|
import { YamlDiagnostic } from "./YamlDiagnostic.js";
|
|
7
|
-
import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
|
7
|
+
import { Effect, Option, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
|
8
8
|
|
|
9
9
|
//#region src/Yaml.ts
|
|
10
10
|
/**
|
|
@@ -34,11 +34,20 @@ var YamlParseOptions = class extends Schema.Class("YamlParseOptions")({
|
|
|
34
34
|
}) {};
|
|
35
35
|
/**
|
|
36
36
|
* Options controlling stringify behavior. All fields are omissible; absent
|
|
37
|
-
* fields resolve to `indent` `2`, `lineWidth` `
|
|
37
|
+
* fields resolve to `indent` `2`, `lineWidth` `0`, `defaultScalarStyle`
|
|
38
38
|
* `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
|
|
39
39
|
* `indentSequences` `false`, `finalNewline` `true` and `forceDefaultStyles`
|
|
40
40
|
* `false`.
|
|
41
41
|
*
|
|
42
|
+
* `lineWidth` controls column-based scalar folding. The default `0` (and any
|
|
43
|
+
* value `<= 0`) never wraps, emitting byte-identical output to the historic
|
|
44
|
+
* no-fold behavior; a positive value folds long plain, double-quoted and
|
|
45
|
+
* block-folded (`>`) scalars at approximately that column, inserting only
|
|
46
|
+
* semantically transparent line breaks. Block-literal (`|`) content is never
|
|
47
|
+
* folded — literal blocks preserve their bytes by definition. Folding is a
|
|
48
|
+
* value-path feature only: `YamlDocument#stringify` and the `YamlFormat`
|
|
49
|
+
* helpers accept these options but do not fold (see `lineWidth`).
|
|
50
|
+
*
|
|
42
51
|
* `indentSequences` controls the presentation of block sequences nested under
|
|
43
52
|
* a mapping key: `false` (the default) emits them at the key's column — the
|
|
44
53
|
* kit's byte-compatible legacy form — while `true` indents them one level,
|
|
@@ -64,6 +73,20 @@ var YamlParseOptions = class extends Schema.Class("YamlParseOptions")({
|
|
|
64
73
|
*/
|
|
65
74
|
var YamlStringifyOptions = class extends Schema.Class("YamlStringifyOptions")({
|
|
66
75
|
indent: Schema.optionalKey(Schema.Number),
|
|
76
|
+
/**
|
|
77
|
+
* Column at which to fold long scalars. Default `0` (and any value `<= 0`)
|
|
78
|
+
* never wraps; a positive value folds plain, double-quoted and block-folded
|
|
79
|
+
* (`>`) scalars at approximately that column, never block-literal (`|`).
|
|
80
|
+
*
|
|
81
|
+
* Takes effect only through {@link Yaml.stringify} and
|
|
82
|
+
* {@link Yaml.stringifySync} — the two entry points that accept these
|
|
83
|
+
* options on the value path. The schema factories ({@link Yaml.fromString},
|
|
84
|
+
* {@link Yaml.schema}, {@link Yaml.YamlFromString}) encode with default
|
|
85
|
+
* stringify options (`lineWidth` `0`), so their output never folds. The
|
|
86
|
+
* document/node path — `YamlDocument#stringify` and the `YamlFormat`
|
|
87
|
+
* helpers built on it — threads the field into its render context but
|
|
88
|
+
* never reads it, so it is inert there.
|
|
89
|
+
*/
|
|
67
90
|
lineWidth: Schema.optionalKey(Schema.Number),
|
|
68
91
|
defaultScalarStyle: Schema.optionalKey(ScalarStyle),
|
|
69
92
|
defaultCollectionStyle: Schema.optionalKey(CollectionStyle),
|
|
@@ -123,6 +146,89 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
|
|
|
123
146
|
};
|
|
124
147
|
const toDiagnostics = (text, records) => records.map((r) => YamlDiagnostic.fromRaw(r, text));
|
|
125
148
|
/**
|
|
149
|
+
* Build the fatal {@link YamlParseError} for an alias-expansion "billion
|
|
150
|
+
* laughs" blow-up. The offending expansion has no source span, so the
|
|
151
|
+
* diagnostic carries zero offsets.
|
|
152
|
+
*/
|
|
153
|
+
const aliasCountExceededError = (message, text) => new YamlParseError({
|
|
154
|
+
diagnostics: [YamlDiagnostic.make({
|
|
155
|
+
code: "AliasCountExceeded",
|
|
156
|
+
message,
|
|
157
|
+
offset: 0,
|
|
158
|
+
length: 0,
|
|
159
|
+
line: 0,
|
|
160
|
+
character: 0
|
|
161
|
+
})],
|
|
162
|
+
input: text
|
|
163
|
+
});
|
|
164
|
+
/**
|
|
165
|
+
* Map an internal stringifier throw to its typed {@link YamlStringifyError},
|
|
166
|
+
* or return `undefined` for any other defect (which the caller re-throws).
|
|
167
|
+
* Shared by the Effect and synchronous stringify paths so both surface the
|
|
168
|
+
* hardening guards (circular reference, nesting-depth cap) identically.
|
|
169
|
+
*/
|
|
170
|
+
const stringifyDefectToError = (defect, value) => {
|
|
171
|
+
if (defect instanceof StringifyFailure) return new YamlStringifyError({
|
|
172
|
+
diagnostics: [YamlDiagnostic.make({
|
|
173
|
+
code: "CircularReference",
|
|
174
|
+
message: defect.reason,
|
|
175
|
+
offset: 0,
|
|
176
|
+
length: 0,
|
|
177
|
+
line: 0,
|
|
178
|
+
character: 0
|
|
179
|
+
})],
|
|
180
|
+
value
|
|
181
|
+
});
|
|
182
|
+
if (defect instanceof StringifyDepthExceeded) return new YamlStringifyError({
|
|
183
|
+
diagnostics: [YamlDiagnostic.make({
|
|
184
|
+
code: "NestingDepthExceeded",
|
|
185
|
+
message: defect.message,
|
|
186
|
+
offset: 0,
|
|
187
|
+
length: 0,
|
|
188
|
+
line: 0,
|
|
189
|
+
character: 0
|
|
190
|
+
})],
|
|
191
|
+
value
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Synchronous single-document parse returning a `Result`. The pure
|
|
196
|
+
* engine bypasses the Effect runtime entirely: the composer, the failure
|
|
197
|
+
* collection and the alias-expansion budget all run inline, and every failure
|
|
198
|
+
* mode (fatal diagnostics, duplicate keys, a "billion laughs" blow-up) yields
|
|
199
|
+
* a `Failure` carrying a typed {@link YamlParseError} — never a throw.
|
|
200
|
+
*/
|
|
201
|
+
const parseSyncImpl = (text, options) => {
|
|
202
|
+
const doc = composeFirstDocument(text, toParseInput(options));
|
|
203
|
+
const failures = failureRecords(doc, options?.uniqueKeys ?? true);
|
|
204
|
+
if (failures.length > 0) return Result.fail(new YamlParseError({
|
|
205
|
+
diagnostics: toDiagnostics(text, failures),
|
|
206
|
+
input: text
|
|
207
|
+
}));
|
|
208
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
209
|
+
try {
|
|
210
|
+
return Result.succeed(nodeToJsValue(doc.contents, anchors, options?.maxAliasCount ?? 100));
|
|
211
|
+
} catch (defect) {
|
|
212
|
+
if (defect instanceof AliasExpansionBudgetExceeded) return Result.fail(aliasCountExceededError(defect.message, text));
|
|
213
|
+
throw defect;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Synchronous stringify returning a `Result`. Mirrors {@link Yaml.stringify}
|
|
218
|
+
* without the Effect wrapper: a circular reference or a value nested past the
|
|
219
|
+
* recursion budget yields a `Failure` carrying a typed {@link YamlStringifyError},
|
|
220
|
+
* never a thrown defect.
|
|
221
|
+
*/
|
|
222
|
+
const stringifySyncImpl = (value, options) => {
|
|
223
|
+
try {
|
|
224
|
+
return Result.succeed(stringifyValue(value, toStringifyInput(options)));
|
|
225
|
+
} catch (defect) {
|
|
226
|
+
const error = stringifyDefectToError(defect, value);
|
|
227
|
+
if (error !== void 0) return Result.fail(error);
|
|
228
|
+
throw defect;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
126
232
|
* Collect the raw diagnostics that make a composed document a parse failure:
|
|
127
233
|
* every fatal-code error, plus DuplicateKey warnings promoted to errors when
|
|
128
234
|
* `uniqueKeys` is in force (the v3 `parse` contract). Order preserved:
|
|
@@ -142,45 +248,15 @@ const failureRecords = (doc, uniqueKeys) => {
|
|
|
142
248
|
const extractDocumentValue = (contents, anchors, maxAliasCount, text) => Effect.try({
|
|
143
249
|
try: () => nodeToJsValue(contents, anchors, maxAliasCount),
|
|
144
250
|
catch: (defect) => {
|
|
145
|
-
if (defect instanceof AliasExpansionBudgetExceeded) return
|
|
146
|
-
diagnostics: [YamlDiagnostic.make({
|
|
147
|
-
code: "AliasCountExceeded",
|
|
148
|
-
message: defect.message,
|
|
149
|
-
offset: 0,
|
|
150
|
-
length: 0,
|
|
151
|
-
line: 0,
|
|
152
|
-
character: 0
|
|
153
|
-
})],
|
|
154
|
-
input: text
|
|
155
|
-
});
|
|
251
|
+
if (defect instanceof AliasExpansionBudgetExceeded) return aliasCountExceededError(defect.message, text);
|
|
156
252
|
throw defect;
|
|
157
253
|
}
|
|
158
254
|
});
|
|
159
255
|
const stringifyOrFail = (value, options) => Effect.try({
|
|
160
256
|
try: () => stringifyValue(value, toStringifyInput(options)),
|
|
161
257
|
catch: (defect) => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
code: "CircularReference",
|
|
165
|
-
message: defect.reason,
|
|
166
|
-
offset: 0,
|
|
167
|
-
length: 0,
|
|
168
|
-
line: 0,
|
|
169
|
-
character: 0
|
|
170
|
-
})],
|
|
171
|
-
value
|
|
172
|
-
});
|
|
173
|
-
if (defect instanceof StringifyDepthExceeded) return new YamlStringifyError({
|
|
174
|
-
diagnostics: [YamlDiagnostic.make({
|
|
175
|
-
code: "NestingDepthExceeded",
|
|
176
|
-
message: defect.message,
|
|
177
|
-
offset: 0,
|
|
178
|
-
length: 0,
|
|
179
|
-
line: 0,
|
|
180
|
-
character: 0
|
|
181
|
-
})],
|
|
182
|
-
value
|
|
183
|
-
});
|
|
258
|
+
const error = stringifyDefectToError(defect, value);
|
|
259
|
+
if (error !== void 0) return error;
|
|
184
260
|
throw defect;
|
|
185
261
|
}
|
|
186
262
|
});
|
|
@@ -268,6 +344,63 @@ var Yaml = class Yaml {
|
|
|
268
344
|
return yield* stringifyOrFail(value, options);
|
|
269
345
|
});
|
|
270
346
|
/**
|
|
347
|
+
* Synchronous single-document parse, returning a `Result` instead of
|
|
348
|
+
* an `Effect`. A pure escape hatch for config-time callers that cannot
|
|
349
|
+
* `await` an Effect (a `vitest.config.ts` is the motivating case): it runs
|
|
350
|
+
* the same engine as {@link Yaml.parse} inline.
|
|
351
|
+
*
|
|
352
|
+
* Preserves the package contract — malformed and adversarial input fails
|
|
353
|
+
* typed, never as a defect. Fatal diagnostics, duplicate keys and a
|
|
354
|
+
* "billion laughs" alias-expansion blow-up all yield a `Failure` carrying a
|
|
355
|
+
* {@link YamlParseError}; the method never throws.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```ts
|
|
359
|
+
* import { Yaml } from "@effected/yaml";
|
|
360
|
+
* import { Result } from "effect";
|
|
361
|
+
*
|
|
362
|
+
* const result = Yaml.parseSync("name: Alice\nage: 30");
|
|
363
|
+
* if (Result.isSuccess(result)) {
|
|
364
|
+
* result.success; // { name: "Alice", age: 30 }
|
|
365
|
+
* } else {
|
|
366
|
+
* result.failure; // YamlParseError
|
|
367
|
+
* }
|
|
368
|
+
* ```
|
|
369
|
+
*
|
|
370
|
+
* @public
|
|
371
|
+
*/
|
|
372
|
+
static parseSync(text, options) {
|
|
373
|
+
return parseSyncImpl(text, options);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Synchronous stringify, returning a `Result` instead of an `Effect`.
|
|
377
|
+
* The pure counterpart to {@link Yaml.stringify} for config-time callers
|
|
378
|
+
* that cannot `await`.
|
|
379
|
+
*
|
|
380
|
+
* Preserves the package contract — a circular reference (`CircularReference`)
|
|
381
|
+
* or a value nested past the recursion budget (`NestingDepthExceeded`)
|
|
382
|
+
* yields a `Failure` carrying a {@link YamlStringifyError} rather than a
|
|
383
|
+
* thrown stack-overflow defect; the method never throws.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* import { Yaml } from "@effected/yaml";
|
|
388
|
+
* import { Result } from "effect";
|
|
389
|
+
*
|
|
390
|
+
* const result = Yaml.stringifySync({ name: "Alice" });
|
|
391
|
+
* if (Result.isFailure(result)) {
|
|
392
|
+
* result.failure; // YamlStringifyError
|
|
393
|
+
* } else {
|
|
394
|
+
* result.success; // "name: Alice\n"
|
|
395
|
+
* }
|
|
396
|
+
* ```
|
|
397
|
+
*
|
|
398
|
+
* @public
|
|
399
|
+
*/
|
|
400
|
+
static stringifySync(value, options) {
|
|
401
|
+
return stringifySyncImpl(value, options);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
271
404
|
* Strip comments from YAML text. Without `replaceCh`, comment characters
|
|
272
405
|
* are removed (line breaks are kept, so line numbers stay stable); with a
|
|
273
406
|
* `replaceCh` (e.g. `" "`), each comment character is replaced instead,
|
package/YamlDocument.js
CHANGED
|
@@ -91,6 +91,17 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
|
|
|
91
91
|
* deeper than the stringifier's recursion budget (`NestingDepthExceeded`)
|
|
92
92
|
* — both surface through the typed error channel rather than as an
|
|
93
93
|
* unhandled stack-overflow defect.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* `YamlStringifyOptions.lineWidth` is not honored here: column-based
|
|
97
|
+
* scalar folding exists only on the value path, through the entry points
|
|
98
|
+
* that accept stringify options ({@link Yaml.stringify} and
|
|
99
|
+
* {@link Yaml.stringifySync}). The
|
|
100
|
+
* document/node path threads `lineWidth` into its render context but
|
|
101
|
+
* never reads it, so long scalars are emitted unfolded regardless of the
|
|
102
|
+
* option. Callers that need folding should render the plain value
|
|
103
|
+
* instead — `Yaml.stringify(doc.toValue(), options)` — at the cost of
|
|
104
|
+
* the document-level framing and styles this path preserves.
|
|
94
105
|
*/
|
|
95
106
|
stringify(options) {
|
|
96
107
|
return Effect.try({
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Data, Effect, Option, Schema, Stream } from "effect";
|
|
1
|
+
import { Data, Effect, Option, Result, Schema, Stream } from "effect";
|
|
2
2
|
//#region src/YamlDiagnostic.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Error codes emitted by the lexer stage.
|
|
@@ -145,6 +145,20 @@ declare const YamlParseOptions_base: Schema.Class<YamlParseOptions, Schema.Struc
|
|
|
145
145
|
declare class YamlParseOptions extends YamlParseOptions_base {}
|
|
146
146
|
declare const YamlStringifyOptions_base: Schema.Class<YamlStringifyOptions, Schema.Struct<{
|
|
147
147
|
readonly indent: Schema.optionalKey<Schema.Number>;
|
|
148
|
+
/**
|
|
149
|
+
* Column at which to fold long scalars. Default `0` (and any value `<= 0`)
|
|
150
|
+
* never wraps; a positive value folds plain, double-quoted and block-folded
|
|
151
|
+
* (`>`) scalars at approximately that column, never block-literal (`|`).
|
|
152
|
+
*
|
|
153
|
+
* Takes effect only through {@link Yaml.stringify} and
|
|
154
|
+
* {@link Yaml.stringifySync} — the two entry points that accept these
|
|
155
|
+
* options on the value path. The schema factories ({@link Yaml.fromString},
|
|
156
|
+
* {@link Yaml.schema}, {@link Yaml.YamlFromString}) encode with default
|
|
157
|
+
* stringify options (`lineWidth` `0`), so their output never folds. The
|
|
158
|
+
* document/node path — `YamlDocument#stringify` and the `YamlFormat`
|
|
159
|
+
* helpers built on it — threads the field into its render context but
|
|
160
|
+
* never reads it, so it is inert there.
|
|
161
|
+
*/
|
|
148
162
|
readonly lineWidth: Schema.optionalKey<Schema.Number>;
|
|
149
163
|
readonly defaultScalarStyle: Schema.optionalKey<Schema.Literals<readonly ["plain", "single-quoted", "double-quoted", "block-literal", "block-folded"]>>;
|
|
150
164
|
readonly defaultCollectionStyle: Schema.optionalKey<Schema.Literals<readonly ["block", "flow"]>>;
|
|
@@ -155,11 +169,20 @@ declare const YamlStringifyOptions_base: Schema.Class<YamlStringifyOptions, Sche
|
|
|
155
169
|
}>, {}>;
|
|
156
170
|
/**
|
|
157
171
|
* Options controlling stringify behavior. All fields are omissible; absent
|
|
158
|
-
* fields resolve to `indent` `2`, `lineWidth` `
|
|
172
|
+
* fields resolve to `indent` `2`, `lineWidth` `0`, `defaultScalarStyle`
|
|
159
173
|
* `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
|
|
160
174
|
* `indentSequences` `false`, `finalNewline` `true` and `forceDefaultStyles`
|
|
161
175
|
* `false`.
|
|
162
176
|
*
|
|
177
|
+
* `lineWidth` controls column-based scalar folding. The default `0` (and any
|
|
178
|
+
* value `<= 0`) never wraps, emitting byte-identical output to the historic
|
|
179
|
+
* no-fold behavior; a positive value folds long plain, double-quoted and
|
|
180
|
+
* block-folded (`>`) scalars at approximately that column, inserting only
|
|
181
|
+
* semantically transparent line breaks. Block-literal (`|`) content is never
|
|
182
|
+
* folded — literal blocks preserve their bytes by definition. Folding is a
|
|
183
|
+
* value-path feature only: `YamlDocument#stringify` and the `YamlFormat`
|
|
184
|
+
* helpers accept these options but do not fold (see `lineWidth`).
|
|
185
|
+
*
|
|
163
186
|
* `indentSequences` controls the presentation of block sequences nested under
|
|
164
187
|
* a mapping key: `false` (the default) emits them at the key's column — the
|
|
165
188
|
* kit's byte-compatible legacy form — while `true` indents them one level,
|
|
@@ -271,6 +294,59 @@ declare class Yaml {
|
|
|
271
294
|
* rather than as an unhandled stack-overflow defect.
|
|
272
295
|
*/
|
|
273
296
|
static readonly stringify: (value: unknown, options?: YamlStringifyOptions | undefined) => Effect.Effect<string, YamlStringifyError, never>;
|
|
297
|
+
/**
|
|
298
|
+
* Synchronous single-document parse, returning a `Result` instead of
|
|
299
|
+
* an `Effect`. A pure escape hatch for config-time callers that cannot
|
|
300
|
+
* `await` an Effect (a `vitest.config.ts` is the motivating case): it runs
|
|
301
|
+
* the same engine as {@link Yaml.parse} inline.
|
|
302
|
+
*
|
|
303
|
+
* Preserves the package contract — malformed and adversarial input fails
|
|
304
|
+
* typed, never as a defect. Fatal diagnostics, duplicate keys and a
|
|
305
|
+
* "billion laughs" alias-expansion blow-up all yield a `Failure` carrying a
|
|
306
|
+
* {@link YamlParseError}; the method never throws.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* import { Yaml } from "@effected/yaml";
|
|
311
|
+
* import { Result } from "effect";
|
|
312
|
+
*
|
|
313
|
+
* const result = Yaml.parseSync("name: Alice\nage: 30");
|
|
314
|
+
* if (Result.isSuccess(result)) {
|
|
315
|
+
* result.success; // { name: "Alice", age: 30 }
|
|
316
|
+
* } else {
|
|
317
|
+
* result.failure; // YamlParseError
|
|
318
|
+
* }
|
|
319
|
+
* ```
|
|
320
|
+
*
|
|
321
|
+
* @public
|
|
322
|
+
*/
|
|
323
|
+
static parseSync(text: string, options?: YamlParseOptions): Result.Result<unknown, YamlParseError>;
|
|
324
|
+
/**
|
|
325
|
+
* Synchronous stringify, returning a `Result` instead of an `Effect`.
|
|
326
|
+
* The pure counterpart to {@link Yaml.stringify} for config-time callers
|
|
327
|
+
* that cannot `await`.
|
|
328
|
+
*
|
|
329
|
+
* Preserves the package contract — a circular reference (`CircularReference`)
|
|
330
|
+
* or a value nested past the recursion budget (`NestingDepthExceeded`)
|
|
331
|
+
* yields a `Failure` carrying a {@link YamlStringifyError} rather than a
|
|
332
|
+
* thrown stack-overflow defect; the method never throws.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```ts
|
|
336
|
+
* import { Yaml } from "@effected/yaml";
|
|
337
|
+
* import { Result } from "effect";
|
|
338
|
+
*
|
|
339
|
+
* const result = Yaml.stringifySync({ name: "Alice" });
|
|
340
|
+
* if (Result.isFailure(result)) {
|
|
341
|
+
* result.failure; // YamlStringifyError
|
|
342
|
+
* } else {
|
|
343
|
+
* result.success; // "name: Alice\n"
|
|
344
|
+
* }
|
|
345
|
+
* ```
|
|
346
|
+
*
|
|
347
|
+
* @public
|
|
348
|
+
*/
|
|
349
|
+
static stringifySync(value: unknown, options?: YamlStringifyOptions): Result.Result<string, YamlStringifyError>;
|
|
274
350
|
/**
|
|
275
351
|
* Strip comments from YAML text. Without `replaceCh`, comment characters
|
|
276
352
|
* are removed (line breaks are kept, so line numbers stay stable); with a
|
|
@@ -655,6 +731,17 @@ declare class YamlDocument extends YamlDocument_base {
|
|
|
655
731
|
* deeper than the stringifier's recursion budget (`NestingDepthExceeded`)
|
|
656
732
|
* — both surface through the typed error channel rather than as an
|
|
657
733
|
* unhandled stack-overflow defect.
|
|
734
|
+
*
|
|
735
|
+
* @remarks
|
|
736
|
+
* `YamlStringifyOptions.lineWidth` is not honored here: column-based
|
|
737
|
+
* scalar folding exists only on the value path, through the entry points
|
|
738
|
+
* that accept stringify options ({@link Yaml.stringify} and
|
|
739
|
+
* {@link Yaml.stringifySync}). The
|
|
740
|
+
* document/node path threads `lineWidth` into its render context but
|
|
741
|
+
* never reads it, so long scalars are emitted unfolded regardless of the
|
|
742
|
+
* option. Callers that need folding should render the plain value
|
|
743
|
+
* instead — `Yaml.stringify(doc.toValue(), options)` — at the cost of
|
|
744
|
+
* the document-level framing and styles this path preserves.
|
|
658
745
|
*/
|
|
659
746
|
stringify(options?: YamlStringifyOptions): Effect.Effect<string, YamlStringifyError>;
|
|
660
747
|
/**
|
package/internal/fold.js
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
//#region src/internal/fold.ts
|
|
2
2
|
/**
|
|
3
|
+
* Column-based line folding for a single logical scalar line (YAML 1.2 flow
|
|
4
|
+
* folding, §7.3 / §8.2.1). Breaks the content at "safe" single-space
|
|
5
|
+
* boundaries — a space whose neighbours are both non-space — so each inserted
|
|
6
|
+
* line break is a *semantically transparent* fold: on read, a lone break
|
|
7
|
+
* between non-empty lines at the same indent folds back to a single space, and
|
|
8
|
+
* the leading indentation of continuation lines is absorbed as separation
|
|
9
|
+
* whitespace. The original space at the break point is consumed, replaced by
|
|
10
|
+
* the break, so no content whitespace is added or lost.
|
|
11
|
+
*
|
|
12
|
+
* Continuation lines are prefixed with `indent`. `indentAtStart` is the column
|
|
13
|
+
* the first line begins at (its content is already `indentAtStart` columns in),
|
|
14
|
+
* used only to budget the first line; it is approximate because the caller's
|
|
15
|
+
* exact column (after a `key: ` prefix, say) is not known here.
|
|
16
|
+
*
|
|
17
|
+
* Only breaks where a break is safe. When no safe break point exists before the
|
|
18
|
+
* width limit, the line overflows unwrapped rather than corrupting the value —
|
|
19
|
+
* width folding is a best-effort presentation concern, never a correctness one.
|
|
20
|
+
* A non-positive `lineWidth` (the default) returns the text unchanged.
|
|
21
|
+
*/
|
|
22
|
+
function foldScalarLine(text, indent, lineWidth, indentAtStart) {
|
|
23
|
+
if (lineWidth <= 0) return text;
|
|
24
|
+
const contentWidth = Math.max(1, lineWidth - indent.length);
|
|
25
|
+
let end = Math.max(1, lineWidth - indentAtStart);
|
|
26
|
+
const folds = [];
|
|
27
|
+
let split;
|
|
28
|
+
let prev;
|
|
29
|
+
for (let i = 0; i < text.length; i++) {
|
|
30
|
+
const ch = text[i];
|
|
31
|
+
if (ch === " " && prev !== void 0 && prev !== " ") {
|
|
32
|
+
const next = text[i + 1];
|
|
33
|
+
if (next !== void 0 && next !== " ") split = i;
|
|
34
|
+
}
|
|
35
|
+
if (i >= end && split !== void 0) {
|
|
36
|
+
folds.push(split);
|
|
37
|
+
end = split + contentWidth;
|
|
38
|
+
split = void 0;
|
|
39
|
+
}
|
|
40
|
+
prev = ch;
|
|
41
|
+
}
|
|
42
|
+
if (folds.length === 0) return text;
|
|
43
|
+
let result = text.slice(0, folds[0]);
|
|
44
|
+
for (let f = 0; f < folds.length; f++) {
|
|
45
|
+
const fold = folds[f];
|
|
46
|
+
const sliceEnd = folds[f + 1] ?? text.length;
|
|
47
|
+
result += `\n${indent}${text.slice(fold + 1, sliceEnd)}`;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply {@link foldScalarLine} to an already-rendered scalar according to its
|
|
53
|
+
* style, inferred from the leading character:
|
|
54
|
+
*
|
|
55
|
+
* - `|` block-literal — returned unchanged; literal blocks preserve bytes by
|
|
56
|
+
* definition and must never be folded.
|
|
57
|
+
* - `>` block-folded — each base-indent body content line is folded; blank
|
|
58
|
+
* lines and more-indented lines (which the reader treats as literal breaks)
|
|
59
|
+
* are left untouched.
|
|
60
|
+
* - `"` double-quoted — the inner content is folded; breaking only at content
|
|
61
|
+
* spaces means no `\`-escaped continuations are needed.
|
|
62
|
+
* - `'` single-quoted — returned unchanged (out of scope for width folding).
|
|
63
|
+
* - otherwise plain — folded directly.
|
|
64
|
+
*
|
|
65
|
+
* `indent` is one indentation level (the continuation prefix); `lineWidth` is
|
|
66
|
+
* the target column. A non-positive `lineWidth` returns the text unchanged.
|
|
67
|
+
*/
|
|
68
|
+
function foldRenderedScalar(rendered, indent, lineWidth) {
|
|
69
|
+
if (lineWidth <= 0 || rendered.length === 0) return rendered;
|
|
70
|
+
const first = rendered[0];
|
|
71
|
+
if (first === "|" || first === "'") return rendered;
|
|
72
|
+
if (first === ">") {
|
|
73
|
+
const lines = rendered.split("\n");
|
|
74
|
+
const out = [lines[0]];
|
|
75
|
+
for (let i = 1; i < lines.length; i++) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
if (line.length > indent.length && line.startsWith(indent) && line[indent.length] !== " " && line[indent.length] !== " ") {
|
|
78
|
+
const content = line.slice(indent.length);
|
|
79
|
+
out.push(indent + foldScalarLine(content, indent, lineWidth, indent.length));
|
|
80
|
+
} else out.push(line);
|
|
81
|
+
}
|
|
82
|
+
return out.join("\n");
|
|
83
|
+
}
|
|
84
|
+
if (first === "\"") return `"${foldScalarLine(rendered.slice(1, -1), indent, lineWidth, indent.length + 1)}"`;
|
|
85
|
+
return foldScalarLine(rendered, indent, lineWidth, indent.length);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
3
88
|
* C0 control characters (except TAB) that must be escaped in double-quoted scalars.
|
|
4
89
|
*/
|
|
5
90
|
function isControlChar(code) {
|
|
@@ -150,4 +235,4 @@ function renderBlockFolded(s, indent) {
|
|
|
150
235
|
}
|
|
151
236
|
|
|
152
237
|
//#endregion
|
|
153
|
-
export { hasInteriorTrailingWhitespace, hasNewlineSpacesTab, isControlChar, renderBlockFolded, renderBlockLiteral, renderSingleQuotedMultiline };
|
|
238
|
+
export { foldRenderedScalar, foldScalarLine, hasInteriorTrailingWhitespace, hasNewlineSpacesTab, isControlChar, renderBlockFolded, renderBlockLiteral, renderSingleQuotedMultiline };
|
package/internal/stringifier.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YamlAlias, YamlMap, YamlPair, YamlScalar, YamlSeq } from "../YamlNode.js";
|
|
2
|
-
import { hasInteriorTrailingWhitespace, hasNewlineSpacesTab, isControlChar, renderBlockFolded, renderBlockLiteral, renderSingleQuotedMultiline } from "./fold.js";
|
|
2
|
+
import { foldRenderedScalar, hasInteriorTrailingWhitespace, hasNewlineSpacesTab, isControlChar, renderBlockFolded, renderBlockLiteral, renderSingleQuotedMultiline } from "./fold.js";
|
|
3
3
|
|
|
4
4
|
//#region src/internal/stringifier.ts
|
|
5
5
|
/**
|
|
@@ -252,7 +252,7 @@ function detectCircular(value, seen) {
|
|
|
252
252
|
function createContext(options) {
|
|
253
253
|
return {
|
|
254
254
|
indent: options?.indent ?? 2,
|
|
255
|
-
lineWidth: options?.lineWidth ??
|
|
255
|
+
lineWidth: options?.lineWidth ?? 0,
|
|
256
256
|
defaultScalarStyle: options?.defaultScalarStyle ?? "plain",
|
|
257
257
|
defaultCollectionStyle: options?.defaultCollectionStyle ?? "block",
|
|
258
258
|
sortKeys: options?.sortKeys ?? false,
|
|
@@ -268,13 +268,17 @@ function createContext(options) {
|
|
|
268
268
|
* responsible for prepending the appropriate indentation prefix to each line.
|
|
269
269
|
* This avoids double-indentation when embedding nested collections.
|
|
270
270
|
*/
|
|
271
|
-
function stringifyLines(value, ctx, depth) {
|
|
271
|
+
function stringifyLines(value, ctx, depth, allowFold = true) {
|
|
272
272
|
if (depth > 256) throw new StringifyDepthExceeded();
|
|
273
273
|
if (value === null || value === void 0) return ["null"];
|
|
274
274
|
if (typeof value === "boolean") return [value ? "true" : "false"];
|
|
275
275
|
if (typeof value === "number") return [renderNumber(value)];
|
|
276
276
|
if (typeof value === "bigint") return [value.toString()];
|
|
277
|
-
if (typeof value === "string")
|
|
277
|
+
if (typeof value === "string") {
|
|
278
|
+
const indentStr = " ".repeat(ctx.indent);
|
|
279
|
+
const rendered = renderString(value, ctx.defaultScalarStyle, indentStr, false, ctx.forceDefaultStyles);
|
|
280
|
+
return (allowFold && ctx.lineWidth > 0 ? foldRenderedScalar(rendered, indentStr, ctx.lineWidth) : rendered).split("\n");
|
|
281
|
+
}
|
|
278
282
|
if (Array.isArray(value)) {
|
|
279
283
|
detectCircular(value, ctx.seen);
|
|
280
284
|
ctx.seen.add(value);
|
|
@@ -300,7 +304,7 @@ function stringifyLines(value, ctx, depth) {
|
|
|
300
304
|
*/
|
|
301
305
|
function stringifyArrayLines(arr, ctx, depth) {
|
|
302
306
|
if (arr.length === 0) return ["[]"];
|
|
303
|
-
if (ctx.defaultCollectionStyle === "flow") return [`[${arr.map((item) => stringifyLines(item, ctx, depth + 1).join(" ")).join(", ")}]`];
|
|
307
|
+
if (ctx.defaultCollectionStyle === "flow") return [`[${arr.map((item) => stringifyLines(item, ctx, depth + 1, false).join(" ")).join(", ")}]`];
|
|
304
308
|
const pad = " ".repeat(ctx.indent);
|
|
305
309
|
const lines = [];
|
|
306
310
|
for (const item of arr) {
|
|
@@ -308,7 +312,7 @@ function stringifyArrayLines(arr, ctx, depth) {
|
|
|
308
312
|
if (itemLines.length === 1) lines.push(`- ${itemLines[0]}`);
|
|
309
313
|
else {
|
|
310
314
|
const first = itemLines[0];
|
|
311
|
-
if (first.startsWith("|") || first.startsWith(">")) {
|
|
315
|
+
if (first.startsWith("|") || first.startsWith(">") || typeof item === "string") {
|
|
312
316
|
lines.push(`- ${first}`);
|
|
313
317
|
for (let i = 1; i < itemLines.length; i++) lines.push(itemLines[i]);
|
|
314
318
|
} else {
|
|
@@ -338,7 +342,7 @@ function stringifyObjectLines(obj, ctx, depth) {
|
|
|
338
342
|
if (keys.length === 0) return ["{}"];
|
|
339
343
|
if (ctx.sortKeys) keys.sort();
|
|
340
344
|
if (ctx.defaultCollectionStyle === "flow") return [`{${keys.map((k) => {
|
|
341
|
-
return `${renderString(k, "plain", "")}: ${stringifyLines(obj[k], ctx, depth + 1).join(" ")}`;
|
|
345
|
+
return `${renderString(k, "plain", "")}: ${stringifyLines(obj[k], ctx, depth + 1, false).join(" ")}`;
|
|
342
346
|
}).join(", ")}}`];
|
|
343
347
|
const renderKey = (k) => k.includes("\n") ? renderDoubleQuoted(k, ctx.forceDefaultStyles) : renderString(k, "plain", "");
|
|
344
348
|
const pad = " ".repeat(ctx.indent);
|
|
@@ -350,7 +354,7 @@ function stringifyObjectLines(obj, ctx, depth) {
|
|
|
350
354
|
if (valLines.length === 1 && !isBlockCollection(val, ctx)) lines.push(`${keyStr}: ${valLines[0]}`);
|
|
351
355
|
else {
|
|
352
356
|
const first = valLines[0];
|
|
353
|
-
if (first.startsWith("|") || first.startsWith(">")) {
|
|
357
|
+
if (first.startsWith("|") || first.startsWith(">") || typeof val === "string") {
|
|
354
358
|
lines.push(`${keyStr}: ${first}`);
|
|
355
359
|
for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
|
|
356
360
|
} else if (Array.isArray(val) && val.length > 0) {
|