@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 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` `80`, `defaultScalarStyle`
37
+ * fields resolve to `indent` `2`, `lineWidth` `0`, `defaultScalarStyle`
26
38
  * `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
27
- * `finalNewline` `true` and `forceDefaultStyles` `false`.
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 new YamlParseError({
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
- if (defect instanceof StringifyFailure) return new YamlStringifyError({
127
- diagnostics: [YamlDiagnostic.make({
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) plus `preserveComments` (default
15
- * `true`) and `range` (restrict edits to a region; see the module-level
16
- * remarks on the `range` parameter vs. this field).
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` `80`, `defaultScalarStyle`
163
+ * fields resolve to `indent` `2`, `lineWidth` `0`, `defaultScalarStyle`
146
164
  * `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
147
- * `finalNewline` `true` and `forceDefaultStyles` `false`.
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) plus `preserveComments` (default
659
- * `true`) and `range` (restrict edits to a region; see the module-level
660
- * remarks on the `range` parameter vs. this field).
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 };
@@ -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 ?? 80,
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") return renderString(value, ctx.defaultScalarStyle, " ".repeat(ctx.indent), false, ctx.forceDefaultStyles).split("\n");
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++) lines.push(valLines[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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/yaml",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Zero-dependency YAML parsing, editing and formatting as Effect schemas.",
6
6
  "keywords": [
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.9"
8
+ "packageVersion": "7.58.10"
9
9
  }
10
10
  ]
11
11
  }