@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 +21 -0
- package/README.md +111 -0
- package/Yaml.js +414 -0
- package/YamlDiagnostic.js +126 -0
- package/YamlDocument.js +181 -0
- package/YamlEdit.js +45 -0
- package/YamlFormat.js +309 -0
- package/YamlNode.js +396 -0
- package/YamlVisitor.js +172 -0
- package/index.d.ts +1331 -0
- package/index.js +9 -0
- package/internal/composer/anchors.js +97 -0
- package/internal/composer/block.js +1155 -0
- package/internal/composer/document.js +677 -0
- package/internal/composer/flow.js +400 -0
- package/internal/composer/scalars.js +885 -0
- package/internal/composer/state.js +117 -0
- package/internal/composer/tags.js +88 -0
- package/internal/cst-parser.js +716 -0
- package/internal/diagnostics.js +80 -0
- package/internal/diff.js +56 -0
- package/internal/fold.js +153 -0
- package/internal/lexer.js +959 -0
- package/internal/stringifier.js +797 -0
- package/package.json +47 -0
- package/tsdoc-metadata.json +11 -0
package/YamlDocument.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { YamlNode } from "./YamlNode.js";
|
|
2
|
+
import { composeAllDocuments, composeFirstDocument } from "./internal/composer/document.js";
|
|
3
|
+
import { isFatalCode } from "./internal/diagnostics.js";
|
|
4
|
+
import { StringifyDepthExceeded, StringifyFailure, stringifyDocument } from "./internal/stringifier.js";
|
|
5
|
+
import { YamlDiagnostic } from "./YamlDiagnostic.js";
|
|
6
|
+
import { YamlParseError, YamlStringifyError } from "./Yaml.js";
|
|
7
|
+
import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
|
8
|
+
|
|
9
|
+
//#region src/YamlDocument.ts
|
|
10
|
+
/**
|
|
11
|
+
* A YAML directive appearing before a document (e.g. `%YAML 1.2` or
|
|
12
|
+
* `%TAG ! tag:example.com,2000:`). `"YAML"` and `"TAG"` are the YAML 1.2
|
|
13
|
+
* spec-defined directives; any other name is a reserved directive preserved
|
|
14
|
+
* for round-trip fidelity.
|
|
15
|
+
*
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
var YamlDirective = class extends Schema.Class("YamlDirective")({
|
|
19
|
+
name: Schema.String,
|
|
20
|
+
parameters: Schema.Array(Schema.String)
|
|
21
|
+
}) {};
|
|
22
|
+
/**
|
|
23
|
+
* A parsed YAML document: the root {@link (YamlNode:type)} (or `null` when
|
|
24
|
+
* empty), recovered `errors` and `warnings` as {@link YamlDiagnostic} data,
|
|
25
|
+
* the {@link YamlDirective} list, an optional document-level comment, and the
|
|
26
|
+
* `---`/`...` framing flags (absent flags read as `false`).
|
|
27
|
+
*
|
|
28
|
+
* Construct via `YamlDocument.parse` / `parseAll`; `YamlDocument.make` is for
|
|
29
|
+
* synthetic documents.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
|
|
34
|
+
contents: Schema.NullOr(Schema.suspend(() => YamlNode)),
|
|
35
|
+
errors: Schema.Array(YamlDiagnostic),
|
|
36
|
+
warnings: Schema.Array(YamlDiagnostic),
|
|
37
|
+
directives: Schema.Array(YamlDirective),
|
|
38
|
+
comment: Schema.optionalKey(Schema.String),
|
|
39
|
+
hasDocumentStart: Schema.optionalKey(Schema.Boolean),
|
|
40
|
+
hasDocumentEnd: Schema.optionalKey(Schema.Boolean),
|
|
41
|
+
hasDocumentStartTab: Schema.optionalKey(Schema.Boolean)
|
|
42
|
+
}) {
|
|
43
|
+
/**
|
|
44
|
+
* Parse a single YAML document, keeping the full AST, directives and
|
|
45
|
+
* recovered diagnostics. Fails with the aggregate {@link YamlParseError}
|
|
46
|
+
* when any fatal-code diagnostic is present; non-fatal diagnostics are
|
|
47
|
+
* data on the returned document.
|
|
48
|
+
*/
|
|
49
|
+
static parse = Effect.fn("YamlDocument.parse")(function* (text, options) {
|
|
50
|
+
const raw = composeFirstDocument(text, toParseInput(options));
|
|
51
|
+
const fatal = raw.errors.filter((e) => isFatalCode(e.code));
|
|
52
|
+
if (fatal.length > 0) return yield* new YamlParseError({
|
|
53
|
+
diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text)),
|
|
54
|
+
input: text
|
|
55
|
+
});
|
|
56
|
+
return fromRawDocument(raw, text);
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Parse a multi-document YAML stream into one {@link YamlDocument} per
|
|
60
|
+
* document. Any fatal diagnostic in any document — or a stream-level
|
|
61
|
+
* directive-placement error — fails the whole Effect.
|
|
62
|
+
*/
|
|
63
|
+
static parseAll = Effect.fn("YamlDocument.parseAll")(function* (text, options) {
|
|
64
|
+
const { documents, streamErrors } = composeAllDocuments(text, toParseInput(options));
|
|
65
|
+
const fatal = [...streamErrors.filter((e) => e.code === "InvalidDirective"), ...documents.flatMap((d) => d.errors.filter((e) => isFatalCode(e.code)))];
|
|
66
|
+
if (fatal.length > 0) return yield* new YamlParseError({
|
|
67
|
+
diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text)),
|
|
68
|
+
input: text
|
|
69
|
+
});
|
|
70
|
+
return documents.map((raw) => fromRawDocument(raw, text));
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* A `Schema<YamlDocument, string>` decoding YAML text into a full
|
|
74
|
+
* document (AST, directives, diagnostics) and encoding a document back to
|
|
75
|
+
* YAML text.
|
|
76
|
+
*
|
|
77
|
+
* Schema-producing: each call returns a fresh schema whose derivation
|
|
78
|
+
* caches are not shared across calls; bind the result to a `const` on hot
|
|
79
|
+
* paths.
|
|
80
|
+
*/
|
|
81
|
+
static schema(options) {
|
|
82
|
+
return Schema.String.pipe(Schema.decodeTo(Schema.instanceOf(YamlDocument), SchemaTransformation.transformOrFail({
|
|
83
|
+
decode: (input) => YamlDocument.parse(input, options).pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(input), { message: error.message }))),
|
|
84
|
+
encode: (doc) => doc.stringify().pipe(Effect.mapError((error) => new SchemaIssue.InvalidValue(Option.some(doc), { message: error.message })))
|
|
85
|
+
})));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Stringify this document (contents, directives and framing) as YAML.
|
|
89
|
+
* Fails with {@link YamlStringifyError} on circular references introduced
|
|
90
|
+
* into a synthetic AST (`CircularReference`) or on a synthetic AST nested
|
|
91
|
+
* deeper than the stringifier's recursion budget (`NestingDepthExceeded`)
|
|
92
|
+
* — both surface through the typed error channel rather than as an
|
|
93
|
+
* unhandled stack-overflow defect.
|
|
94
|
+
*/
|
|
95
|
+
stringify(options) {
|
|
96
|
+
return Effect.try({
|
|
97
|
+
try: () => stringifyDocument(toRawDocument(this), toStringifyInput(options)),
|
|
98
|
+
catch: (defect) => {
|
|
99
|
+
if (defect instanceof StringifyFailure) return new YamlStringifyError({
|
|
100
|
+
diagnostics: [YamlDiagnostic.make({
|
|
101
|
+
code: "CircularReference",
|
|
102
|
+
message: defect.reason,
|
|
103
|
+
offset: 0,
|
|
104
|
+
length: 0,
|
|
105
|
+
line: 0,
|
|
106
|
+
character: 0
|
|
107
|
+
})],
|
|
108
|
+
value: this
|
|
109
|
+
});
|
|
110
|
+
if (defect instanceof StringifyDepthExceeded) return new YamlStringifyError({
|
|
111
|
+
diagnostics: [YamlDiagnostic.make({
|
|
112
|
+
code: "NestingDepthExceeded",
|
|
113
|
+
message: defect.message,
|
|
114
|
+
offset: 0,
|
|
115
|
+
length: 0,
|
|
116
|
+
line: 0,
|
|
117
|
+
character: 0
|
|
118
|
+
})],
|
|
119
|
+
value: this
|
|
120
|
+
});
|
|
121
|
+
throw defect;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Reconstruct the plain JavaScript value of this document's contents,
|
|
127
|
+
* resolving anchors and aliases. `null` for an empty document. Pure and
|
|
128
|
+
* total.
|
|
129
|
+
*/
|
|
130
|
+
toValue() {
|
|
131
|
+
if (this.contents === null) return null;
|
|
132
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
133
|
+
return this.contents.toValue(anchors);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const toParseInput = (options) => options === void 0 ? {} : {
|
|
137
|
+
strict: options.strict,
|
|
138
|
+
maxAliasCount: options.maxAliasCount,
|
|
139
|
+
uniqueKeys: options.uniqueKeys
|
|
140
|
+
};
|
|
141
|
+
const toStringifyInput = (options) => options === void 0 ? {} : {
|
|
142
|
+
indent: options.indent,
|
|
143
|
+
lineWidth: options.lineWidth,
|
|
144
|
+
defaultScalarStyle: options.defaultScalarStyle,
|
|
145
|
+
defaultCollectionStyle: options.defaultCollectionStyle,
|
|
146
|
+
sortKeys: options.sortKeys,
|
|
147
|
+
finalNewline: options.finalNewline,
|
|
148
|
+
forceDefaultStyles: options.forceDefaultStyles
|
|
149
|
+
};
|
|
150
|
+
/** Materialize a raw engine document into the public class. */
|
|
151
|
+
function fromRawDocument(raw, text) {
|
|
152
|
+
return new YamlDocument({
|
|
153
|
+
contents: raw.contents,
|
|
154
|
+
errors: raw.errors.map((e) => YamlDiagnostic.fromRaw(e, text)),
|
|
155
|
+
warnings: raw.warnings.map((w) => YamlDiagnostic.fromRaw(w, text)),
|
|
156
|
+
directives: raw.directives.map((d) => new YamlDirective({
|
|
157
|
+
name: d.name,
|
|
158
|
+
parameters: d.parameters
|
|
159
|
+
})),
|
|
160
|
+
...raw.comment !== void 0 ? { comment: raw.comment } : {},
|
|
161
|
+
hasDocumentStart: raw.hasDocumentStart,
|
|
162
|
+
hasDocumentEnd: raw.hasDocumentEnd,
|
|
163
|
+
hasDocumentStartTab: raw.hasDocumentStartTab
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** Project the public class back onto the raw shape the engine consumes. */
|
|
167
|
+
function toRawDocument(doc) {
|
|
168
|
+
return {
|
|
169
|
+
contents: doc.contents,
|
|
170
|
+
errors: [],
|
|
171
|
+
warnings: [],
|
|
172
|
+
directives: doc.directives,
|
|
173
|
+
...doc.comment !== void 0 ? { comment: doc.comment } : {},
|
|
174
|
+
hasDocumentStart: doc.hasDocumentStart ?? false,
|
|
175
|
+
hasDocumentEnd: doc.hasDocumentEnd ?? false,
|
|
176
|
+
hasDocumentStartTab: doc.hasDocumentStartTab ?? false
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
export { YamlDirective, YamlDocument };
|
package/YamlEdit.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/YamlEdit.ts
|
|
4
|
+
/**
|
|
5
|
+
* A range within a YAML document, expressed as a zero-based character
|
|
6
|
+
* `offset` and a `length` in UTF-16 code units. Pass to `YamlFormat.format`
|
|
7
|
+
* to restrict formatting to a region.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
var YamlRange = class extends Schema.Class("YamlRange")({
|
|
12
|
+
offset: Schema.Number,
|
|
13
|
+
length: Schema.Number
|
|
14
|
+
}) {};
|
|
15
|
+
/**
|
|
16
|
+
* A non-mutating text edit: replace the span `[offset, offset + length)` with
|
|
17
|
+
* `content`. Set `length` to `0` to insert, `content` to `""` to delete.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Structurally identical to `@effected/jsonc`'s edit shape (same field names,
|
|
21
|
+
* types and semantics) per the jsonc/yaml parity convention, so consumer code
|
|
22
|
+
* can be written once over "a document codec's Edit/Range/Path".
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
var YamlEdit = class extends Schema.Class("YamlEdit")({
|
|
27
|
+
offset: Schema.Number,
|
|
28
|
+
length: Schema.Number,
|
|
29
|
+
content: Schema.String
|
|
30
|
+
}) {
|
|
31
|
+
/**
|
|
32
|
+
* Apply `edits` to `text`, producing a new string. Edits are applied in
|
|
33
|
+
* reverse-offset order so earlier offsets stay valid; the input `edits`
|
|
34
|
+
* array is not mutated. Pure and total.
|
|
35
|
+
*/
|
|
36
|
+
static applyAll(text, edits) {
|
|
37
|
+
const sorted = [...edits].sort((a, b) => b.offset - a.offset);
|
|
38
|
+
let result = text;
|
|
39
|
+
for (const edit of sorted) result = result.substring(0, edit.offset) + edit.content + result.substring(edit.offset + edit.length);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { YamlEdit, YamlRange };
|
package/YamlFormat.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { YamlMap, YamlPair, YamlScalar, YamlSeq } from "./YamlNode.js";
|
|
2
|
+
import { composeFirstDocument } from "./internal/composer/document.js";
|
|
3
|
+
import { isFatalCode } from "./internal/diagnostics.js";
|
|
4
|
+
import { stringifyDocument, stripNodeComments } from "./internal/stringifier.js";
|
|
5
|
+
import { YamlDiagnostic } from "./YamlDiagnostic.js";
|
|
6
|
+
import { YamlStringifyOptions } from "./Yaml.js";
|
|
7
|
+
import { YamlEdit, YamlRange } from "./YamlEdit.js";
|
|
8
|
+
import { computeEdits } from "./internal/diff.js";
|
|
9
|
+
import { Effect, Schema } from "effect";
|
|
10
|
+
|
|
11
|
+
//#region src/YamlFormat.ts
|
|
12
|
+
/**
|
|
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).
|
|
17
|
+
*
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
var YamlFormattingOptions = class extends Schema.Class("YamlFormattingOptions")({
|
|
21
|
+
...YamlStringifyOptions.fields,
|
|
22
|
+
preserveComments: Schema.optionalKey(Schema.Boolean),
|
|
23
|
+
range: Schema.optionalKey(YamlRange)
|
|
24
|
+
}) {};
|
|
25
|
+
/**
|
|
26
|
+
* Raised when `YamlFormat.modify` cannot navigate the requested path against
|
|
27
|
+
* the composed AST (a structural mismatch) or the source fails to parse.
|
|
28
|
+
* Carries structured {@link YamlDiagnostic} entries — never a collapsed
|
|
29
|
+
* `reason` string (the structure-preserving-errors house rule).
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
var YamlModificationError = class extends Schema.TaggedErrorClass()("YamlModificationError", {
|
|
34
|
+
path: Schema.Array(Schema.Union([Schema.String, Schema.Number])),
|
|
35
|
+
diagnostics: Schema.Array(YamlDiagnostic)
|
|
36
|
+
}) {
|
|
37
|
+
get message() {
|
|
38
|
+
const summary = this.diagnostics.map((d) => d.message).join("; ");
|
|
39
|
+
return `Modification failed at path [${this.path.join(", ")}]: ${summary}`;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Thrown by the pure AST-navigation helpers on a structural mismatch.
|
|
44
|
+
* `modify` catches this and materializes {@link YamlModificationError}.
|
|
45
|
+
*/
|
|
46
|
+
var ModifyFailure = class extends Error {
|
|
47
|
+
code;
|
|
48
|
+
offset;
|
|
49
|
+
length;
|
|
50
|
+
constructor(code, message, offset, length) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "ModifyFailure";
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.offset = offset;
|
|
55
|
+
this.length = length;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const toStringifyInput = (options) => options === void 0 ? {} : {
|
|
59
|
+
indent: options.indent,
|
|
60
|
+
lineWidth: options.lineWidth,
|
|
61
|
+
defaultScalarStyle: options.defaultScalarStyle,
|
|
62
|
+
defaultCollectionStyle: options.defaultCollectionStyle,
|
|
63
|
+
sortKeys: options.sortKeys,
|
|
64
|
+
finalNewline: options.finalNewline,
|
|
65
|
+
forceDefaultStyles: options.forceDefaultStyles
|
|
66
|
+
};
|
|
67
|
+
/** Normalize a `format`-positional range and the options-bag fallback into one plain shape (or `undefined`). */
|
|
68
|
+
function resolveRange(positional, fromOptions) {
|
|
69
|
+
const range = positional ?? fromOptions;
|
|
70
|
+
return range === void 0 ? void 0 : {
|
|
71
|
+
offset: range.offset,
|
|
72
|
+
length: range.length
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Copy only the defined entries of `fields` — never emits an explicit `undefined` into a v4 `optionalKey` field. */
|
|
76
|
+
function definedFields(fields) {
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const key of Object.keys(fields)) if (fields[key] !== void 0) out[key] = fields[key];
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Format a document via the parse → stringify round-trip and diff the
|
|
83
|
+
* output against the source, returning `undefined` when the input has a
|
|
84
|
+
* fatal parse error (never corrupt malformed input).
|
|
85
|
+
*/
|
|
86
|
+
function formatDocument(text, options) {
|
|
87
|
+
const doc = composeFirstDocument(text, {});
|
|
88
|
+
if (doc.errors.some((e) => isFatalCode(e.code))) return void 0;
|
|
89
|
+
const preserveComments = options?.preserveComments ?? true;
|
|
90
|
+
return stringifyDocument({
|
|
91
|
+
contents: !preserveComments && doc.contents !== null ? stripNodeComments(doc.contents) : doc.contents,
|
|
92
|
+
errors: doc.errors,
|
|
93
|
+
warnings: doc.warnings,
|
|
94
|
+
directives: doc.directives,
|
|
95
|
+
...preserveComments ? definedFields({ comment: doc.comment }) : {},
|
|
96
|
+
hasDocumentStart: doc.hasDocumentStart,
|
|
97
|
+
hasDocumentEnd: doc.hasDocumentEnd,
|
|
98
|
+
hasDocumentStartTab: doc.hasDocumentStartTab
|
|
99
|
+
}, toStringifyInput(options));
|
|
100
|
+
}
|
|
101
|
+
/** Convert a plain JS scalar value into a synthetic `YamlScalar` (offset/length are irrelevant — immediately re-stringified). */
|
|
102
|
+
function jsValueToNode(value) {
|
|
103
|
+
return YamlScalar.make({
|
|
104
|
+
value,
|
|
105
|
+
style: "plain",
|
|
106
|
+
offset: 0,
|
|
107
|
+
length: 0
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function modifyDocument(doc, path, value) {
|
|
111
|
+
if (path.length === 0) return value === void 0 ? null : jsValueToNode(value);
|
|
112
|
+
if (doc.contents === null) throw new ModifyFailure("EmptyDocument", "Cannot navigate path in empty document", 0, 0);
|
|
113
|
+
return modifyNode(doc.contents, path, 0, value);
|
|
114
|
+
}
|
|
115
|
+
function modifyNode(node, path, depth, value) {
|
|
116
|
+
const segment = path[depth];
|
|
117
|
+
const isLast = depth === path.length - 1;
|
|
118
|
+
if (node instanceof YamlMap) {
|
|
119
|
+
const pairIndex = node.items.findIndex((pair) => pair.key instanceof YamlScalar && pair.key.value === segment);
|
|
120
|
+
if (isLast) {
|
|
121
|
+
if (value === void 0) {
|
|
122
|
+
if (pairIndex < 0) return node;
|
|
123
|
+
const newItems = [...node.items];
|
|
124
|
+
newItems.splice(pairIndex, 1);
|
|
125
|
+
return rebuildMap(node, newItems);
|
|
126
|
+
}
|
|
127
|
+
const newValueNode = jsValueToNode(value);
|
|
128
|
+
if (pairIndex >= 0) {
|
|
129
|
+
const newItems = [...node.items];
|
|
130
|
+
const oldPair = newItems[pairIndex];
|
|
131
|
+
newItems[pairIndex] = YamlPair.make({
|
|
132
|
+
key: oldPair.key,
|
|
133
|
+
value: newValueNode,
|
|
134
|
+
...definedFields({ comment: oldPair.comment })
|
|
135
|
+
});
|
|
136
|
+
return rebuildMap(node, newItems);
|
|
137
|
+
}
|
|
138
|
+
const keyNode = YamlScalar.make({
|
|
139
|
+
value: String(segment),
|
|
140
|
+
style: "plain",
|
|
141
|
+
offset: 0,
|
|
142
|
+
length: 0
|
|
143
|
+
});
|
|
144
|
+
const newPair = YamlPair.make({
|
|
145
|
+
key: keyNode,
|
|
146
|
+
value: newValueNode
|
|
147
|
+
});
|
|
148
|
+
return rebuildMap(node, [...node.items, newPair]);
|
|
149
|
+
}
|
|
150
|
+
if (pairIndex < 0) throw new ModifyFailure("PathNotFound", `Key "${String(segment)}" not found in mapping`, node.offset, node.length);
|
|
151
|
+
const pair = node.items[pairIndex];
|
|
152
|
+
if (pair.value === null) throw new ModifyFailure("PathNotFound", `Value at key "${String(segment)}" is null`, node.offset, node.length);
|
|
153
|
+
const newValue = modifyNode(pair.value, path, depth + 1, value);
|
|
154
|
+
const newItems = [...node.items];
|
|
155
|
+
newItems[pairIndex] = YamlPair.make({
|
|
156
|
+
key: pair.key,
|
|
157
|
+
value: newValue,
|
|
158
|
+
...definedFields({ comment: pair.comment })
|
|
159
|
+
});
|
|
160
|
+
return rebuildMap(node, newItems);
|
|
161
|
+
}
|
|
162
|
+
if (node instanceof YamlSeq) {
|
|
163
|
+
const idx = typeof segment === "number" ? segment : Number(segment);
|
|
164
|
+
if (Number.isNaN(idx) || idx < 0) throw new ModifyFailure("InvalidIndex", `Invalid sequence index: ${String(segment)}`, node.offset, node.length);
|
|
165
|
+
if (isLast) {
|
|
166
|
+
const newItems = [...node.items];
|
|
167
|
+
if (value === void 0) {
|
|
168
|
+
if (idx < newItems.length) newItems.splice(idx, 1);
|
|
169
|
+
} else if (idx < newItems.length) newItems[idx] = jsValueToNode(value);
|
|
170
|
+
else newItems.push(jsValueToNode(value));
|
|
171
|
+
return rebuildSeq(node, newItems);
|
|
172
|
+
}
|
|
173
|
+
if (idx >= node.items.length) throw new ModifyFailure("InvalidIndex", `Index ${idx} out of bounds`, node.offset, node.length);
|
|
174
|
+
const child = node.items[idx];
|
|
175
|
+
const newChild = modifyNode(child, path, depth + 1, value);
|
|
176
|
+
const newItems = [...node.items];
|
|
177
|
+
newItems[idx] = newChild;
|
|
178
|
+
return rebuildSeq(node, newItems);
|
|
179
|
+
}
|
|
180
|
+
throw new ModifyFailure("NotNavigable", `Cannot navigate through ${node._tag} at segment "${String(segment)}"`, node.offset, node.length);
|
|
181
|
+
}
|
|
182
|
+
function rebuildMap(node, items) {
|
|
183
|
+
return YamlMap.make({
|
|
184
|
+
items,
|
|
185
|
+
style: node.style,
|
|
186
|
+
...definedFields({
|
|
187
|
+
tag: node.tag,
|
|
188
|
+
anchor: node.anchor,
|
|
189
|
+
comment: node.comment,
|
|
190
|
+
sourceMultiline: node.sourceMultiline
|
|
191
|
+
}),
|
|
192
|
+
offset: node.offset,
|
|
193
|
+
length: node.length
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function rebuildSeq(node, items) {
|
|
197
|
+
return YamlSeq.make({
|
|
198
|
+
items,
|
|
199
|
+
style: node.style,
|
|
200
|
+
...definedFields({
|
|
201
|
+
tag: node.tag,
|
|
202
|
+
anchor: node.anchor,
|
|
203
|
+
comment: node.comment,
|
|
204
|
+
sourceMultiline: node.sourceMultiline
|
|
205
|
+
}),
|
|
206
|
+
offset: node.offset,
|
|
207
|
+
length: node.length
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Formatting and modification statics. Not instantiable.
|
|
212
|
+
*
|
|
213
|
+
* @remarks
|
|
214
|
+
* `format`/`formatToString` are pure and total (edit computation never fails
|
|
215
|
+
* — malformed input yields no edits rather than corrupting the document).
|
|
216
|
+
* `modify`/`modifyToString` carry a real error channel: navigation failures
|
|
217
|
+
* against the composed AST raise {@link YamlModificationError}, which — per
|
|
218
|
+
* the structure-preserving-errors house rule — carries
|
|
219
|
+
* `diagnostics: ReadonlyArray<YamlDiagnostic>`, never a collapsed `reason`
|
|
220
|
+
* string.
|
|
221
|
+
*
|
|
222
|
+
* @public
|
|
223
|
+
*/
|
|
224
|
+
var YamlFormat = class YamlFormat {
|
|
225
|
+
constructor() {}
|
|
226
|
+
/**
|
|
227
|
+
* Compute formatting edits for a YAML document. Non-mutating — apply the
|
|
228
|
+
* result with `YamlEdit.applyAll` (or use {@link YamlFormat.formatToString}).
|
|
229
|
+
* Pure and total: malformed input (a fatal parse error) yields `[]` rather
|
|
230
|
+
* than corrupting the document.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* The positional `range` argument takes precedence over
|
|
234
|
+
* `options?.range` when both are given; either accepts a plain
|
|
235
|
+
* `{ offset, length }` object as well as a {@link YamlRange} instance, so
|
|
236
|
+
* callers do not need `YamlRange.make(...)` for the common case.
|
|
237
|
+
*/
|
|
238
|
+
static format(text, range, options) {
|
|
239
|
+
const formatted = formatDocument(text, options);
|
|
240
|
+
if (formatted === void 0) return [];
|
|
241
|
+
let edits = computeEdits(text, formatted);
|
|
242
|
+
const effectiveRange = resolveRange(range, options?.range);
|
|
243
|
+
if (effectiveRange !== void 0) {
|
|
244
|
+
const rangeStart = effectiveRange.offset;
|
|
245
|
+
const rangeEnd = effectiveRange.offset + effectiveRange.length;
|
|
246
|
+
edits = edits.filter((e) => e.offset >= rangeStart && e.offset + e.length <= rangeEnd);
|
|
247
|
+
}
|
|
248
|
+
return edits.map((e) => YamlEdit.make(e));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Format `text` and apply the resulting edits in one step
|
|
252
|
+
* (`YamlEdit.applyAll ∘ format`). Pure and total.
|
|
253
|
+
*/
|
|
254
|
+
static formatToString(text, range, options) {
|
|
255
|
+
return YamlEdit.applyAll(text, YamlFormat.format(text, range, options));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Compute the edits that insert, replace, or remove a value at `path`.
|
|
259
|
+
* Passing `value === undefined` removes the target key/element; a missing
|
|
260
|
+
* insertion target appends after the last pair/element. Only
|
|
261
|
+
* scalar-compatible values are supported (matching v3 — arbitrary object
|
|
262
|
+
* graphs are not recursively lowered into AST nodes). Fails with
|
|
263
|
+
* {@link YamlModificationError} on a fatal parse error or a structural
|
|
264
|
+
* navigation mismatch.
|
|
265
|
+
*
|
|
266
|
+
* @remarks
|
|
267
|
+
* `options` is a bare {@link YamlStringifyOptions} — it controls only the
|
|
268
|
+
* internal re-stringify step, not a range (there is no range to restrict
|
|
269
|
+
* for a path-targeted modification).
|
|
270
|
+
*/
|
|
271
|
+
static modify = Effect.fn("YamlFormat.modify")(function* (text, path, value, options) {
|
|
272
|
+
const doc = composeFirstDocument(text, {});
|
|
273
|
+
const fatal = doc.errors.filter((e) => isFatalCode(e.code));
|
|
274
|
+
if (fatal.length > 0) return yield* new YamlModificationError({
|
|
275
|
+
path,
|
|
276
|
+
diagnostics: fatal.map((e) => YamlDiagnostic.fromRaw(e, text))
|
|
277
|
+
});
|
|
278
|
+
let newContents;
|
|
279
|
+
try {
|
|
280
|
+
newContents = modifyDocument(doc, path, value);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (!(err instanceof ModifyFailure)) throw err;
|
|
283
|
+
return yield* new YamlModificationError({
|
|
284
|
+
path,
|
|
285
|
+
diagnostics: [YamlDiagnostic.fromRaw({
|
|
286
|
+
code: err.code,
|
|
287
|
+
message: err.message,
|
|
288
|
+
offset: err.offset,
|
|
289
|
+
length: err.length
|
|
290
|
+
}, text)]
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
return computeEdits(text, stringifyDocument({
|
|
294
|
+
...doc,
|
|
295
|
+
contents: newContents
|
|
296
|
+
}, toStringifyInput(options))).map((e) => YamlEdit.make(e));
|
|
297
|
+
});
|
|
298
|
+
/**
|
|
299
|
+
* Modify `text` and apply the resulting edits in one step
|
|
300
|
+
* (`YamlEdit.applyAll ∘ modify`).
|
|
301
|
+
*/
|
|
302
|
+
static modifyToString = Effect.fn("YamlFormat.modifyToString")(function* (text, path, value, options) {
|
|
303
|
+
const edits = yield* YamlFormat.modify(text, path, value, options);
|
|
304
|
+
return YamlEdit.applyAll(text, edits);
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
//#endregion
|
|
309
|
+
export { YamlFormat, YamlFormattingOptions, YamlModificationError };
|