@effected/yaml 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -7
- package/Yaml.js +80 -32
- package/YamlDocument.js +22 -3
- package/YamlFormat.js +170 -19
- package/YamlLint.js +239 -0
- package/YamlLintRule.js +36 -0
- package/YamlNode.js +36 -2
- package/YamlToken.js +139 -0
- package/YamlVisitor.js +40 -5
- package/index.d.ts +439 -12
- package/index.js +4 -1
- package/internal/composer/anchors.js +2 -27
- package/internal/composer/block.js +246 -58
- package/internal/composer/comments.js +187 -0
- package/internal/composer/document.js +49 -12
- package/internal/composer/flow.js +82 -9
- package/internal/composer/scalars.js +48 -11
- package/internal/composer/state.js +16 -2
- package/internal/cst-parser.js +2 -1
- package/internal/diagnostics.js +12 -3
- package/internal/fold.js +22 -10
- package/internal/rules/catalog.js +39 -0
- package/internal/rules/colon-spacing.js +77 -0
- package/internal/rules/comments-spacing.js +68 -0
- package/internal/rules/document-end.js +65 -0
- package/internal/rules/document-start.js +64 -0
- package/internal/rules/empty-lines.js +70 -0
- package/internal/rules/eof-newline.js +33 -0
- package/internal/rules/hyphen-spacing.js +52 -0
- package/internal/rules/indentation.js +103 -0
- package/internal/rules/key-duplicates.js +49 -0
- package/internal/rules/line-length.js +36 -0
- package/internal/rules/parse-validity.js +33 -0
- package/internal/rules/quoted-strings.js +81 -0
- package/internal/rules/trailing-spaces.js +39 -0
- package/internal/rules/truthy.js +84 -0
- package/internal/rules/util.js +92 -0
- package/internal/stringifier.js +266 -54
- package/package.json +3 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { YamlMap, YamlScalar, YamlSeq } from "../../YamlNode.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/composer/comments.ts
|
|
4
|
+
/** True when there is no line break between `start` and `end` in `text`. */
|
|
5
|
+
function sameLineSpan(text, start, end) {
|
|
6
|
+
if (start < 0) return false;
|
|
7
|
+
return !text.slice(Math.max(0, start), Math.max(0, end)).includes("\n");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* True when only horizontal whitespace precedes `offset` on its line — i.e.
|
|
11
|
+
* the token at `offset` starts its own line. Purely local, so it stays
|
|
12
|
+
* correct even when a preceding node's span over-extends past line ends.
|
|
13
|
+
*/
|
|
14
|
+
function isOwnLineAt(text, offset) {
|
|
15
|
+
let i = offset - 1;
|
|
16
|
+
while (i >= 0) {
|
|
17
|
+
const ch = text[i];
|
|
18
|
+
if (ch === " " || ch === " ") {
|
|
19
|
+
i--;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
return ch === "\n" || ch === "\r";
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* True when the line immediately above the line containing `offset` is blank
|
|
28
|
+
* (empty or horizontal whitespace only). Purely local — see
|
|
29
|
+
* {@link isOwnLineAt} for why span-based gap checks are not used.
|
|
30
|
+
*/
|
|
31
|
+
function hasBlankLineAbove(text, offset) {
|
|
32
|
+
return blankLineAboveStart(text, offset) >= 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start offset of the blank line immediately above the line containing
|
|
36
|
+
* `offset`, or `-1` when that line is not blank. The offset form of
|
|
37
|
+
* {@link hasBlankLineAbove}, for callers that must locate the blank line
|
|
38
|
+
* (e.g. to test whether it falls inside a preceding scalar token's span).
|
|
39
|
+
*/
|
|
40
|
+
function blankLineAboveStart(text, offset) {
|
|
41
|
+
const lineBreak = text.lastIndexOf("\n", Math.max(0, offset - 1));
|
|
42
|
+
if (lineBreak < 0) return -1;
|
|
43
|
+
const prevBreak = text.lastIndexOf("\n", lineBreak - 1);
|
|
44
|
+
return text.slice(prevBreak + 1, lineBreak).trim() === "" ? prevBreak + 1 : -1;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The deepest trailing scalar reached by descending last-child edges from
|
|
48
|
+
* `node` (a map's last pair's value, a seq's last item) — the node whose
|
|
49
|
+
* token span a textually-following blank line could fall inside.
|
|
50
|
+
*/
|
|
51
|
+
function deepestTrailingScalar(node) {
|
|
52
|
+
let current = node;
|
|
53
|
+
for (;;) {
|
|
54
|
+
if (current instanceof YamlScalar) return current;
|
|
55
|
+
if (current instanceof YamlMap) {
|
|
56
|
+
const last = current.items[current.items.length - 1];
|
|
57
|
+
if (last === void 0) return void 0;
|
|
58
|
+
current = last.value ?? last.key;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (current instanceof YamlSeq) {
|
|
62
|
+
const last = current.items[current.items.length - 1];
|
|
63
|
+
if (last === void 0) return void 0;
|
|
64
|
+
current = last;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True when the blank line immediately above `offset` is CONTENT of the
|
|
72
|
+
* preceding keep-chomp block scalar, not a stylistic blank. Under `+`
|
|
73
|
+
* chomping the trailing line breaks are part of the scalar's VALUE (the
|
|
74
|
+
* composer already stores them there), so recording the same blank line as
|
|
75
|
+
* `spaceBefore` (or a leading-blank embed on a terminal comment run) would
|
|
76
|
+
* double-count it: emission renders both the kept newline and the stylistic
|
|
77
|
+
* blank, growing the document on every format pass. Clip/strip scalars are
|
|
78
|
+
* deliberately NOT excluded — their emission drops the trailing blank from
|
|
79
|
+
* the value, so the `spaceBefore` capture is exactly what re-creates it.
|
|
80
|
+
*
|
|
81
|
+
* `prev` is the last composed node before `offset` (a pair's value, a seq
|
|
82
|
+
* item); the check descends to its deepest trailing scalar and requires the
|
|
83
|
+
* blank line to start inside that scalar's token span.
|
|
84
|
+
*/
|
|
85
|
+
function blankAboveIsKeepChompContent(text, offset, prev) {
|
|
86
|
+
if (prev === void 0) return false;
|
|
87
|
+
const scalar = deepestTrailingScalar(prev);
|
|
88
|
+
if (scalar === void 0 || scalar.chomp !== "keep") return false;
|
|
89
|
+
if (scalar.style !== "block-literal" && scalar.style !== "block-folded") return false;
|
|
90
|
+
const start = blankLineAboveStart(text, offset);
|
|
91
|
+
return start >= scalar.offset && start < scalar.offset + scalar.length;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* True when the line immediately below the line containing `offset` is blank
|
|
95
|
+
* (empty or horizontal whitespace only) AND is itself followed by another
|
|
96
|
+
* line. Purely local, the mirror of {@link hasBlankLineAbove} — used to embed
|
|
97
|
+
* a blank line AFTER a comment run as a trailing empty line in the stored
|
|
98
|
+
* comment string.
|
|
99
|
+
*/
|
|
100
|
+
function hasBlankLineBelow(text, offset) {
|
|
101
|
+
const lineEnd = text.indexOf("\n", Math.max(0, offset));
|
|
102
|
+
if (lineEnd < 0) return false;
|
|
103
|
+
const nextEnd = text.indexOf("\n", lineEnd + 1);
|
|
104
|
+
if (nextEnd < 0) return false;
|
|
105
|
+
return text.slice(lineEnd + 1, nextEnd).trim() === "";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The stored text of a comment token: the RAW post-`#` slice, reference
|
|
109
|
+
* parity with the `yaml` npm package — `# section` stores `" section"`,
|
|
110
|
+
* `#no-space` stores `"no-space"`, `# aligned` keeps its alignment.
|
|
111
|
+
*
|
|
112
|
+
* The one reserved string is `""`, which encodes an embedded blank line
|
|
113
|
+
* inside a joined comment run — so a spaces-only raw slice stores with ONE
|
|
114
|
+
* extra trailing space and the renderers strip it back off. A bare `#`
|
|
115
|
+
* (raw `""`) stores `" "`, `# ` (raw `" "`) stores `" "`, and so on; the
|
|
116
|
+
* escape is injective, so every comment spelling roundtrips byte-intact.
|
|
117
|
+
* Raw storage is what makes byte-intact roundtrip possible; trimming would
|
|
118
|
+
* canonicalize every comment to `# text`.
|
|
119
|
+
*/
|
|
120
|
+
function rawCommentText(source) {
|
|
121
|
+
const raw = source.startsWith("#") ? source.slice(1) : source;
|
|
122
|
+
return /^ *$/.test(raw) ? `${raw} ` : raw;
|
|
123
|
+
}
|
|
124
|
+
/** Join two optional comment blocks with a newline. */
|
|
125
|
+
function joinComments(a, b) {
|
|
126
|
+
return a === void 0 ? b : `${a}\n${b}`;
|
|
127
|
+
}
|
|
128
|
+
/** Zero-based column of `offset` within its line. */
|
|
129
|
+
function columnAt(text, offset) {
|
|
130
|
+
if (offset <= 0) return 0;
|
|
131
|
+
return offset - (text.lastIndexOf("\n", offset - 1) + 1);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Rebuild `node` with the given comment fields merged in (existing fields are
|
|
135
|
+
* kept unless overridden; `commentBefore`/`comment` join with a newline when
|
|
136
|
+
* both sides are present). Aliases are returned unchanged — they carry no
|
|
137
|
+
* comment fields.
|
|
138
|
+
*/
|
|
139
|
+
function withCommentFields(node, fields) {
|
|
140
|
+
if (node instanceof YamlScalar) return new YamlScalar({
|
|
141
|
+
value: node.value,
|
|
142
|
+
style: node.style,
|
|
143
|
+
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
144
|
+
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
145
|
+
...mergedCommentFields(node, fields),
|
|
146
|
+
...node.chomp !== void 0 ? { chomp: node.chomp } : {},
|
|
147
|
+
...node.blockIndent !== void 0 ? { blockIndent: node.blockIndent } : {},
|
|
148
|
+
...node.raw !== void 0 ? { raw: node.raw } : {},
|
|
149
|
+
...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
|
|
150
|
+
offset: node.offset,
|
|
151
|
+
length: node.length
|
|
152
|
+
});
|
|
153
|
+
if (node instanceof YamlMap) return new YamlMap({
|
|
154
|
+
items: node.items,
|
|
155
|
+
style: node.style,
|
|
156
|
+
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
157
|
+
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
158
|
+
...mergedCommentFields(node, fields),
|
|
159
|
+
...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
|
|
160
|
+
offset: node.offset,
|
|
161
|
+
length: node.length
|
|
162
|
+
});
|
|
163
|
+
if (node instanceof YamlSeq) return new YamlSeq({
|
|
164
|
+
items: node.items,
|
|
165
|
+
style: node.style,
|
|
166
|
+
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
167
|
+
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
168
|
+
...mergedCommentFields(node, fields),
|
|
169
|
+
...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
|
|
170
|
+
offset: node.offset,
|
|
171
|
+
length: node.length
|
|
172
|
+
});
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
function mergedCommentFields(existing, incoming) {
|
|
176
|
+
const commentBefore = incoming.commentBefore !== void 0 ? existing.commentBefore !== void 0 ? `${existing.commentBefore}\n${incoming.commentBefore}` : incoming.commentBefore : existing.commentBefore;
|
|
177
|
+
const comment = incoming.comment !== void 0 ? existing.comment !== void 0 ? `${existing.comment}\n${incoming.comment}` : incoming.comment : existing.comment;
|
|
178
|
+
const spaceBefore = incoming.spaceBefore ?? existing.spaceBefore;
|
|
179
|
+
return {
|
|
180
|
+
...commentBefore !== void 0 ? { commentBefore } : {},
|
|
181
|
+
...comment !== void 0 ? { comment } : {},
|
|
182
|
+
...spaceBefore !== void 0 ? { spaceBefore } : {}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//#endregion
|
|
187
|
+
export { blankAboveIsKeepChompContent, blankLineAboveStart, columnAt, hasBlankLineAbove, hasBlankLineBelow, isOwnLineAt, joinComments, rawCommentText, sameLineSpan, withCommentFields };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { YamlAlias, YamlMap, YamlPair, YamlScalar, YamlSeq } from "../../YamlNode.js";
|
|
2
|
-
import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
|
|
3
2
|
import { parseCSTAll } from "../cst-parser.js";
|
|
4
|
-
import {
|
|
3
|
+
import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
|
|
4
|
+
import { hasBlankLineAbove, hasBlankLineBelow, rawCommentText } from "./comments.js";
|
|
5
|
+
import { clearMeta, commentProps, createState, hasMeta, sameLine } from "./state.js";
|
|
5
6
|
import { parseDirective, validateTagHandlesInDocument } from "./tags.js";
|
|
6
7
|
import { collectMultilinePlainScalar, findNextContentChild, getScalarStyle, hasBlockMapAfterInList, hasValueSepAfter, indexOfChild, makeScalar, resolveScalar } from "./scalars.js";
|
|
7
8
|
import { composeBlockMap, composeBlockSeq, composeFlatBlockMap } from "./block.js";
|
|
@@ -131,6 +132,9 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
131
132
|
const directives = [];
|
|
132
133
|
let contents = null;
|
|
133
134
|
let documentComment;
|
|
135
|
+
let documentCommentAfter;
|
|
136
|
+
let lastLeadingCommentOffset = -1;
|
|
137
|
+
let sawDocEndMarker = false;
|
|
134
138
|
const hasDocStart = children.some((c) => c.type === "whitespace" && c.source === "---");
|
|
135
139
|
let i = 0;
|
|
136
140
|
const meta = {};
|
|
@@ -166,6 +170,7 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
166
170
|
}
|
|
167
171
|
if (child.type === "newline" && hasMeta(meta)) sawNewlineSinceMeta = true;
|
|
168
172
|
if (child.type === "whitespace" || child.type === "newline") {
|
|
173
|
+
if (child.type === "whitespace" && child.source === "...") sawDocEndMarker = true;
|
|
169
174
|
if (child.type === "whitespace" && (child.source === "]" || child.source === "}")) state.errors.push({
|
|
170
175
|
code: "MalformedFlowCollection",
|
|
171
176
|
message: `Unexpected flow indicator '${child.source}' at document level`,
|
|
@@ -175,8 +180,18 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
175
180
|
i++;
|
|
176
181
|
continue;
|
|
177
182
|
}
|
|
178
|
-
if (child.type === "comment" && contents === null) {
|
|
179
|
-
|
|
183
|
+
if (child.type === "comment" && contents === null && !sawDocEndMarker) {
|
|
184
|
+
const text = rawCommentText(child.source);
|
|
185
|
+
if (documentComment !== void 0 && hasBlankLineAbove(state.text, child.offset)) documentComment = `${documentComment}\n`;
|
|
186
|
+
documentComment = documentComment === void 0 ? text : `${documentComment}\n${text}`;
|
|
187
|
+
lastLeadingCommentOffset = child.offset;
|
|
188
|
+
i++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (child.type === "comment") {
|
|
192
|
+
const text = rawCommentText(child.source);
|
|
193
|
+
if (documentCommentAfter !== void 0 && hasBlankLineAbove(state.text, child.offset)) documentCommentAfter = `${documentCommentAfter}\n`;
|
|
194
|
+
documentCommentAfter = documentCommentAfter === void 0 ? text : `${documentCommentAfter}\n${text}`;
|
|
180
195
|
i++;
|
|
181
196
|
continue;
|
|
182
197
|
}
|
|
@@ -395,6 +410,8 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
395
410
|
break;
|
|
396
411
|
}
|
|
397
412
|
}
|
|
413
|
+
if (documentComment !== void 0 && contents !== null && hasBlankLineBelow(state.text, lastLeadingCommentOffset)) documentComment = `${documentComment}\n`;
|
|
414
|
+
state.escapedComments.length = 0;
|
|
398
415
|
return {
|
|
399
416
|
contents,
|
|
400
417
|
errors: [...state.errors],
|
|
@@ -403,7 +420,8 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
403
420
|
hasDocumentStart: hasDocStart,
|
|
404
421
|
hasDocumentEnd: hasDocEnd,
|
|
405
422
|
hasDocumentStartTab: hasDocStartTab,
|
|
406
|
-
...documentComment !== void 0 ? { comment: documentComment } : {}
|
|
423
|
+
...documentComment !== void 0 ? { comment: documentComment } : {},
|
|
424
|
+
...documentCommentAfter !== void 0 ? { commentAfter: documentCommentAfter } : {}
|
|
407
425
|
};
|
|
408
426
|
}
|
|
409
427
|
/**
|
|
@@ -568,8 +586,9 @@ function decorateSourceMultiline(node, text) {
|
|
|
568
586
|
style: node.style,
|
|
569
587
|
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
570
588
|
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
571
|
-
...node
|
|
589
|
+
...commentProps(node),
|
|
572
590
|
...node.chomp !== void 0 ? { chomp: node.chomp } : {},
|
|
591
|
+
...node.blockIndent !== void 0 ? { blockIndent: node.blockIndent } : {},
|
|
573
592
|
...node.raw !== void 0 ? { raw: node.raw } : {},
|
|
574
593
|
sourceMultiline: true,
|
|
575
594
|
offset: node.offset,
|
|
@@ -580,7 +599,7 @@ function decorateSourceMultiline(node, text) {
|
|
|
580
599
|
const newItems = node.items.map((pair) => new YamlPair({
|
|
581
600
|
key: decorateSourceMultiline(pair.key, text) ?? pair.key,
|
|
582
601
|
value: pair.value === null ? null : decorateSourceMultiline(pair.value, text),
|
|
583
|
-
...pair
|
|
602
|
+
...commentProps(pair)
|
|
584
603
|
}));
|
|
585
604
|
const multiline = isSourceMultiline(text, node.offset, node.length);
|
|
586
605
|
return new YamlMap({
|
|
@@ -588,7 +607,7 @@ function decorateSourceMultiline(node, text) {
|
|
|
588
607
|
style: node.style,
|
|
589
608
|
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
590
609
|
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
591
|
-
...node
|
|
610
|
+
...commentProps(node),
|
|
592
611
|
...multiline ? { sourceMultiline: true } : {},
|
|
593
612
|
offset: node.offset,
|
|
594
613
|
length: node.length
|
|
@@ -602,7 +621,7 @@ function decorateSourceMultiline(node, text) {
|
|
|
602
621
|
style: node.style,
|
|
603
622
|
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
604
623
|
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
605
|
-
...node
|
|
624
|
+
...commentProps(node),
|
|
606
625
|
...multiline ? { sourceMultiline: true } : {},
|
|
607
626
|
offset: node.offset,
|
|
608
627
|
length: node.length
|
|
@@ -619,6 +638,7 @@ function decorateDocumentSourceMultiline(doc, text) {
|
|
|
619
638
|
warnings: doc.warnings,
|
|
620
639
|
directives: doc.directives,
|
|
621
640
|
...doc.comment !== void 0 ? { comment: doc.comment } : {},
|
|
641
|
+
...doc.commentAfter !== void 0 ? { commentAfter: doc.commentAfter } : {},
|
|
622
642
|
hasDocumentStart: doc.hasDocumentStart,
|
|
623
643
|
hasDocumentEnd: doc.hasDocumentEnd,
|
|
624
644
|
hasDocumentStartTab: doc.hasDocumentStartTab
|
|
@@ -641,12 +661,29 @@ const EMPTY_DOCUMENT = {
|
|
|
641
661
|
* state and therefore appear in the returned document's `errors`.
|
|
642
662
|
*/
|
|
643
663
|
function composeFirstDocument(text, options) {
|
|
664
|
+
return composeFirstDocumentCounted(text, options).document;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* {@link composeFirstDocument} plus the CST-level document count of the whole
|
|
668
|
+
* stream, from the single CST parse the compose already performs — no second
|
|
669
|
+
* walk, and correct where a regex on `---` is not (a `---` inside a block
|
|
670
|
+
* scalar or quoted string is content, not a document marker). Callers that
|
|
671
|
+
* must refuse multi-document input (the `YamlFormat` single-document
|
|
672
|
+
* contract) read `documentCount` instead of re-parsing.
|
|
673
|
+
*/
|
|
674
|
+
function composeFirstDocumentCounted(text, options) {
|
|
644
675
|
const cstNodes = parseCSTAll(text);
|
|
645
676
|
const state = createState(text, FLOW, options);
|
|
646
677
|
validateCrossDocumentDirectives(cstNodes, state);
|
|
647
678
|
const doc = cstNodes[0];
|
|
648
|
-
if (!doc) return
|
|
649
|
-
|
|
679
|
+
if (!doc) return {
|
|
680
|
+
document: EMPTY_DOCUMENT,
|
|
681
|
+
documentCount: 0
|
|
682
|
+
};
|
|
683
|
+
return {
|
|
684
|
+
document: decorateDocumentSourceMultiline(composeDocument(doc, state, cstNodes.length > 1, cstNodes[1]), text),
|
|
685
|
+
documentCount: cstNodes.length
|
|
686
|
+
};
|
|
650
687
|
}
|
|
651
688
|
/**
|
|
652
689
|
* Compose every document of `text` with full error recovery — v3
|
|
@@ -674,4 +711,4 @@ function composeAllDocuments(text, options) {
|
|
|
674
711
|
}
|
|
675
712
|
|
|
676
713
|
//#endregion
|
|
677
|
-
export { composeAllDocuments, composeDocument, composeFirstDocument, validateCrossDocumentDirectives };
|
|
714
|
+
export { EMPTY_DOCUMENT, composeAllDocuments, composeDocument, composeFirstDocument, composeFirstDocumentCounted, validateCrossDocumentDirectives };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { YamlMap, YamlScalar, YamlSeq } from "../../YamlNode.js";
|
|
2
2
|
import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
|
|
3
|
+
import { blankLineAboveStart, hasBlankLineAbove, isOwnLineAt, joinComments, rawCommentText, withCommentFields } from "./comments.js";
|
|
3
4
|
import { enterNesting, exitNesting, hasMeta } from "./state.js";
|
|
4
5
|
import { collectMultilineKey, collectMultilinePlainScalar, getScalarStyle, hasValueSepThroughPlainScalars, makeScalar, resolveScalar } from "./scalars.js";
|
|
5
6
|
import { buildPairs, checkDuplicateKeys, checkMultilineImplicitKeys } from "./block.js";
|
|
@@ -117,8 +118,9 @@ function composeFlowMapInner(cst, state, meta, parentBlockColumn) {
|
|
|
117
118
|
validateFlowContentIndent(cst, state, parentBlockColumn);
|
|
118
119
|
validateFlowSeparators(children, state, "{", "}");
|
|
119
120
|
const items = flattenFlowChildren(children.filter((c) => !(c.type === "whitespace" && (c.source === "{" || c.source === "}" || c.source.trim() === ""))), state);
|
|
120
|
-
buildPairs(items, pairs, state.text);
|
|
121
|
+
const trailingComment = buildPairs(items, pairs, state.text);
|
|
121
122
|
if (state.options.uniqueKeys) checkDuplicateKeys(pairs, state);
|
|
123
|
+
const mapComment = meta?.comment !== void 0 ? trailingComment !== void 0 ? `${meta.comment}\n${trailingComment}` : meta.comment : trailingComment;
|
|
122
124
|
const map = new YamlMap({
|
|
123
125
|
items: pairs,
|
|
124
126
|
style: "flow",
|
|
@@ -126,7 +128,7 @@ function composeFlowMapInner(cst, state, meta, parentBlockColumn) {
|
|
|
126
128
|
length: cst.length,
|
|
127
129
|
...meta?.tag !== void 0 ? { tag: meta.tag } : {},
|
|
128
130
|
...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
|
|
129
|
-
...
|
|
131
|
+
...mapComment !== void 0 ? { comment: mapComment } : {}
|
|
130
132
|
});
|
|
131
133
|
if (meta?.anchor) registerAnchor(map, meta.anchor, state, cst.offset);
|
|
132
134
|
return map;
|
|
@@ -188,7 +190,8 @@ function flattenFlowChildren(children, state) {
|
|
|
188
190
|
if (child.type === "comment") {
|
|
189
191
|
items.push({
|
|
190
192
|
kind: "comment",
|
|
191
|
-
comment:
|
|
193
|
+
comment: rawCommentText(child.source),
|
|
194
|
+
offset: child.offset
|
|
192
195
|
});
|
|
193
196
|
continue;
|
|
194
197
|
}
|
|
@@ -370,25 +373,95 @@ function composeFlowSeqInner(cst, state, meta, parentBlockColumn) {
|
|
|
370
373
|
current.push(child);
|
|
371
374
|
}
|
|
372
375
|
if (current.length > 0) segments.push(current);
|
|
376
|
+
let seqTrailing;
|
|
377
|
+
let pending = [];
|
|
378
|
+
let pendingSpace = false;
|
|
379
|
+
let lastItemEnd = -1;
|
|
380
|
+
let pushedIdx = -1;
|
|
381
|
+
const joinPending = (parts) => {
|
|
382
|
+
if (parts.length === 0) return void 0;
|
|
383
|
+
let out = parts[0].text;
|
|
384
|
+
for (let k = 1; k < parts.length; k++) {
|
|
385
|
+
const part = parts[k];
|
|
386
|
+
out += part.blankAbove ? `\n\n${part.text}` : `\n${part.text}`;
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
};
|
|
390
|
+
const blankIsBetweenItems = (anchorOffset) => blankLineAboveStart(state.text, anchorOffset) >= lastItemEnd;
|
|
391
|
+
const acceptOwnLineComment = (cText, cOff) => {
|
|
392
|
+
const blankAbove = cOff >= 0 && hasBlankLineAbove(state.text, cOff);
|
|
393
|
+
if (blankAbove && pending.length === 0 && lastItemEnd >= 0 && blankIsBetweenItems(cOff)) pendingSpace = true;
|
|
394
|
+
pending.push({
|
|
395
|
+
text: cText,
|
|
396
|
+
offset: cOff,
|
|
397
|
+
blankAbove: blankAbove && pending.length > 0
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
/** The pending comment fields for the item being pushed, then reset. */
|
|
401
|
+
const takePendingFields = (anchorOffset) => {
|
|
402
|
+
let commentBefore = joinPending(pending);
|
|
403
|
+
if (anchorOffset >= 0 && hasBlankLineAbove(state.text, anchorOffset)) {
|
|
404
|
+
if (commentBefore !== void 0) commentBefore = `${commentBefore}\n`;
|
|
405
|
+
else if (!pendingSpace && lastItemEnd >= 0 && blankIsBetweenItems(anchorOffset)) pendingSpace = true;
|
|
406
|
+
}
|
|
407
|
+
const fields = {
|
|
408
|
+
...commentBefore !== void 0 ? { commentBefore } : {},
|
|
409
|
+
...pendingSpace ? { spaceBefore: true } : {}
|
|
410
|
+
};
|
|
411
|
+
pending = [];
|
|
412
|
+
pendingSpace = false;
|
|
413
|
+
return fields;
|
|
414
|
+
};
|
|
415
|
+
const hasPendingFields = (fields) => fields.commentBefore !== void 0 || fields.spaceBefore === true;
|
|
373
416
|
for (const segment of segments) if (segment.some((c) => c.type === "whitespace" && c.source === ":")) {
|
|
374
|
-
const
|
|
417
|
+
const content = segment.filter((c) => !(c.type === "whitespace" && c.source.trim() === ""));
|
|
418
|
+
const semItems = flattenFlowChildren(content, state);
|
|
375
419
|
const pairs = [];
|
|
376
|
-
buildPairs(semItems, pairs, state.text);
|
|
420
|
+
const segTrailing = buildPairs(semItems, pairs, state.text);
|
|
377
421
|
if (!segment.some((c) => c.type === "whitespace" && c.source === "?")) checkMultilineImplicitKeys(pairs, state, semItems);
|
|
378
422
|
const firstPair = pairs[0];
|
|
379
423
|
if (firstPair) {
|
|
424
|
+
const segFirst = content[0];
|
|
425
|
+
const fields = takePendingFields(segFirst !== void 0 ? segFirst.offset : -1);
|
|
380
426
|
const map = new YamlMap({
|
|
381
427
|
items: pairs,
|
|
382
428
|
style: "flow",
|
|
383
429
|
offset: firstPair.key.offset,
|
|
384
|
-
length: 0
|
|
430
|
+
length: 0,
|
|
431
|
+
...fields,
|
|
432
|
+
...segTrailing !== void 0 ? { comment: segTrailing } : {}
|
|
385
433
|
});
|
|
386
434
|
items.push(map);
|
|
387
|
-
|
|
435
|
+
pushedIdx = items.length - 1;
|
|
436
|
+
lastItemEnd = map.offset + map.length;
|
|
437
|
+
} else if (segTrailing !== void 0) seqTrailing = joinComments(seqTrailing, segTrailing);
|
|
388
438
|
} else {
|
|
389
439
|
const semItems = flattenFlowChildren(segment.filter((c) => !(c.type === "whitespace" && c.source.trim() === "")), state);
|
|
390
|
-
for (const si of semItems)
|
|
440
|
+
for (const si of semItems) {
|
|
441
|
+
if (si.kind === "comment") {
|
|
442
|
+
const cText = si.comment ?? "";
|
|
443
|
+
if (!(si.offset === void 0 ? true : isOwnLineAt(state.text, si.offset)) && pushedIdx >= 0) {
|
|
444
|
+
const prev = items[pushedIdx];
|
|
445
|
+
if (prev) items[pushedIdx] = withCommentFields(prev, { comment: cText });
|
|
446
|
+
} else acceptOwnLineComment(cText, si.offset ?? -1);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (si.kind === "node" && si.node) {
|
|
450
|
+
const fields = takePendingFields(si.node.length > 0 ? si.node.offset : -1);
|
|
451
|
+
const node = hasPendingFields(fields) ? withCommentFields(si.node, fields) : si.node;
|
|
452
|
+
items.push(node);
|
|
453
|
+
pushedIdx = items.length - 1;
|
|
454
|
+
lastItemEnd = si.node.offset + si.node.length;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (pending.length > 0) {
|
|
459
|
+
const joined = joinPending(pending);
|
|
460
|
+
const first = pending[0];
|
|
461
|
+
const withLead = joined !== void 0 && first !== void 0 && lastItemEnd >= 0 && first.offset >= 0 && hasBlankLineAbove(state.text, first.offset) ? `\n${joined}` : joined;
|
|
462
|
+
if (withLead !== void 0) seqTrailing = joinComments(seqTrailing, withLead);
|
|
391
463
|
}
|
|
464
|
+
const seqComment = meta?.comment !== void 0 ? seqTrailing !== void 0 ? `${meta.comment}\n${seqTrailing}` : meta.comment : seqTrailing;
|
|
392
465
|
const seq = new YamlSeq({
|
|
393
466
|
items,
|
|
394
467
|
style: "flow",
|
|
@@ -396,7 +469,7 @@ function composeFlowSeqInner(cst, state, meta, parentBlockColumn) {
|
|
|
396
469
|
length: cst.length,
|
|
397
470
|
...meta?.tag !== void 0 ? { tag: meta.tag } : {},
|
|
398
471
|
...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
|
|
399
|
-
...
|
|
472
|
+
...seqComment !== void 0 ? { comment: seqComment } : {}
|
|
400
473
|
});
|
|
401
474
|
if (meta?.anchor) registerAnchor(seq, meta.anchor, state, cst.offset);
|
|
402
475
|
return seq;
|
|
@@ -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);
|
|
@@ -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")
|
|
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")
|
|
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
|
-
...
|
|
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 };
|
package/internal/cst-parser.js
CHANGED
|
@@ -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
|
-
|
|
368
|
+
advance(state);
|
|
369
|
+
nodes.push(makeLeafNode("whitespace", token, state.text));
|
|
369
370
|
continue;
|
|
370
371
|
}
|
|
371
372
|
if (token.kind === "newline") break;
|
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
|