@effected/yaml 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +80 -7
  2. package/Yaml.js +80 -32
  3. package/YamlDocument.js +23 -2
  4. package/YamlFormat.js +162 -21
  5. package/YamlLint.js +239 -0
  6. package/YamlLintRule.js +36 -0
  7. package/YamlNode.js +43 -4
  8. package/YamlToken.js +139 -0
  9. package/YamlVisitor.js +52 -16
  10. package/index.d.ts +446 -12
  11. package/index.js +4 -1
  12. package/internal/composer/anchors.js +2 -27
  13. package/internal/composer/block.js +291 -67
  14. package/internal/composer/comments.js +217 -0
  15. package/internal/composer/document.js +141 -14
  16. package/internal/composer/flow.js +103 -10
  17. package/internal/composer/scalars.js +48 -11
  18. package/internal/composer/state.js +16 -2
  19. package/internal/cst-parser.js +2 -1
  20. package/internal/diagnostics.js +12 -3
  21. package/internal/fold.js +22 -10
  22. package/internal/rules/catalog.js +39 -0
  23. package/internal/rules/colon-spacing.js +77 -0
  24. package/internal/rules/comments-spacing.js +68 -0
  25. package/internal/rules/document-end.js +65 -0
  26. package/internal/rules/document-start.js +64 -0
  27. package/internal/rules/empty-lines.js +70 -0
  28. package/internal/rules/eof-newline.js +33 -0
  29. package/internal/rules/hyphen-spacing.js +52 -0
  30. package/internal/rules/indentation.js +103 -0
  31. package/internal/rules/key-duplicates.js +49 -0
  32. package/internal/rules/line-length.js +36 -0
  33. package/internal/rules/parse-validity.js +33 -0
  34. package/internal/rules/quoted-strings.js +81 -0
  35. package/internal/rules/trailing-spaces.js +39 -0
  36. package/internal/rules/truthy.js +84 -0
  37. package/internal/rules/util.js +92 -0
  38. package/internal/stringifier.js +342 -54
  39. package/package.json +3 -2
@@ -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 { clearMeta, createState, hasMeta, sameLine } from "./state.js";
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
- documentComment = child.source.startsWith("#") ? child.source.slice(1).trim() : child.source;
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
- ...documentComment !== void 0 ? { comment: documentComment } : {}
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.comment !== void 0 ? { comment: node.comment } : {},
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.comment !== void 0 ? { comment: node.comment } : {},
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.comment !== void 0 ? { comment: node.comment } : {},
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 EMPTY_DOCUMENT;
649
- return decorateDocumentSourceMultiline(composeDocument(doc, state, cstNodes.length > 1, cstNodes[1]), text);
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
- ...meta?.comment !== void 0 ? { comment: meta.comment } : {}
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: child.source.startsWith("#") ? child.source.slice(1).trim() : child.source
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 semItems = flattenFlowChildren(segment.filter((c) => !(c.type === "whitespace" && c.source.trim() === "")), state);
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) if (si.kind === "node" && si.node) items.push(si.node);
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
- ...meta?.comment !== void 0 ? { comment: meta.comment } : {}
492
+ ...seqComment !== void 0 ? { comment: seqComment } : {}
400
493
  });
401
494
  if (meta?.anchor) registerAnchor(seq, meta.anchor, state, cst.offset);
402
495
  return seq;