@effected/yaml 0.6.1 → 0.8.0

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