@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
@@ -1,5 +1,6 @@
1
1
  import { YamlScalar } from "../../YamlNode.js";
2
2
  import { registerAnchor } from "./anchors.js";
3
+ import { rawCommentText } from "./comments.js";
3
4
  import { lineCol } from "./state.js";
4
5
  import { resolveTagHandle } from "./tags.js";
5
6
 
@@ -98,13 +99,42 @@ function getScalarStyle(node) {
98
99
  */
99
100
  function getBlockChomp(node) {
100
101
  if (node.type !== "block-scalar") return void 0;
101
- const src = node.source.trimStart();
102
- const headerEnd = src.indexOf("\n");
103
- const header = headerEnd === -1 ? src : src.slice(0, headerEnd);
104
- if (header.includes("+")) return "keep";
105
- if (header.includes("-")) return "strip";
102
+ const indicators = node.source.trimStart().match(/^[|>]([0-9+-]*)/)?.[1] ?? "";
103
+ if (indicators.includes("+")) return "keep";
104
+ if (indicators.includes("-")) return "strip";
106
105
  return "clip";
107
106
  }
107
+ /**
108
+ * Extracts the EXPLICIT indentation-indicator digit from a block scalar's
109
+ * header (`|2`, `>1+`), or undefined when the header carries none (the
110
+ * reader auto-detects) or the node is not a block scalar. Inspects only the
111
+ * indicator run — see {@link getBlockChomp} for why the rest of the header
112
+ * line (a comment can contain digits) must not be read.
113
+ */
114
+ function getBlockIndent(node) {
115
+ if (node.type !== "block-scalar") return void 0;
116
+ const digit = (node.source.trimStart().match(/^[|>]([0-9+-]*)/)?.[1] ?? "").match(/[1-9]/)?.[0];
117
+ return digit === void 0 ? void 0 : Number(digit);
118
+ }
119
+ /**
120
+ * The trailing comment on a block scalar's HEADER line (`key: | # c`,
121
+ * `- >-2 # c`) as stored raw-slice text, or `undefined` when the header
122
+ * carries none. The header is the only line of a block scalar that can carry
123
+ * a `#` comment — body lines absorb `#` as content — and the lexer packs the
124
+ * whole scalar (header comment included) into one CST token, so the comment
125
+ * never surfaces as a sibling comment token for the ordinary attribution
126
+ * pass (#341).
127
+ */
128
+ function blockScalarHeaderComment(cst) {
129
+ const src = cst.source.trimStart();
130
+ const nl = src.search(/[\r\n]/);
131
+ const header = nl < 0 ? src : src.slice(0, nl);
132
+ const hash = header.indexOf("#");
133
+ if (hash < 1) return void 0;
134
+ const before = header[hash - 1];
135
+ if (before !== " " && before !== " ") return void 0;
136
+ return rawCommentText(header.slice(hash));
137
+ }
108
138
  function getScalarValue(node, fullText) {
109
139
  if (node.type === "block-scalar") return decodeBlockScalar(node.source, fullText, node.offset);
110
140
  const style = getScalarStyle(node);
@@ -790,18 +820,22 @@ function decodeBlockScalar(raw, fullText, nodeOffset) {
790
820
  }
791
821
  if (hadContent || trailingNewlines.length > 0) {
792
822
  if (chomp === "keep") {
793
- result += "\n";
823
+ if (hadContent) result += "\n";
794
824
  for (const _nl of trailingNewlines) result += "\n";
795
- } else if (chomp !== "strip") result += "\n";
825
+ } else if (chomp !== "strip") {
826
+ if (hadContent) result += "\n";
827
+ }
796
828
  }
797
829
  value = result;
798
830
  } else {
799
831
  value = lines.join("\n");
800
832
  if (lines.length > 0 || trailingNewlines.length > 0) {
801
833
  if (chomp === "keep") {
802
- value += "\n";
834
+ if (lines.length > 0) value += "\n";
803
835
  for (const _nl of trailingNewlines) value += "\n";
804
- } else if (chomp !== "strip") value += "\n";
836
+ } else if (chomp !== "strip") {
837
+ if (lines.length > 0) value += "\n";
838
+ }
805
839
  }
806
840
  }
