@effected/yaml 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @effected/yaml
2
+
3
+ [![npm](https://img.shields.io/npm/v/@effected%2Fyaml?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/yaml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 6.0](https://img.shields.io/badge/TypeScript-6.0-3178c6.svg)](https://www.typescriptlang.org/)
7
+
8
+ Zero-dependency YAML 1.2 parsing, editing and formatting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` and decode straight into a validated domain schema.
9
+
10
+ > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
+ > development against a single pinned Effect v4 beta. Packages graduate to
12
+ > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
+ > exactly the ones the kit is built and tested against, install
14
+ > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
+ >
16
+ > **Stability: unstable.** This package's API surface is not yet considered
17
+ > complete and may change across `0.x` releases. Pin an exact version — even a
18
+ > package marked *stable* before `1.0.0` can introduce a breaking change by
19
+ > accident, and an exact pin turns that into a type-check error rather than a
20
+ > runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
21
+
22
+ ## Why @effected/yaml
23
+
24
+ YAML is where untrusted text meets production systems: CI pipeline definitions, Kubernetes manifests and config files that arrive from a pull request, an API payload or a user's home directory. The format is also large enough that a parser has real attack surface. An anchor that references an anchor that references an anchor — the "billion laughs" bomb — is a few hundred bytes of YAML that expands into gigabytes of nodes, and a deeply nested flow collection is a few kilobytes that overflows a recursive-descent parser's stack.
25
+
26
+ This package treats that as a first-class requirement rather than a footnote. An alias-expansion budget bounds the number of materialized nodes, a depth cap bounds collection nesting on both the parse and the stringify side, and both fire into the typed error channel. Hostile input produces a `YamlParseError` carrying structured `YamlDiagnostic` values with codes and positions; it never produces a `RangeError`, an unhandled defect or an exhausted heap.
27
+
28
+ The rest follows from the same discipline. Parsing recovers from errors and aggregates every diagnostic into one failure rather than throwing on the first. Modifications are computed as edits against the original bytes, so comments and layout survive a change. Everything is a pure function or a schema: no IO, no services and no runtime dependency other than `effect` — the lexer, CST parser, composer and stringifier are vendored into the package with attribution rather than taken as a dependency. It is the largest package in the repo, and it earns that by owning its engine.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ npm install @effected/yaml effect
34
+ ```
35
+
36
+ ```bash
37
+ pnpm add @effected/yaml effect
38
+ ```
39
+
40
+ Requires Node.js >=24.11.0. `effect` v4 is a peer dependency; the package itself adds no other runtime dependencies.
41
+
42
+ ## Quick start
43
+
44
+ Compose your schema with `Yaml.schema` to decode YAML straight into a validated domain value:
45
+
46
+ ```ts
47
+ import { Yaml } from "@effected/yaml";
48
+ import { Effect, Schema } from "effect";
49
+
50
+ const Config = Schema.Struct({ port: Schema.Number });
51
+ const ConfigFromYaml = Yaml.schema(Config);
52
+
53
+ const program = Effect.gen(function* () {
54
+ return yield* Schema.decodeUnknownEffect(ConfigFromYaml)("port: 3000 # dev server");
55
+ });
56
+
57
+ Effect.runPromise(program).then(console.log);
58
+ // { port: 3000 }
59
+ ```
60
+
61
+ `Yaml.stringify` goes the other way, failing typed on circular references rather than throwing:
62
+
63
+ ```ts
64
+ import { Yaml } from "@effected/yaml";
65
+ import { Effect } from "effect";
66
+
67
+ Effect.runPromise(Yaml.stringify({ port: 3000, hosts: ["a", "b"] })).then(console.log);
68
+ // port: 3000
69
+ // hosts:
70
+ // - a
71
+ // - b
72
+ ```
73
+
74
+ ## Hostile input fails typed
75
+
76
+ An alias bomb — nested anchors whose expansion multiplies at every level — is bounded by an expansion budget and surfaces as a `YamlParseError`, not as an out-of-memory kill:
77
+
78
+ ```ts
79
+ import { Yaml } from "@effected/yaml";
80
+ import { Effect } from "effect";
81
+
82
+ const bomb = [
83
+ "a: &a [x,x,x,x,x,x,x,x,x]",
84
+ "b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]",
85
+ // ...further levels, each multiplying the one before
86
+ "z: [*g,*g,*g,*g,*g,*g,*g,*g,*g]",
87
+ ].join("\n");
88
+
89
+ Effect.runPromise(Effect.result(Yaml.parse(bomb))).then(console.log);
90
+ // Failure with YamlParseError, whose `diagnostics` carry:
91
+ // { code: "AliasCountExceeded", message: "Alias expansion exceeded budget of ... nodes" }
92
+ ```
93
+
94
+ Collection nesting past the depth cap behaves the same way, yielding a `NestingDepthExceeded` diagnostic instead of a stack overflow, and `Yaml.stringify` caps the mirror-image recursion when encoding a value back to text. The guarantee is the same everywhere: every fallible entry point carries a real error channel, and nothing reaches your process as a defect.
95
+
96
+ ## Features
97
+
98
+ - `Yaml.parse` / `Yaml.parseAll` — error-recovery parsing of a single document or a `---`-separated stream into plain values, resolving anchors and aliases and aggregating every diagnostic into one `YamlParseError`.
99
+ - `Yaml.stringify` — serialize a plain value back to YAML, failing typed with `YamlStringifyError` on circular references or on excessively deep nesting.
100
+ - `Yaml.stripComments` — quote-aware comment removal that keeps line numbers stable, or every byte offset stable when given a replacement character.
101
+ - `Yaml.equals` / `Yaml.equalsValue` — semantic equality that ignores comments, whitespace, formatting and mapping key order while keeping sequence order significant.
102
+ - `Yaml.schema` / `Yaml.fromString` / `Yaml.YamlFromString` / `Yaml.allFromString` — string→domain schema factories that decode a single document or a whole stream into a validated Effect `Schema` value.
103
+ - `YamlNode` — an offset-preserving AST (`YamlScalar`, `YamlMap`, `YamlSeq`, `YamlPair`, `YamlAlias`) for locating and reading nodes by position or path.
104
+ - `YamlDocument` — the full parsed AST plus the recovered `errors` and `warnings` arrays, so a partially valid document is still inspectable.
105
+ - `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch.
106
+ - `YamlVisitor` — walk a parsed document as a `Stream` of a tagged-enum event union, with `Stream.take` early termination on large inputs.
107
+ - `YamlParseError` / `YamlStringifyError` / `YamlModificationError` — tagged errors carrying structured, positional `YamlDiagnostic` arrays rather than opaque messages.
108
+
109
+ ## License
110
+
111
+ [MIT](LICENSE)
package/Yaml.js ADDED
@@ -0,0 +1,414 @@
1
+ import { AliasExpansionBudgetExceeded, CollectionStyle, ScalarStyle, nodeToJsValue } from "./YamlNode.js";
2
+ import { buildAnchorMap } from "./internal/composer/anchors.js";
3
+ import { composeAllDocuments, composeFirstDocument } from "./internal/composer/document.js";
4
+ import { isFatalCode } from "./internal/diagnostics.js";
5
+ import { StringifyDepthExceeded, StringifyFailure, stringifyValue } from "./internal/stringifier.js";
6
+ import { YamlDiagnostic } from "./YamlDiagnostic.js";
7
+ import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
8
+
9
+ //#region src/Yaml.ts
10
+ /**
11
+ * Options controlling parse behavior. All fields are omissible; absent fields
12
+ * resolve to `strict` `true`, `maxAliasCount` `100` (the alias-based
13
+ * denial-of-service guard) and `uniqueKeys` `true` (duplicate mapping keys
14
+ * are errors).
15
+ *
16
+ * @public
17
+ */
18
+ var YamlParseOptions = class extends Schema.Class("YamlParseOptions")({
19
+ strict: Schema.optionalKey(Schema.Boolean),
20
+ maxAliasCount: Schema.optionalKey(Schema.Number),
21
+ uniqueKeys: Schema.optionalKey(Schema.Boolean)
22
+ }) {};
23
+ /**
24
+ * Options controlling stringify behavior. All fields are omissible; absent
25
+ * fields resolve to `indent` `2`, `lineWidth` `80`, `defaultScalarStyle`
26
+ * `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
27
+ * `finalNewline` `true` and `forceDefaultStyles` `false`.
28
+ *
29
+ * @public
30
+ */
31
+ var YamlStringifyOptions = class extends Schema.Class("YamlStringifyOptions")({
32
+ indent: Schema.optionalKey(Schema.Number),
33
+ lineWidth: Schema.optionalKey(Schema.Number),
34
+ defaultScalarStyle: Schema.optionalKey(ScalarStyle),
35
+ defaultCollectionStyle: Schema.optionalKey(CollectionStyle),
36
+ sortKeys: Schema.optionalKey(Schema.Boolean),
37
+ finalNewline: Schema.optionalKey(Schema.Boolean),
38
+ forceDefaultStyles: Schema.optionalKey(Schema.Boolean)
39
+ }) {};
40
+ /**
41
+ * Error-recovery parse failure: aggregates every fatal {@link YamlDiagnostic}
42
+ * encountered, so a single failure reports the whole batch. Raised by
43
+ * {@link Yaml.parse}, {@link Yaml.parseAll}, `YamlDocument.parse`/`parseAll`
44
+ * and the decode direction of the schema factories.
45
+ *
46
+ * @public
47
+ */
48
+ var YamlParseError = class extends Schema.TaggedErrorClass()("YamlParseError", {
49
+ diagnostics: Schema.Array(YamlDiagnostic),
50
+ input: Schema.String
51
+ }) {
52
+ get message() {
53
+ const count = this.diagnostics.length;
54
+ const summary = this.diagnostics.map((d) => `${d.code} at ${d.line}:${d.character}`).join("; ");
55
+ return `YAML parse failed with ${count} error${count === 1 ? "" : "s"}: ${summary}`;
56
+ }
57
+ };
58
+ /**
59
+ * Stringification failure (the circular-reference guard), carrying structured
60
+ * {@link YamlDiagnostic} entries and the offending value. Raised by
61
+ * {@link Yaml.stringify}, `YamlDocument#stringify` and the encode direction of
62
+ * the schema factories.
63
+ *
64
+ * @public
65
+ */
66
+ var YamlStringifyError = class extends Schema.TaggedErrorClass()("YamlStringifyError", {
67
+ diagnostics: Schema.Array(YamlDiagnostic),
68
+ value: Schema.Unknown
69
+ }) {
70
+ get message() {
71
+ return `YAML stringify failed: ${this.diagnostics.map((d) => d.message).join("; ")}`;
72
+ }
73
+ };
74
+ const toParseInput = (options) => options === void 0 ? {} : {
75
+ strict: options.strict,
76
+ maxAliasCount: options.maxAliasCount,
77
+ uniqueKeys: options.uniqueKeys
78
+ };
79
+ const toStringifyInput = (options) => options === void 0 ? {} : {
80
+ indent: options.indent,
81
+ lineWidth: options.lineWidth,
82
+ defaultScalarStyle: options.defaultScalarStyle,
83
+ defaultCollectionStyle: options.defaultCollectionStyle,
84
+ sortKeys: options.sortKeys,
85
+ finalNewline: options.finalNewline,
86
+ forceDefaultStyles: options.forceDefaultStyles
87
+ };
88
+ const toDiagnostics = (text, records) => records.map((r) => YamlDiagnostic.fromRaw(r, text));
89
+ /**
90
+ * Collect the raw diagnostics that make a composed document a parse failure:
91
+ * every fatal-code error, plus DuplicateKey warnings promoted to errors when
92
+ * `uniqueKeys` is in force (the v3 `parse` contract). Order preserved:
93
+ * fatals first, then promotions.
94
+ */
95
+ const failureRecords = (doc, uniqueKeys) => {
96
+ const fatal = doc.errors.filter((e) => isFatalCode(e.code));
97
+ if (fatal.length > 0) return fatal;
98
+ return uniqueKeys ? doc.warnings.filter((w) => w.code === "DuplicateKey") : [];
99
+ };
100
+ /**
101
+ * Extract a document's plain-JS value with an alias-expansion budget derived
102
+ * from `maxAliasCount`, materializing a "billion laughs" blow-up as a typed
103
+ * fatal {@link YamlParseError} (`AliasCountExceeded`) instead of letting the
104
+ * heap-exhausting expansion escape as an unhandled defect.
105
+ */
106
+ const extractDocumentValue = (contents, anchors, maxAliasCount, text) => Effect.try({
107
+ try: () => nodeToJsValue(contents, anchors, maxAliasCount),
108
+ 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
+ });
120
+ throw defect;
121
+ }
122
+ });
123
+ const stringifyOrFail = (value, options) => Effect.try({
124
+ try: () => stringifyValue(value, toStringifyInput(options)),
125
+ 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
+ });
148
+ throw defect;
149
+ }
150
+ });
151
+ /**
152
+ * Static entry points for YAML parsing, stringification, comment stripping,
153
+ * semantic equality and the schema factories. Not instantiable.
154
+ *
155
+ * @remarks
156
+ * `parse`/`parseAll`/`stringify` and the schema factories carry real typed
157
+ * error channels — including the hardening guards (an alias-expansion budget
158
+ * on decode, a nesting-depth cap on encode) that keep malformed or
159
+ * adversarial input on the typed channel instead of surfacing as an unhandled
160
+ * defect. `stripComments`/`equals`/`equalsValue` are pure total functions.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { Yaml } from "@effected/yaml";
165
+ * import { Effect } from "effect";
166
+ *
167
+ * const program = Effect.gen(function* () {
168
+ * const value = yield* Yaml.parse("name: Alice\nage: 30");
169
+ * return value; // { name: "Alice", age: 30 }
170
+ * });
171
+ * ```
172
+ *
173
+ * @public
174
+ */
175
+ var Yaml = class Yaml {
176
+ constructor() {}
177
+ /**
178
+ * Parse a single YAML document into a plain JavaScript value, resolving
179
+ * anchors and aliases. Error-recovery parsing: collects every fatal
180
+ * diagnostic and fails once with the aggregate {@link YamlParseError}.
181
+ * Returns `unknown`, never `any`.
182
+ *
183
+ * A "billion laughs" alias-expansion blow-up (an alias chain whose
184
+ * resolved size grows exponentially relative to `maxAliasCount`) also
185
+ * fails through {@link YamlParseError} with an `AliasCountExceeded`
186
+ * diagnostic, never as an unhandled defect.
187
+ */
188
+ static parse = Effect.fn("Yaml.parse")(function* (text, options) {
189
+ const doc = composeFirstDocument(text, toParseInput(options));
190
+ const failures = failureRecords(doc, options?.uniqueKeys ?? true);
191
+ if (failures.length > 0) return yield* new YamlParseError({
192
+ diagnostics: toDiagnostics(text, failures),
193
+ input: text
194
+ });
195
+ const anchors = /* @__PURE__ */ new Map();
196
+ return yield* extractDocumentValue(doc.contents, anchors, options?.maxAliasCount ?? 100, text);
197
+ });
198
+ /**
199
+ * Parse a multi-document YAML stream into an array of plain JavaScript
200
+ * values (one per document, in order). Any fatal diagnostic in any
201
+ * document — or a stream-level directive-placement error — fails the
202
+ * whole Effect with the aggregate {@link YamlParseError}.
203
+ *
204
+ * A "billion laughs" alias-expansion blow-up in any document also fails
205
+ * through {@link YamlParseError} with an `AliasCountExceeded` diagnostic,
206
+ * never as an unhandled defect.
207
+ */
208
+ static parseAll = Effect.fn("Yaml.parseAll")(function* (text, options) {
209
+ const { documents, streamErrors } = composeAllDocuments(text, toParseInput(options));
210
+ const uniqueKeys = options?.uniqueKeys ?? true;
211
+ const failures = [...streamErrors.filter((e) => e.code === "InvalidDirective"), ...documents.flatMap((d) => failureRecords(d, uniqueKeys))];
212
+ if (failures.length > 0) return yield* new YamlParseError({
213
+ diagnostics: toDiagnostics(text, failures),
214
+ input: text
215
+ });
216
+ const maxAliasCount = options?.maxAliasCount ?? 100;
217
+ const values = [];
218
+ for (const d of documents) {
219
+ const anchors = buildAnchorMap(d.contents);
220
+ values.push(yield* extractDocumentValue(d.contents, anchors, maxAliasCount, text));
221
+ }
222
+ return values;
223
+ });
224
+ /**
225
+ * Stringify a plain JavaScript value as YAML. Fails with
226
+ * {@link YamlStringifyError} on circular references (`CircularReference`)
227
+ * or on a value nested deeper than the stringifier's recursion budget
228
+ * (`NestingDepthExceeded`) — both surface through the typed error channel
229
+ * rather than as an unhandled stack-overflow defect.
230
+ */
231
+ static stringify = Effect.fn("Yaml.stringify")(function* (value, options) {
232
+ return yield* stringifyOrFail(value, options);
233
+ });
234
+ /**
235
+ * Strip comments from YAML text. Without `replaceCh`, comment characters
236
+ * are removed (line breaks are kept, so line numbers stay stable); with a
237
+ * `replaceCh` (e.g. `" "`), each comment character is replaced instead,
238
+ * keeping all offsets stable. Quote-aware: `#` inside quoted scalars is
239
+ * content, not a comment. Pure and total.
240
+ */
241
+ static stripComments(text, replaceCh) {
242
+ let result = "";
243
+ let i = 0;
244
+ let inComment = false;
245
+ let inSingleQuote = false;
246
+ let inDoubleQuote = false;
247
+ while (i < text.length) {
248
+ const ch = text[i];
249
+ if (inComment) {
250
+ if (ch === "\n") {
251
+ inComment = false;
252
+ result += ch;
253
+ } else if (replaceCh !== void 0) result += replaceCh;
254
+ } else if (inDoubleQuote) {
255
+ result += ch;
256
+ if (ch === "\\" && i + 1 < text.length) {
257
+ i++;
258
+ result += text[i];
259
+ } else if (ch === "\"") inDoubleQuote = false;
260
+ } else if (inSingleQuote) {
261
+ result += ch;
262
+ if (ch === "'" && i + 1 < text.length && text[i + 1] === "'") {
263
+ i++;
264
+ result += text[i];
265
+ } else if (ch === "'") inSingleQuote = false;
266
+ } else if (ch === "\"") {
267
+ inDoubleQuote = true;
268
+ result += ch;
269
+ } else if (ch === "'") {
270
+ inSingleQuote = true;
271
+ result += ch;
272
+ } else if (ch === "#") {
273
+ const prev = i > 0 ? text[i - 1] : "\n";
274
+ if (prev === " " || prev === " " || prev === "\n" || i === 0) {
275
+ inComment = true;
276
+ if (replaceCh !== void 0) result += replaceCh;
277
+ } else result += ch;
278
+ } else result += ch;
279
+ i++;
280
+ }
281
+ return result;
282
+ }
283
+ /**
284
+ * Compare two YAML strings for semantic equality: comments, whitespace,
285
+ * formatting and mapping key order are ignored; sequence order is
286
+ * significant. Malformed input is never equal to anything — parse errors
287
+ * (or duplicate keys) on either side yield `false` rather than comparing
288
+ * recovery-parser artifacts. Pure and total.
289
+ */
290
+ static equals(a, b) {
291
+ const va = parseForEquality(a);
292
+ const vb = parseForEquality(b);
293
+ if (va.malformed || vb.malformed) return false;
294
+ return deepEqualValues(va.value, vb.value);
295
+ }
296
+ /**
297
+ * Compare a YAML string against an existing JavaScript value with the
298
+ * same semantics as {@link Yaml.equals}: malformed `text` yields `false`.
299
+ * Pure and total.
300
+ */
301
+ static equalsValue(text, value) {
302
+ const v = parseForEquality(text);
303
+ if (v.malformed) return false;
304
+ return deepEqualValues(v.value, value);
305
+ }
306
+ /**
307
+ * A `Schema<unknown, string>` decoding a single YAML document with the
308
+ * given `options` (defaults when omitted) and encoding values back to
309
+ * YAML text with default stringify options.
310
+ *
311
+ * Schema-producing: each call returns a fresh schema whose derivation
312
+ * caches are not shared across calls. Bind the result to a `const` on hot
313
+ * paths; for the default-options case use {@link Yaml.YamlFromString}.
314
+ */
315
+ static fromString(options) {
316
+ return Schema.String.pipe(Schema.decodeTo(Schema.Unknown, SchemaTransformation.transformOrFail({
317
+ decode: (input) => Yaml.parse(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
318
+ encode: (value) => stringifyOrFail(value).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(value), { message: error.message })))
319
+ })));
320
+ }
321
+ /**
322
+ * The zero-config `Schema<unknown, string>` — `Yaml.fromString()` with
323
+ * default options, pre-bound so the common case needs no memoization
324
+ * discipline.
325
+ */
326
+ static YamlFromString = Yaml.fromString();
327
+ /**
328
+ * A `Schema<ReadonlyArray<unknown>, string>` decoding a multi-document
329
+ * YAML stream into one value per document, and encoding an array of
330
+ * values back into a `---`-separated stream.
331
+ *
332
+ * Schema-producing: bind the result to a `const` on hot paths (see
333
+ * {@link Yaml.fromString}).
334
+ */
335
+ static allFromString(options) {
336
+ return Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.Unknown), SchemaTransformation.transformOrFail({
337
+ decode: (input) => Yaml.parseAll(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
338
+ encode: (values) => Effect.gen(function* () {
339
+ if (values.length === 0) return "";
340
+ const parts = [];
341
+ for (let index = 0; index < values.length; index++) {
342
+ const yaml = yield* stringifyOrFail(values[index]).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(values), { message: error.message })));
343
+ parts.push(index > 0 ? `---\n${yaml}` : yaml);
344
+ }
345
+ return parts.join("");
346
+ })
347
+ })));
348
+ }
349
+ /**
350
+ * Compose {@link Yaml.fromString} with a target schema, yielding a
351
+ * `Schema<A, string>` that decodes YAML straight into a validated domain
352
+ * value — the single best consumer-facing feature of the library. The
353
+ * target's decoding/encoding service requirements flow through.
354
+ *
355
+ * Schema-producing: bind the result to a `const` on hot paths (see
356
+ * {@link Yaml.fromString}).
357
+ */
358
+ static schema(target, options) {
359
+ return Yaml.fromString(options).pipe(Schema.decodeTo(target));
360
+ }
361
+ };
362
+ /**
363
+ * Parse for `equals`/`equalsValue`: any recorded error — fatal or not — or a
364
+ * DuplicateKey warning marks the input malformed (never equal to anything).
365
+ */
366
+ function parseForEquality(text) {
367
+ const doc = composeFirstDocument(text, {});
368
+ if (doc.errors.length > 0 || doc.warnings.some((w) => w.code === "DuplicateKey")) return {
369
+ malformed: true,
370
+ value: void 0
371
+ };
372
+ const anchors = /* @__PURE__ */ new Map();
373
+ try {
374
+ return {
375
+ malformed: false,
376
+ value: nodeToJsValue(doc.contents, anchors, 100)
377
+ };
378
+ } catch (err) {
379
+ if (err instanceof AliasExpansionBudgetExceeded) return {
380
+ malformed: true,
381
+ value: void 0
382
+ };
383
+ throw err;
384
+ }
385
+ }
386
+ /**
387
+ * Deep structural equality over plain values: NaN equals NaN, mapping key
388
+ * order ignored, sequence order significant.
389
+ */
390
+ function deepEqualValues(a, b) {
391
+ if (a === b) return true;
392
+ if (typeof a === "number" && typeof b === "number" && Number.isNaN(a) && Number.isNaN(b)) return true;
393
+ if (a === null || b === null) return false;
394
+ if (typeof a !== typeof b) return false;
395
+ if (Array.isArray(a)) {
396
+ if (!Array.isArray(b) || a.length !== b.length) return false;
397
+ for (let i = 0; i < a.length; i++) if (!deepEqualValues(a[i], b[i])) return false;
398
+ return true;
399
+ }
400
+ if (Array.isArray(b)) return false;
401
+ if (typeof a === "object" && typeof b === "object") {
402
+ const aObj = a;
403
+ const bObj = b;
404
+ const aKeys = Object.keys(aObj);
405
+ const bKeys = Object.keys(bObj);
406
+ if (aKeys.length !== bKeys.length) return false;
407
+ for (const key of aKeys) if (!Object.hasOwn(bObj, key) || !deepEqualValues(aObj[key], bObj[key])) return false;
408
+ return true;
409
+ }
410
+ return false;
411
+ }
412
+
413
+ //#endregion
414
+ export { Yaml, YamlParseError, YamlParseOptions, YamlStringifyError, YamlStringifyOptions };
@@ -0,0 +1,126 @@
1
+ import { YAML_COMPOSE_ERROR_CODES, YAML_LEX_ERROR_CODES, YAML_MODIFY_ERROR_CODES, YAML_PARSE_ERROR_CODES, YAML_STRINGIFY_ERROR_CODES, isFatalCode } from "./internal/diagnostics.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/YamlDiagnostic.ts
5
+ /**
6
+ * Error codes emitted by the lexer stage.
7
+ *
8
+ * @public
9
+ */
10
+ const YamlLexErrorCode = Schema.Literals(YAML_LEX_ERROR_CODES);
11
+ /**
12
+ * Error codes emitted by the CST-parser stage.
13
+ *
14
+ * @public
15
+ */
16
+ const YamlParseErrorCode = Schema.Literals(YAML_PARSE_ERROR_CODES);
17
+ /**
18
+ * Error codes emitted by the composer stage.
19
+ *
20
+ * @public
21
+ */
22
+ const YamlComposerErrorCode = Schema.Literals(YAML_COMPOSE_ERROR_CODES);
23
+ /**
24
+ * Error codes emitted by the stringifier (the circular-reference guard).
25
+ *
26
+ * @public
27
+ */
28
+ const YamlStringifyErrorCode = Schema.Literals(YAML_STRINGIFY_ERROR_CODES);
29
+ /**
30
+ * Error codes emitted by `YamlFormat.modify`'s path navigation against an
31
+ * already-composed AST — not raised by the parser/composer/stringifier.
32
+ *
33
+ * @public
34
+ */
35
+ const YamlModifyErrorCode = Schema.Literals(YAML_MODIFY_ERROR_CODES);
36
+ /**
37
+ * Union of all YAML error codes across all pipeline stages. Stage
38
+ * discrimination lives here (in the code), not in separate error classes.
39
+ *
40
+ * @public
41
+ */
42
+ const YamlErrorCode = Schema.Union([
43
+ YamlLexErrorCode,
44
+ YamlParseErrorCode,
45
+ YamlComposerErrorCode,
46
+ YamlStringifyErrorCode,
47
+ YamlModifyErrorCode
48
+ ]);
49
+ /**
50
+ * One structured diagnostic: its {@link (YamlErrorCode:type)}, a
51
+ * human-readable `message`, and its exact position (`offset`/`length`, plus
52
+ * zero-based `line`/`character`). Used for both errors and warnings-as-data;
53
+ * fatality is a property of the code — see {@link YamlDiagnostic.isFatal}.
54
+ *
55
+ * @remarks
56
+ * The five-field positional core (`code`/`offset`/`length`/`line`/`character`)
57
+ * is structurally identical to `@effected/jsonc`'s parse-error detail shape;
58
+ * `message` is this package's additive extra.
59
+ *
60
+ * @public
61
+ */
62
+ var YamlDiagnostic = class YamlDiagnostic extends Schema.Class("YamlDiagnostic")({
63
+ code: YamlErrorCode,
64
+ message: Schema.String,
65
+ offset: Schema.Number,
66
+ length: Schema.Number,
67
+ line: Schema.Number,
68
+ character: Schema.Number
69
+ }) {
70
+ /**
71
+ * The single fatal-code predicate: whether diagnostics with this code
72
+ * abort a parse (vs. being recoverable warnings-as-data). Declared once,
73
+ * as a property of the code — replacing the v3 source's three
74
+ * subtly-differing inline fatal lists.
75
+ */
76
+ static isFatal(code) {
77
+ return isFatalCode(code);
78
+ }
79
+ /**
80
+ * Materialize a raw engine diagnostic record into a `YamlDiagnostic`,
81
+ * deriving `line`/`character` from `offset` against the source `text`.
82
+ * Advanced — the parse/stringify entry points call this for you.
83
+ */
84
+ static fromRaw(raw, text) {
85
+ const { line, character } = lineChar(text, raw.offset);
86
+ return YamlDiagnostic.make({
87
+ code: raw.code,
88
+ message: raw.message,
89
+ offset: raw.offset,
90
+ length: raw.length,
91
+ line,
92
+ character
93
+ });
94
+ }
95
+ };
96
+ /**
97
+ * Compute the zero-based line/character position of `offset` within `text`.
98
+ * Recognizes `\n`, `\r`, `\r\n`, LS and PS as line breaks, matching the
99
+ * jsonc counterpart so positions are codec-generic.
100
+ */
101
+ function lineChar(text, offset) {
102
+ let line = 0;
103
+ let lineStart = 0;
104
+ const limit = Math.min(offset, text.length);
105
+ for (let i = 0; i < limit; i++) {
106
+ const ch = text.charCodeAt(i);
107
+ if (ch === 10) {
108
+ line++;
109
+ lineStart = i + 1;
110
+ } else if (ch === 13) {
111
+ if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) i++;
112
+ line++;
113
+ lineStart = i + 1;
114
+ } else if (ch === 8232 || ch === 8233) {
115
+ line++;
116
+ lineStart = i + 1;
117
+ }
118
+ }
119
+ return {
120
+ line,
121
+ character: offset - lineStart
122
+ };
123
+ }
124
+
125
+ //#endregion
126
+ export { YamlComposerErrorCode, YamlDiagnostic, YamlErrorCode, YamlLexErrorCode, YamlModifyErrorCode, YamlParseErrorCode, YamlStringifyErrorCode };