@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.
Files changed (39) hide show
  1. package/README.md +80 -7
  2. package/Yaml.js +80 -32
  3. package/YamlDocument.js +23 -2
  4. package/YamlFormat.js +162 -21
  5. package/YamlLint.js +239 -0
  6. package/YamlLintRule.js +36 -0
  7. package/YamlNode.js +43 -4
  8. package/YamlToken.js +139 -0
  9. package/YamlVisitor.js +52 -16
  10. package/index.d.ts +446 -12
  11. package/index.js +4 -1
  12. package/internal/composer/anchors.js +2 -27
  13. package/internal/composer/block.js +291 -67
  14. package/internal/composer/comments.js +217 -0
  15. package/internal/composer/document.js +141 -14
  16. package/internal/composer/flow.js +103 -10
  17. package/internal/composer/scalars.js +48 -11
  18. package/internal/composer/state.js +16 -2
  19. package/internal/cst-parser.js +2 -1
  20. package/internal/diagnostics.js +12 -3
  21. package/internal/fold.js +22 -10
  22. package/internal/rules/catalog.js +39 -0
  23. package/internal/rules/colon-spacing.js +77 -0
  24. package/internal/rules/comments-spacing.js +68 -0
  25. package/internal/rules/document-end.js +65 -0
  26. package/internal/rules/document-start.js +64 -0
  27. package/internal/rules/empty-lines.js +70 -0
  28. package/internal/rules/eof-newline.js +33 -0
  29. package/internal/rules/hyphen-spacing.js +52 -0
  30. package/internal/rules/indentation.js +103 -0
  31. package/internal/rules/key-duplicates.js +49 -0
  32. package/internal/rules/line-length.js +36 -0
  33. package/internal/rules/parse-validity.js +33 -0
  34. package/internal/rules/quoted-strings.js +81 -0
  35. package/internal/rules/trailing-spaces.js +39 -0
  36. package/internal/rules/truthy.js +84 -0
  37. package/internal/rules/util.js +92 -0
  38. package/internal/stringifier.js +342 -54
  39. 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 };
@@ -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
- * - `comment` — optional trailing or leading comment text.
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.comment !== void 0) yield YamlVisitorEvent.Comment({
79
+ if (doc.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
80
80
  path,
81
81
  depth,
82
- text: doc.comment
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) yield YamlVisitorEvent.Alias({
106
- path,
107
- depth,
108
- name: node.name
109
- });
110
- else if (node instanceof YamlMap) {
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,