@effected/yaml 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -7
- package/Yaml.js +80 -32
- package/YamlDocument.js +23 -2
- package/YamlFormat.js +162 -21
- package/YamlLint.js +239 -0
- package/YamlLintRule.js +36 -0
- package/YamlNode.js +43 -4
- package/YamlToken.js +139 -0
- package/YamlVisitor.js +52 -16
- package/index.d.ts +446 -12
- package/index.js +4 -1
- package/internal/composer/anchors.js +2 -27
- package/internal/composer/block.js +291 -67
- package/internal/composer/comments.js +217 -0
- package/internal/composer/document.js +141 -14
- package/internal/composer/flow.js +103 -10
- 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 +342 -54
- package/package.json +3 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { YamlAlias, 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 only thing before `offset` on its line is a block indicator —
|
|
28
|
+
* `?`, `:` or `-` — plus whitespace. Such a comment sits on an indicator line
|
|
29
|
+
* with its node BELOW, so it leads that node rather than trailing whatever
|
|
30
|
+
* came before (`? # c` / ` - seq1`). Without this, the `? ` prefix makes the
|
|
31
|
+
* comment look like a trailing comment on the previous entry.
|
|
32
|
+
*/
|
|
33
|
+
function isAfterIndicatorOnly(text, offset) {
|
|
34
|
+
let i = offset - 1;
|
|
35
|
+
let sawIndicator = false;
|
|
36
|
+
while (i >= 0) {
|
|
37
|
+
const ch = text[i];
|
|
38
|
+
if (ch === " " || ch === " ") {
|
|
39
|
+
i--;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (!sawIndicator && (ch === "?" || ch === "-" || ch === ":")) {
|
|
43
|
+
sawIndicator = true;
|
|
44
|
+
i--;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return sawIndicator && (ch === "\n" || ch === "\r");
|
|
48
|
+
}
|
|
49
|
+
return sawIndicator;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* True when the line immediately above the line containing `offset` is blank
|
|
53
|
+
* (empty or horizontal whitespace only). Purely local — see
|
|
54
|
+
* {@link isOwnLineAt} for why span-based gap checks are not used.
|
|
55
|
+
*/
|
|
56
|
+
function hasBlankLineAbove(text, offset) {
|
|
57
|
+
return blankLineAboveStart(text, offset) >= 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start offset of the blank line immediately above the line containing
|
|
61
|
+
* `offset`, or `-1` when that line is not blank. The offset form of
|
|
62
|
+
* {@link hasBlankLineAbove}, for callers that must locate the blank line
|
|
63
|
+
* (e.g. to test whether it falls inside a preceding scalar token's span).
|
|
64
|
+
*/
|
|
65
|
+
function blankLineAboveStart(text, offset) {
|
|
66
|
+
const lineBreak = text.lastIndexOf("\n", Math.max(0, offset - 1));
|
|
67
|
+
if (lineBreak < 0) return -1;
|
|
68
|
+
const prevBreak = text.lastIndexOf("\n", lineBreak - 1);
|
|
69
|
+
return text.slice(prevBreak + 1, lineBreak).trim() === "" ? prevBreak + 1 : -1;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The deepest trailing scalar reached by descending last-child edges from
|
|
73
|
+
* `node` (a map's last pair's value, a seq's last item) — the node whose
|
|
74
|
+
* token span a textually-following blank line could fall inside.
|
|
75
|
+
*/
|
|
76
|
+
function deepestTrailingScalar(node) {
|
|
77
|
+
let current = node;
|
|
78
|
+
for (;;) {
|
|
79
|
+
if (current instanceof YamlScalar) return current;
|
|
80
|
+
if (current instanceof YamlMap) {
|
|
81
|
+
const last = current.items[current.items.length - 1];
|
|
82
|
+
if (last === void 0) return void 0;
|
|
83
|
+
current = last.value ?? last.key;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (current instanceof YamlSeq) {
|
|
87
|
+
const last = current.items[current.items.length - 1];
|
|
88
|
+
if (last === void 0) return void 0;
|
|
89
|
+
current = last;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* True when the blank line immediately above `offset` is CONTENT of the
|
|
97
|
+
* preceding keep-chomp block scalar, not a stylistic blank. Under `+`
|
|
98
|
+
* chomping the trailing line breaks are part of the scalar's VALUE (the
|
|
99
|
+
* composer already stores them there), so recording the same blank line as
|
|
100
|
+
* `spaceBefore` (or a leading-blank embed on a terminal comment run) would
|
|
101
|
+
* double-count it: emission renders both the kept newline and the stylistic
|
|
102
|
+
* blank, growing the document on every format pass. Clip/strip scalars are
|
|
103
|
+
* deliberately NOT excluded — their emission drops the trailing blank from
|
|
104
|
+
* the value, so the `spaceBefore` capture is exactly what re-creates it.
|
|
105
|
+
*
|
|
106
|
+
* `prev` is the last composed node before `offset` (a pair's value, a seq
|
|
107
|
+
* item); the check descends to its deepest trailing scalar and requires the
|
|
108
|
+
* blank line to start inside that scalar's token span.
|
|
109
|
+
*/
|
|
110
|
+
function blankAboveIsKeepChompContent(text, offset, prev) {
|
|
111
|
+
if (prev === void 0) return false;
|
|
112
|
+
const scalar = deepestTrailingScalar(prev);
|
|
113
|
+
if (scalar === void 0 || scalar.chomp !== "keep") return false;
|
|
114
|
+
if (scalar.style !== "block-literal" && scalar.style !== "block-folded") return false;
|
|
115
|
+
const start = blankLineAboveStart(text, offset);
|
|
116
|
+
return start >= scalar.offset && start < scalar.offset + scalar.length;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* True when the line immediately below the line containing `offset` is blank
|
|
120
|
+
* (empty or horizontal whitespace only) AND is itself followed by another
|
|
121
|
+
* line. Purely local, the mirror of {@link hasBlankLineAbove} — used to embed
|
|
122
|
+
* a blank line AFTER a comment run as a trailing empty line in the stored
|
|
123
|
+
* comment string.
|
|
124
|
+
*/
|
|
125
|
+
function hasBlankLineBelow(text, offset) {
|
|
126
|
+
const lineEnd = text.indexOf("\n", Math.max(0, offset));
|
|
127
|
+
if (lineEnd < 0) return false;
|
|
128
|
+
const nextEnd = text.indexOf("\n", lineEnd + 1);
|
|
129
|
+
if (nextEnd < 0) return false;
|
|
130
|
+
return text.slice(lineEnd + 1, nextEnd).trim() === "";
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The stored text of a comment token: the RAW post-`#` slice, reference
|
|
134
|
+
* parity with the `yaml` npm package — `# section` stores `" section"`,
|
|
135
|
+
* `#no-space` stores `"no-space"`, `# aligned` keeps its alignment.
|
|
136
|
+
*
|
|
137
|
+
* The one reserved string is `""`, which encodes an embedded blank line
|
|
138
|
+
* inside a joined comment run — so a spaces-only raw slice stores with ONE
|
|
139
|
+
* extra trailing space and the renderers strip it back off. A bare `#`
|
|
140
|
+
* (raw `""`) stores `" "`, `# ` (raw `" "`) stores `" "`, and so on; the
|
|
141
|
+
* escape is injective, so every comment spelling roundtrips byte-intact.
|
|
142
|
+
* Raw storage is what makes byte-intact roundtrip possible; trimming would
|
|
143
|
+
* canonicalize every comment to `# text`.
|
|
144
|
+
*/
|
|
145
|
+
function rawCommentText(source) {
|
|
146
|
+
const raw = source.startsWith("#") ? source.slice(1) : source;
|
|
147
|
+
return /^ *$/.test(raw) ? `${raw} ` : raw;
|
|
148
|
+
}
|
|
149
|
+
/** Join two optional comment blocks with a newline. */
|
|
150
|
+
function joinComments(a, b) {
|
|
151
|
+
return a === void 0 ? b : `${a}\n${b}`;
|
|
152
|
+
}
|
|
153
|
+
/** Zero-based column of `offset` within its line. */
|
|
154
|
+
function columnAt(text, offset) {
|
|
155
|
+
if (offset <= 0) return 0;
|
|
156
|
+
return offset - (text.lastIndexOf("\n", offset - 1) + 1);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Rebuild `node` with the given comment fields merged in (existing fields are
|
|
160
|
+
* kept unless overridden; `commentBefore`/`comment` join with a newline when
|
|
161
|
+
* both sides are present). Every node class carries the triple, aliases
|
|
162
|
+
* included.
|
|
163
|
+
*/
|
|
164
|
+
function withCommentFields(node, fields) {
|
|
165
|
+
if (node instanceof YamlScalar) return new YamlScalar({
|
|
166
|
+
value: node.value,
|
|
167
|
+
style: node.style,
|
|
168
|
+
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
169
|
+
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
170
|
+
...mergedCommentFields(node, fields),
|
|
171
|
+
...node.chomp !== void 0 ? { chomp: node.chomp } : {},
|
|
172
|
+
...node.blockIndent !== void 0 ? { blockIndent: node.blockIndent } : {},
|
|
173
|
+
...node.raw !== void 0 ? { raw: node.raw } : {},
|
|
174
|
+
...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
|
|
175
|
+
offset: node.offset,
|
|
176
|
+
length: node.length
|
|
177
|
+
});
|
|
178
|
+
if (node instanceof YamlMap) return new YamlMap({
|
|
179
|
+
items: node.items,
|
|
180
|
+
style: node.style,
|
|
181
|
+
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
182
|
+
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
183
|
+
...mergedCommentFields(node, fields),
|
|
184
|
+
...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
|
|
185
|
+
offset: node.offset,
|
|
186
|
+
length: node.length
|
|
187
|
+
});
|
|
188
|
+
if (node instanceof YamlSeq) return new YamlSeq({
|
|
189
|
+
items: node.items,
|
|
190
|
+
style: node.style,
|
|
191
|
+
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
192
|
+
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
193
|
+
...mergedCommentFields(node, fields),
|
|
194
|
+
...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
|
|
195
|
+
offset: node.offset,
|
|
196
|
+
length: node.length
|
|
197
|
+
});
|
|
198
|
+
return new YamlAlias({
|
|
199
|
+
name: node.name,
|
|
200
|
+
...mergedCommentFields(node, fields),
|
|
201
|
+
offset: node.offset,
|
|
202
|
+
length: node.length
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
function mergedCommentFields(existing, incoming) {
|
|
206
|
+
const commentBefore = incoming.commentBefore !== void 0 ? existing.commentBefore !== void 0 ? `${existing.commentBefore}\n${incoming.commentBefore}` : incoming.commentBefore : existing.commentBefore;
|
|
207
|
+
const comment = incoming.comment !== void 0 ? existing.comment !== void 0 ? `${existing.comment}\n${incoming.comment}` : incoming.comment : existing.comment;
|
|
208
|
+
const spaceBefore = incoming.spaceBefore ?? existing.spaceBefore;
|
|
209
|
+
return {
|
|
210
|
+
...commentBefore !== void 0 ? { commentBefore } : {},
|
|
211
|
+
...comment !== void 0 ? { comment } : {},
|
|
212
|
+
...spaceBefore !== void 0 ? { spaceBefore } : {}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
export { blankAboveIsKeepChompContent, blankLineAboveStart, columnAt, hasBlankLineAbove, hasBlankLineBelow, isAfterIndicatorOnly, 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, withCommentFields } 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,12 @@ 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 lastAfterMarkerCommentOffset = -1;
|
|
138
|
+
let sawDocEndMarker = false;
|
|
139
|
+
let sawDocStartMarker = false;
|
|
140
|
+
let afterMarkerComment;
|
|
134
141
|
const hasDocStart = children.some((c) => c.type === "whitespace" && c.source === "---");
|
|
135
142
|
let i = 0;
|
|
136
143
|
const meta = {};
|
|
@@ -166,6 +173,8 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
166
173
|
}
|
|
167
174
|
if (child.type === "newline" && hasMeta(meta)) sawNewlineSinceMeta = true;
|
|
168
175
|
if (child.type === "whitespace" || child.type === "newline") {
|
|
176
|
+
if (child.type === "whitespace" && child.source === "...") sawDocEndMarker = true;
|
|
177
|
+
if (child.type === "whitespace" && child.source === "---") sawDocStartMarker = true;
|
|
169
178
|
if (child.type === "whitespace" && (child.source === "]" || child.source === "}")) state.errors.push({
|
|
170
179
|
code: "MalformedFlowCollection",
|
|
171
180
|
message: `Unexpected flow indicator '${child.source}' at document level`,
|
|
@@ -175,8 +184,24 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
175
184
|
i++;
|
|
176
185
|
continue;
|
|
177
186
|
}
|
|
178
|
-
if (child.type === "comment" && contents === null) {
|
|
179
|
-
|
|
187
|
+
if (child.type === "comment" && contents === null && !sawDocEndMarker) {
|
|
188
|
+
const text = rawCommentText(child.source);
|
|
189
|
+
if (sawDocStartMarker) {
|
|
190
|
+
if (afterMarkerComment !== void 0 && hasBlankLineAbove(state.text, child.offset)) afterMarkerComment = `${afterMarkerComment}\n`;
|
|
191
|
+
afterMarkerComment = afterMarkerComment === void 0 ? text : `${afterMarkerComment}\n${text}`;
|
|
192
|
+
lastAfterMarkerCommentOffset = child.offset;
|
|
193
|
+
} else {
|
|
194
|
+
if (documentComment !== void 0 && hasBlankLineAbove(state.text, child.offset)) documentComment = `${documentComment}\n`;
|
|
195
|
+
documentComment = documentComment === void 0 ? text : `${documentComment}\n${text}`;
|
|
196
|
+
lastLeadingCommentOffset = child.offset;
|
|
197
|
+
}
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (child.type === "comment") {
|
|
202
|
+
const text = rawCommentText(child.source);
|
|
203
|
+
if (documentCommentAfter !== void 0 && hasBlankLineAbove(state.text, child.offset)) documentCommentAfter = `${documentCommentAfter}\n`;
|
|
204
|
+
documentCommentAfter = documentCommentAfter === void 0 ? text : `${documentCommentAfter}\n${text}`;
|
|
180
205
|
i++;
|
|
181
206
|
continue;
|
|
182
207
|
}
|
|
@@ -395,15 +420,33 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
|
|
|
395
420
|
break;
|
|
396
421
|
}
|
|
397
422
|
}
|
|
423
|
+
if (documentComment !== void 0 && contents !== null && hasBlankLineBelow(state.text, lastLeadingCommentOffset)) documentComment = `${documentComment}\n`;
|
|
424
|
+
if (afterMarkerComment !== void 0 && contents !== null && hasBlankLineBelow(state.text, lastAfterMarkerCommentOffset)) afterMarkerComment = `${afterMarkerComment}\n`;
|
|
425
|
+
state.escapedComments.length = 0;
|
|
426
|
+
if (documentCommentAfter === void 0 && contents !== null && contents.comment !== void 0) {
|
|
427
|
+
if ((contents instanceof YamlMap || contents instanceof YamlSeq) && contents.style !== "flow") {
|
|
428
|
+
documentCommentAfter = contents.comment;
|
|
429
|
+
contents = stripOwnComment(contents);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
let headerForDocument;
|
|
433
|
+
let decorated = contents;
|
|
434
|
+
if (documentComment !== void 0) {
|
|
435
|
+
if (hasDocStart || contents === null) headerForDocument = documentComment;
|
|
436
|
+
else decorated = attachHeaderToFirstEntry(contents, documentComment);
|
|
437
|
+
}
|
|
438
|
+
if (afterMarkerComment !== void 0 && decorated !== null) decorated = withCommentFields(decorated, { commentBefore: afterMarkerComment });
|
|
439
|
+
else if (afterMarkerComment !== void 0) headerForDocument = headerForDocument === void 0 ? afterMarkerComment : `${headerForDocument}\n${afterMarkerComment}`;
|
|
398
440
|
return {
|
|
399
|
-
contents,
|
|
441
|
+
contents: decorated,
|
|
400
442
|
errors: [...state.errors],
|
|
401
443
|
warnings: [...state.warnings],
|
|
402
444
|
directives,
|
|
403
445
|
hasDocumentStart: hasDocStart,
|
|
404
446
|
hasDocumentEnd: hasDocEnd,
|
|
405
447
|
hasDocumentStartTab: hasDocStartTab,
|
|
406
|
-
...
|
|
448
|
+
...headerForDocument !== void 0 ? { commentBefore: headerForDocument } : {},
|
|
449
|
+
...documentCommentAfter !== void 0 ? { comment: documentCommentAfter } : {}
|
|
407
450
|
};
|
|
408
451
|
}
|
|
409
452
|
/**
|
|
@@ -568,8 +611,9 @@ function decorateSourceMultiline(node, text) {
|
|
|
568
611
|
style: node.style,
|
|
569
612
|
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
570
613
|
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
571
|
-
...node
|
|
614
|
+
...commentProps(node),
|
|
572
615
|
...node.chomp !== void 0 ? { chomp: node.chomp } : {},
|
|
616
|
+
...node.blockIndent !== void 0 ? { blockIndent: node.blockIndent } : {},
|
|
573
617
|
...node.raw !== void 0 ? { raw: node.raw } : {},
|
|
574
618
|
sourceMultiline: true,
|
|
575
619
|
offset: node.offset,
|
|
@@ -579,8 +623,7 @@ function decorateSourceMultiline(node, text) {
|
|
|
579
623
|
if (node instanceof YamlMap) {
|
|
580
624
|
const newItems = node.items.map((pair) => new YamlPair({
|
|
581
625
|
key: decorateSourceMultiline(pair.key, text) ?? pair.key,
|
|
582
|
-
value: pair.value === null ? null : decorateSourceMultiline(pair.value, text)
|
|
583
|
-
...pair.comment !== void 0 ? { comment: pair.comment } : {}
|
|
626
|
+
value: pair.value === null ? null : decorateSourceMultiline(pair.value, text)
|
|
584
627
|
}));
|
|
585
628
|
const multiline = isSourceMultiline(text, node.offset, node.length);
|
|
586
629
|
return new YamlMap({
|
|
@@ -588,7 +631,7 @@ function decorateSourceMultiline(node, text) {
|
|
|
588
631
|
style: node.style,
|
|
589
632
|
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
590
633
|
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
591
|
-
...node
|
|
634
|
+
...commentProps(node),
|
|
592
635
|
...multiline ? { sourceMultiline: true } : {},
|
|
593
636
|
offset: node.offset,
|
|
594
637
|
length: node.length
|
|
@@ -602,7 +645,7 @@ function decorateSourceMultiline(node, text) {
|
|
|
602
645
|
style: node.style,
|
|
603
646
|
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
604
647
|
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
605
|
-
...node
|
|
648
|
+
...commentProps(node),
|
|
606
649
|
...multiline ? { sourceMultiline: true } : {},
|
|
607
650
|
offset: node.offset,
|
|
608
651
|
length: node.length
|
|
@@ -618,6 +661,7 @@ function decorateDocumentSourceMultiline(doc, text) {
|
|
|
618
661
|
errors: doc.errors,
|
|
619
662
|
warnings: doc.warnings,
|
|
620
663
|
directives: doc.directives,
|
|
664
|
+
...doc.commentBefore !== void 0 ? { commentBefore: doc.commentBefore } : {},
|
|
621
665
|
...doc.comment !== void 0 ? { comment: doc.comment } : {},
|
|
622
666
|
hasDocumentStart: doc.hasDocumentStart,
|
|
623
667
|
hasDocumentEnd: doc.hasDocumentEnd,
|
|
@@ -641,12 +685,29 @@ const EMPTY_DOCUMENT = {
|
|
|
641
685
|
* state and therefore appear in the returned document's `errors`.
|
|
642
686
|
*/
|
|
643
687
|
function composeFirstDocument(text, options) {
|
|
688
|
+
return composeFirstDocumentCounted(text, options).document;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* {@link composeFirstDocument} plus the CST-level document count of the whole
|
|
692
|
+
* stream, from the single CST parse the compose already performs — no second
|
|
693
|
+
* walk, and correct where a regex on `---` is not (a `---` inside a block
|
|
694
|
+
* scalar or quoted string is content, not a document marker). Callers that
|
|
695
|
+
* must refuse multi-document input (the `YamlFormat` single-document
|
|
696
|
+
* contract) read `documentCount` instead of re-parsing.
|
|
697
|
+
*/
|
|
698
|
+
function composeFirstDocumentCounted(text, options) {
|
|
644
699
|
const cstNodes = parseCSTAll(text);
|
|
645
700
|
const state = createState(text, FLOW, options);
|
|
646
701
|
validateCrossDocumentDirectives(cstNodes, state);
|
|
647
702
|
const doc = cstNodes[0];
|
|
648
|
-
if (!doc) return
|
|
649
|
-
|
|
703
|
+
if (!doc) return {
|
|
704
|
+
document: EMPTY_DOCUMENT,
|
|
705
|
+
documentCount: 0
|
|
706
|
+
};
|
|
707
|
+
return {
|
|
708
|
+
document: decorateDocumentSourceMultiline(composeDocument(doc, state, cstNodes.length > 1, cstNodes[1]), text),
|
|
709
|
+
documentCount: cstNodes.length
|
|
710
|
+
};
|
|
650
711
|
}
|
|
651
712
|
/**
|
|
652
713
|
* Compose every document of `text` with full error recovery — v3
|
|
@@ -672,6 +733,72 @@ function composeAllDocuments(text, options) {
|
|
|
672
733
|
streamErrors: crossDocState.errors
|
|
673
734
|
};
|
|
674
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Attach a marker-less document header to the FIRST entry of the content — the
|
|
738
|
+
* first pair's key for a mapping, the first item for a sequence, the node
|
|
739
|
+
* itself for a scalar. Without a `---` boundary the header is just the first
|
|
740
|
+
* own-line comment in the document's item stream, so it forward-attributes
|
|
741
|
+
* like any other.
|
|
742
|
+
*/
|
|
743
|
+
function attachHeaderToFirstEntry(contents, header) {
|
|
744
|
+
if (contents instanceof YamlMap && contents.items.length > 0) {
|
|
745
|
+
const first = contents.items[0];
|
|
746
|
+
const items = [...contents.items];
|
|
747
|
+
items[0] = new YamlPair({
|
|
748
|
+
key: withCommentFields(first.key, { commentBefore: header }),
|
|
749
|
+
value: first.value
|
|
750
|
+
});
|
|
751
|
+
return new YamlMap({
|
|
752
|
+
items,
|
|
753
|
+
style: contents.style,
|
|
754
|
+
...contents.tag !== void 0 ? { tag: contents.tag } : {},
|
|
755
|
+
...contents.anchor !== void 0 ? { anchor: contents.anchor } : {},
|
|
756
|
+
...contents.commentBefore !== void 0 ? { commentBefore: contents.commentBefore } : {},
|
|
757
|
+
...contents.comment !== void 0 ? { comment: contents.comment } : {},
|
|
758
|
+
...contents.spaceBefore !== void 0 ? { spaceBefore: contents.spaceBefore } : {},
|
|
759
|
+
...contents.sourceMultiline !== void 0 ? { sourceMultiline: contents.sourceMultiline } : {},
|
|
760
|
+
offset: contents.offset,
|
|
761
|
+
length: contents.length
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (contents instanceof YamlSeq && contents.items.length > 0) {
|
|
765
|
+
const items = [...contents.items];
|
|
766
|
+
items[0] = withCommentFields(items[0], { commentBefore: header });
|
|
767
|
+
return new YamlSeq({
|
|
768
|
+
items,
|
|
769
|
+
style: contents.style,
|
|
770
|
+
...contents.tag !== void 0 ? { tag: contents.tag } : {},
|
|
771
|
+
...contents.anchor !== void 0 ? { anchor: contents.anchor } : {},
|
|
772
|
+
...contents.commentBefore !== void 0 ? { commentBefore: contents.commentBefore } : {},
|
|
773
|
+
...contents.comment !== void 0 ? { comment: contents.comment } : {},
|
|
774
|
+
...contents.spaceBefore !== void 0 ? { spaceBefore: contents.spaceBefore } : {},
|
|
775
|
+
...contents.sourceMultiline !== void 0 ? { sourceMultiline: contents.sourceMultiline } : {},
|
|
776
|
+
offset: contents.offset,
|
|
777
|
+
length: contents.length
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return withCommentFields(contents, { commentBefore: header });
|
|
781
|
+
}
|
|
782
|
+
/** Rebuild a collection without its own trailing `comment`. */
|
|
783
|
+
function stripOwnComment(node) {
|
|
784
|
+
const shared = {
|
|
785
|
+
style: node.style,
|
|
786
|
+
...node.tag !== void 0 ? { tag: node.tag } : {},
|
|
787
|
+
...node.anchor !== void 0 ? { anchor: node.anchor } : {},
|
|
788
|
+
...node.commentBefore !== void 0 ? { commentBefore: node.commentBefore } : {},
|
|
789
|
+
...node.spaceBefore !== void 0 ? { spaceBefore: node.spaceBefore } : {},
|
|
790
|
+
...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
|
|
791
|
+
offset: node.offset,
|
|
792
|
+
length: node.length
|
|
793
|
+
};
|
|
794
|
+
return node instanceof YamlMap ? new YamlMap({
|
|
795
|
+
items: node.items,
|
|
796
|
+
...shared
|
|
797
|
+
}) : new YamlSeq({
|
|
798
|
+
items: node.items,
|
|
799
|
+
...shared
|
|
800
|
+
});
|
|
801
|
+
}
|
|
675
802
|
|
|
676
803
|
//#endregion
|
|
677
|
-
export { composeAllDocuments, composeDocument, composeFirstDocument, validateCrossDocumentDirectives };
|
|
804
|
+
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";
|
|
@@ -76,7 +77,7 @@ function validateFlowContentIndent(cst, state, parentBlockColumn) {
|
|
|
76
77
|
i++;
|
|
77
78
|
continue;
|
|
78
79
|
}
|
|
79
|
-
if (i - lineStart <= parentBlockColumn) {
|
|
80
|
+
if (i - lineStart <= parentBlockColumn && !isClosersOnly(text, i, end)) {
|
|
80
81
|
state.errors.push({
|
|
81
82
|
code: "InvalidIndentation",
|
|
82
83
|
message: "Flow content continuation line must be indented past the parent block",
|
|
@@ -90,6 +91,26 @@ function validateFlowContentIndent(cst, state, parentBlockColumn) {
|
|
|
90
91
|
i++;
|
|
91
92
|
}
|
|
92
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* True when everything from `start` to the end of that line (bounded by `end`)
|
|
96
|
+
* is flow closing punctuation — `}`, `]`, separating commas and whitespace.
|
|
97
|
+
* Such a line closes the collection rather than carrying content, so the
|
|
98
|
+
* continuation-indent floor does not apply to it.
|
|
99
|
+
*/
|
|
100
|
+
function isClosersOnly(text, start, end) {
|
|
101
|
+
let sawCloser = false;
|
|
102
|
+
for (let i = start; i < end && i < text.length; i++) {
|
|
103
|
+
const ch = text[i];
|
|
104
|
+
if (ch === "\n") break;
|
|
105
|
+
if (ch === "}" || ch === "]") {
|
|
106
|
+
sawCloser = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ch === "," || ch === " " || ch === " " || ch === "\r") continue;
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return sawCloser;
|
|
113
|
+
}
|
|
93
114
|
function composeFlowMap(cst, state, meta, parentBlockColumn) {
|
|
94
115
|
if (!enterNesting(state, cst)) return new YamlMap({
|
|
95
116
|
items: [],
|
|
@@ -117,8 +138,9 @@ function composeFlowMapInner(cst, state, meta, parentBlockColumn) {
|
|
|
117
138
|
validateFlowContentIndent(cst, state, parentBlockColumn);
|
|
118
139
|
validateFlowSeparators(children, state, "{", "}");
|
|
119
140
|
const items = flattenFlowChildren(children.filter((c) => !(c.type === "whitespace" && (c.source === "{" || c.source === "}" || c.source.trim() === ""))), state);
|
|
120
|
-
buildPairs(items, pairs, state.text);
|
|
141
|
+
const trailingComment = buildPairs(items, pairs, state.text);
|
|
121
142
|
if (state.options.uniqueKeys) checkDuplicateKeys(pairs, state);
|
|
143
|
+
const mapComment = meta?.comment !== void 0 ? trailingComment !== void 0 ? `${meta.comment}\n${trailingComment}` : meta.comment : trailingComment;
|
|
122
144
|
const map = new YamlMap({
|
|
123
145
|
items: pairs,
|
|
124
146
|
style: "flow",
|
|
@@ -126,7 +148,7 @@ function composeFlowMapInner(cst, state, meta, parentBlockColumn) {
|
|
|
126
148
|
length: cst.length,
|
|
127
149
|
...meta?.tag !== void 0 ? { tag: meta.tag } : {},
|
|
128
150
|
...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
|
|
129
|
-
...
|
|
151
|
+
...mapComment !== void 0 ? { comment: mapComment } : {}
|
|
130
152
|
});
|
|
131
153
|
if (meta?.anchor) registerAnchor(map, meta.anchor, state, cst.offset);
|
|
132
154
|
return map;
|
|
@@ -188,7 +210,8 @@ function flattenFlowChildren(children, state) {
|
|
|
188
210
|
if (child.type === "comment") {
|
|
189
211
|
items.push({
|
|
190
212
|
kind: "comment",
|
|
191
|
-
comment:
|
|
213
|
+
comment: rawCommentText(child.source),
|
|
214
|
+
offset: child.offset
|
|
192
215
|
});
|
|
193
216
|
continue;
|
|
194
217
|
}
|
|
@@ -370,25 +393,95 @@ function composeFlowSeqInner(cst, state, meta, parentBlockColumn) {
|
|
|
370
393
|
current.push(child);
|
|
371
394
|
}
|
|
372
395
|
if (current.length > 0) segments.push(current);
|
|
396
|
+
let seqTrailing;
|
|
397
|
+
let pending = [];
|
|
398
|
+
let pendingSpace = false;
|
|
399
|
+
let lastItemEnd = -1;
|
|
400
|
+
let pushedIdx = -1;
|
|
401
|
+
const joinPending = (parts) => {
|
|
402
|
+
if (parts.length === 0) return void 0;
|
|
403
|
+
let out = parts[0].text;
|
|
404
|
+
for (let k = 1; k < parts.length; k++) {
|
|
405
|
+
const part = parts[k];
|
|
406
|
+
out += part.blankAbove ? `\n\n${part.text}` : `\n${part.text}`;
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
};
|
|
410
|
+
const blankIsBetweenItems = (anchorOffset) => blankLineAboveStart(state.text, anchorOffset) >= lastItemEnd;
|
|
411
|
+
const acceptOwnLineComment = (cText, cOff) => {
|
|
412
|
+
const blankAbove = cOff >= 0 && hasBlankLineAbove(state.text, cOff);
|
|
413
|
+
if (blankAbove && pending.length === 0 && lastItemEnd >= 0 && blankIsBetweenItems(cOff)) pendingSpace = true;
|
|
414
|
+
pending.push({
|
|
415
|
+
text: cText,
|
|
416
|
+
offset: cOff,
|
|
417
|
+
blankAbove: blankAbove && pending.length > 0
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
/** The pending comment fields for the item being pushed, then reset. */
|
|
421
|
+
const takePendingFields = (anchorOffset) => {
|
|
422
|
+
let commentBefore = joinPending(pending);
|
|
423
|
+
if (anchorOffset >= 0 && hasBlankLineAbove(state.text, anchorOffset)) {
|
|
424
|
+
if (commentBefore !== void 0) commentBefore = `${commentBefore}\n`;
|
|
425
|
+
else if (!pendingSpace && lastItemEnd >= 0 && blankIsBetweenItems(anchorOffset)) pendingSpace = true;
|
|
426
|
+
}
|
|
427
|
+
const fields = {
|
|
428
|
+
...commentBefore !== void 0 ? { commentBefore } : {},
|
|
429
|
+
...pendingSpace ? { spaceBefore: true } : {}
|
|
430
|
+
};
|
|
431
|
+
pending = [];
|
|
432
|
+
pendingSpace = false;
|
|
433
|
+
return fields;
|
|
434
|
+
};
|
|
435
|
+
const hasPendingFields = (fields) => fields.commentBefore !== void 0 || fields.spaceBefore === true;
|
|
373
436
|
for (const segment of segments) if (segment.some((c) => c.type === "whitespace" && c.source === ":")) {
|
|
374
|
-
const
|
|
437
|
+
const content = segment.filter((c) => !(c.type === "whitespace" && c.source.trim() === ""));
|
|
438
|
+
const semItems = flattenFlowChildren(content, state);
|
|
375
439
|
const pairs = [];
|
|
376
|
-
buildPairs(semItems, pairs, state.text);
|
|
440
|
+
const segTrailing = buildPairs(semItems, pairs, state.text);
|
|
377
441
|
if (!segment.some((c) => c.type === "whitespace" && c.source === "?")) checkMultilineImplicitKeys(pairs, state, semItems);
|
|
378
442
|
const firstPair = pairs[0];
|
|
379
443
|
if (firstPair) {
|
|
444
|
+
const segFirst = content[0];
|
|
445
|
+
const fields = takePendingFields(segFirst !== void 0 ? segFirst.offset : -1);
|
|
380
446
|
const map = new YamlMap({
|
|
381
447
|
items: pairs,
|
|
382
448
|
style: "flow",
|
|
383
449
|
offset: firstPair.key.offset,
|
|
384
|
-
length: 0
|
|
450
|
+
length: 0,
|
|
451
|
+
...fields,
|
|
452
|
+
...segTrailing !== void 0 ? { comment: segTrailing } : {}
|
|
385
453
|
});
|
|
386
454
|
items.push(map);
|
|
387
|
-
|
|
455
|
+
pushedIdx = items.length - 1;
|
|
456
|
+
lastItemEnd = map.offset + map.length;
|
|
457
|
+
} else if (segTrailing !== void 0) seqTrailing = joinComments(seqTrailing, segTrailing);
|
|
388
458
|
} else {
|
|
389
459
|
const semItems = flattenFlowChildren(segment.filter((c) => !(c.type === "whitespace" && c.source.trim() === "")), state);
|
|
390
|
-
for (const si of semItems)
|
|
460
|
+
for (const si of semItems) {
|
|
461
|
+
if (si.kind === "comment") {
|
|
462
|
+
const cText = si.comment ?? "";
|
|
463
|
+
if (!(si.offset === void 0 ? true : isOwnLineAt(state.text, si.offset)) && pushedIdx >= 0) {
|
|
464
|
+
const prev = items[pushedIdx];
|
|
465
|
+
if (prev) items[pushedIdx] = withCommentFields(prev, { comment: cText });
|
|
466
|
+
} else acceptOwnLineComment(cText, si.offset ?? -1);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (si.kind === "node" && si.node) {
|
|
470
|
+
const fields = takePendingFields(si.node.length > 0 ? si.node.offset : -1);
|
|
471
|
+
const node = hasPendingFields(fields) ? withCommentFields(si.node, fields) : si.node;
|
|
472
|
+
items.push(node);
|
|
473
|
+
pushedIdx = items.length - 1;
|
|
474
|
+
lastItemEnd = si.node.offset + si.node.length;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (pending.length > 0) {
|
|
479
|
+
const joined = joinPending(pending);
|
|
480
|
+
const first = pending[0];
|
|
481
|
+
const withLead = joined !== void 0 && first !== void 0 && lastItemEnd >= 0 && first.offset >= 0 && hasBlankLineAbove(state.text, first.offset) ? `\n${joined}` : joined;
|
|
482
|
+
if (withLead !== void 0) seqTrailing = joinComments(seqTrailing, withLead);
|
|
391
483
|
}
|
|
484
|
+
const seqComment = meta?.comment !== void 0 ? seqTrailing !== void 0 ? `${meta.comment}\n${seqTrailing}` : meta.comment : seqTrailing;
|
|
392
485
|
const seq = new YamlSeq({
|
|
393
486
|
items,
|
|
394
487
|
style: "flow",
|
|
@@ -396,7 +489,7 @@ function composeFlowSeqInner(cst, state, meta, parentBlockColumn) {
|
|
|
396
489
|
length: cst.length,
|
|
397
490
|
...meta?.tag !== void 0 ? { tag: meta.tag } : {},
|
|
398
491
|
...meta?.anchor !== void 0 ? { anchor: meta.anchor } : {},
|
|
399
|
-
...
|
|
492
|
+
...seqComment !== void 0 ? { comment: seqComment } : {}
|
|
400
493
|
});
|
|
401
494
|
if (meta?.anchor) registerAnchor(seq, meta.anchor, state, cst.offset);
|
|
402
495
|
return seq;
|