@effected/yaml 0.1.0 → 0.3.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/README.md +2 -0
- package/Yaml.js +194 -36
- package/YamlDocument.js +1 -0
- package/YamlFormat.js +20 -3
- package/index.d.ts +123 -6
- package/internal/fold.js +86 -1
- package/internal/stringifier.js +18 -10
- package/package.json +1 -1
- package/tsdoc-metadata.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ pnpm add @effected/yaml effect
|
|
|
39
39
|
|
|
40
40
|
Requires Node.js >=24.11.0. `effect` v4 is a peer dependency; the package itself adds no other runtime dependencies.
|
|
41
41
|
|
|
42
|
+
All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
|
|
43
|
+
|
|
42
44
|
## Quick start
|
|
43
45
|
|
|
44
46
|
Compose your schema with `Yaml.schema` to decode YAML straight into a validated domain value:
|
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
|
/**
|
|
@@ -13,6 +13,18 @@ import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effec
|
|
|
13
13
|
* denial-of-service guard) and `uniqueKeys` `true` (duplicate mapping keys
|
|
14
14
|
* are errors).
|
|
15
15
|
*
|
|
16
|
+
* Construct with the validated `YamlParseOptions.make({ ... })` static — the
|
|
17
|
+
* kit convention (never `new`). Call sites that take a `YamlParseOptions`
|
|
18
|
+
* also accept a structurally-matching plain literal.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { Yaml, YamlParseOptions } from "@effected/yaml";
|
|
23
|
+
*
|
|
24
|
+
* const options = YamlParseOptions.make({ maxAliasCount: 50 });
|
|
25
|
+
* const parsed = Yaml.parse("a: 1", options);
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
16
28
|
* @public
|
|
17
29
|
*/
|
|
18
30
|
var YamlParseOptions = class extends Schema.Class("YamlParseOptions")({
|
|
@@ -22,18 +34,53 @@ var YamlParseOptions = class extends Schema.Class("YamlParseOptions")({
|
|
|
22
34
|
}) {};
|
|
23
35
|
/**
|
|
24
36
|
* Options controlling stringify behavior. All fields are omissible; absent
|
|
25
|
-
* fields resolve to `indent` `2`, `lineWidth` `
|
|
37
|
+
* fields resolve to `indent` `2`, `lineWidth` `0`, `defaultScalarStyle`
|
|
26
38
|
* `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
|
|
27
|
-
* `finalNewline` `true` and `forceDefaultStyles`
|
|
39
|
+
* `indentSequences` `false`, `finalNewline` `true` and `forceDefaultStyles`
|
|
40
|
+
* `false`.
|
|
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.
|
|
48
|
+
*
|
|
49
|
+
* `indentSequences` controls the presentation of block sequences nested under
|
|
50
|
+
* a mapping key: `false` (the default) emits them at the key's column — the
|
|
51
|
+
* kit's byte-compatible legacy form — while `true` indents them one level,
|
|
52
|
+
* matching the `yaml` npm package's default output. Top-level sequences stay
|
|
53
|
+
* at column zero in both modes.
|
|
54
|
+
*
|
|
55
|
+
* Construct with the validated `YamlStringifyOptions.make({ ... })` static —
|
|
56
|
+
* the kit convention (never `new`). Call sites that take a
|
|
57
|
+
* `YamlStringifyOptions` also accept a structurally-matching plain literal.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { Yaml, YamlStringifyOptions } from "@effected/yaml";
|
|
62
|
+
*
|
|
63
|
+
* const options = YamlStringifyOptions.make({ indentSequences: true });
|
|
64
|
+
* const yaml = Yaml.stringify({ key: ["a", "b"] }, options);
|
|
65
|
+
* // key:
|
|
66
|
+
* // - a
|
|
67
|
+
* // - b
|
|
68
|
+
* ```
|
|
28
69
|
*
|
|
29
70
|
* @public
|
|
30
71
|
*/
|
|
31
72
|
var YamlStringifyOptions = class extends Schema.Class("YamlStringifyOptions")({
|
|
32
73
|
indent: Schema.optionalKey(Schema.Number),
|
|
74
|
+
/**
|
|
75
|
+
* Column at which to fold long scalars. Default `0` (and any value `<= 0`)
|
|
76
|
+
* never wraps; a positive value folds plain, double-quoted and block-folded
|
|
77
|
+
* (`>`) scalars at approximately that column, never block-literal (`|`).
|
|
78
|
+
*/
|
|
33
79
|
lineWidth: Schema.optionalKey(Schema.Number),
|
|
34
80
|
defaultScalarStyle: Schema.optionalKey(ScalarStyle),
|
|
35
81
|
defaultCollectionStyle: Schema.optionalKey(CollectionStyle),
|
|
36
82
|
sortKeys: Schema.optionalKey(Schema.Boolean),
|
|
83
|
+
indentSequences: Schema.optionalKey(Schema.Boolean),
|
|
37
84
|
finalNewline: Schema.optionalKey(Schema.Boolean),
|
|
38
85
|
forceDefaultStyles: Schema.optionalKey(Schema.Boolean)
|
|
39
86
|
}) {};
|
|
@@ -82,11 +129,95 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
|
|
|
82
129
|
defaultScalarStyle: options.defaultScalarStyle,
|
|
83
130
|
defaultCollectionStyle: options.defaultCollectionStyle,
|
|
84
131
|
sortKeys: options.sortKeys,
|
|
132
|
+
indentSequences: options.indentSequences,
|
|
85
133
|
finalNewline: options.finalNewline,
|
|
86
134
|
forceDefaultStyles: options.forceDefaultStyles
|
|
87
135
|
};
|
|
88
136
|
const toDiagnostics = (text, records) => records.map((r) => YamlDiagnostic.fromRaw(r, text));
|
|
89
137
|
/**
|
|
138
|
+
* Build the fatal {@link YamlParseError} for an alias-expansion "billion
|
|
139
|
+
* laughs" blow-up. The offending expansion has no source span, so the
|
|
140
|
+
* diagnostic carries zero offsets.
|
|
141
|
+
*/
|
|
142
|
+
const aliasCountExceededError = (message, text) => new YamlParseError({
|
|
143
|
+
diagnostics: [YamlDiagnostic.make({
|
|
144
|
+
code: "AliasCountExceeded",
|
|
145
|
+
message,
|
|
146
|
+
offset: 0,
|
|
147
|
+
length: 0,
|
|
148
|
+
line: 0,
|
|
149
|
+
character: 0
|
|
150
|
+
})],
|
|
151
|
+
input: text
|
|
152
|
+
});
|
|
153
|
+
/**
|
|
154
|
+
* Map an internal stringifier throw to its typed {@link YamlStringifyError},
|
|
155
|
+
* or return `undefined` for any other defect (which the caller re-throws).
|
|
156
|
+
* Shared by the Effect and synchronous stringify paths so both surface the
|
|
157
|
+
* hardening guards (circular reference, nesting-depth cap) identically.
|
|
158
|
+
*/
|
|
159
|
+
const stringifyDefectToError = (defect, value) => {
|
|
160
|
+
if (defect instanceof StringifyFailure) return new YamlStringifyError({
|
|
161
|
+
diagnostics: [YamlDiagnostic.make({
|
|
162
|
+
code: "CircularReference",
|
|
163
|
+
message: defect.reason,
|
|
164
|
+
offset: 0,
|
|
165
|
+
length: 0,
|
|
166
|
+
line: 0,
|
|
167
|
+
character: 0
|
|
168
|
+
})],
|
|
169
|
+
value
|
|
170
|
+
});
|
|
171
|
+
if (defect instanceof StringifyDepthExceeded) return new YamlStringifyError({
|
|
172
|
+
diagnostics: [YamlDiagnostic.make({
|
|
173
|
+
code: "NestingDepthExceeded",
|
|
174
|
+
message: defect.message,
|
|
175
|
+
offset: 0,
|
|
176
|
+
length: 0,
|
|
177
|
+
line: 0,
|
|
178
|
+
character: 0
|
|
179
|
+
})],
|
|
180
|
+
value
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Synchronous single-document parse returning a {@link Result}. The pure
|
|
185
|
+
* engine bypasses the Effect runtime entirely: the composer, the failure
|
|
186
|
+
* collection and the alias-expansion budget all run inline, and every failure
|
|
187
|
+
* mode (fatal diagnostics, duplicate keys, a "billion laughs" blow-up) yields
|
|
188
|
+
* a `Failure` carrying a typed {@link YamlParseError} — never a throw.
|
|
189
|
+
*/
|
|
190
|
+
const parseSyncImpl = (text, options) => {
|
|
191
|
+
const doc = composeFirstDocument(text, toParseInput(options));
|
|
192
|
+
const failures = failureRecords(doc, options?.uniqueKeys ?? true);
|
|
193
|
+
if (failures.length > 0) return Result.fail(new YamlParseError({
|
|
194
|
+
diagnostics: toDiagnostics(text, failures),
|
|
195
|
+
input: text
|
|
196
|
+
}));
|
|
197
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
198
|
+
try {
|
|
199
|
+
return Result.succeed(nodeToJsValue(doc.contents, anchors, options?.maxAliasCount ?? 100));
|
|
200
|
+
} catch (defect) {
|
|
201
|
+
if (defect instanceof AliasExpansionBudgetExceeded) return Result.fail(aliasCountExceededError(defect.message, text));
|
|
202
|
+
throw defect;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Synchronous stringify returning a {@link Result}. Mirrors {@link Yaml.stringify}
|
|
207
|
+
* without the Effect wrapper: a circular reference or a value nested past the
|
|
208
|
+
* recursion budget yields a `Failure` carrying a typed {@link YamlStringifyError},
|
|
209
|
+
* never a thrown defect.
|
|
210
|
+
*/
|
|
211
|
+
const stringifySyncImpl = (value, options) => {
|
|
212
|
+
try {
|
|
213
|
+
return Result.succeed(stringifyValue(value, toStringifyInput(options)));
|
|
214
|
+
} catch (defect) {
|
|
215
|
+
const error = stringifyDefectToError(defect, value);
|
|
216
|
+
if (error !== void 0) return Result.fail(error);
|
|
217
|
+
throw defect;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
90
221
|
* Collect the raw diagnostics that make a composed document a parse failure:
|
|
91
222
|
* every fatal-code error, plus DuplicateKey warnings promoted to errors when
|
|
92
223
|
* `uniqueKeys` is in force (the v3 `parse` contract). Order preserved:
|
|
@@ -106,45 +237,15 @@ const failureRecords = (doc, uniqueKeys) => {
|
|
|
106
237
|
const extractDocumentValue = (contents, anchors, maxAliasCount, text) => Effect.try({
|
|
107
238
|
try: () => nodeToJsValue(contents, anchors, maxAliasCount),
|
|
108
239
|
catch: (defect) => {
|
|
109
|
-
if (defect instanceof AliasExpansionBudgetExceeded) return
|
|
110
|
-
diagnostics: [YamlDiagnostic.make({
|
|
111
|
-
code: "AliasCountExceeded",
|
|
112
|
-
message: defect.message,
|
|
113
|
-
offset: 0,
|
|
114
|
-
length: 0,
|
|
115
|
-
line: 0,
|
|
116
|
-
character: 0
|
|
117
|
-
})],
|
|
118
|
-
input: text
|
|
119
|
-
});
|
|
240
|
+
if (defect instanceof AliasExpansionBudgetExceeded) return aliasCountExceededError(defect.message, text);
|
|
120
241
|
throw defect;
|
|
121
242
|
}
|
|
122
243
|
});
|
|
123
244
|
const stringifyOrFail = (value, options) => Effect.try({
|
|
124
245
|
try: () => stringifyValue(value, toStringifyInput(options)),
|
|
125
246
|
catch: (defect) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
code: "CircularReference",
|
|
129
|
-
message: defect.reason,
|
|
130
|
-
offset: 0,
|
|
131
|
-
length: 0,
|
|
132
|
-
line: 0,
|
|
133
|
-
character: 0
|
|
134
|
-
})],
|
|
135
|
-
value
|
|
136
|
-
});
|
|
137
|
-
if (defect instanceof StringifyDepthExceeded) return new YamlStringifyError({
|
|
138
|
-
diagnostics: [YamlDiagnostic.make({
|
|
139
|
-
code: "NestingDepthExceeded",
|
|
140
|
-
message: defect.message,
|
|
141
|
-
offset: 0,
|
|
142
|
-
length: 0,
|
|
143
|
-
line: 0,
|
|
144
|
-
character: 0
|
|
145
|
-
})],
|
|
146
|
-
value
|
|
147
|
-
});
|
|
247
|
+
const error = stringifyDefectToError(defect, value);
|
|
248
|
+
if (error !== void 0) return error;
|
|
148
249
|
throw defect;
|
|
149
250
|
}
|
|
150
251
|
});
|
|
@@ -232,6 +333,63 @@ var Yaml = class Yaml {
|
|
|
232
333
|
return yield* stringifyOrFail(value, options);
|
|
233
334
|
});
|
|
234
335
|
/**
|
|
336
|
+
* Synchronous single-document parse, returning a {@link Result} instead of
|
|
337
|
+
* an `Effect`. A pure escape hatch for config-time callers that cannot
|
|
338
|
+
* `await` an Effect (a `vitest.config.ts` is the motivating case): it runs
|
|
339
|
+
* the same engine as {@link Yaml.parse} inline.
|
|
340
|
+
*
|
|
341
|
+
* Preserves the package contract — malformed and adversarial input fails
|
|
342
|
+
* typed, never as a defect. Fatal diagnostics, duplicate keys and a
|
|
343
|
+
* "billion laughs" alias-expansion blow-up all yield a `Failure` carrying a
|
|
344
|
+
* {@link YamlParseError}; the method never throws.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```ts
|
|
348
|
+
* import { Yaml } from "@effected/yaml";
|
|
349
|
+
* import { Result } from "effect";
|
|
350
|
+
*
|
|
351
|
+
* const result = Yaml.parseSync("name: Alice\nage: 30");
|
|
352
|
+
* if (Result.isSuccess(result)) {
|
|
353
|
+
* result.success; // { name: "Alice", age: 30 }
|
|
354
|
+
* } else {
|
|
355
|
+
* result.failure; // YamlParseError
|
|
356
|
+
* }
|
|
357
|
+
* ```
|
|
358
|
+
*
|
|
359
|
+
* @public
|
|
360
|
+
*/
|
|
361
|
+
static parseSync(text, options) {
|
|
362
|
+
return parseSyncImpl(text, options);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Synchronous stringify, returning a {@link Result} instead of an `Effect`.
|
|
366
|
+
* The pure counterpart to {@link Yaml.stringify} for config-time callers
|
|
367
|
+
* that cannot `await`.
|
|
368
|
+
*
|
|
369
|
+
* Preserves the package contract — a circular reference (`CircularReference`)
|
|
370
|
+
* or a value nested past the recursion budget (`NestingDepthExceeded`)
|
|
371
|
+
* yields a `Failure` carrying a {@link YamlStringifyError} rather than a
|
|
372
|
+
* thrown stack-overflow defect; the method never throws.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* import { Yaml } from "@effected/yaml";
|
|
377
|
+
* import { Result } from "effect";
|
|
378
|
+
*
|
|
379
|
+
* const result = Yaml.stringifySync({ name: "Alice" });
|
|
380
|
+
* if (Result.isFailure(result)) {
|
|
381
|
+
* result.failure; // YamlStringifyError
|
|
382
|
+
* } else {
|
|
383
|
+
* result.success; // "name: Alice\n"
|
|
384
|
+
* }
|
|
385
|
+
* ```
|
|
386
|
+
*
|
|
387
|
+
* @public
|
|
388
|
+
*/
|
|
389
|
+
static stringifySync(value, options) {
|
|
390
|
+
return stringifySyncImpl(value, options);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
235
393
|
* Strip comments from YAML text. Without `replaceCh`, comment characters
|
|
236
394
|
* are removed (line breaks are kept, so line numbers stay stable); with a
|
|
237
395
|
* `replaceCh` (e.g. `" "`), each comment character is replaced instead,
|
package/YamlDocument.js
CHANGED
|
@@ -144,6 +144,7 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
|
|
|
144
144
|
defaultScalarStyle: options.defaultScalarStyle,
|
|
145
145
|
defaultCollectionStyle: options.defaultCollectionStyle,
|
|
146
146
|
sortKeys: options.sortKeys,
|
|
147
|
+
indentSequences: options.indentSequences,
|
|
147
148
|
finalNewline: options.finalNewline,
|
|
148
149
|
forceDefaultStyles: options.forceDefaultStyles
|
|
149
150
|
};
|
package/YamlFormat.js
CHANGED
|
@@ -11,9 +11,25 @@ import { Effect, Schema } from "effect";
|
|
|
11
11
|
//#region src/YamlFormat.ts
|
|
12
12
|
/**
|
|
13
13
|
* Options controlling formatting behavior: every {@link YamlStringifyOptions}
|
|
14
|
-
* field (derived, not hand-duplicated
|
|
15
|
-
* `true`) and `range` (restrict edits to a
|
|
16
|
-
* remarks on the `range` parameter vs. this
|
|
14
|
+
* field (derived, not hand-duplicated — including `indentSequences`) plus
|
|
15
|
+
* `preserveComments` (default `true`) and `range` (restrict edits to a
|
|
16
|
+
* region; see the module-level remarks on the `range` parameter vs. this
|
|
17
|
+
* field).
|
|
18
|
+
*
|
|
19
|
+
* Construct with the validated `YamlFormattingOptions.make({ ... })` static —
|
|
20
|
+
* the kit convention (never `new`). Call sites that take a
|
|
21
|
+
* `YamlFormattingOptions` also accept a structurally-matching plain literal.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { YamlFormat, YamlFormattingOptions } from "@effected/yaml";
|
|
26
|
+
*
|
|
27
|
+
* const options = YamlFormattingOptions.make({ indentSequences: true });
|
|
28
|
+
* const formatted = YamlFormat.formatToString("key:\n- a\n- b\n", undefined, options);
|
|
29
|
+
* // key:
|
|
30
|
+
* // - a
|
|
31
|
+
* // - b
|
|
32
|
+
* ```
|
|
17
33
|
*
|
|
18
34
|
* @public
|
|
19
35
|
*/
|
|
@@ -61,6 +77,7 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
|
|
|
61
77
|
defaultScalarStyle: options.defaultScalarStyle,
|
|
62
78
|
defaultCollectionStyle: options.defaultCollectionStyle,
|
|
63
79
|
sortKeys: options.sortKeys,
|
|
80
|
+
indentSequences: options.indentSequences,
|
|
64
81
|
finalNewline: options.finalNewline,
|
|
65
82
|
forceDefaultStyles: options.forceDefaultStyles
|
|
66
83
|
};
|
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.
|
|
@@ -128,23 +128,70 @@ declare const YamlParseOptions_base: Schema.Class<YamlParseOptions, Schema.Struc
|
|
|
128
128
|
* denial-of-service guard) and `uniqueKeys` `true` (duplicate mapping keys
|
|
129
129
|
* are errors).
|
|
130
130
|
*
|
|
131
|
+
* Construct with the validated `YamlParseOptions.make({ ... })` static — the
|
|
132
|
+
* kit convention (never `new`). Call sites that take a `YamlParseOptions`
|
|
133
|
+
* also accept a structurally-matching plain literal.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* import { Yaml, YamlParseOptions } from "@effected/yaml";
|
|
138
|
+
*
|
|
139
|
+
* const options = YamlParseOptions.make({ maxAliasCount: 50 });
|
|
140
|
+
* const parsed = Yaml.parse("a: 1", options);
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
131
143
|
* @public
|
|
132
144
|
*/
|
|
133
145
|
declare class YamlParseOptions extends YamlParseOptions_base {}
|
|
134
146
|
declare const YamlStringifyOptions_base: Schema.Class<YamlStringifyOptions, Schema.Struct<{
|
|
135
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
|
+
*/
|
|
136
153
|
readonly lineWidth: Schema.optionalKey<Schema.Number>;
|
|
137
154
|
readonly defaultScalarStyle: Schema.optionalKey<Schema.Literals<readonly ["plain", "single-quoted", "double-quoted", "block-literal", "block-folded"]>>;
|
|
138
155
|
readonly defaultCollectionStyle: Schema.optionalKey<Schema.Literals<readonly ["block", "flow"]>>;
|
|
139
156
|
readonly sortKeys: Schema.optionalKey<Schema.Boolean>;
|
|
157
|
+
readonly indentSequences: Schema.optionalKey<Schema.Boolean>;
|
|
140
158
|
readonly finalNewline: Schema.optionalKey<Schema.Boolean>;
|
|
141
159
|
readonly forceDefaultStyles: Schema.optionalKey<Schema.Boolean>;
|
|
142
160
|
}>, {}>;
|
|
143
161
|
/**
|
|
144
162
|
* Options controlling stringify behavior. All fields are omissible; absent
|
|
145
|
-
* fields resolve to `indent` `2`, `lineWidth` `
|
|
163
|
+
* fields resolve to `indent` `2`, `lineWidth` `0`, `defaultScalarStyle`
|
|
146
164
|
* `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
|
|
147
|
-
* `finalNewline` `true` and `forceDefaultStyles`
|
|
165
|
+
* `indentSequences` `false`, `finalNewline` `true` and `forceDefaultStyles`
|
|
166
|
+
* `false`.
|
|
167
|
+
*
|
|
168
|
+
* `lineWidth` controls column-based scalar folding. The default `0` (and any
|
|
169
|
+
* value `<= 0`) never wraps, emitting byte-identical output to the historic
|
|
170
|
+
* no-fold behavior; a positive value folds long plain, double-quoted and
|
|
171
|
+
* block-folded (`>`) scalars at approximately that column, inserting only
|
|
172
|
+
* semantically transparent line breaks. Block-literal (`|`) content is never
|
|
173
|
+
* folded — literal blocks preserve their bytes by definition.
|
|
174
|
+
*
|
|
175
|
+
* `indentSequences` controls the presentation of block sequences nested under
|
|
176
|
+
* a mapping key: `false` (the default) emits them at the key's column — the
|
|
177
|
+
* kit's byte-compatible legacy form — while `true` indents them one level,
|
|
178
|
+
* matching the `yaml` npm package's default output. Top-level sequences stay
|
|
179
|
+
* at column zero in both modes.
|
|
180
|
+
*
|
|
181
|
+
* Construct with the validated `YamlStringifyOptions.make({ ... })` static —
|
|
182
|
+
* the kit convention (never `new`). Call sites that take a
|
|
183
|
+
* `YamlStringifyOptions` also accept a structurally-matching plain literal.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* import { Yaml, YamlStringifyOptions } from "@effected/yaml";
|
|
188
|
+
*
|
|
189
|
+
* const options = YamlStringifyOptions.make({ indentSequences: true });
|
|
190
|
+
* const yaml = Yaml.stringify({ key: ["a", "b"] }, options);
|
|
191
|
+
* // key:
|
|
192
|
+
* // - a
|
|
193
|
+
* // - b
|
|
194
|
+
* ```
|
|
148
195
|
*
|
|
149
196
|
* @public
|
|
150
197
|
*/
|
|
@@ -236,6 +283,59 @@ declare class Yaml {
|
|
|
236
283
|
* rather than as an unhandled stack-overflow defect.
|
|
237
284
|
*/
|
|
238
285
|
static readonly stringify: (value: unknown, options?: YamlStringifyOptions | undefined) => Effect.Effect<string, YamlStringifyError, never>;
|
|
286
|
+
/**
|
|
287
|
+
* Synchronous single-document parse, returning a {@link Result} instead of
|
|
288
|
+
* an `Effect`. A pure escape hatch for config-time callers that cannot
|
|
289
|
+
* `await` an Effect (a `vitest.config.ts` is the motivating case): it runs
|
|
290
|
+
* the same engine as {@link Yaml.parse} inline.
|
|
291
|
+
*
|
|
292
|
+
* Preserves the package contract — malformed and adversarial input fails
|
|
293
|
+
* typed, never as a defect. Fatal diagnostics, duplicate keys and a
|
|
294
|
+
* "billion laughs" alias-expansion blow-up all yield a `Failure` carrying a
|
|
295
|
+
* {@link YamlParseError}; the method never throws.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```ts
|
|
299
|
+
* import { Yaml } from "@effected/yaml";
|
|
300
|
+
* import { Result } from "effect";
|
|
301
|
+
*
|
|
302
|
+
* const result = Yaml.parseSync("name: Alice\nage: 30");
|
|
303
|
+
* if (Result.isSuccess(result)) {
|
|
304
|
+
* result.success; // { name: "Alice", age: 30 }
|
|
305
|
+
* } else {
|
|
306
|
+
* result.failure; // YamlParseError
|
|
307
|
+
* }
|
|
308
|
+
* ```
|
|
309
|
+
*
|
|
310
|
+
* @public
|
|
311
|
+
*/
|
|
312
|
+
static parseSync(text: string, options?: YamlParseOptions): Result.Result<unknown, YamlParseError>;
|
|
313
|
+
/**
|
|
314
|
+
* Synchronous stringify, returning a {@link Result} instead of an `Effect`.
|
|
315
|
+
* The pure counterpart to {@link Yaml.stringify} for config-time callers
|
|
316
|
+
* that cannot `await`.
|
|
317
|
+
*
|
|
318
|
+
* Preserves the package contract — a circular reference (`CircularReference`)
|
|
319
|
+
* or a value nested past the recursion budget (`NestingDepthExceeded`)
|
|
320
|
+
* yields a `Failure` carrying a {@link YamlStringifyError} rather than a
|
|
321
|
+
* thrown stack-overflow defect; the method never throws.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```ts
|
|
325
|
+
* import { Yaml } from "@effected/yaml";
|
|
326
|
+
* import { Result } from "effect";
|
|
327
|
+
*
|
|
328
|
+
* const result = Yaml.stringifySync({ name: "Alice" });
|
|
329
|
+
* if (Result.isFailure(result)) {
|
|
330
|
+
* result.failure; // YamlStringifyError
|
|
331
|
+
* } else {
|
|
332
|
+
* result.success; // "name: Alice\n"
|
|
333
|
+
* }
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* @public
|
|
337
|
+
*/
|
|
338
|
+
static stringifySync(value: unknown, options?: YamlStringifyOptions): Result.Result<string, YamlStringifyError>;
|
|
239
339
|
/**
|
|
240
340
|
* Strip comments from YAML text. Without `replaceCh`, comment characters
|
|
241
341
|
* are removed (line breaks are kept, so line numbers stay stable); with a
|
|
@@ -648,6 +748,7 @@ declare const YamlFormattingOptions_base: Schema.Class<YamlFormattingOptions, Sc
|
|
|
648
748
|
readonly defaultScalarStyle: Schema.optionalKey<Schema.Literals<readonly ["plain", "single-quoted", "double-quoted", "block-literal", "block-folded"]>>;
|
|
649
749
|
readonly defaultCollectionStyle: Schema.optionalKey<Schema.Literals<readonly ["block", "flow"]>>;
|
|
650
750
|
readonly sortKeys: Schema.optionalKey<Schema.Boolean>;
|
|
751
|
+
readonly indentSequences: Schema.optionalKey<Schema.Boolean>;
|
|
651
752
|
readonly finalNewline: Schema.optionalKey<Schema.Boolean>;
|
|
652
753
|
readonly forceDefaultStyles: Schema.optionalKey<Schema.Boolean>;
|
|
653
754
|
readonly preserveComments: Schema.optionalKey<Schema.Boolean>;
|
|
@@ -655,9 +756,25 @@ declare const YamlFormattingOptions_base: Schema.Class<YamlFormattingOptions, Sc
|
|
|
655
756
|
}>, {}>;
|
|
656
757
|
/**
|
|
657
758
|
* Options controlling formatting behavior: every {@link YamlStringifyOptions}
|
|
658
|
-
* field (derived, not hand-duplicated
|
|
659
|
-
* `true`) and `range` (restrict edits to a
|
|
660
|
-
* remarks on the `range` parameter vs. this
|
|
759
|
+
* field (derived, not hand-duplicated — including `indentSequences`) plus
|
|
760
|
+
* `preserveComments` (default `true`) and `range` (restrict edits to a
|
|
761
|
+
* region; see the module-level remarks on the `range` parameter vs. this
|
|
762
|
+
* field).
|
|
763
|
+
*
|
|
764
|
+
* Construct with the validated `YamlFormattingOptions.make({ ... })` static —
|
|
765
|
+
* the kit convention (never `new`). Call sites that take a
|
|
766
|
+
* `YamlFormattingOptions` also accept a structurally-matching plain literal.
|
|
767
|
+
*
|
|
768
|
+
* @example
|
|
769
|
+
* ```ts
|
|
770
|
+
* import { YamlFormat, YamlFormattingOptions } from "@effected/yaml";
|
|
771
|
+
*
|
|
772
|
+
* const options = YamlFormattingOptions.make({ indentSequences: true });
|
|
773
|
+
* const formatted = YamlFormat.formatToString("key:\n- a\n- b\n", undefined, options);
|
|
774
|
+
* // key:
|
|
775
|
+
* // - a
|
|
776
|
+
* // - b
|
|
777
|
+
* ```
|
|
661
778
|
*
|
|
662
779
|
* @public
|
|
663
780
|
*/
|
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,10 +252,11 @@ 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,
|
|
259
|
+
indentSequences: options?.indentSequences ?? false,
|
|
259
260
|
forceDefaultStyles: options?.forceDefaultStyles ?? false,
|
|
260
261
|
seen: /* @__PURE__ */ new Set()
|
|
261
262
|
};
|
|
@@ -267,13 +268,17 @@ function createContext(options) {
|
|
|
267
268
|
* responsible for prepending the appropriate indentation prefix to each line.
|
|
268
269
|
* This avoids double-indentation when embedding nested collections.
|
|
269
270
|
*/
|
|
270
|
-
function stringifyLines(value, ctx, depth) {
|
|
271
|
+
function stringifyLines(value, ctx, depth, allowFold = true) {
|
|
271
272
|
if (depth > 256) throw new StringifyDepthExceeded();
|
|
272
273
|
if (value === null || value === void 0) return ["null"];
|
|
273
274
|
if (typeof value === "boolean") return [value ? "true" : "false"];
|
|
274
275
|
if (typeof value === "number") return [renderNumber(value)];
|
|
275
276
|
if (typeof value === "bigint") return [value.toString()];
|
|
276
|
-
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
|
+
}
|
|
277
282
|
if (Array.isArray(value)) {
|
|
278
283
|
detectCircular(value, ctx.seen);
|
|
279
284
|
ctx.seen.add(value);
|
|
@@ -299,7 +304,7 @@ function stringifyLines(value, ctx, depth) {
|
|
|
299
304
|
*/
|
|
300
305
|
function stringifyArrayLines(arr, ctx, depth) {
|
|
301
306
|
if (arr.length === 0) return ["[]"];
|
|
302
|
-
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(", ")}]`];
|
|
303
308
|
const pad = " ".repeat(ctx.indent);
|
|
304
309
|
const lines = [];
|
|
305
310
|
for (const item of arr) {
|
|
@@ -307,7 +312,7 @@ function stringifyArrayLines(arr, ctx, depth) {
|
|
|
307
312
|
if (itemLines.length === 1) lines.push(`- ${itemLines[0]}`);
|
|
308
313
|
else {
|
|
309
314
|
const first = itemLines[0];
|
|
310
|
-
if (first.startsWith("|") || first.startsWith(">")) {
|
|
315
|
+
if (first.startsWith("|") || first.startsWith(">") || typeof item === "string") {
|
|
311
316
|
lines.push(`- ${first}`);
|
|
312
317
|
for (let i = 1; i < itemLines.length; i++) lines.push(itemLines[i]);
|
|
313
318
|
} else {
|
|
@@ -337,7 +342,7 @@ function stringifyObjectLines(obj, ctx, depth) {
|
|
|
337
342
|
if (keys.length === 0) return ["{}"];
|
|
338
343
|
if (ctx.sortKeys) keys.sort();
|
|
339
344
|
if (ctx.defaultCollectionStyle === "flow") return [`{${keys.map((k) => {
|
|
340
|
-
return `${renderString(k, "plain", "")}: ${stringifyLines(obj[k], ctx, depth + 1).join(" ")}`;
|
|
345
|
+
return `${renderString(k, "plain", "")}: ${stringifyLines(obj[k], ctx, depth + 1, false).join(" ")}`;
|
|
341
346
|
}).join(", ")}}`];
|
|
342
347
|
const renderKey = (k) => k.includes("\n") ? renderDoubleQuoted(k, ctx.forceDefaultStyles) : renderString(k, "plain", "");
|
|
343
348
|
const pad = " ".repeat(ctx.indent);
|
|
@@ -349,12 +354,12 @@ function stringifyObjectLines(obj, ctx, depth) {
|
|
|
349
354
|
if (valLines.length === 1 && !isBlockCollection(val, ctx)) lines.push(`${keyStr}: ${valLines[0]}`);
|
|
350
355
|
else {
|
|
351
356
|
const first = valLines[0];
|
|
352
|
-
if (first.startsWith("|") || first.startsWith(">")) {
|
|
357
|
+
if (first.startsWith("|") || first.startsWith(">") || typeof val === "string") {
|
|
353
358
|
lines.push(`${keyStr}: ${first}`);
|
|
354
359
|
for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
|
|
355
360
|
} else if (Array.isArray(val) && val.length > 0) {
|
|
356
361
|
lines.push(`${keyStr}:`);
|
|
357
|
-
for (const vl of valLines) lines.push(vl);
|
|
362
|
+
for (const vl of valLines) lines.push(ctx.indentSequences && vl !== "" ? `${pad}${vl}` : vl);
|
|
358
363
|
} else {
|
|
359
364
|
lines.push(`${keyStr}:`);
|
|
360
365
|
for (const vl of valLines) lines.push(`${pad}${vl}`);
|
|
@@ -619,7 +624,10 @@ function stringifyMapNodeLines(node, ctx, depth) {
|
|
|
619
624
|
const seqMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
|
|
620
625
|
const startIdx = seqMeta ? 1 : 0;
|
|
621
626
|
lines.push(seqMeta ? `${keyStr}${sep} ${seqMeta}` : `${keyStr}${sep}`);
|
|
622
|
-
for (let i = startIdx; i < valLines.length; i++)
|
|
627
|
+
for (let i = startIdx; i < valLines.length; i++) {
|
|
628
|
+
const vl = valLines[i];
|
|
629
|
+
lines.push(ctx.indentSequences && vl !== "" ? `${pad}${vl}` : vl);
|
|
630
|
+
}
|
|
623
631
|
} else if (valNode instanceof YamlMap && valNode.items.length > 0 && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block") {
|
|
624
632
|
const mapMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
|
|
625
633
|
const startIdx = mapMeta ? 1 : 0;
|
package/package.json
CHANGED