@effected/yaml 0.6.1 → 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.
Files changed (40) hide show
  1. package/README.md +78 -7
  2. package/Yaml.js +87 -39
  3. package/YamlDocument.js +25 -6
  4. package/YamlFormat.js +171 -20
  5. package/YamlLint.js +239 -0
  6. package/YamlLintRule.js +36 -0
  7. package/YamlNode.js +36 -2
  8. package/YamlToken.js +139 -0
  9. package/YamlVisitor.js +40 -5
  10. package/index.d.ts +439 -12
  11. package/index.js +4 -1
  12. package/internal/composer/anchors.js +2 -27
  13. package/internal/composer/block.js +246 -58
  14. package/internal/composer/comments.js +187 -0
  15. package/internal/composer/document.js +49 -12
  16. package/internal/composer/flow.js +82 -9
  17. package/internal/composer/scalars.js +77 -28
  18. package/internal/composer/state.js +16 -2
  19. package/internal/cst-parser.js +11 -2
  20. package/internal/diagnostics.js +12 -3
  21. package/internal/fold.js +22 -10
  22. package/internal/lexer.js +23 -17
  23. package/internal/rules/catalog.js +39 -0
  24. package/internal/rules/colon-spacing.js +77 -0
  25. package/internal/rules/comments-spacing.js +68 -0
  26. package/internal/rules/document-end.js +65 -0
  27. package/internal/rules/document-start.js +64 -0
  28. package/internal/rules/empty-lines.js +70 -0
  29. package/internal/rules/eof-newline.js +33 -0
  30. package/internal/rules/hyphen-spacing.js +52 -0
  31. package/internal/rules/indentation.js +103 -0
  32. package/internal/rules/key-duplicates.js +49 -0
  33. package/internal/rules/line-length.js +36 -0
  34. package/internal/rules/parse-validity.js +33 -0
  35. package/internal/rules/quoted-strings.js +81 -0
  36. package/internal/rules/trailing-spaces.js +39 -0
  37. package/internal/rules/truthy.js +84 -0
  38. package/internal/rules/util.js +92 -0
  39. package/internal/stringifier.js +285 -69
  40. package/package.json +4 -3
@@ -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);
@@ -265,10 +295,11 @@ function foldFlowLines(text) {
265
295
  }
266
296
  const isLast = i === lines.length - 1;
267
297
  const trimmed = isLast ? line.trimStart() : line.trim();
268
- if (trimmed === "") if (isLast) {
269
- if (result.length === 0 || result[result.length - 1] !== "\n") result += " ";
270
- } else result += "\n";
271
- else {
298
+ if (trimmed === "") {
299
+ if (isLast) {
300
+ if (result.length === 0 || result[result.length - 1] !== "\n") result += " ";
301
+ } else result += "\n";
302
+ } else {
272
303
  if (result.length > 0 && result[result.length - 1] !== "\n") result += " ";
273
304
  result += trimmed;
274
305
  }
@@ -681,8 +712,10 @@ function decodeBlockScalar(raw, fullText, nodeOffset) {
681
712
  } else break;
682
713
  }
683
714
  while (i < raw.length && raw[i] !== "\n" && raw[i] !== "\r") i++;
684
- if (i < raw.length) if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
685
- else i++;
715
+ if (i < raw.length) {
716
+ if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
717
+ else i++;
718
+ }
686
719
  let contentIndent = explicitIndent;
687
720
  let foundContent = explicitIndent > 0;
688
721
  if (explicitIndent > 0 && fullText !== void 0 && nodeOffset !== void 0) {
@@ -745,8 +778,10 @@ function decodeBlockScalar(raw, fullText, nodeOffset) {
745
778
  trailingNewlines.length = 0;
746
779
  lines.push(" ".repeat(spaces - contentIndent));
747
780
  } else trailingNewlines.push("");
748
- if (i < raw.length) if (raw[i] === "\r" && i + 1 < raw.length && raw[i + 1] === "\n") i += 2;
749
- else i++;
781
+ if (i < raw.length) {
782
+ if (raw[i] === "\r" && i + 1 < raw.length && raw[i + 1] === "\n") i += 2;
783
+ else i++;
784
+ }
750
785
  continue;
751
786
  }
752
787
  if (spaces < contentIndent) break;
@@ -756,8 +791,10 @@ function decodeBlockScalar(raw, fullText, nodeOffset) {
756
791
  const contentStart = i;
757
792
  while (i < raw.length && raw[i] !== "\n" && raw[i] !== "\r") i++;
758
793
  lines.push(extra + raw.slice(contentStart, i));
759
- if (i < raw.length) if (raw[i] === "\r" && i + 1 < raw.length && raw[i + 1] === "\n") i += 2;
760
- else i++;
794
+ if (i < raw.length) {
795
+ if (raw[i] === "\r" && i + 1 < raw.length && raw[i + 1] === "\n") i += 2;
796
+ else i++;
797
+ }
761
798
  }
762
799
  let value;