807
841
  return value;
@@ -860,6 +894,8 @@ function makeScalar(cst, state, meta) {
860
894
  const rawValue = getScalarValue(cst, state.text);
861
895
  const value = resolveScalar(rawValue, style, meta?.tag, state);
862
896
  const chomp = getBlockChomp(cst);
897
+ const blockIndent = getBlockIndent(cst);
898
+ const comment = (style === "block-literal" || style === "block-folded" ? blockScalarHeaderComment(cst) : void 0) ?? meta?.comment;
863
899
  const needsRaw = style === "plain" && typeof value !== "string" && value !== void 0 && shouldPreserveRaw(rawValue, value);
864
900
  const scalar = new YamlScalar({
865
901
  value,
@@ -868,8 +904,9 @@ function makeScalar(cst, state, meta) {
868
904
  length: cst.length,
869
905
  ...meta?.tag !== void 0 ? { tag: meta.tag } : {},
870
906
  ...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
871
- ...meta?.comment !== void 0 ? { comment: meta.comment } : {},
907
+ ...comment !== void 0 ? { comment } : {},
872
908
  ...chomp !== void 0 ? { chomp } : {},
909
+ ...blockIndent !== void 0 ? { blockIndent } : {},
873
910
  ...needsRaw ? { raw: rawValue } : {}
874
911
  });
875
912
  if (meta?.anchor) registerAnchor(scalar, meta.anchor, state, cst.offset);
@@ -894,4 +931,4 @@ function shouldPreserveRaw(rawValue, value) {
894
931
  }
895
932
 
896
933
  //#endregion
897
- export { blockMapStartsWithValueSep, classifyPlainNumeric, collectMultilineKey, collectMultilinePlainScalar, findFirstContent, findLastContent, findNextContentChild, findNextContentInList, findNextSignificantChild, findValueSepOffset, foldFlowLines, getBlockChomp, getScalarStyle, getScalarValue, hasBlockMapAfterInList, hasValueSepAfter, hasValueSepAfterInList, hasValueSepBetween, hasValueSepThroughPlainScalars, indexOfChild, makeScalar, resolveScalar, shouldPreserveRaw };
934
+ export { blockMapStartsWithValueSep, classifyPlainNumeric, collectMultilineKey, collectMultilinePlainScalar, findFirstContent, findLastContent, findNextContentChild, findNextContentInList, findNextSignificantChild, findValueSepOffset, foldFlowLines, getBlockChomp, getBlockIndent, getScalarStyle, getScalarValue, hasBlockMapAfterInList, hasValueSepAfter, hasValueSepAfterInList, hasValueSepBetween, hasValueSepThroughPlainScalars, indexOfChild, makeScalar, resolveScalar, shouldPreserveRaw };
@@ -63,6 +63,19 @@ function clearMeta(m) {
63
63
  delete m.tag;
64
64
  delete m.comment;
65
65
  }
66
+ /**
67
+ * The comment-fidelity field triple carried by the four public node classes
68
+ * (and `YamlPair`). Conditional-spread helper so AST rebuild sites copy all
69
+ * three without hand-maintaining the list — and never emit an explicit
70
+ * `undefined` into a v4 `optionalKey` field.
71
+ */
72
+ function commentProps(n) {
73
+ return {
74
+ ...n.commentBefore !== void 0 ? { commentBefore: n.commentBefore } : {},
75
+ ...n.comment !== void 0 ? { comment: n.comment } : {},
76
+ ...n.spaceBefore !== void 0 ? { spaceBefore: n.spaceBefore } : {}
77
+ };
78
+ }
66
79
  function createState(text, flow, options) {
67
80
  return {
68
81
  text,
@@ -77,7 +90,8 @@ function createState(text, flow, options) {
77
90
  },
78
91
  tagMap: /* @__PURE__ */ new Map(),
79
92
  flow,
80
- depth: 0
93
+ depth: 0,
94
+ escapedComments: []
81
95
  };
82
96
  }
83
97
  /**
@@ -114,4 +128,4 @@ function exitNesting(state) {
114
128
  }
115
129
 
116
130
  //#endregion
117
- export { clearMeta, createState, enterNesting, exitNesting, getLineStarts, hasMeta, hasNonWhitespaceBeforeOnLine, lineCol, lineIndentColumn, sameLine };
131
+ export { clearMeta, commentProps, createState, enterNesting, exitNesting, getLineStarts, hasMeta, hasNonWhitespaceBeforeOnLine, lineCol, lineIndentColumn, sameLine };
@@ -365,7 +365,8 @@ function parseBlockValue(state, parentIndent, explicitKey = false) {
365
365
  const token = peek(state);
366
366
  if (!token) break;
367
367
  if (token.kind === "whitespace") {
368
- nodes.push(...consumeTrivia(state));
368
+ advance(state);
369
+ nodes.push(makeLeafNode("whitespace", token, state.text));
369
370
  continue;
370
371
  }
371
372
  if (token.kind === "newline") break;
@@ -43,14 +43,23 @@ const YAML_COMPOSE_ERROR_CODES = [
43
43
  */
44
44
  const YAML_STRINGIFY_ERROR_CODES = ["CircularReference"];
45
45
  /**
46
- * Error codes for the modify stage (`YamlFormat.modify`'s path navigation
47
- * against an already-composed AST). Not raised by the parser/composer.
46
+ * Error codes for the modify stage (`YamlFormat.modify`'s guards and path
47
+ * navigation against an already-composed AST). Not raised by the
48
+ * parser/composer. `MultiDocumentStream` is the single-document-contract
49
+ * refusal: `modify` re-emits exactly one document, so a multi-document
50
+ * stream fails typed rather than silently truncating documents 2..n.
51
+ * `DirectiveCarryingDocument` is the directive refusal: the stringifier does
52
+ * not re-emit `%YAML`/`%TAG` directive lines, and re-emitting a document
53
+ * without its `%TAG` orphans every shorthand tag that depends on it — the
54
+ * output would be unparseable — so modify fails typed rather than corrupting.
48
55
  */
49
56
  const YAML_MODIFY_ERROR_CODES = [
50
57
  "EmptyDocument",
51
58
  "PathNotFound",
52
59
  "InvalidIndex",
53
- "NotNavigable"
60
+ "NotNavigable",
61
+ "MultiDocumentStream",
62
+ "DirectiveCarryingDocument"
54
63
  ];
55
64
  /**
56
65
  * The single source of truth for which diagnostic codes are fatal to a
package/internal/fold.js CHANGED
@@ -174,12 +174,18 @@ function renderSingleQuotedMultiline(s, indent) {
174
174
  * @param explicitChomp - Original chomp indicator from the AST, when known.
175
175
  * `keep` (`+`) and `strip` (`-`) preserve trailing-newline semantics that
176
176
  * cannot be inferred from the resolved value alone.
177
+ * @param preserveKeep - Re-emit a REDUNDANT explicit `+` (a keep header whose
178
+ * value ends in exactly one newline, where clip would read identically) —
179
+ * fidelity-path header preservation; canonical mode passes false.
180
+ * @param explicitIndent - Explicit indentation-indicator digit from the AST,
181
+ * re-emitted only when it matches the rendered indent (fidelity path).
177
182
  */
178
- function renderBlockLiteral(s, indent, explicitChomp, parentPosition) {
183
+ function renderBlockLiteral(s, indent, explicitChomp, parentPosition, preserveKeep = false, explicitIndent) {
179
184
  let chomp = "";
180
185
  const onlyNewlines = s.length > 0 && /^\n+$/.test(s);
181
186
  if (s.endsWith("\n\n") || onlyNewlines && explicitChomp === "keep") chomp = "+";
182
187
  else if (!s.endsWith("\n")) chomp = "-";
188
+ else if (preserveKeep && explicitChomp === "keep") chomp = "+";
183
189
  const lines = s.split("\n");
184
190
  if (s.endsWith("\n")) lines.pop();
185
191
  let indentIndicator = "";
@@ -187,6 +193,7 @@ function renderBlockLiteral(s, indent, explicitChomp, parentPosition) {
187
193
  const hasContent = firstContent !== void 0;
188
194
  if (firstContent?.startsWith(" ") || lines.length > 0 && lines[0] === "" && hasContent) indentIndicator = String(indent.length);
189
195
  else if (!hasContent && chomp === "+" && parentPosition === "block-map-value" && indent.length > 0) indentIndicator = String(indent.length);
196
+ else if (explicitIndent !== void 0 && explicitIndent === indent.length) indentIndicator = String(explicitIndent);
190
197
  return `|${indentIndicator}${chomp}\n${lines.map((l) => l === "" ? "" : `${indent}${l}`).join("\n")}`;
191
198
  }
192
199
  /**
@@ -197,34 +204,39 @@ function renderBlockLiteral(s, indent, explicitChomp, parentPosition) {
197
204
  * the output must contain an empty line (double newline). Each empty line
198
205
  * in the value already produces the correct number of blank lines.
199
206
  */
200
- function renderBlockFolded(s, indent) {
207
+ function renderBlockFolded(s, indent, explicitChomp, explicitIndent) {
201
208
  let chomp = "";
202
209
  if (s.endsWith("\n\n")) chomp = "+";
203
210
  else if (!s.endsWith("\n")) chomp = "-";
211
+ else if (explicitChomp === "keep") chomp = "+";
204
212
  const valueLines = s.split("\n");
205
213
  if (s.endsWith("\n")) valueLines.pop();
206
214
  let indentIndicator = "";
207
215
  const firstContent = valueLines.find((l) => l !== "");
208
216
  if (firstContent?.startsWith(" ") || valueLines.length >= 2 && valueLines[0] === "" && valueLines[1] === "" && firstContent !== void 0) indentIndicator = String(indent.length);
217
+ else if (explicitIndent !== void 0 && explicitIndent === indent.length) indentIndicator = String(explicitIndent);
209
218
  const outputLines = [];
219
+ let lastContentIdx = -1;
220
+ for (let i = valueLines.length - 1; i >= 0; i--) if (valueLines[i] !== "") {
221
+ lastContentIdx = i;
222
+ break;
223
+ }
210
224
  let prevNonEmpty = false;
211
225
  let prevMoreIndented = false;
226
+ let pendingCompensation = false;
212
227
  for (let i = 0; i < valueLines.length; i++) {
213
228
  const line = valueLines[i];
214
229
  if (line === "") {
215
- if (prevNonEmpty && !prevMoreIndented) {
216
- let nextContentMoreIndented = false;
217
- for (let j = i + 1; j < valueLines.length; j++) if (valueLines[j] !== "") {
218
- nextContentMoreIndented = valueLines[j].startsWith(" ") || valueLines[j].startsWith(" ");
219
- break;
220
- }
221
- if (!nextContentMoreIndented) outputLines.push("");
222
- }
230
+ if (prevNonEmpty && !prevMoreIndented && i < lastContentIdx) pendingCompensation = true;
223
231
  outputLines.push("");
224
232
  prevNonEmpty = false;
225
233
  prevMoreIndented = false;
226
234
  } else {
227
235
  const isMoreIndented = line.startsWith(" ") || line.startsWith(" ");
236
+ if (pendingCompensation) {
237
+ if (!isMoreIndented) outputLines.push("");
238
+ pendingCompensation = false;
239
+ }
228
240
  if (prevNonEmpty && !isMoreIndented) outputLines.push("");
229
241
  outputLines.push(`${indent}${line}`);
230
242
  prevNonEmpty = true;
@@ -0,0 +1,39 @@
1
+ import { colonSpacing, colonSpacingOptions } from "./colon-spacing.js";
2
+ import { commentsSpacing, commentsSpacingOptions } from "./comments-spacing.js";
3
+ import { documentEnd, documentEndOptions } from "./document-end.js";
4
+ import { documentStart, documentStartOptions } from "./document-start.js";
5
+ import { emptyLines, emptyLinesOptions } from "./empty-lines.js";
6
+ import { eofNewline, eofNewlineOptions } from "./eof-newline.js";
7
+ import { hyphenSpacing, hyphenSpacingOptions } from "./hyphen-spacing.js";
8
+ import { indentation, indentationOptions } from "./indentation.js";
9
+ import { keyDuplicates, keyDuplicatesOptions } from "./key-duplicates.js";
10
+ import { lineLength, lineLengthOptions } from "./line-length.js";
11
+ import { parseValidity, parseValidityOptions } from "./parse-validity.js";
12
+ import { quotedStrings, quotedStringsOptions } from "./quoted-strings.js";
13
+ import { trailingSpaces, trailingSpacesOptions } from "./trailing-spaces.js";
14
+ import { truthy, truthyOptions } from "./truthy.js";
15
+
16
+ //#region src/internal/rules/catalog.ts
17
+ const catalog = [
18
+ [parseValidity, parseValidityOptions],
19
+ [lineLength, lineLengthOptions],
20
+ [trailingSpaces, trailingSpacesOptions],
21
+ [emptyLines, emptyLinesOptions],
22
+ [eofNewline, eofNewlineOptions],
23
+ [documentStart, documentStartOptions],
24
+ [documentEnd, documentEndOptions],
25
+ [keyDuplicates, keyDuplicatesOptions],
26
+ [quotedStrings, quotedStringsOptions],
27
+ [truthy, truthyOptions],
28
+ [commentsSpacing, commentsSpacingOptions],
29
+ [colonSpacing, colonSpacingOptions],
30
+ [hyphenSpacing, hyphenSpacingOptions],
31
+ [indentation, indentationOptions]
32
+ ];
33
+ /** The built-in rules, in catalog order (parse-validity is rule `#1`). */
34
+ const builtinRules = catalog.map(([rule]) => rule);
35
+ /** Per-rule options schemas — the rule-aware half of config validation. */
36
+ const builtinOptionsSchemas = new Map(catalog.map(([rule, options]) => [rule.id, options]));
37
+
38
+ //#endregion
39
+ export { builtinOptionsSchemas, builtinRules };
@@ -0,0 +1,77 @@
1
+ import { YamlEdit } from "../../YamlEdit.js";
2
+ import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
+ import { nonNegativeIntegerOption, positiveIntegerOption } from "./util.js";
4
+ import { Schema } from "effect";
5
+
6
+ //#region src/internal/rules/colon-spacing.ts
7
+ /**
8
+ * Options for `colon-spacing`: `maxSpacesBefore` (default 0) and
9
+ * `maxSpacesAfter` (default 1) around the `:` indicator. `maxSpacesBefore: 0`
10
+ * is legal (`key:` needs no space before the colon), but at least one
11
+ * separation space must FOLLOW it — `0` would make the fix emit `a:val`, a
12
+ * plain scalar, not a mapping entry.
13
+ */
14
+ const colonSpacingOptions = Schema.Struct({
15
+ severity: Schema.optionalKey(YamlLintSeverity),
16
+ maxSpacesBefore: Schema.optionalKey(nonNegativeIntegerOption),
17
+ maxSpacesAfter: Schema.optionalKey(positiveIntegerOption)
18
+ });
19
+ /** Spacing around the block-mapping `:` indicator. */
20
+ const colonSpacing = {
21
+ id: "colon-spacing",
22
+ check: (ctx, options) => {
23
+ const opts = options ?? {};
24
+ const maxBefore = opts.maxSpacesBefore ?? 0;
25
+ const maxAfter = Math.max(1, opts.maxSpacesAfter ?? 1);
26
+ const out = [];
27
+ for (const token of ctx.tokens) {
28
+ if (token.kind !== "block-map-value") continue;
29
+ let i = token.offset - 1;
30
+ let before = 0;
31
+ while (i >= 0 && ctx.text[i] === " ") {
32
+ before++;
33
+ i--;
34
+ }
35
+ if (i >= 0 && ctx.text[i] !== "\n" && ctx.text[i] !== "\r" && ctx.text[i] !== " " && before > maxBefore) out.push(new YamlLintDiagnostic({
36
+ rule: "colon-spacing",
37
+ severity: "error",
38
+ message: `Too many spaces before ":" (${before} > ${maxBefore})`,
39
+ offset: token.offset - before,
40
+ length: before,
41
+ line: token.line,
42
+ character: token.character - before,
43
+ fix: YamlEdit.make({
44
+ offset: token.offset - before,
45
+ length: before - maxBefore,
46
+ content: ""
47
+ })
48
+ }));
49
+ let j = token.offset + token.length;
50
+ let after = 0;
51
+ while (j < ctx.text.length && ctx.text[j] === " ") {
52
+ after++;
53
+ j++;
54
+ }
55
+ const next = ctx.text[j];
56
+ if (next === void 0 || next === "\n" || next === "\r" || next === "#") continue;
57
+ if (after > maxAfter) out.push(new YamlLintDiagnostic({
58
+ rule: "colon-spacing",
59
+ severity: "error",
60
+ message: `Too many spaces after ":" (${after} > ${maxAfter})`,
61
+ offset: token.offset + token.length,
62
+ length: after,
63
+ line: token.line,
64
+ character: token.character + token.length,
65
+ fix: YamlEdit.make({
66
+ offset: token.offset + token.length,
67
+ length: after - maxAfter,
68
+ content: ""
69
+ })
70
+ }));
71
+ }
72
+ return out;
73
+ }
74
+ };
75
+
76
+ //#endregion
77
+ export { colonSpacing, colonSpacingOptions };
@@ -0,0 +1,68 @@
1
+ import { YamlEdit } from "../../YamlEdit.js";
2
+ import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
+ import { nonNegativeIntegerOption } from "./util.js";
4
+ import { Schema } from "effect";
5
+
6
+ //#region src/internal/rules/comments-spacing.ts
7
+ /**
8
+ * Options for `comments-spacing`: `minSpacesBefore` between content and a
9
+ * trailing `#` (default 1 — the kit's own emission spelling) and
10
+ * `requireSpaceAfter` the `#` (default `true`).
11
+ */
12
+ const commentsSpacingOptions = Schema.Struct({
13
+ severity: Schema.optionalKey(YamlLintSeverity),
14
+ minSpacesBefore: Schema.optionalKey(nonNegativeIntegerOption),
15
+ requireSpaceAfter: Schema.optionalKey(Schema.Boolean)
16
+ });
17
+ /** Comment spacing: after the `#`, and before a trailing comment's `#`. */
18
+ const commentsSpacing = {
19
+ id: "comments-spacing",
20
+ check: (ctx, options) => {
21
+ const opts = options ?? {};
22
+ const minBefore = opts.minSpacesBefore ?? 1;
23
+ const requireAfter = opts.requireSpaceAfter ?? true;
24
+ const out = [];
25
+ for (const token of ctx.tokens) {
26
+ if (token.kind !== "comment") continue;
27
+ if (token.offset === 0 && token.text.startsWith("#!")) continue;
28
+ if (requireAfter && token.text.length > 1 && !/[ \t]/.test(token.text[1])) out.push(new YamlLintDiagnostic({
29
+ rule: "comments-spacing",
30
+ severity: "error",
31
+ message: "Missing space after \"#\"",
32
+ offset: token.offset,
33
+ length: 1,
34
+ line: token.line,
35
+ character: token.character,
36
+ fix: YamlEdit.make({
37
+ offset: token.offset + 1,
38
+ length: 0,
39
+ content: " "
40
+ })
41
+ }));
42
+ let i = token.offset - 1;
43
+ let spaces = 0;
44
+ while (i >= 0 && (ctx.text[i] === " " || ctx.text[i] === " ")) {
45
+ spaces++;
46
+ i--;
47
+ }
48
+ if (i >= 0 && ctx.text[i] !== "\n" && ctx.text[i] !== "\r" && spaces < minBefore) out.push(new YamlLintDiagnostic({
49
+ rule: "comments-spacing",
50
+ severity: "error",
51
+ message: `Too few spaces before comment (${spaces} < ${minBefore})`,
52
+ offset: token.offset - spaces,
53
+ length: spaces,
54
+ line: token.line,
55
+ character: token.character - spaces,
56
+ fix: YamlEdit.make({
57
+ offset: token.offset,
58
+ length: 0,
59
+ content: " ".repeat(minBefore - spaces)
60
+ })
61
+ }));
62
+ }
63
+ return out;
64
+ }
65
+ };
66
+
67
+ //#endregion
68
+ export { commentsSpacing, commentsSpacingOptions };
@@ -0,0 +1,65 @@
1
+ import { YamlEdit } from "../../YamlEdit.js";
2
+ import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
+ import { Schema } from "effect";
4
+
5
+ //#region src/internal/rules/document-end.ts
6
+ /** Options for `document-end`: require (`true`, default) or forbid the marker. */
7
+ const documentEndOptions = Schema.Struct({
8
+ severity: Schema.optionalKey(YamlLintSeverity),
9
+ present: Schema.optionalKey(Schema.Boolean)
10
+ });
11
+ const TRIVIA = /* @__PURE__ */ new Set([
12
+ "newline",
13
+ "whitespace",
14
+ "comment",
15
+ "byte-order-mark"
16
+ ]);
17
+ /** The `...` marker at the tail of the stream. */
18
+ const documentEnd = {
19
+ id: "document-end",
20
+ check: (ctx, options) => {
21
+ const present = options?.present ?? true;
22
+ const tail = [...ctx.tokens].reverse().find((t) => !TRIVIA.has(t.kind));
23
+ const ended = tail?.kind === "document-end";
24
+ if (present && !ended && ctx.text.trim() !== "") {
25
+ const lastLine = ctx.lines[ctx.lines.length - 1];
26
+ const trailingNewline = ctx.text.endsWith("\n");
27
+ return [new YamlLintDiagnostic({
28
+ rule: "document-end",
29
+ severity: "error",
30
+ message: "Missing \"...\" document end marker",
31
+ offset: ctx.text.length,
32
+ length: 0,
33
+ line: (lastLine?.number ?? 0) + (trailingNewline ? 1 : 0),
34
+ character: trailingNewline ? 0 : lastLine?.text.length ?? 0,
35
+ fix: YamlEdit.make({
36
+ offset: ctx.text.length,
37
+ length: 0,
38
+ content: ctx.text.endsWith("\n") ? "...\n" : "\n...\n"
39
+ })
40
+ })];
41
+ }
42
+ if (!present && tail !== void 0 && tail.kind === "document-end") {
43
+ const alone = (ctx.lines[tail.line]?.text ?? "").trim() === "...";
44
+ const terminator = ctx.text.startsWith("\r\n", tail.offset + tail.length) ? 2 : 1;
45
+ return [new YamlLintDiagnostic({
46
+ rule: "document-end",
47
+ severity: "error",
48
+ message: "Forbidden \"...\" document end marker",
49
+ offset: tail.offset,
50
+ length: tail.length,
51
+ line: tail.line,
52
+ character: tail.character,
53
+ ...alone ? { fix: YamlEdit.make({
54
+ offset: tail.offset,
55
+ length: tail.length + terminator,
56
+ content: ""
57
+ }) } : {}
58
+ })];
59
+ }
60
+ return [];
61
+ }
62
+ };
63
+
64
+ //#endregion
65
+ export { documentEnd, documentEndOptions };
@@ -0,0 +1,64 @@
1
+ import { YamlEdit } from "../../YamlEdit.js";
2
+ import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
+ import { Schema } from "effect";
4
+
5
+ //#region src/internal/rules/document-start.ts
6
+ /** Options for `document-start`: require (`true`, default) or forbid the marker. */
7
+ const documentStartOptions = Schema.Struct({
8
+ severity: Schema.optionalKey(YamlLintSeverity),
9
+ present: Schema.optionalKey(Schema.Boolean)
10
+ });
11
+ const TRIVIA = /* @__PURE__ */ new Set([
12
+ "newline",
13
+ "whitespace",
14
+ "comment",
15
+ "byte-order-mark",
16
+ "directive"
17
+ ]);
18
+ /** The `---` marker at the head of the stream. */
19
+ const documentStart = {
20
+ id: "document-start",
21
+ check: (ctx, options) => {
22
+ const present = options?.present ?? true;
23
+ const hasDirectives = ctx.tokens.some((t) => t.kind === "directive");
24
+ const first = ctx.tokens.find((t) => !TRIVIA.has(t.kind));
25
+ const headed = first?.kind === "document-start";
26
+ if (present && !headed && ctx.text.trim() !== "") return [new YamlLintDiagnostic({
27
+ rule: "document-start",
28
+ severity: "error",
29
+ message: "Missing \"---\" document start marker",
30
+ offset: 0,
31
+ length: 0,
32
+ line: 0,
33
+ character: 0,
34
+ ...hasDirectives ? {} : { fix: YamlEdit.make({
35
+ offset: 0,
36
+ length: 0,
37
+ content: "---\n"
38
+ }) }
39
+ })];
40
+ if (!present && headed && first !== void 0) {
41
+ const lineText = ctx.lines[first.line]?.text ?? "";
42
+ const alone = !hasDirectives && lineText.trim() === "---";
43
+ const terminator = ctx.text.startsWith("\r\n", first.offset + first.length) ? 2 : 1;
44
+ return [new YamlLintDiagnostic({
45
+ rule: "document-start",
46
+ severity: "error",
47
+ message: "Forbidden \"---\" document start marker",
48
+ offset: first.offset,
49
+ length: first.length,
50
+ line: first.line,
51
+ character: first.character,
52
+ ...alone ? { fix: YamlEdit.make({
53
+ offset: first.offset,
54
+ length: first.length + terminator,
55
+ content: ""
56
+ }) } : {}
57
+ })];
58
+ }
59
+ return [];
60
+ }
61
+ };
62
+
63
+ //#endregion
64
+ export { documentStart, documentStartOptions };
@@ -0,0 +1,70 @@
1
+ import { YamlEdit } from "../../YamlEdit.js";
2
+ import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
+ import { insideScalarSpan, nonNegativeIntegerOption } from "./util.js";
4
+ import { Schema } from "effect";
5
+
6
+ //#region src/internal/rules/empty-lines.ts
7
+ /**
8
+ * Options for `empty-lines`: `max` consecutive blank lines in the body
9
+ * (default 2), `maxStart` at the document start and `maxEnd` at the end
10
+ * (both default 0).
11
+ */
12
+ const emptyLinesOptions = Schema.Struct({
13
+ severity: Schema.optionalKey(YamlLintSeverity),
14
+ max: Schema.optionalKey(nonNegativeIntegerOption),
15
+ maxStart: Schema.optionalKey(nonNegativeIntegerOption),
16
+ maxEnd: Schema.optionalKey(nonNegativeIntegerOption)
17
+ });
18
+ /** Runs of blank lines beyond the configured caps, with a deleting fix. */
19
+ const emptyLines = {
20
+ id: "empty-lines",
21
+ check: (ctx, options) => {
22
+ const opts = options ?? {};
23
+ const max = opts.max ?? 2;
24
+ const maxStart = opts.maxStart ?? 0;
25
+ const maxEnd = opts.maxEnd ?? 0;
26
+ const out = [];
27
+ const lines = ctx.lines;
28
+ let i = 0;
29
+ while (i < lines.length) {
30
+ const line = lines[i];
31
+ if (line === void 0 || line.text !== "" || insideScalarSpan(ctx.tokens, line.offset)) {
32
+ i++;
33
+ continue;
34
+ }
35
+ let end = i;
36
+ while (end < lines.length && (lines[end]?.text ?? "x") === "") end++;
37
+ const runLength = end - i;
38
+ const atStart = i === 0;
39
+ const atEnd = end === lines.length;
40
+ const allowed = atStart ? maxStart : atEnd ? maxEnd : max;
41
+ if (runLength > allowed) {
42
+ const firstExcess = lines[i + allowed];
43
+ const lastBlank = lines[end - 1];
44
+ if (firstExcess !== void 0 && lastBlank !== void 0) {
45
+ const nextLine = lines[end];
46
+ const deleteEnd = nextLine !== void 0 ? nextLine.offset : ctx.text.length;
47
+ out.push(new YamlLintDiagnostic({
48
+ rule: "empty-lines",
49
+ severity: "error",
50
+ message: `Too many consecutive blank lines (${runLength} > ${allowed})`,
51
+ offset: firstExcess.offset,
52
+ length: deleteEnd - firstExcess.offset,
53
+ line: firstExcess.number,
54
+ character: 0,
55
+ fix: YamlEdit.make({
56
+ offset: firstExcess.offset,
57
+ length: deleteEnd - firstExcess.offset,
58
+ content: ""
59
+ })
60
+ }));
61
+ }
62
+ }
63
+ i = end;
64
+ }
65
+ return out;
66
+ }
67
+ };
68
+
69
+ //#endregion
70
+ export { emptyLines, emptyLinesOptions };