@effected/yaml 0.7.0 → 0.8.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 +78 -7
- package/Yaml.js +80 -32
- package/YamlDocument.js +22 -3
- package/YamlFormat.js +170 -19
- package/YamlLint.js +239 -0
- package/YamlLintRule.js +36 -0
- package/YamlNode.js +36 -2
- package/YamlToken.js +139 -0
- package/YamlVisitor.js +40 -5
- package/index.d.ts +439 -12
- package/index.js +4 -1
- package/internal/composer/anchors.js +2 -27
- package/internal/composer/block.js +246 -58
- package/internal/composer/comments.js +187 -0
- package/internal/composer/document.js +49 -12
- package/internal/composer/flow.js +82 -9
- 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 +266 -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,
|
|
@@ -157,18 +168,31 @@ const YamlNode = Schema.suspend(() => Schema.Union([
|
|
|
157
168
|
* A YAML key-value pair AST node, representing one entry within a mapping.
|
|
158
169
|
* `value` is `null` when absent (e.g. `key:` with no value).
|
|
159
170
|
*
|
|
171
|
+
* - `commentBefore` — own-line comment text directly above the pair's key
|
|
172
|
+
* (multiple consecutive comment lines join with `\n`).
|
|
173
|
+
* - `comment` — trailing comment text on the pair's line (strictly trailing).
|
|
174
|
+
* - `spaceBefore` — `true` when a blank line precedes the pair (and its
|
|
175
|
+
* `commentBefore` block, when present) in the source.
|
|
176
|
+
*
|
|
160
177
|
* @public
|
|
161
178
|
*/
|
|
162
179
|
var YamlPair = class extends Schema.TaggedClass()("YamlPair", {
|
|
163
180
|
key: Schema.suspend(() => YamlNode),
|
|
164
181
|
value: Schema.NullOr(Schema.suspend(() => YamlNode)),
|
|
165
|
-
|
|
182
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
183
|
+
comment: Schema.optionalKey(Schema.String),
|
|
184
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean)
|
|
166
185
|
}) {};
|
|
167
186
|
/**
|
|
168
187
|
* A YAML mapping AST node, representing a collection of {@link YamlPair}
|
|
169
188
|
* entries.
|
|
170
189
|
*
|
|
171
190
|
* - `style` — the presentation style: `"block"` or `"flow"`.
|
|
191
|
+
* - `commentBefore` — own-line comment text directly above the mapping.
|
|
192
|
+
* - `comment` — trailing comment text: own-line comment lines after the
|
|
193
|
+
* mapping's last entry (still at the mapping's item indent), or a same-line
|
|
194
|
+
* trailing comment for a flow mapping.
|
|
195
|
+
* - `spaceBefore` — `true` when a blank line precedes the mapping.
|
|
172
196
|
* - `sourceMultiline` — `true` when the source span covers two or more lines;
|
|
173
197
|
* used by the canonical stringifier. Absent on synthetic nodes.
|
|
174
198
|
*
|
|
@@ -179,7 +203,9 @@ var YamlMap = class extends Schema.TaggedClass()("YamlMap", {
|
|
|
179
203
|
tag: Schema.optionalKey(Schema.String),
|
|
180
204
|
anchor: Schema.optionalKey(Schema.String),
|
|
181
205
|
style: CollectionStyle,
|
|
206
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
182
207
|
comment: Schema.optionalKey(Schema.String),
|
|
208
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
183
209
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
184
210
|
offset: Schema.Number,
|
|
185
211
|
length: Schema.Number
|
|
@@ -205,6 +231,12 @@ var YamlMap = class extends Schema.TaggedClass()("YamlMap", {
|
|
|
205
231
|
* A YAML sequence AST node, representing an ordered list of
|
|
206
232
|
* {@link (YamlNode:type)} values.
|
|
207
233
|
*
|
|
234
|
+
* - `commentBefore` — own-line comment text directly above the sequence.
|
|
235
|
+
* - `comment` — trailing comment text: own-line comment lines after the
|
|
236
|
+
* sequence's last item (still at the sequence's item indent), or a
|
|
237
|
+
* same-line trailing comment for a flow sequence.
|
|
238
|
+
* - `spaceBefore` — `true` when a blank line precedes the sequence.
|
|
239
|
+
*
|
|
208
240
|
* @public
|
|
209
241
|
*/
|
|
210
242
|
var YamlSeq = class extends Schema.TaggedClass()("YamlSeq", {
|
|
@@ -212,7 +244,9 @@ var YamlSeq = class extends Schema.TaggedClass()("YamlSeq", {
|
|
|
212
244
|
tag: Schema.optionalKey(Schema.String),
|
|
213
245
|
anchor: Schema.optionalKey(Schema.String),
|
|
214
246
|
style: CollectionStyle,
|
|
247
|
+
commentBefore: Schema.optionalKey(Schema.String),
|
|
215
248
|
comment: Schema.optionalKey(Schema.String),
|
|
249
|
+
spaceBefore: Schema.optionalKey(Schema.Boolean),
|
|
216
250
|
sourceMultiline: Schema.optionalKey(Schema.Boolean),
|
|
217
251
|
offset: Schema.Number,
|
|
218
252
|
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
|
@@ -79,9 +79,16 @@ function* walkDocument(doc, text) {
|
|
|
79
79
|
if (doc.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
80
80
|
path,
|
|
81
81
|
depth,
|
|
82
|
-
text: doc.comment
|
|
82
|
+
text: doc.comment,
|
|
83
|
+
placement: "leading"
|
|
83
84
|
});
|
|
84
85
|
if (doc.contents !== null) yield* walkNode(doc.contents, path, depth);
|
|
86
|
+
if (doc.commentAfter !== void 0) yield YamlVisitorEvent.Comment({
|
|
87
|
+
path,
|
|
88
|
+
depth,
|
|
89
|
+
text: doc.commentAfter,
|
|
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,
|
|
@@ -108,10 +122,17 @@ function* walkNode(node, path, depth) {
|
|
|
108
122
|
name: node.name
|
|
109
123
|
});
|
|
110
124
|
else if (node instanceof YamlMap) {
|
|
125
|
+
if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
126
|
+
path,
|
|
127
|
+
depth,
|
|
128
|
+
text: node.commentBefore,
|
|
129
|
+
placement: "leading"
|
|
130
|
+
});
|
|
111
131
|
if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
112
132
|
path,
|
|
113
133
|
depth,
|
|
114
|
-
text: node.comment
|
|
134
|
+
text: node.comment,
|
|
135
|
+
placement: "trailing"
|
|
115
136
|
});
|
|
116
137
|
yield YamlVisitorEvent.MapStart({
|
|
117
138
|
path,
|
|
@@ -126,10 +147,17 @@ function* walkNode(node, path, depth) {
|
|
|
126
147
|
depth
|
|
127
148
|
});
|
|
128
149
|
} else if (node instanceof YamlSeq) {
|
|
150
|
+
if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
151
|
+
path,
|
|
152
|
+
depth,
|
|
153
|
+
text: node.commentBefore,
|
|
154
|
+
placement: "leading"
|
|
155
|
+
});
|
|
129
156
|
if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
130
157
|
path,
|
|
131
158
|
depth,
|
|
132
|
-
text: node.comment
|
|
159
|
+
text: node.comment,
|
|
160
|
+
placement: "trailing"
|
|
133
161
|
});
|
|
134
162
|
yield YamlVisitorEvent.SeqStart({
|
|
135
163
|
path,
|
|
@@ -153,10 +181,17 @@ function* walkPair(pair, parentPath, depth) {
|
|
|
153
181
|
const resolvedValue = pair.value instanceof YamlScalar ? pair.value.value : null;
|
|
154
182
|
const keySegment = typeof resolvedKey === "string" ? resolvedKey : typeof resolvedKey === "number" ? resolvedKey : String(resolvedKey);
|
|
155
183
|
const pairPath = [...parentPath, keySegment];
|
|
184
|
+
if (pair.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
|
|
185
|
+
path: pairPath,
|
|
186
|
+
depth,
|
|
187
|
+
text: pair.commentBefore,
|
|
188
|
+
placement: "leading"
|
|
189
|
+
});
|
|
156
190
|
if (pair.comment !== void 0) yield YamlVisitorEvent.Comment({
|
|
157
191
|
path: pairPath,
|
|
158
192
|
depth,
|
|
159
|
-
text: pair.comment
|
|
193
|
+
text: pair.comment,
|
|
194
|
+
placement: "trailing"
|
|
160
195
|
});
|
|
161
196
|
yield YamlVisitorEvent.Pair({
|
|
162
197
|
path: pairPath,
|