763
800
  if (isFolded) {
@@ -773,27 +810,32 @@ function decodeBlockScalar(raw, fullText, nodeOffset) {
773
810
  prevMoreIndented = isMoreIndented;
774
811
  hadContent = true;
775
812
  } else {
776
- if (result[result.length - 1] === "\n") if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
777
- else result += ln;
778
- else if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
813
+ if (result[result.length - 1] === "\n") {
814
+ if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
815
+ else result += ln;
816
+ } else if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
779
817
  else result += ` ${ln}`;
780
818
  prevMoreIndented = isMoreIndented;
781
819
  }
782
820
  }
783
821
  if (hadContent || trailingNewlines.length > 0) {
784
822
  if (chomp === "keep") {
785
- result += "\n";
823
+ if (hadContent) result += "\n";
786
824
  for (const _nl of trailingNewlines) result += "\n";
787
- } else if (chomp !== "strip") result += "\n";
825
+ } else if (chomp !== "strip") {
826
+ if (hadContent) result += "\n";
827
+ }
788
828
  }
789
829
  value = result;
790
830
  } else {
791
831
  value = lines.join("\n");
792
832
  if (lines.length > 0 || trailingNewlines.length > 0) {
793
833
  if (chomp === "keep") {
794
- value += "\n";
834
+ if (lines.length > 0) value += "\n";
795
835
  for (const _nl of trailingNewlines) value += "\n";
796
- } else if (chomp !== "strip") value += "\n";
836
+ } else if (chomp !== "strip") {
837
+ if (lines.length > 0) value += "\n";
838
+ }
797
839
  }
798
840
  }
799
841
  return value;
@@ -809,8 +851,10 @@ function validateBlockScalarLeadingEmpties(cst, state) {
809
851
  const raw = cst.source;
810
852
  let i = 0;
811
853
  while (i < raw.length && raw[i] !== "\n" && raw[i] !== "\r") i++;
812
- if (i < raw.length) if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
813
- else i++;
854
+ if (i < raw.length) {
855
+ if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
856
+ else i++;
857
+ }
814
858
  const emptyIndents = [];
815
859
  while (i < raw.length) {
816
860
  const lineStart = i;
@@ -824,8 +868,10 @@ function validateBlockScalarLeadingEmpties(cst, state) {
824
868
  indent: spaces,
825
869
  offsetInRaw: lineStart
826
870
  });
827
- if (i < raw.length) if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
828
- else i++;
871
+ if (i < raw.length) {
872
+ if (raw[i] === "\r" && raw[i + 1] === "\n") i += 2;
873
+ else i++;
874
+ }
829
875
  continue;
830
876
  }
831
877
  const contentIndent = spaces;
