@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/YamlNode.js ADDED
@@ -0,0 +1,396 @@
1
+ import { Option, Schema } from "effect";
2
+
3
+ //#region src/YamlNode.ts
4
+ /**
5
+ * YAML scalar presentation styles.
6
+ *
7
+ * @public
8
+ */
9
+ const ScalarStyle = Schema.Literals([
10
+ "plain",
11
+ "single-quoted",
12
+ "double-quoted",
13
+ "block-literal",
14
+ "block-folded"
15
+ ]);
16
+ /**
17
+ * YAML collection presentation styles.
18
+ *
19
+ * @public
20
+ */
21
+ const CollectionStyle = Schema.Literals(["block", "flow"]);
22
+ /**
23
+ * Block-scalar chomping indicators (`-` strip, default clip, `+` keep).
24
+ * Referenced by the {@link YamlScalar} `chomp` field schema.
25
+ *
26
+ * @public
27
+ */
28
+ const ScalarChomp = Schema.Literals([
29
+ "strip",
30
+ "clip",
31
+ "keep"
32
+ ]);
33
+ /**
34
+ * A YAML scalar AST node, representing a leaf value such as a string,
35
+ * number, boolean, or null.
36
+ *
37
+ * - `value` — the resolved JavaScript value (null, boolean, number, bigint or
38
+ * string).
39
+ * - `style` — the scalar presentation style in the source document.
40
+ * - `tag` — optional explicit YAML tag (e.g. `!!str`, `!!int`).
41
+ * - `anchor` — optional anchor name for aliasing.
42
+ * - `comment` — optional trailing or leading comment text.
43
+ * - `chomp` — block-scalar chomping indicator, when the scalar is a block
44
+ * scalar.
45
+ * - `raw` — the raw source text, preserved when it differs from the resolved
46
+ * value in a way stringification needs to know about.
47
+ * - `sourceMultiline` — `true` when the source span covers two or more lines;
48
+ * absent on synthetic nodes.
49
+ * - `offset` / `length` — the node's span in the source.
50
+ *
51
+ * @public
52
+ */
53
+ var YamlScalar = class extends Schema.TaggedClass()("YamlScalar", {
54
+ value: Schema.Unknown,
55
+ tag: Schema.optionalKey(Schema.String),
56
+ style: ScalarStyle,
57
+ anchor: Schema.optionalKey(Schema.String),
58
+ comment: Schema.optionalKey(Schema.String),
59
+ chomp: Schema.optionalKey(ScalarChomp),
60
+ raw: Schema.optionalKey(Schema.String),
61
+ sourceMultiline: Schema.optionalKey(Schema.Boolean),
62
+ offset: Schema.Number,
63
+ length: Schema.Number
64
+ }) {
65
+ /**
66
+ * Navigate to a descendant by path (string segments for mapping keys,
67
+ * numbers for sequence indices). `Option.none()` when any segment cannot
68
+ * be resolved. Pure.
69
+ */
70
+ find(path) {
71
+ return findByPath(this, path);
72
+ }
73
+ /**
74
+ * Find the deepest node whose span contains `offset` (half-open interval),
75
+ * or `Option.none()` when the offset falls outside this subtree. Pure.
76
+ */
77
+ findAtOffset(offset) {
78
+ return findDeepestAtOffset(this, offset);
79
+ }
80
+ /**
81
+ * Return the path from this node to the given descendant node (matched by
82
+ * reference identity), or `Option.none()` when it is not in this subtree.
83
+ * The inverse of {@link YamlScalar.find}. Pure.
84
+ */
85
+ pathOf(node) {
86
+ return pathToNode(this, node);
87
+ }
88
+ /**
89
+ * Reconstruct the plain JavaScript value of this subtree. Aliases resolve
90
+ * through `anchors` (anchors encountered during the walk register
91
+ * incrementally, so an alias sees the most recent definition at its point
92
+ * of use); unresolvable aliases yield `null`. Pure and total.
93
+ */
94
+ toValue(anchors) {
95
+ return nodeToValue(this, anchors, defaultBudget());
96
+ }
97
+ };
98
+ /**
99
+ * A YAML alias AST node, referencing a previously defined anchor by name
100
+ * (without the leading `*`).
101
+ *
102
+ * @public
103
+ */
104
+ var YamlAlias = class extends Schema.TaggedClass()("YamlAlias", {
105
+ name: Schema.String,
106
+ offset: Schema.Number,
107
+ length: Schema.Number
108
+ }) {
109
+ /** See `YamlScalar.find`. Pure. */
110
+ find(path) {
111
+ return findByPath(this, path);
112
+ }
113
+ /** See `YamlScalar.findAtOffset`. Pure. */
114
+ findAtOffset(offset) {
115
+ return findDeepestAtOffset(this, offset);
116
+ }
117
+ /** See `YamlScalar.pathOf`. Pure. */
118
+ pathOf(node) {
119
+ return pathToNode(this, node);
120
+ }
121
+ /** See `YamlScalar.toValue`. Pure and total. */
122
+ toValue(anchors) {
123
+ return nodeToValue(this, anchors, defaultBudget());
124
+ }
125
+ };
126
+ /**
127
+ * A discriminated-union schema covering all four YAML AST value node types:
128
+ * {@link YamlScalar}, {@link YamlMap}, {@link YamlSeq} and {@link YamlAlias}.
129
+ * Defined lazily via `Schema.suspend` to break the recursive reference chain
130
+ * `YamlNode → YamlMap → YamlPair → YamlNode`.
131
+ *
132
+ * @remarks
133
+ * Construct member nodes via their `.make(...)` static (e.g.
134
+ * `YamlScalar.make(...)`), never `new YamlScalar(...)` — the internal
135
+ * composer's hot-path `new` construction is the one recorded exception, kept
136
+ * internal to the engine for its allocation-sensitive walk.
137
+ *
138
+ * @public
139
+ */
140
+ const YamlNode = Schema.suspend(() => Schema.Union([
141
+ YamlScalar,
142
+ YamlMap,
143
+ YamlSeq,
144
+ YamlAlias
145
+ ]));
146
+ /**
147
+ * A YAML key-value pair AST node, representing one entry within a mapping.
148
+ * `value` is `null` when absent (e.g. `key:` with no value).
149
+ *
150
+ * @public
151
+ */
152
+ var YamlPair = class extends Schema.TaggedClass()("YamlPair", {
153
+ key: Schema.suspend(() => YamlNode),
154
+ value: Schema.NullOr(Schema.suspend(() => YamlNode)),
155
+ comment: Schema.optionalKey(Schema.String)
156
+ }) {};
157
+ /**
158
+ * A YAML mapping AST node, representing a collection of {@link YamlPair}
159
+ * entries.
160
+ *
161
+ * - `style` — the presentation style: `"block"` or `"flow"`.
162
+ * - `sourceMultiline` — `true` when the source span covers two or more lines;
163
+ * used by the canonical stringifier. Absent on synthetic nodes.
164
+ *
165
+ * @public
166
+ */
167
+ var YamlMap = class extends Schema.TaggedClass()("YamlMap", {
168
+ items: Schema.Array(Schema.suspend(() => YamlPair)),
169
+ tag: Schema.optionalKey(Schema.String),
170
+ anchor: Schema.optionalKey(Schema.String),
171
+ style: CollectionStyle,
172
+ comment: Schema.optionalKey(Schema.String),
173
+ sourceMultiline: Schema.optionalKey(Schema.Boolean),
174
+ offset: Schema.Number,
175
+ length: Schema.Number
176
+ }) {
177
+ /** See `YamlScalar.find`. Pure. */
178
+ find(path) {
179
+ return findByPath(this, path);
180
+ }
181
+ /** See `YamlScalar.findAtOffset`. Pure. */
182
+ findAtOffset(offset) {
183
+ return findDeepestAtOffset(this, offset);
184
+ }
185
+ /** See `YamlScalar.pathOf`. Pure. */
186
+ pathOf(node) {
187
+ return pathToNode(this, node);
188
+ }
189
+ /** See `YamlScalar.toValue`. Pure and total. */
190
+ toValue(anchors) {
191
+ return nodeToValue(this, anchors, defaultBudget());
192
+ }
193
+ };
194
+ /**
195
+ * A YAML sequence AST node, representing an ordered list of
196
+ * {@link (YamlNode:type)} values.
197
+ *
198
+ * @public
199
+ */
200
+ var YamlSeq = class extends Schema.TaggedClass()("YamlSeq", {
201
+ items: Schema.Array(Schema.suspend(() => YamlNode)),
202
+ tag: Schema.optionalKey(Schema.String),
203
+ anchor: Schema.optionalKey(Schema.String),
204
+ style: CollectionStyle,
205
+ comment: Schema.optionalKey(Schema.String),
206
+ sourceMultiline: Schema.optionalKey(Schema.Boolean),
207
+ offset: Schema.Number,
208
+ length: Schema.Number
209
+ }) {
210
+ /** See `YamlScalar.find`. Pure. */
211
+ find(path) {
212
+ return findByPath(this, path);
213
+ }
214
+ /** See `YamlScalar.findAtOffset`. Pure. */
215
+ findAtOffset(offset) {
216
+ return findDeepestAtOffset(this, offset);
217
+ }
218
+ /** See `YamlScalar.pathOf`. Pure. */
219
+ pathOf(node) {
220
+ return pathToNode(this, node);
221
+ }
222
+ /** See `YamlScalar.toValue`. Pure and total. */
223
+ toValue(anchors) {
224
+ return nodeToValue(this, anchors, defaultBudget());
225
+ }
226
+ };
227
+ function findByPath(root, path) {
228
+ let current = root;
229
+ for (const segment of path) {
230
+ if (current === null) return Option.none();
231
+ if (typeof segment === "string") {
232
+ if (!(current instanceof YamlMap)) return Option.none();
233
+ const pair = current.items.find((p) => p.key instanceof YamlScalar && typeof p.key.value === "string" && p.key.value === segment);
234
+ if (!pair || pair.value === null) return Option.none();
235
+ current = pair.value;
236
+ } else {
237
+ if (!(current instanceof YamlSeq)) return Option.none();
238
+ const item = current.items[segment];
239
+ if (item === void 0) return Option.none();
240
+ current = item;
241
+ }
242
+ }
243
+ return current === null ? Option.none() : Option.some(current);
244
+ }
245
+ /**
246
+ * Half-open interval test `[offset, offset + length)` so a cursor positioned
247
+ * immediately after a node is NOT considered inside it.
248
+ */
249
+ function containsOffset(node, offset) {
250
+ return offset >= node.offset && offset < node.offset + node.length;
251
+ }
252
+ function findDeepestAtOffset(node, offset) {
253
+ if (!containsOffset(node, offset)) return Option.none();
254
+ if (node instanceof YamlMap) for (const pair of node.items) {
255
+ const keyResult = findDeepestAtOffset(pair.key, offset);
256
+ if (Option.isSome(keyResult)) return keyResult;
257
+ if (pair.value !== null) {
258
+ const valResult = findDeepestAtOffset(pair.value, offset);
259
+ if (Option.isSome(valResult)) return valResult;
260
+ }
261
+ }
262
+ if (node instanceof YamlSeq) for (const item of node.items) {
263
+ const itemResult = findDeepestAtOffset(item, offset);
264
+ if (Option.isSome(itemResult)) return itemResult;
265
+ }
266
+ return Option.some(node);
267
+ }
268
+ function pathToNode(root, target) {
269
+ const path = [];
270
+ return descendToNode(root, target, path) ? Option.some(path) : Option.none();
271
+ }
272
+ /**
273
+ * Depth-first identity search accumulating mapping-key/sequence-index
274
+ * segments. Only scalar string keys produce navigable segments (matching
275
+ * `find`); descendants reachable only through complex keys are not
276
+ * addressable by path.
277
+ */
278
+ function descendToNode(node, target, path) {
279
+ if (node === target) return true;
280
+ if (node instanceof YamlMap) {
281
+ for (const pair of node.items) if (pair.key instanceof YamlScalar && typeof pair.key.value === "string") {
282
+ if (pair.key === target) {
283
+ path.push(pair.key.value);
284
+ return true;
285
+ }
286
+ if (pair.value !== null) {
287
+ path.push(pair.key.value);
288
+ if (descendToNode(pair.value, target, path)) return true;
289
+ path.pop();
290
+ }
291
+ }
292
+ }
293
+ if (node instanceof YamlSeq) for (let i = 0; i < node.items.length; i++) {
294
+ const item = node.items[i];
295
+ path.push(i);
296
+ if (descendToNode(item, target, path)) return true;
297
+ path.pop();
298
+ }
299
+ return false;
300
+ }
301
+ /** Set a mapping key as an own data property — `__proto__` included. */
302
+ function setOwnProperty(obj, key, value) {
303
+ if (key === "__proto__") Object.defineProperty(obj, key, {
304
+ value,
305
+ writable: true,
306
+ enumerable: true,
307
+ configurable: true
308
+ });
309
+ else obj[key] = value;
310
+ }
311
+ /**
312
+ * Thrown by the value-extraction walk when alias expansion materializes more
313
+ * output nodes than the budget allows — the YAML "billion laughs" guard. A
314
+ * chain of aliases each referencing the previous (`a2: [*a1×10]`, `a3:
315
+ * [*a2×10]`, …) multiplies output size exponentially while the alias-*token*
316
+ * count stays small, so the composer's per-token `maxAliasCount` limit does
317
+ * not catch it; only bounding the expanded node count does.
318
+ *
319
+ * Not re-exported from the package entry point (`index.ts`) — the facade
320
+ * catches it and materializes a fatal `AliasCountExceeded` `YamlParseError`
321
+ * (or, for `Yaml.equals`, treats the input as malformed).
322
+ */
323
+ var AliasExpansionBudgetExceeded = class extends Error {
324
+ constructor(limit) {
325
+ super(`Alias expansion exceeded budget of ${limit} nodes`);
326
+ this.name = "AliasExpansionBudgetExceeded";
327
+ }
328
+ };
329
+ /**
330
+ * Multiplier converting a `maxAliasCount` budget into a cap on the number of
331
+ * output nodes materialized *through alias expansion*. Deliberately generous:
332
+ * alias-free content never ticks the counter (see {@link nodeToValue}), so a
333
+ * large but benign document — or a single alias referencing a large alias-free
334
+ * block — stays far under the cap, while an exponential alias chain accumulates
335
+ * across the shared budget and trips it long before the heap is exhausted.
336
+ */
337
+ const ALIAS_EXPANSION_FACTOR = 1e4;
338
+ /** The output-node cap for a given `maxAliasCount`. */
339
+ function aliasExpansionLimit(maxAliasCount) {
340
+ return (maxAliasCount + 1) * ALIAS_EXPANSION_FACTOR;
341
+ }
342
+ /** Default cap for a direct `toValue()` call, matching the default `maxAliasCount` of 100. */
343
+ const DEFAULT_ALIAS_EXPANSION_LIMIT = aliasExpansionLimit(100);
344
+ /** A fresh default budget for a direct `toValue()` call. */
345
+ function defaultBudget() {
346
+ return {
347
+ count: 0,
348
+ limit: DEFAULT_ALIAS_EXPANSION_LIMIT
349
+ };
350
+ }
351
+ /**
352
+ * Value extraction with an explicit alias-expansion budget derived from
353
+ * `maxAliasCount`. The facade drives this so a `maxAliasCount` from parse
354
+ * options bounds the "billion laughs" expansion; throws
355
+ * {@link AliasExpansionBudgetExceeded} when the cap is exceeded. Not
356
+ * re-exported from the package entry point.
357
+ */
358
+ function nodeToJsValue(node, anchors, maxAliasCount) {
359
+ return nodeToValue(node, anchors, {
360
+ count: 0,
361
+ limit: aliasExpansionLimit(maxAliasCount)
362
+ });
363
+ }
364
+ function nodeToValue(node, anchors, budget, counting = false) {
365
+ if (node === null) return null;
366
+ if (counting && budget !== void 0) {
367
+ budget.count++;
368
+ if (budget.count > budget.limit) throw new AliasExpansionBudgetExceeded(budget.limit);
369
+ }
370
+ if (anchors !== void 0 && !(node instanceof YamlAlias) && node.anchor !== void 0) anchors.set(node.anchor, node);
371
+ if (node instanceof YamlScalar) return node.value;
372
+ if (node instanceof YamlMap) {
373
+ const result = {};
374
+ for (const pair of node.items) {
375
+ let key;
376
+ if (pair.key instanceof YamlScalar) {
377
+ if (anchors !== void 0 && pair.key.anchor !== void 0) anchors.set(pair.key.anchor, pair.key);
378
+ key = String(pair.key.value ?? "");
379
+ } else if (pair.key instanceof YamlAlias) {
380
+ const resolved = anchors?.get(pair.key.name);
381
+ key = resolved !== void 0 ? String(nodeToValue(resolved, anchors, budget, true) ?? "") : "";
382
+ } else key = "";
383
+ setOwnProperty(result, key, nodeToValue(pair.value, anchors, budget, counting));
384
+ }
385
+ return result;
386
+ }
387
+ if (node instanceof YamlSeq) return node.items.map((item) => nodeToValue(item, anchors, budget, counting));
388
+ if (node instanceof YamlAlias) {
389
+ const resolved = anchors?.get(node.name);
390
+ return resolved !== void 0 ? nodeToValue(resolved, anchors, budget, true) : null;
391
+ }
392
+ return null;
393
+ }
394
+
395
+ //#endregion
396
+ export { AliasExpansionBudgetExceeded, CollectionStyle, ScalarChomp, ScalarStyle, YamlAlias, YamlMap, YamlNode, YamlPair, YamlScalar, YamlSeq, aliasExpansionLimit, nodeToJsValue };
package/YamlVisitor.js ADDED
@@ -0,0 +1,172 @@
1
+ import { YamlAlias, YamlMap, YamlScalar, YamlSeq } from "./YamlNode.js";
2
+ import { composeAllDocuments } from "./internal/composer/document.js";
3
+ import { YamlDiagnostic } from "./YamlDiagnostic.js";
4
+ import { Data, Stream } from "effect";
5
+
6
+ //#region src/YamlVisitor.ts
7
+ /**
8
+ * Constructors and matchers for the `YamlVisitorEvent` union (e.g.
9
+ * `YamlVisitorEvent.Scalar({ path, depth, value, style })`,
10
+ * `YamlVisitorEvent.$is("MapStart")`).
11
+ *
12
+ * @public
13
+ */
14
+ const YamlVisitorEvent = Data.taggedEnum();
15
+ /**
16
+ * SAX-style YAML AST visitor statics. Not instantiable.
17
+ *
18
+ * @public
19
+ */
20
+ var YamlVisitor = class {
21
+ constructor() {}
22
+ /**
23
+ * Create a lazy `Stream` of `YamlVisitorEvent` from YAML text, in document
24
+ * order. Multi-document streams (separated by `---`) produce a separate
25
+ * `DocumentStart`/`DocumentEnd` pair per document. Events are produced on
26
+ * demand, so combining with `Stream.take` allows efficient partial scans
27
+ * of large documents without materializing the whole event sequence.
28
+ *
29
+ * @remarks
30
+ * Infallible at the type level: diagnostics recorded while composing
31
+ * (fatal or not, including an exceeded `maxAliasCount`, recorded as
32
+ * `AliasCountExceeded`) surface as `Error` events inside the stream rather
33
+ * than failing it.
34
+ */
35
+ static visit(text, options) {
36
+ return Stream.fromIterable(visitGen(text, options));
37
+ }
38
+ };
39
+ function* visitGen(text, options) {
40
+ const { documents, streamErrors } = composeAllDocuments(text, {
41
+ strict: options?.strict,
42
+ maxAliasCount: options?.maxAliasCount,
43
+ uniqueKeys: options?.uniqueKeys
44
+ });
45
+ for (const raw of streamErrors) yield YamlVisitorEvent.Error({
46
+ path: [],
47
+ depth: 0,
48
+ diagnostic: YamlDiagnostic.fromRaw(raw, text)
49
+ });
50
+ for (const doc of documents) yield* walkDocument(doc, text);
51
+ }
52
+ function* walkDocument(doc, text) {
53
+ const path = [];
54
+ const depth = 0;
55
+ for (const dir of doc.directives) yield YamlVisitorEvent.Directive({
56
+ path,
57
+ depth,
58
+ name: dir.name,
59
+ parameters: dir.parameters.join(" ")
60
+ });
61
+ yield YamlVisitorEvent.DocumentStart({
62
+ path,
63
+ depth,
64
+ directives: doc.directives.map((d) => ({
65
+ name: d.name,
66
+ parameters: d.parameters
67
+ }))
68
+ });
69
+ for (const raw of doc.errors) yield YamlVisitorEvent.Error({
70
+ path,
71
+ depth,
72
+ diagnostic: YamlDiagnostic.fromRaw(raw, text)
73
+ });
74
+ for (const raw of doc.warnings) yield YamlVisitorEvent.Error({
75
+ path,
76
+ depth,
77
+ diagnostic: YamlDiagnostic.fromRaw(raw, text)
78
+ });
79
+ if (doc.comment !== void 0) yield YamlVisitorEvent.Comment({
80
+ path,
81
+ depth,
82
+ text: doc.comment
83
+ });
84
+ if (doc.contents !== null) yield* walkNode(doc.contents, path, depth);
85
+ yield YamlVisitorEvent.DocumentEnd({
86
+ path,
87
+ depth
88
+ });
89
+ }
90
+ function* walkNode(node, path, depth) {
91
+ if (node instanceof YamlScalar) {
92
+ if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
93
+ path,
94
+ depth,
95
+ text: node.comment
96
+ });
97
+ yield YamlVisitorEvent.Scalar({
98
+ path,
99
+ depth,
100
+ value: node.value,
101
+ style: node.style,
102
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
103
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {}
104
+ });
105
+ } else if (node instanceof YamlAlias) yield YamlVisitorEvent.Alias({
106
+ path,
107
+ depth,
108
+ name: node.name
109
+ });
110
+ else if (node instanceof YamlMap) {
111
+ if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
112
+ path,
113
+ depth,
114
+ text: node.comment
115
+ });
116
+ yield YamlVisitorEvent.MapStart({
117
+ path,
118
+ depth,
119
+ style: node.style,
120
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
121
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {}
122
+ });
123
+ for (const pair of node.items) yield* walkPair(pair, path, depth + 1);
124
+ yield YamlVisitorEvent.MapEnd({
125
+ path,
126
+ depth
127
+ });
128
+ } else if (node instanceof YamlSeq) {
129
+ if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
130
+ path,
131
+ depth,
132
+ text: node.comment
133
+ });
134
+ yield YamlVisitorEvent.SeqStart({
135
+ path,
136
+ depth,
137
+ style: node.style,
138
+ ...node.tag !== void 0 ? { tag: node.tag } : {},
139
+ ...node.anchor !== void 0 ? { anchor: node.anchor } : {}
140
+ });
141
+ for (let i = 0; i < node.items.length; i++) {
142
+ const item = node.items[i];
143
+ yield* walkNode(item, [...path, i], depth + 1);
144
+ }
145
+ yield YamlVisitorEvent.SeqEnd({
146
+ path,
147
+ depth
148
+ });
149
+ }
150
+ }
151
+ function* walkPair(pair, parentPath, depth) {
152
+ const resolvedKey = pair.key instanceof YamlScalar ? pair.key.value : null;
153
+ const resolvedValue = pair.value instanceof YamlScalar ? pair.value.value : null;
154
+ const keySegment = typeof resolvedKey === "string" ? resolvedKey : typeof resolvedKey === "number" ? resolvedKey : String(resolvedKey);
155
+ const pairPath = [...parentPath, keySegment];
156
+ if (pair.comment !== void 0) yield YamlVisitorEvent.Comment({
157
+ path: pairPath,
158
+ depth,
159
+ text: pair.comment
160
+ });
161
+ yield YamlVisitorEvent.Pair({
162
+ path: pairPath,
163
+ depth,
164
+ key: resolvedKey,
165
+ value: resolvedValue
166
+ });
167
+ yield* walkNode(pair.key, pairPath, depth + 1);
168
+ if (pair.value !== null) yield* walkNode(pair.value, pairPath, depth + 1);
169
+ }
170
+
171
+ //#endregion
172
+ export { YamlVisitor, YamlVisitorEvent };