@effected/yaml 0.7.0 → 0.9.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 +80 -7
- package/Yaml.js +80 -32
- package/YamlDocument.js +23 -2
- package/YamlFormat.js +162 -21
- package/YamlLint.js +239 -0
- package/YamlLintRule.js +36 -0
- package/YamlNode.js +43 -4
- package/YamlToken.js +139 -0
- package/YamlVisitor.js +52 -16
- package/index.d.ts +446 -12
- package/index.js +4 -1
- package/internal/composer/anchors.js +2 -27
- package/internal/composer/block.js +291 -67
- package/internal/composer/comments.js +217 -0
- package/internal/composer/document.js +141 -14
- package/internal/composer/flow.js +103 -10
- package/internal/composer/scalars.js +48 -11
- package/internal/composer/state.js +16 -2
- package/internal/cst-parser.js +2 -1
- package/internal/diagnostics.js +12 -3
- package/internal/fold.js +22 -10
- package/internal/rules/catalog.js +39 -0
- package/internal/rules/colon-spacing.js +77 -0
- package/internal/rules/comments-spacing.js +68 -0
- package/internal/rules/document-end.js +65 -0
- package/internal/rules/document-start.js +64 -0
- package/internal/rules/empty-lines.js +70 -0
- package/internal/rules/eof-newline.js +33 -0
- package/internal/rules/hyphen-spacing.js +52 -0
- package/internal/rules/indentation.js +103 -0
- package/internal/rules/key-duplicates.js +49 -0
- package/internal/rules/line-length.js +36 -0
- package/internal/rules/parse-validity.js +33 -0
- package/internal/rules/quoted-strings.js +81 -0
- package/internal/rules/trailing-spaces.js +39 -0
- package/internal/rules/truthy.js +84 -0
- package/internal/rules/util.js +92 -0
- package/internal/stringifier.js +342 -54
- package/package.json +3 -2
package/YamlLint.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { composeFirstDocument } from "./internal/composer/document.js";
|
|
2
|
+
import { isFatalCode } from "./internal/diagnostics.js";
|
|
3
|
+
import { YamlParseError } from "./Yaml.js";
|
|
4
|
+
import { documentFromRaw } from "./YamlDocument.js";
|
|
5
|
+
import { YamlEdit } from "./YamlEdit.js";
|
|
6
|
+
import { YamlLintDiagnostic } from "./YamlLintRule.js";
|
|
7
|
+
import { builtinOptionsSchemas, builtinRules } from "./internal/rules/catalog.js";
|
|
8
|
+
import { YamlTokens } from "./YamlToken.js";
|
|
9
|
+
import { Result, Schema } from "effect";
|
|
10
|
+
|
|
11
|
+
//#region src/YamlLint.ts
|
|
12
|
+
/**
|
|
13
|
+
* One entry of the config `rules` map: a bare severity literal (the common
|
|
14
|
+
* case), `"off"` to disable, or a typed per-rule options object (the tuning
|
|
15
|
+
* case; may carry its own `severity`).
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
const YamlLintRuleSetting = Schema.Union([Schema.Literals([
|
|
20
|
+
"error",
|
|
21
|
+
"warning",
|
|
22
|
+
"off"
|
|
23
|
+
]), Schema.Record(Schema.String, Schema.Unknown)]);
|
|
24
|
+
/** Rule-aware validation of the config `rules` map (undefined = valid). */
|
|
25
|
+
const validateRulesMap = (rules) => {
|
|
26
|
+
for (const [id, entry] of Object.entries(rules)) {
|
|
27
|
+
if (id === "parse-validity") {
|
|
28
|
+
if (entry === "off" || entry === "warning") return `Rule "parse-validity" is always-on and cannot be set to "${entry}"`;
|
|
29
|
+
if (typeof entry === "object") return `Rule "parse-validity" accepts no options`;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (typeof entry === "object") {
|
|
33
|
+
const optionsSchema = builtinOptionsSchemas.get(id);
|
|
34
|
+
if (optionsSchema !== void 0) {
|
|
35
|
+
const decoded = Schema.decodeUnknownResult(optionsSchema, { onExcessProperty: "error" })(entry);
|
|
36
|
+
if (Result.isFailure(decoded)) return `Invalid options for rule "${id}": ${String(decoded.failure)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The lint configuration: a `rules` map keying rule ids (built-in or custom)
|
|
43
|
+
* to a severity literal or a typed per-rule options object.
|
|
44
|
+
*
|
|
45
|
+
* Validation is rule-aware for the built-in catalog — a mistyped option on a
|
|
46
|
+
* built-in rule fails schema validation with a typed error naming the rule,
|
|
47
|
+
* and any attempt to demote or disable the always-on `parse-validity` rule
|
|
48
|
+
* is rejected. Custom rule ids are accepted with a bare severity or an
|
|
49
|
+
* opaque options object the custom rule validates itself.
|
|
50
|
+
*
|
|
51
|
+
* Presets ship as the statics {@link YamlLintConfig.default} and
|
|
52
|
+
* {@link YamlLintConfig.relaxed} — composing one is object spread over a
|
|
53
|
+
* static, not an `extends` string resolution step (this package owns no
|
|
54
|
+
* config-file loader).
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
var YamlLintConfig = class YamlLintConfig extends Schema.Class("YamlLintConfig")({ rules: Schema.Record(Schema.String, YamlLintRuleSetting).pipe(Schema.check(Schema.makeFilter(validateRulesMap))) }) {
|
|
59
|
+
/**
|
|
60
|
+
* The default preset. Rule entries accrete as built-ins land; the
|
|
61
|
+
* `quoted-strings` rule defaults to DOUBLE quotes here (the one taste
|
|
62
|
+
* call the design pins).
|
|
63
|
+
*/
|
|
64
|
+
static default = YamlLintConfig.make({ rules: {
|
|
65
|
+
"line-length": "error",
|
|
66
|
+
"trailing-spaces": "error",
|
|
67
|
+
"empty-lines": "error",
|
|
68
|
+
"eof-newline": "error",
|
|
69
|
+
"key-duplicates": "error",
|
|
70
|
+
"quoted-strings": { quoteType: "double" },
|
|
71
|
+
truthy: "error",
|
|
72
|
+
"comments-spacing": "error",
|
|
73
|
+
"colon-spacing": "error",
|
|
74
|
+
"hyphen-spacing": "error",
|
|
75
|
+
indentation: "error"
|
|
76
|
+
} });
|
|
77
|
+
/** The relaxed preset: style rules demoted to warnings. */
|
|
78
|
+
static relaxed = YamlLintConfig.make({ rules: {
|
|
79
|
+
"line-length": "warning",
|
|
80
|
+
"trailing-spaces": "warning",
|
|
81
|
+
"empty-lines": "warning",
|
|
82
|
+
"eof-newline": "warning",
|
|
83
|
+
"key-duplicates": "error",
|
|
84
|
+
"quoted-strings": {
|
|
85
|
+
quoteType: "double",
|
|
86
|
+
severity: "warning"
|
|
87
|
+
},
|
|
88
|
+
truthy: "warning",
|
|
89
|
+
"comments-spacing": "warning",
|
|
90
|
+
"colon-spacing": "warning",
|
|
91
|
+
"hyphen-spacing": "warning",
|
|
92
|
+
indentation: "warning"
|
|
93
|
+
} });
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Split `text` into positioned lines. Line text excludes the terminator
|
|
97
|
+
* whole — for CRLF input the `\r` is part of the terminator and is stripped
|
|
98
|
+
* too, so every rule sees the same line content regardless of line-ending
|
|
99
|
+
* style. Empty input has no lines: rules that count blank lines must not see
|
|
100
|
+
* a phantom first line.
|
|
101
|
+
*/
|
|
102
|
+
const buildLines = (text) => {
|
|
103
|
+
if (text === "") return [];
|
|
104
|
+
const lines = [];
|
|
105
|
+
let offset = 0;
|
|
106
|
+
let number = 0;
|
|
107
|
+
while (offset <= text.length) {
|
|
108
|
+
const nl = text.indexOf("\n", offset);
|
|
109
|
+
const end = nl === -1 ? text.length : nl > offset && text[nl - 1] === "\r" ? nl - 1 : nl;
|
|
110
|
+
if (offset === text.length && number > 0) break;
|
|
111
|
+
lines.push({
|
|
112
|
+
text: text.slice(offset, end),
|
|
113
|
+
offset,
|
|
114
|
+
number
|
|
115
|
+
});
|
|
116
|
+
if (nl === -1) break;
|
|
117
|
+
offset = nl + 1;
|
|
118
|
+
number++;
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Build the eager lint context: tokenize once, compose once (through the
|
|
124
|
+
* RECOVERED path, so the context exists for malformed input too — the
|
|
125
|
+
* `parse-validity` rule reports what went wrong).
|
|
126
|
+
*/
|
|
127
|
+
const buildContext = (text) => {
|
|
128
|
+
const tokens = Result.match(YamlTokens.tokenize(text), {
|
|
129
|
+
onSuccess: (value) => value,
|
|
130
|
+
onFailure: (error) => {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
text,
|
|
136
|
+
lines: buildLines(text),
|
|
137
|
+
tokens,
|
|
138
|
+
document: documentFromRaw(composeFirstDocument(text, { uniqueKeys: false }), text)
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
/** Resolve the effective severity of a configured entry. */
|
|
142
|
+
const resolveSeverity = (entry) => {
|
|
143
|
+
if (entry === "error" || entry === "warning") return entry;
|
|
144
|
+
if (typeof entry === "object" && (entry.severity === "error" || entry.severity === "warning")) return entry.severity;
|
|
145
|
+
return "error";
|
|
146
|
+
};
|
|
147
|
+
const byPosition = (a, b) => a.offset - b.offset || a.length - b.length || (a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0);
|
|
148
|
+
/**
|
|
149
|
+
* The rule loop shared by `run` and `fix` over one already-built context —
|
|
150
|
+
* `fix` must not tokenize and compose the same input twice.
|
|
151
|
+
*/
|
|
152
|
+
const runRules = (ctx, rules, config) => {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const rule of rules) {
|
|
155
|
+
const alwaysOn = rule.id === "parse-validity";
|
|
156
|
+
const entry = alwaysOn ? "error" : config.rules[rule.id];
|
|
157
|
+
if (entry === void 0 || entry === "off") continue;
|
|
158
|
+
const severity = resolveSeverity(entry);
|
|
159
|
+
const options = typeof entry === "object" ? entry : void 0;
|
|
160
|
+
for (const diagnostic of rule.check(ctx, options)) out.push(alwaysOn || diagnostic.severity === severity ? diagnostic : new YamlLintDiagnostic({
|
|
161
|
+
...diagnostic,
|
|
162
|
+
severity
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
return out.sort(byPosition);
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Linting statics. Not instantiable.
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* Pure and synchronous throughout — the lint engine is the pure half only:
|
|
172
|
+
* strings in, diagnostics or a fixed string out. File discovery, config-file
|
|
173
|
+
* loading and autofix-to-disk belong to a consumer's tier, not here.
|
|
174
|
+
*
|
|
175
|
+
* @public
|
|
176
|
+
*/
|
|
177
|
+
var YamlLint = class {
|
|
178
|
+
constructor() {}
|
|
179
|
+
/**
|
|
180
|
+
* The built-in rule catalog. Custom usage is array concatenation:
|
|
181
|
+
* `YamlLint.run(text, [...YamlLint.builtins, myRule], config)`.
|
|
182
|
+
*/
|
|
183
|
+
static builtins = builtinRules;
|
|
184
|
+
/**
|
|
185
|
+
* Run `rules` over `text` under `config`, returning every finding sorted
|
|
186
|
+
* by position.
|
|
187
|
+
*
|
|
188
|
+
* Document-driven rules see the FIRST document of the stream (matching
|
|
189
|
+
* `Yaml.parse`; split the stream `Yaml.parseAll`-style to lint every
|
|
190
|
+
* document), while token- and line-driven rules cover the full source —
|
|
191
|
+
* see {@link LintContext}.
|
|
192
|
+
*
|
|
193
|
+
* A rule runs when its config entry is a severity or an options object;
|
|
194
|
+
* `"off"` and absent entries skip it — except `parse-validity`, which is
|
|
195
|
+
* always-on. The resolved severity (explicit-in-options, else the bare
|
|
196
|
+
* literal, else `"error"`) overrides what the rule emitted, again except
|
|
197
|
+
* for `parse-validity`, whose bridged engine diagnostics keep the
|
|
198
|
+
* engine's own grading.
|
|
199
|
+
*/
|
|
200
|
+
static run(text, rules, config) {
|
|
201
|
+
return runRules(buildContext(text), rules, config);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Run `rules` and apply every non-overlapping surgical fix, returning the
|
|
205
|
+
* fixed text. Fails with {@link YamlParseError} when the FIRST document
|
|
206
|
+
* of the stream has a fatal parse error — a document the engine cannot
|
|
207
|
+
* compose is not safely fixable. The gate matches `Yaml.parse`'s scope:
|
|
208
|
+
* later documents of a multi-document stream are not composed (split the
|
|
209
|
+
* stream `Yaml.parseAll`-style to lint every document), though token- and
|
|
210
|
+
* line-driven fixes still cover the full source. Fixes route exclusively through `YamlEdit.applyAll` (never a
|
|
211
|
+
* reformat), so applying them is comment-safe by construction; when two
|
|
212
|
+
* fixes overlap — or start at the same offset — the earlier one in
|
|
213
|
+
* {@link YamlLint.run} order (position, then rule id) wins and the later
|
|
214
|
+
* is dropped (its diagnostic remains reported by {@link YamlLint.run}).
|
|
215
|
+
*/
|
|
216
|
+
static fix(text, rules, config) {
|
|
217
|
+
const ctx = buildContext(text);
|
|
218
|
+
const fatal = ctx.document.errors.filter((e) => isFatalCode(e.code));
|
|
219
|
+
if (fatal.length > 0) return Result.fail(new YamlParseError({
|
|
220
|
+
diagnostics: fatal,
|
|
221
|
+
input: text
|
|
222
|
+
}));
|
|
223
|
+
const fixes = [];
|
|
224
|
+
let lastEnd = -1;
|
|
225
|
+
let lastOffset = -1;
|
|
226
|
+
for (const diagnostic of runRules(ctx, rules, config)) {
|
|
227
|
+
const fix = diagnostic.fix;
|
|
228
|
+
if (fix === void 0) continue;
|
|
229
|
+
if (fix.offset < lastEnd || fix.offset === lastOffset) continue;
|
|
230
|
+
fixes.push(fix);
|
|
231
|
+
lastEnd = fix.offset + fix.length;
|
|
232
|
+
lastOffset = fix.offset;
|
|
233
|
+
}
|
|
234
|
+
return Result.succeed(YamlEdit.applyAll(text, fixes));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
//#endregion
|
|
239
|
+
export { YamlLint, YamlLintConfig, YamlLintRuleSetting };
|
package/YamlLintRule.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { YamlEdit } from "./YamlEdit.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/YamlLintRule.ts
|
|
5
|
+
/**
|
|
6
|
+
* Lint diagnostic severities. `"off"` is a config-level disable only and
|
|
7
|
+
* never reaches a diagnostic — a rule set to `"off"` is not run.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
const YamlLintSeverity = Schema.Literals(["error", "warning"]);
|
|
12
|
+
/**
|
|
13
|
+
* A single lint finding: the reporting rule, its severity, a positioned span
|
|
14
|
+
* and optionally a surgical fix.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately separate from the engine's `YamlDiagnostic`: that type is the
|
|
17
|
+
* lexer/parser/composer/stringifier error-code union and the single source
|
|
18
|
+
* of truth for engine fatality — it carries no severity and no fix, and
|
|
19
|
+
* lint-layer concerns must not pollute it. The `parse-validity` rule bridges
|
|
20
|
+
* the two by mapping engine diagnostics into this shape.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
var YamlLintDiagnostic = class extends Schema.Class("YamlLintDiagnostic")({
|
|
25
|
+
rule: Schema.String,
|
|
26
|
+
severity: YamlLintSeverity,
|
|
27
|
+
message: Schema.String,
|
|
28
|
+
offset: Schema.Number,
|
|
29
|
+
length: Schema.Number,
|
|
30
|
+
line: Schema.Number,
|
|
31
|
+
character: Schema.Number,
|
|
32
|
+
fix: Schema.optionalKey(YamlEdit)
|
|
33
|
+
}) {};
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { YamlLintDiagnostic, YamlLintSeverity };
|
package/YamlNode.js
CHANGED
|
@@ -49,9 +49,17 @@ const ScalarChomp = Schema.Literals([
|
|
|
49
49
|
* - `style` — the scalar presentation style in the source document.
|
|
50
50
|
* - `tag` — optional explicit YAML tag (e.g. `!!str`, `!!int`).
|
|
51
51
|
* - `anchor` — optional anchor name for aliasing.
|
|
52
|
-
* - `
|
|
52
|
+
* - `commentBefore` — own-line comment text directly above the node
|
|
53
|
+
* (multiple consecutive comment lines join with `\n`).
|
|
54
|
+
* - `comment` — trailing comment text on the node's line (strictly trailing;
|
|
55
|
+
* own-line comments live on `commentBefore`).
|
|
56
|
+
* - `spaceBefore` — `true` when a blank line precedes the node (and its
|
|
57
|
+
* `commentBefore` block, when present) in the source.
|
|
53
58
|
* - `chomp` — block-scalar chomping indicator, when the scalar is a block
|
|
54
59
|
* scalar.
|
|
60
|
+
* - `blockIndent` — the EXPLICIT indentation-indicator digit from a block
|
|
61
|
+
* scalar's header (`|2`, `>1+`), when the source spelled one; absent when
|
|
62
|
+
* the header let the reader auto-detect the indent.
|
|
55
63
|
* - `raw` — the raw source text, preserved when it differs from the resolved
|
|
56
64
|
* value in a way stringification needs to know about.
|
|
57
65
|
* - `sourceMultiline` — `true` when the source span covers two or more lines;
|
|
@@ -65,8 +73,11 @@ var YamlScalar = class extends Schema.TaggedClass()("YamlScalar", {
|
|
|
65
73
|
tag: Schema.optionalKey(Schema.String),
|
|
66
74
|
style: ScalarStyle,
|
|
67
75
|
anchor: Schema.optionalKey(Schema.String),
|
|
76
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
68
77
|
comment: Schema.optionalKey(Schema.String),
|
|
78
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
69
79
|
chomp: Schema.optionalKey(ScalarChomp),
|
|
80
|
+
blockIndent: Schema.optionalKey(Schema.Number),
|
|
70
81
|
raw: Schema.optionalKey(Schema.String),
|
|
71
82
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
72
83
|
offset: Schema.Number,
|
|
@@ -109,12 +120,19 @@ var YamlScalar = class extends Schema.TaggedClass()("YamlScalar", {
|
|
|
109
120
|
* A YAML alias AST node, referencing a previously defined anchor by name
|
|
110
121
|
* (without the leading `*`).
|
|
111
122
|
*
|
|
123
|
+
* Carries the same comment triple as every other node class — see
|
|
124
|
+
* {@link YamlScalar} for the field semantics. An alias is a node like any
|
|
125
|
+
* other and a comment can legally sit above or after one.
|
|
126
|
+
*
|
|
112
127
|
* @public
|
|
113
128
|
*/
|
|
114
129
|
var YamlAlias = class extends Schema.TaggedClass()("YamlAlias", {
|
|
115
130
|
name: Schema.String,
|
|
116
131
|
offset: Schema.Number,
|
|
117
|
-
length: Schema.Number
|
|
132
|
+
length: Schema.Number,
|
|
133
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
134
|
+
comment: Schema.optionalKey(Schema.String),
|
|
135
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean)
|
|
118
136
|
}) {
|
|
119
137
|
/** See `YamlScalar.find`. Pure. */
|
|
120
138
|
find(path) {
|
|
@@ -157,18 +175,29 @@ const YamlNode = Schema.suspend(() => Schema.Union([
|
|
|
157
175
|
* A YAML key-value pair AST node, representing one entry within a mapping.
|
|
158
176
|
* `value` is `null` when absent (e.g. `key:` with no value).
|
|
159
177
|
*
|
|
178
|
+
* A pair carries **no comment fields**. Comments belong to the pair's `key`
|
|
179
|
+
* and `value` nodes, which have one comment slot each: an own-line comment
|
|
180
|
+
* above the entry leads the `key`, and a trailing comment on the entry's line
|
|
181
|
+
* follows the `value`. Two slots rather than one is what lets `a: # kc` keep
|
|
182
|
+
* its comment where the author wrote it instead of relocating it onto the
|
|
183
|
+
* value's line.
|
|
184
|
+
*
|
|
160
185
|
* @public
|
|
161
186
|
*/
|
|
162
187
|
var YamlPair = class extends Schema.TaggedClass()("YamlPair", {
|
|
163
188
|
key: Schema.suspend(() => YamlNode),
|
|
164
|
-
value: Schema.NullOr(Schema.suspend(() => YamlNode))
|
|
165
|
-
comment: Schema.optionalKey(Schema.String)
|
|
189
|
+
value: Schema.NullOr(Schema.suspend(() => YamlNode))
|
|
166
190
|
}) {};
|
|
167
191
|
/**
|
|
168
192
|
* A YAML mapping AST node, representing a collection of {@link YamlPair}
|
|
169
193
|
* entries.
|
|
170
194
|
*
|
|
171
195
|
* - `style` — the presentation style: `"block"` or `"flow"`.
|
|
196
|
+
* - `commentBefore` — own-line comment text directly above the mapping.
|
|
197
|
+
* - `comment` — trailing comment text: own-line comment lines after the
|
|
198
|
+
* mapping's last entry (still at the mapping's item indent), or a same-line
|
|
199
|
+
* trailing comment for a flow mapping.
|
|
200
|
+
* - `spaceBefore` — `true` when a blank line precedes the mapping.
|
|
172
201
|
* - `sourceMultiline` — `true` when the source span covers two or more lines;
|
|
173
202
|
* used by the canonical stringifier. Absent on synthetic nodes.
|
|
174
203
|
*
|
|
@@ -179,7 +208,9 @@ var YamlMap = class extends Schema.TaggedClass()("YamlMap", {
|
|
|
179
208
|
tag: Schema.optionalKey(Schema.String),
|
|
180
209
|
anchor: Schema.optionalKey(Schema.String),
|
|
181
210
|
style: CollectionStyle,
|
|
211
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
182
212
|
comment: Schema.optionalKey(Schema.String),
|
|
213
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
183
214
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
184
215
|
offset: Schema.Number,
|
|
185
216
|
length: Schema.Number
|
|
@@ -205,6 +236,12 @@ var YamlMap = class extends Schema.TaggedClass()("YamlMap", {
|
|
|
205
236
|
* A YAML sequence AST node, representing an ordered list of
|
|
206
237
|
* {@link (YamlNode:type)} values.
|
|
207
238
|
*
|
|
239
|
+
* - `commentBefore` — own-line comment text directly above the sequence.
|
|
240
|
+
* - `comment` — trailing comment text: own-line comment lines after the
|
|
241
|
+
* sequence's last item (still at the sequence's item indent), or a
|
|
242
|
+
* same-line trailing comment for a flow sequence.
|
|
243
|
+
* - `spaceBefore` — `true` when a blank line precedes the sequence.
|
|
244
|
+
*
|
|
208
245
|
* @public
|
|
209
246
|
*/
|
|
210
247
|
var YamlSeq = class extends Schema.TaggedClass()("YamlSeq", {
|
|
@@ -212,7 +249,9 @@ var YamlSeq = class extends Schema.TaggedClass()("YamlSeq", {
|
|
|
212
249
|
tag: Schema.optionalKey(Schema.String),
|
|
213
250
|
anchor: Schema.optionalKey(Schema.String),
|
|
214
251
|
style: CollectionStyle,
|
|
252
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
215
253
|
comment: Schema.optionalKey(Schema.String),
|
|
254
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
216
255
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
217
256
|
offset: Schema.Number,
|
|
218
257
|
length: Schema.Number
|
package/YamlToken.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { lexAll } from "./internal/lexer.js";
|
|
2
|
+
import { Result, Schema, Stream } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/YamlToken.ts
|
|
5
|
+
/**
|
|
6
|
+
* The 22 lexical token kinds produced by the YAML tokenizer.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
const YamlTokenKind = Schema.Literals([
|
|
11
|
+
"document-start",
|
|
12
|
+
"document-end",
|
|
13
|
+
"directive",
|
|
14
|
+
"tag",
|
|
15
|
+
"anchor",
|
|
16
|
+
"alias",
|
|
17
|
+
"scalar",
|
|
18
|
+
"block-map-start",
|
|
19
|
+
"block-map-key",
|
|
20
|
+
"block-map-value",
|
|
21
|
+
"block-seq-start",
|
|
22
|
+
"block-seq-entry",
|
|
23
|
+
"flow-map-start",
|
|
24
|
+
"flow-map-end",
|
|
25
|
+
"flow-seq-start",
|
|
26
|
+
"flow-seq-end",
|
|
27
|
+
"flow-separator",
|
|
28
|
+
"newline",
|
|
29
|
+
"whitespace",
|
|
30
|
+
"comment",
|
|
31
|
+
"byte-order-mark",
|
|
32
|
+
"error"
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* A single positioned YAML lexical token.
|
|
36
|
+
*
|
|
37
|
+
* - `kind` — the {@link (YamlTokenKind:type)}.
|
|
38
|
+
* - `text` — the raw source slice the token covers; the position-fidelity
|
|
39
|
+
* invariant is `source.slice(offset, offset + length) === text`.
|
|
40
|
+
* - `offset` / `length` — the token's span in UTF-16 code units.
|
|
41
|
+
* - `line` / `character` — the zero-based position of the token's start,
|
|
42
|
+
* matching the `YamlDiagnostic` position vocabulary.
|
|
43
|
+
*
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
var YamlToken = class extends Schema.Class("YamlToken")({
|
|
47
|
+
kind: YamlTokenKind,
|
|
48
|
+
text: Schema.String,
|
|
49
|
+
offset: Schema.Number,
|
|
50
|
+
length: Schema.Number,
|
|
51
|
+
line: Schema.Number,
|
|
52
|
+
character: Schema.Number
|
|
53
|
+
}) {};
|
|
54
|
+
/**
|
|
55
|
+
* Promote the internal lexer tokens to the public shape.
|
|
56
|
+
*
|
|
57
|
+
* `line`/`character` are DERIVED from each token's offset against a
|
|
58
|
+
* line-start index rather than copied from the internal token: the internal
|
|
59
|
+
* `column` is a CST-parser vocabulary that carries the construct's INDENT on
|
|
60
|
+
* the synthetic `block-map-start`/`block-seq-start` markers, not the token's
|
|
61
|
+
* own position — the public surface promises the position. `text` is the RAW
|
|
62
|
+
* source slice for the same reason: the internal `value` is the PROCESSED
|
|
63
|
+
* form on quoted scalars (content without the quotes), while the public
|
|
64
|
+
* contract is byte fidelity — the corpus conformance suite pins the tokens
|
|
65
|
+
* tiling the source exactly.
|
|
66
|
+
*/
|
|
67
|
+
const promoteAll = (text, tokens) => {
|
|
68
|
+
const lineStarts = [0];
|
|
69
|
+
for (let i = 0; i < text.length; i++) if (text[i] === "\n") lineStarts.push(i + 1);
|
|
70
|
+
let line = 0;
|
|
71
|
+
return tokens.map((token) => {
|
|
72
|
+
if (token.offset < lineStarts[line]) line = 0;
|
|
73
|
+
while (line + 1 < lineStarts.length && lineStarts[line + 1] <= token.offset) line++;
|
|
74
|
+
return new YamlToken({
|
|
75
|
+
kind: token.kind,
|
|
76
|
+
text: text.slice(token.offset, token.offset + token.length),
|
|
77
|
+
offset: token.offset,
|
|
78
|
+
length: token.length,
|
|
79
|
+
line,
|
|
80
|
+
character: token.offset - lineStarts[line]
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Tokenizer statics. Not instantiable.
|
|
86
|
+
*
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
var YamlTokens = class YamlTokens {
|
|
90
|
+
constructor() {}
|
|
91
|
+
/**
|
|
92
|
+
* Tokenize YAML text into the full positioned token array — the sync
|
|
93
|
+
* `Result` primitive (tokenizing is a pure batch transform; the
|
|
94
|
+
* {@link YamlTokens.stream} form is derived from this one, per the kit's
|
|
95
|
+
* sync-primitive policy).
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* The failure channel is **reserved** (for future input-hardening guards)
|
|
99
|
+
* and never fires today: the lexer is total, and lexical errors surface as
|
|
100
|
+
* `"error"`-kind tokens **in the success array** so that linting can run
|
|
101
|
+
* on malformed input — the `parse-validity` lint rule exists precisely for
|
|
102
|
+
* documents that do not parse. Do not "fix" this method to fail on
|
|
103
|
+
* `"error"` tokens; that would make malformed documents unlintable.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { YamlTokens } from "@effected/yaml";
|
|
108
|
+
* import { Result } from "effect";
|
|
109
|
+
*
|
|
110
|
+
* const result = YamlTokens.tokenize("a: 1\n");
|
|
111
|
+
* if (Result.isSuccess(result)) {
|
|
112
|
+
* result.success.map((t) => t.kind); // ["scalar", "block-map-start", ...]
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
static tokenize(text) {
|
|
117
|
+
return Result.succeed(promoteAll(text, lexAll(text)));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Tokenize YAML text as a lazy `Stream` of tokens — the derived form of
|
|
121
|
+
* {@link YamlTokens.tokenize} for genuinely incremental (SAX-style)
|
|
122
|
+
* consumers, parallel to `YamlVisitor.visit`.
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* Derived from the sync primitive, so it shares its contract: lexical
|
|
126
|
+
* errors arrive as `"error"`-kind tokens in the stream, never as a stream
|
|
127
|
+
* failure. (The primitive's reserved failure channel would surface as a
|
|
128
|
+
* defect here; it never fires today.)
|
|
129
|
+
*/
|
|
130
|
+
static stream(text) {
|
|
131
|
+
return Stream.suspend(() => Result.match(YamlTokens.tokenize(text), {
|
|
132
|
+
onSuccess: (tokens) => Stream.fromIterable(tokens),
|
|
133
|
+
onFailure: (error) => Stream.die(error)
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
export { YamlToken, YamlTokenKind, YamlTokens };
|
package/YamlVisitor.js
CHANGED
|
@@ -76,12 +76,19 @@ function* walkDocument(doc, text) {
|
|
|
76
76
|
depth,
|
|
77
77
|
diagnostic: YamlDiagnostic.fromRaw(raw, text)
|
|
78
78
|
});
|
|
79
|
-
if (doc.
|
|
79
|
+
if (doc.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
80
80
|
path,
|
|
81
81
|
depth,
|
|
82
|
-
text: doc.
|
|
82
|
+
text: doc.commentBefore,
|
|
83
|
+
placement: "leading"
|
|
83
84
|
});
|
|
84
85
|
if (doc.contents !== null) yield* walkNode(doc.contents, path, depth);
|
|
86
|
+
if (doc.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
87
|
+
path,
|
|
88
|
+
depth,
|
|
89
|
+
text: doc.comment,
|
|
90
|
+
placement: "trailing"
|
|
91
|
+
});
|
|
85
92
|
yield YamlVisitorEvent.DocumentEnd({
|
|
86
93
|
path,
|
|
87
94
|
depth
|
|
@@ -89,10 +96,17 @@ function* walkDocument(doc, text) {
|
|
|
89
96
|
}
|
|
90
97
|
function* walkNode(node, path, depth) {
|
|
91
98
|
if (node instanceof YamlScalar) {
|
|
99
|
+
if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
100
|
+
path,
|
|
101
|
+
depth,
|
|
102
|
+
text: node.commentBefore,
|
|
103
|
+
placement: "leading"
|
|
104
|
+
});
|
|
92
105
|
if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
93
106
|
path,
|
|
94
107
|
depth,
|
|
95
|
-
text: node.comment
|
|
108
|
+
text: node.comment,
|
|
109
|
+
placement: "trailing"
|
|
96
110
|
});
|
|
97
111
|
yield YamlVisitorEvent.Scalar({
|
|
98
112
|
path,
|
|
@@ -102,16 +116,36 @@ function* walkNode(node, path, depth) {
|
|
|
102
116
|
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
103
117
|
...node.anchor !== void 0 ? { anchor: node.anchor } : {}
|
|
104
118
|
});
|
|
105
|
-
} else if (node instanceof YamlAlias)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
} else if (node instanceof YamlAlias) {
|
|
120
|
+
if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
121
|
+
path,
|
|
122
|
+
depth,
|
|
123
|
+
text: node.commentBefore,
|
|
124
|
+
placement: "leading"
|
|
125
|
+
});
|
|
126
|
+
if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
127
|
+
path,
|
|
128
|
+
depth,
|
|
129
|
+
text: node.comment,
|
|
130
|
+
placement: "trailing"
|
|
131
|
+
});
|
|
132
|
+
yield YamlVisitorEvent.Alias({
|
|
133
|
+
path,
|
|
134
|
+
depth,
|
|
135
|
+
name: node.name
|
|
136
|
+
});
|
|
137
|
+
} else if (node instanceof YamlMap) {
|
|
138
|
+
if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
139
|
+
path,
|
|
140
|
+
depth,
|
|
141
|
+
text: node.commentBefore,
|
|
142
|
+
placement: "leading"
|
|
143
|
+
});
|
|
111
144
|
if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
112
145
|
path,
|
|
113
146
|
depth,
|
|
114
|
-
text: node.comment
|
|
147
|
+
text: node.comment,
|
|
148
|
+
placement: "trailing"
|
|
115
149
|
});
|
|
116
150
|
yield YamlVisitorEvent.MapStart({
|
|
117
151
|
path,
|
|
@@ -126,10 +160,17 @@ function* walkNode(node, path, depth) {
|
|
|
126
160
|
depth
|
|
127
161
|
});
|
|
128
162
|
} else if (node instanceof YamlSeq) {
|
|
163
|
+
if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
164
|
+
path,
|
|
165
|
+
depth,
|
|
166
|
+
text: node.commentBefore,
|
|
167
|
+
placement: "leading"
|
|
168
|
+
});
|
|
129
169
|
if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
130
170
|
path,
|
|
131
171
|
depth,
|
|
132
|
-
text: node.comment
|
|
172
|
+
text: node.comment,
|
|
173
|
+
placement: "trailing"
|
|
133
174
|
});
|
|
134
175
|
yield YamlVisitorEvent.SeqStart({
|
|
135
176
|
path,
|
|
@@ -153,11 +194,6 @@ function* walkPair(pair, parentPath, depth) {
|
|
|
153
194
|
const resolvedValue = pair.value instanceof YamlScalar ? pair.value.value : null;
|
|
154
195
|
const keySegment = typeof resolvedKey === "string" ? resolvedKey : typeof resolvedKey === "number" ? resolvedKey : String(resolvedKey);
|
|
155
196
|
const pairPath = [...parentPath, keySegment];
|
|
156
|
-
if (pair.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
157
|
-
path: pairPath,
|
|
158
|
-
depth,
|
|
159
|
-
text: pair.comment
|
|
160
|
-
});
|
|
161
197
|
yield YamlVisitorEvent.Pair({
|
|
162
198
|
path: pairPath,
|
|
163
199
|
depth,
|