@@ -848,6 +894,8 @@ function makeScalar(cst, state, meta) {
848
894
  const rawValue = getScalarValue(cst, state.text);
849
895
  const value = resolveScalar(rawValue, style, meta?.tag, state);
850
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;
851
899
  const needsRaw = style === "plain" && typeof value !== "string" && value !== void 0 && shouldPreserveRaw(rawValue, value);
852
900
  const scalar = new YamlScalar({
853
901
  value,
@@ -856,8 +904,9 @@ function makeScalar(cst, state, meta) {
856
904
  length: cst.length,
857
905
  ...meta?.tag !== void 0 ? { tag: meta.tag } : {},
858
906
  ...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
859
- ...meta?.comment !== void 0 ? { comment: meta.comment } : {},
907
+ ...comment !== void 0 ? { comment } : {},
860
908
  ...chomp !== void 0 ? { chomp } : {},
909
+ ...blockIndent !== void 0 ? { blockIndent } : {},
861
910
  ...needsRaw ? { raw: rawValue } : {}
862
911
  });
863
912
  if (meta?.anchor) registerAnchor(scalar, meta.anchor, state, cst.offset);
@@ -882,4 +931,4 @@ function shouldPreserveRaw(rawValue, value) {
882
931
  }
883
932
 
884
933
  //#endregion
885
- 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 };
@@ -286,12 +286,14 @@ function parseBlockMappingInner(state, indent) {
286
286
  continue;
287
287
  }
288
288
  if (token.kind === "block-map-key") {
289
+ if (token.column < indent && children.length > 0) break;
289
290
  sawExplicitKey = true;
290
291
  const leaf = consumeLeafToken(state);
291
292
  if (leaf) children.push(leaf);
292
293
  continue;
293
294
  }
294
295
  if (token.kind === "block-map-value") {
296
+ if (token.column < indent && children.length > 0) break;
295
297
  const leaf = consumeLeafToken(state);
296
298
  if (leaf) children.push(leaf);
297
299
  children.push(...parseBlockValue(state, indent, sawExplicitKey));
@@ -357,13 +359,14 @@ function parseBlockMappingInner(state, indent) {
357
359
  /**
358
360
  * Parse the value part after a ":" in a block mapping.
359
361
  */
360
- function parseBlockValue(state, _parentIndent, explicitKey = false) {
362
+ function parseBlockValue(state, parentIndent, explicitKey = false) {
361
363
  const nodes = [];
362
364
  while (!atEnd(state)) {
363
365
  const token = peek(state);
364
366
  if (!token) break;
365
367
  if (token.kind === "whitespace") {
366
- nodes.push(...consumeTrivia(state));
368
+ advance(state);
369
+ nodes.push(makeLeafNode("whitespace", token, state.text));
367
370
  continue;
368
371
  }
369
372
  if (token.kind === "newline") break;
@@ -385,6 +388,10 @@ function parseBlockValue(state, _parentIndent, explicitKey = false) {
385
388
  nodes.push(parseBlockSequence(state, seqIndent));
386
389
  break;
387
390
  }
391
+ if (explicitKey && token.kind === "scalar" && !isBlockScalarToken(state) && hasImplicitMapAhead(state, parentIndent)) {
392
+ nodes.push(parseImplicitBlockMapping(state, parentIndent));
393
+ break;
394
+ }
388
395
  if (token.kind === "scalar" || token.kind === "anchor" || token.kind === "alias" || token.kind === "tag") {
389
396
  const leaf = consumeLeafToken(state);
390
397
  if (leaf) nodes.push(leaf);
@@ -540,11 +547,13 @@ function parseImplicitBlockMappingInner(state, seqIndent) {
540
547
  if (!token) break;
541
548
  if (isDocumentBoundary(token)) break;
542
549
  if (token.kind === "block-seq-entry" && token.column <= seqIndent) break;
550
+ if (token.kind === "block-map-key" && token.column <= seqIndent) break;
543
551
  if (isTrivia(token)) {
544
552
  children.push(...consumeTrivia(state));
545
553
  continue;
546
554
  }
547
555
  if (token.kind === "block-map-value") {
556
+ if (token.column <= seqIndent && children.length > 0) break;
548
557
  const leaf = consumeLeafToken(state);
549
558
  if (leaf) children.push(leaf);
550
559
  children.push(...parseBlockValue(state, seqIndent));
@@ -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;
package/internal/lexer.js CHANGED
@@ -216,14 +216,15 @@ function createScanner(text) {
216
216
  let value = "";
217
217
  while (pos < text.length) {
218
218
  const ch = peek();
219
- if (ch === "'") if (peek(1) === "'") {
220
- value += "'";
221
- advance(2);
222
- } else {
223
- advance();
224
- return makeToken("scalar", value, start, sLine, sCol, pos - start);
225
- }
226
- else if (ch === "\n" || ch === "\r" && peek(1) === "\n") {
219
+ if (ch === "'") {
220
+ if (peek(1) === "'") {
221
+ value += "'";
222
+ advance(2);
223
+ } else {
224
+ advance();
225
+ return makeToken("scalar", value, start, sLine, sCol, pos - start);
226
+ }
227
+ } else if (ch === "\n" || ch === "\r" && peek(1) === "\n") {
227
228
  value += " ";
228
229
  if (ch === "\r") advance(2);
229
230
  else advance();
@@ -402,13 +403,17 @@ function createScanner(text) {
402
403
  hadHeaderWhitespace = true;
403
404
  advance();
404
405
  }
405
- if (pos < text.length && !isNewline(peek()) && peek() !== "") if (peek() === "#" && hadHeaderWhitespace) while (pos < text.length && !isNewline(peek())) advance();
406
- else {
407
- while (pos < text.length && !isNewline(peek())) advance();
408
- return makeToken("error", text.slice(start, pos), start, sLine, sCol);
406
+ if (pos < text.length && !isNewline(peek()) && peek() !== "") {
407
+ if (peek() === "#" && hadHeaderWhitespace) while (pos < text.length && !isNewline(peek())) advance();
408
+ else {
409
+ while (pos < text.length && !isNewline(peek())) advance();
410
+ return makeToken("error", text.slice(start, pos), start, sLine, sCol);
411
+ }
412
+ }
413
+ if (pos < text.length && isNewline(peek())) {
414
+ if (peek() === "\r" && peek(1) === "\n") advance(2);
415
+ else advance();
409
416
  }
410
- if (pos < text.length && isNewline(peek())) if (peek() === "\r" && peek(1) === "\n") advance(2);
411
- else advance();
412
417
  let contentIndent = explicitIndent;
413
418
  let foundContent = explicitIndent > 0;
414
419
  if (explicitIndent > 0) {
@@ -564,9 +569,10 @@ function createScanner(text) {
564
569
  prevMoreIndented = isMoreIndented;
565
570
  hadContent = true;
566
571
  } else {
567
- if (result[result.length - 1] === "\n") if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
568
- else result += ln;
569
- else if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
572
+ if (result[result.length - 1] === "\n") {
573
+ if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
574
+ else result += ln;
575
+ } else if (isMoreIndented || prevMoreIndented) result += `\n${ln}`;
570
576
  else result += ` ${ln}`;
571
577
  prevMoreIndented = isMoreIndented;
572
578
  }
@@ -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 };