@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.
- package/README.md +78 -7
- package/Yaml.js +87 -39
- package/YamlDocument.js +25 -6
- package/YamlFormat.js +171 -20
- 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 +77 -28
- package/internal/composer/state.js +16 -2
- package/internal/cst-parser.js +11 -2
- package/internal/diagnostics.js +12 -3
- package/internal/fold.js +22 -10
- package/internal/lexer.js +23 -17
- 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 +285 -69
- 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
|
|
102
|
-
|
|
103
|
-
|
|
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 === "")
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
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)
|
|
685
|
-
|
|
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)
|
|
749
|
-
|
|
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)
|
|
760
|
-
|
|
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")
|
|
777
|
-
|
|
778
|
-
|
|
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")
|
|
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")
|
|
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)
|
|
813
|
-
|
|
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)
|
|
828
|
-
|
|
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
|
-
...
|
|
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 };
|
package/internal/cst-parser.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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));
|
package/internal/diagnostics.js
CHANGED
|
@@ -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
|
|
47
|
-
* against an already-composed AST). Not raised by the
|
|
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 === "'")
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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() !== "")
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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")
|
|
568
|
-
|
|
569
|
-
|
|
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 };
|