@effected/yaml 0.8.0 → 0.10.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 CHANGED
@@ -8,7 +8,7 @@
8
8
  Zero-dependency YAML 1.2 parsing, editing, formatting and linting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, read and preserve per-node comments, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` of AST events or positioned tokens, lint it against a rule catalog with surgical autofix, and decode straight into a validated domain schema.
9
9
 
10
10
  > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
- > development against a single pinned Effect v4 beta. Packages graduate to
11
+ > development against a single pinned Effect v4 prerelease. Packages graduate to
12
12
  > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
13
  > exactly the ones the kit is built and tested against, install
14
14
  > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
@@ -126,7 +126,9 @@ console.log(YamlFormat.formatToString(source, undefined, YamlFormattingOptions.m
126
126
  // port: 3000
127
127
  ```
128
128
 
129
- Comments are readable as data too. `YamlScalar`, `YamlMap`, `YamlSeq` and `YamlPair` each carry `commentBefore` (the own-line block above the node), `comment` (strictly the trailing comment on the node's own line) and `spaceBefore` (a blank line came first); `YamlDocument` adds `commentAfter` for a trailing block, and `YamlVisitor` tags every `Comment` event with `placement: "leading" | "trailing"`.
129
+ Comments are readable as data too. Every node class — `YamlScalar`, `YamlMap`, `YamlSeq` and `YamlAlias` — carries `commentBefore` (the own-line block above the node), `comment` (strictly the trailing comment on the node's own line) and `spaceBefore` (a blank line came first). `YamlPair` carries none of them: an entry is its `key` and its `value`, and the comments live on those two nodes. `YamlDocument` uses the same two names as the nodes do, `commentBefore` for a header block sitting ahead of a `---` marker and `comment` for the trailing block, and `YamlVisitor` tags every `Comment` event with `placement: "leading" | "trailing"`.
130
+
131
+ Which node a comment lands on follows one rule. An own-line comment above an entry leads that entry's key node, setting `commentBefore` and — when a blank line came first — `spaceBefore`. A trailing comment belongs to the last node on its line: in `a: 1 # t` the value ends the line, so `# t` is the value's `comment`; in `push: # only main` the value renders below, so the comment is the key's. Two slots per entry rather than one is what lets `push: # only main` and an own-line comment above the value both round-trip byte-intact, instead of one spelling being rewritten into the other. A header block with no `---` marker to separate it from the item stream leads the first entry's key by the same rule; one after the marker leads the root node.
130
132
 
131
133
  ## Lint and autofix
132
134
 
@@ -182,8 +184,8 @@ Multi-document streams — a Kubernetes manifest, or the two-document `pnpm-lock
182
184
  - `Yaml.stripComments` — quote-aware comment removal that keeps line numbers stable, or every byte offset stable when given a replacement character.
183
185
  - `Yaml.equals` / `Yaml.equalsValue` — semantic equality that ignores comments, whitespace, formatting and mapping key order while keeping sequence order significant.
184
186
  - `Yaml.schema` / `Yaml.fromString` / `Yaml.YamlFromString` / `Yaml.allFromString` — string→domain schema factories that decode a single document or a whole stream into a validated Effect `Schema` value.
185
- - `YamlNode` — an offset-preserving AST (`YamlScalar`, `YamlMap`, `YamlSeq`, `YamlPair`, `YamlAlias`) for locating and reading nodes by position or path, each collection and scalar carrying its own `commentBefore`, `comment` and `spaceBefore`.
186
- - `YamlDocument` — the full parsed AST plus the recovered `errors` and `warnings` arrays, so a partially valid document is still inspectable, and the leading and trailing comment blocks as `comment` / `commentAfter`.
187
+ - `YamlNode` — an offset-preserving AST (`YamlScalar`, `YamlMap`, `YamlSeq`, `YamlPair`, `YamlAlias`) for locating and reading nodes by position or path; every node class except `YamlPair` carries its own `commentBefore`, `comment` and `spaceBefore`, and a pair is just its `key` and `value`.
188
+ - `YamlDocument` — the full parsed AST plus the recovered `errors` and `warnings` arrays, so a partially valid document is still inspectable, and the header and trailing comment blocks as `commentBefore` / `comment`, the same two names the node classes use.
187
189
  - `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch; `preserveComments: false` opts out. `format` handles multi-document streams whole, re-emitting every document and its framing; `modify` is single-document by contract (a path names no particular document of a stream) and fails typed with `MultiDocumentStream` rather than guessing.
188
190
  - `YamlVisitor` — walk a parsed document as a `Stream` of a tagged-enum event union, with `Stream.take` early termination on large inputs and a `placement` on every comment event.
189
191
  - `YamlToken` / `YamlTokens` — the positioned token stream the lint rules read: `tokenize` is a synchronous `Result` that always succeeds, surfacing lexical problems as `error`-kind tokens, and `stream` derives an Effect `Stream` from the same walk.
package/YamlDocument.js CHANGED
@@ -22,12 +22,14 @@ var YamlDirective = class extends Schema.Class("YamlDirective")({
22
22
  /**
23
23
  * A parsed YAML document: the root {@link (YamlNode:type)} (or `null` when
24
24
  * empty), recovered `errors` and `warnings` as {@link YamlDiagnostic} data,
25
- * the {@link YamlDirective} list, the optional document-level comments
26
- * (`comment` is the LEADING header block, `commentAfter` the trailing block
27
- * after the content or the `...` marker — the reference `yaml` package calls
28
- * these `commentBefore`/`comment`; the naming here keeps the pre-existing
29
- * `comment`-as-header field stable), and the `---`/`...` framing flags
30
- * (absent flags read as `false`).
25
+ * the {@link YamlDirective} list, the optional document-level comments and the
26
+ * `---`/`...` framing flags (absent flags read as `false`).
27
+ *
28
+ * `commentBefore` is a header block sitting AHEAD of a `---` marker; `comment`
29
+ * is the trailing block after the content or the `...` marker. A header with
30
+ * no marker, or one after the marker, belongs to the content rather than the
31
+ * document — it leads the root node, or the first entry when there is no
32
+ * marker to separate it from the item stream.
31
33
  *
32
34
  * Construct via `YamlDocument.parse` / `parseAll`; `YamlDocument.make` is for
33
35
  * synthetic documents.
@@ -39,8 +41,8 @@ var YamlDocument = class YamlDocument extends Schema.Class("YamlDocument")({
39
41
  errors: Schema.Array(YamlDiagnostic),
40
42
  warnings: Schema.Array(YamlDiagnostic),
41
43
  directives: Schema.Array(YamlDirective),
44
+ commentBefore: Schema.optionalKey(Schema.String),
42
45
  comment: Schema.optionalKey(Schema.String),
43
- commentAfter: Schema.optionalKey(Schema.String),
44
46
  hasDocumentStart: Schema.optionalKey(Schema.Boolean),
45
47
  hasDocumentEnd: Schema.optionalKey(Schema.Boolean),
46
48
  hasDocumentStartTab: Schema.optionalKey(Schema.Boolean)
@@ -186,8 +188,8 @@ function fromRawDocument(raw, text) {
186
188
  name: d.name,
187
189
  parameters: d.parameters
188
190
  })),
191
+ ...raw.commentBefore !== void 0 ? { commentBefore: raw.commentBefore } : {},
189
192
  ...raw.comment !== void 0 ? { comment: raw.comment } : {},
190
- ...raw.commentAfter !== void 0 ? { commentAfter: raw.commentAfter } : {},
191
193
  hasDocumentStart: raw.hasDocumentStart,
192
194
  hasDocumentEnd: raw.hasDocumentEnd,
193
195
  hasDocumentStartTab: raw.hasDocumentStartTab
@@ -200,8 +202,8 @@ function toRawDocument(doc) {
200
202
  errors: [],
201
203
  warnings: [],
202
204
  directives: doc.directives,
205
+ ...doc.commentBefore !== void 0 ? { commentBefore: doc.commentBefore } : {},
203
206
  ...doc.comment !== void 0 ? { comment: doc.comment } : {},
204
- ...doc.commentAfter !== void 0 ? { commentAfter: doc.commentAfter } : {},
205
207
  hasDocumentStart: doc.hasDocumentStart ?? false,
206
208
  hasDocumentEnd: doc.hasDocumentEnd ?? false,
207
209
  hasDocumentStartTab: doc.hasDocumentStartTab ?? false
package/YamlFormat.js CHANGED
@@ -112,8 +112,8 @@ function toOutputDocument(doc, preserveComments) {
112
112
  warnings: doc.warnings,
113
113
  directives: doc.directives,
114
114
  ...preserveComments ? definedFields({
115
- comment: doc.comment,
116
- commentAfter: doc.commentAfter
115
+ commentBefore: doc.commentBefore,
116
+ comment: doc.comment
117
117
  }) : {},
118
118
  hasDocumentStart: doc.hasDocumentStart,
119
119
  hasDocumentEnd: doc.hasDocumentEnd,
@@ -208,12 +208,7 @@ function modifyNode(node, path, depth, value) {
208
208
  const oldPair = newItems[pairIndex];
209
209
  newItems[pairIndex] = YamlPair.make({
210
210
  key: oldPair.key,
211
- value: newValueNode,
212
- ...definedFields({
213
- commentBefore: oldPair.commentBefore,
214
- comment: oldPair.comment,
215
- spaceBefore: oldPair.spaceBefore
216
- })
211
+ value: newValueNode
217
212
  });
218
213
  return rebuildMap(node, newItems);
219
214
  }
@@ -236,12 +231,7 @@ function modifyNode(node, path, depth, value) {
236
231
  const newItems = [...node.items];
237
232
  newItems[pairIndex] = YamlPair.make({
238
233
  key: pair.key,
239
- value: newValue,
240
- ...definedFields({
241
- commentBefore: pair.commentBefore,
242
- comment: pair.comment,
243
- spaceBefore: pair.spaceBefore
244
- })
234
+ value: newValue
245
235
  });
246
236
  return rebuildMap(node, newItems);
247
237
  }
package/YamlNode.js CHANGED
@@ -120,12 +120,19 @@ var YamlScalar = class extends Schema.TaggedClass()("YamlScalar", {
120
120
  * A YAML alias AST node, referencing a previously defined anchor by name
121
121
  * (without the leading `*`).
122
122
  *
123
+ * Carries the same comment triple as every other node class — see
124
+ * {@link YamlScalar} for the field semantics. An alias is a node like any
125
+ * other and a comment can legally sit above or after one.
126
+ *
123
127
  * @public
124
128
  */
125
129
  var YamlAlias = class extends Schema.TaggedClass()("YamlAlias", {
126
130
  name: Schema.String,
127
131
  offset: Schema.Number,
128
- length: Schema.Number
132
+ length: Schema.Number,
133
+ commentBefore: Schema.optionalKey(Schema.String),
134
+ comment: Schema.optionalKey(Schema.String),
135
+ spaceBefore: Schema.optionalKey(Schema.Boolean)
129
136
  }) {
130
137
  /** See `YamlScalar.find`. Pure. */
131
138
  find(path) {
@@ -168,20 +175,18 @@ const YamlNode = Schema.suspend(() => Schema.Union([
168
175
  * A YAML key-value pair AST node, representing one entry within a mapping.
169
176
  * `value` is `null` when absent (e.g. `key:` with no value).
170
177
  *
171
- * - `commentBefore` — own-line comment text directly above the pair's key
172
- * (multiple consecutive comment lines join with `\n`).
173
- * - `comment` — trailing comment text on the pair's line (strictly trailing).
174
- * - `spaceBefore` — `true` when a blank line precedes the pair (and its
175
- * `commentBefore` block, when present) in the source.
178
+ * A pair carries **no comment fields**. Comments belong to the pair's `key`
179
+ * and `value` nodes, which have one comment slot each: an own-line comment
180
+ * above the entry leads the `key`, and a trailing comment on the entry's line
181
+ * follows the `value`. Two slots rather than one is what lets `a: # kc` keep
182
+ * its comment where the author wrote it instead of relocating it onto the
183
+ * value's line.
176
184
  *
177
185
  * @public
178
186
  */
179
187
  var YamlPair = class extends Schema.TaggedClass()("YamlPair", {
180
188
  key: Schema.suspend(() => YamlNode),
181
- value: Schema.NullOr(Schema.suspend(() => YamlNode)),
182
- commentBefore: Schema.optionalKey(Schema.String),
183
- comment: Schema.optionalKey(Schema.String),
184
- spaceBefore: Schema.optionalKey(Schema.Boolean)
189
+ value: Schema.NullOr(Schema.suspend(() => YamlNode))
185
190
  }) {};
186
191
  /**
187
192
  * A YAML mapping AST node, representing a collection of {@link YamlPair}
package/YamlVisitor.js CHANGED
@@ -76,17 +76,17 @@ function* walkDocument(doc, text) {
76
76
  depth,
77
77
  diagnostic: YamlDiagnostic.fromRaw(raw, text)
78
78
  });
79
- if (doc.comment !== void 0) yield YamlVisitorEvent.Comment({
79
+ if (doc.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
80
80
  path,
81
81
  depth,
82
- text: doc.comment,
82
+ text: doc.commentBefore,
83
83
  placement: "leading"
84
84
  });
85
85
  if (doc.contents !== null) yield* walkNode(doc.contents, path, depth);
86
- if (doc.commentAfter !== void 0) yield YamlVisitorEvent.Comment({
86
+ if (doc.comment !== void 0) yield YamlVisitorEvent.Comment({
87
87
  path,
88
88
  depth,
89
- text: doc.commentAfter,
89
+ text: doc.comment,
90
90
  placement: "trailing"
91
91
  });
92
92
  yield YamlVisitorEvent.DocumentEnd({
@@ -116,12 +116,25 @@ function* walkNode(node, path, depth) {
116
116
  ...node.tag !== void 0 ? { tag: node.tag } : {},
117
117
  ...node.anchor !== void 0 ? { anchor: node.anchor } : {}
118
118
  });
119
- } else if (node instanceof YamlAlias) yield YamlVisitorEvent.Alias({
120
- path,
121
- depth,
122
- name: node.name
123
- });
124
- else if (node instanceof YamlMap) {
119
+ } else if (node instanceof YamlAlias) {
120
+ if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
121
+ path,
122
+ depth,
123
+ text: node.commentBefore,
124
+ placement: "leading"
125
+ });
126
+ if (node.comment !== void 0) yield YamlVisitorEvent.Comment({
127
+ path,
128
+ depth,
129
+ text: node.comment,
130
+ placement: "trailing"
131
+ });
132
+ yield YamlVisitorEvent.Alias({
133
+ path,
134
+ depth,
135
+ name: node.name
136
+ });
137
+ } else if (node instanceof YamlMap) {
125
138
  if (node.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
126
139
  path,
127
140
  depth,
@@ -181,18 +194,6 @@ function* walkPair(pair, parentPath, depth) {
181
194
  const resolvedValue = pair.value instanceof YamlScalar ? pair.value.value : null;
182
195
  const keySegment = typeof resolvedKey === "string" ? resolvedKey : typeof resolvedKey === "number" ? resolvedKey : String(resolvedKey);
183
196
  const pairPath = [...parentPath, keySegment];
184
- if (pair.commentBefore !== void 0) yield YamlVisitorEvent.Comment({
185
- path: pairPath,
186
- depth,
187
- text: pair.commentBefore,
188
- placement: "leading"
189
- });
190
- if (pair.comment !== void 0) yield YamlVisitorEvent.Comment({
191
- path: pairPath,
192
- depth,
193
- text: pair.comment,
194
- placement: "trailing"
195
- });
196
197
  yield YamlVisitorEvent.Pair({
197
198
  path: pairPath,
198
199
  depth,
package/index.d.ts CHANGED
@@ -727,11 +727,18 @@ declare const YamlAlias_base: Schema.Class<YamlAlias, Schema.TaggedStruct<"YamlA
727
727
  readonly name: Schema.String;
728
728
  readonly offset: Schema.Number;
729
729
  readonly length: Schema.Number;
730
+ readonly commentBefore: Schema.optionalKey<Schema.String>;
731
+ readonly comment: Schema.optionalKey<Schema.String>;
732
+ readonly spaceBefore: Schema.optionalKey<Schema.Boolean>;
730
733
  }>, {}>;
731
734
  /**
732
735
  * A YAML alias AST node, referencing a previously defined anchor by name
733
736
  * (without the leading `*`).
734
737
  *
738
+ * Carries the same comment triple as every other node class — see
739
+ * {@link YamlScalar} for the field semantics. An alias is a node like any
740
+ * other and a comment can legally sit above or after one.
741
+ *
735
742
  * @public
736
743
  */
737
744
  declare class YamlAlias extends YamlAlias_base {
@@ -797,19 +804,17 @@ type YamlNode = YamlScalar | YamlMap | YamlSeq | YamlAlias;
797
804
  declare const YamlPair_base: Schema.Class<YamlPair, Schema.TaggedStruct<"YamlPair", {
798
805
  readonly key: Schema.suspend<Schema.Codec<YamlAlias | YamlMap | YamlScalar | YamlSeq, YamlAliasEncoded | YamlMapEncoded | YamlScalarEncoded | YamlSeqEncoded, never, never>>;
799
806
  readonly value: Schema.NullOr<Schema.suspend<Schema.Codec<YamlAlias | YamlMap | YamlScalar | YamlSeq, YamlAliasEncoded | YamlMapEncoded | YamlScalarEncoded | YamlSeqEncoded, never, never>>>;
800
- readonly commentBefore: Schema.optionalKey<Schema.String>;
801
- readonly comment: Schema.optionalKey<Schema.String>;
802
- readonly spaceBefore: Schema.optionalKey<Schema.Boolean>;
803
807
  }>, {}>;
804
808
  /**
805
809
  * A YAML key-value pair AST node, representing one entry within a mapping.
806
810
  * `value` is `null` when absent (e.g. `key:` with no value).
807
811
  *
808
- * - `commentBefore` — own-line comment text directly above the pair's key
809
- * (multiple consecutive comment lines join with `\n`).
810
- * - `comment` — trailing comment text on the pair's line (strictly trailing).
811
- * - `spaceBefore` — `true` when a blank line precedes the pair (and its
812
- * `commentBefore` block, when present) in the source.
812
+ * A pair carries **no comment fields**. Comments belong to the pair's `key`
813
+ * and `value` nodes, which have one comment slot each: an own-line comment
814
+ * above the entry leads the `key`, and a trailing comment on the entry's line
815
+ * follows the `value`. Two slots rather than one is what lets `a: # kc` keep
816
+ * its comment where the author wrote it instead of relocating it onto the
817
+ * value's line.
813
818
  *
814
819
  * @public
815
820
  */
@@ -905,8 +910,8 @@ declare const YamlDocument_base: Schema.Class<YamlDocument, Schema.Struct<{
905
910
  readonly errors: Schema.$Array<typeof YamlDiagnostic>;
906
911
  readonly warnings: Schema.$Array<typeof YamlDiagnostic>;
907
912
  readonly directives: Schema.$Array<typeof YamlDirective>;
913
+ readonly commentBefore: Schema.optionalKey<Schema.String>;
908
914
  readonly comment: Schema.optionalKey<Schema.String>;
909
- readonly commentAfter: Schema.optionalKey<Schema.String>;
910
915
  readonly hasDocumentStart: Schema.optionalKey<Schema.Boolean>;
911
916
  readonly hasDocumentEnd: Schema.optionalKey<Schema.Boolean>;
912
917
  readonly hasDocumentStartTab: Schema.optionalKey<Schema.Boolean>;
@@ -914,12 +919,14 @@ declare const YamlDocument_base: Schema.Class<YamlDocument, Schema.Struct<{
914
919
  /**
915
920
  * A parsed YAML document: the root {@link (YamlNode:type)} (or `null` when
916
921
  * empty), recovered `errors` and `warnings` as {@link YamlDiagnostic} data,
917
- * the {@link YamlDirective} list, the optional document-level comments
918
- * (`comment` is the LEADING header block, `commentAfter` the trailing block
919
- * after the content or the `...` marker — the reference `yaml` package calls
920
- * these `commentBefore`/`comment`; the naming here keeps the pre-existing
921
- * `comment`-as-header field stable), and the `---`/`...` framing flags
922
- * (absent flags read as `false`).
922
+ * the {@link YamlDirective} list, the optional document-level comments and the
923
+ * `---`/`...` framing flags (absent flags read as `false`).
924
+ *
925
+ * `commentBefore` is a header block sitting AHEAD of a `---` marker; `comment`
926
+ * is the trailing block after the content or the `...` marker. A header with
927
+ * no marker, or one after the marker, belongs to the content rather than the
928
+ * document — it leads the root node, or the first entry when there is no
929
+ * marker to separate it from the item stream.
923
930
  *
924
931
  * Construct via `YamlDocument.parse` / `parseAll`; `YamlDocument.make` is for
925
932
  * synthetic documents.
@@ -1,6 +1,6 @@
1
- import { YamlMap, YamlPair, YamlScalar, YamlSeq } from "../../YamlNode.js";
1
+ import { YamlAlias, YamlMap, YamlPair, YamlScalar, YamlSeq } from "../../YamlNode.js";
2
2
  import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
3
- import { blankAboveIsKeepChompContent, columnAt, hasBlankLineAbove, isOwnLineAt, joinComments, rawCommentText, sameLineSpan, withCommentFields } from "./comments.js";
3
+ import { blankAboveIsKeepChompContent, columnAt, hasBlankLineAbove, isAfterIndicatorOnly, isOwnLineAt, joinComments, rawCommentText, sameLineSpan, withCommentFields } from "./comments.js";
4
4
  import { enterNesting, exitNesting, hasMeta, hasNonWhitespaceBeforeOnLine, lineCol, lineIndentColumn, sameLine } from "./state.js";
5
5
  import { blockMapStartsWithValueSep, classifyPlainNumeric, collectMultilineKey, collectMultilinePlainScalar, findFirstContent, findLastContent, findNextContentInList, findNextSignificantChild, findValueSepOffset, getScalarStyle, hasValueSepAfterInList, hasValueSepBetween, hasValueSepThroughPlainScalars, makeScalar, resolveScalar, shouldPreserveRaw } from "./scalars.js";
6
6
 
@@ -507,6 +507,29 @@ function buildPairs(items, pairs, text, escaped) {
507
507
  let lastNode;
508
508
  let contentColumn = -1;
509
509
  const nodeEnd = (n) => n.offset + n.length;
510
+ /**
511
+ * Rebuild `pair` with `text` appended to the trailing comment of whichever
512
+ * node owns the pair's last line: the value when present, otherwise the key.
513
+ */
514
+ const withTrailingComment = (pair, text, onKey = false) => pair.value !== null && !onKey ? new YamlPair({
515
+ key: pair.key,
516
+ value: withCommentFields(pair.value, { comment: text })
517
+ }) : new YamlPair({
518
+ key: withCommentFields(pair.key, { comment: text }),
519
+ value: pair.value
520
+ });
521
+ /**
522
+ * Finish a pair: the pending leading fields go to its KEY node, the entry's
523
+ * trailing comment to whichever node owns the last line. The one place the
524
+ * node-level split is applied, so no construction site has to know it.
525
+ */
526
+ const pushPair = (pair, leading, trailing, trailingOnKey = false) => {
527
+ const withLeading = Object.keys(leading).length === 0 ? pair : new YamlPair({
528
+ key: withCommentFields(pair.key, leading),
529
+ value: pair.value
530
+ });
531
+ return trailing === void 0 ? withLeading : withTrailingComment(withLeading, trailing, trailingOnKey);
532
+ };
510
533
  const noteContentColumn = (offset) => {
511
534
  if (contentColumn < 0 && offset >= 0) contentColumn = columnAt(text, offset);
512
535
  };
@@ -544,15 +567,9 @@ function buildPairs(items, pairs, text, escaped) {
544
567
  if (item.kind === "comment") {
545
568
  const cOff = item.offset ?? -1;
546
569
  const cText = item.comment ?? "";
547
- if (!(cOff < 0 ? true : isOwnLineAt(text, cOff)) && pairs.length > 0) {
570
+ if (!(cOff < 0 ? true : isOwnLineAt(text, cOff) || isAfterIndicatorOnly(text, cOff)) && pairs.length > 0) {
548
571
  const last = pairs[pairs.length - 1];
549
- if (last) pairs[pairs.length - 1] = new YamlPair({
550
- key: last.key,
551
- value: last.value,
552
- ...last.commentBefore !== void 0 ? { commentBefore: last.commentBefore } : {},
553
- comment: last.comment !== void 0 ? `${last.comment}\n${cText}` : cText,
554
- ...last.spaceBefore !== void 0 ? { spaceBefore: last.spaceBefore } : {}
555
- });
572
+ if (last) pairs[pairs.length - 1] = withTrailingComment(last, cText);
556
573
  } else {
557
574
  const blankAbove = cOff >= 0 && hasBlankLineAbove(text, cOff) && !blankAboveIsKeepChompContent(text, cOff, lastNode);
558
575
  if (blankAbove && pending.length === 0 && lastEnd >= 0) pendingSpace = true;
@@ -578,20 +595,18 @@ function buildPairs(items, pairs, text, escaped) {
578
595
  length: 0
579
596
  });
580
597
  if (valueNode) {
581
- pairs.push(new YamlPair({
598
+ pairs.push(pushPair(new YamlPair({
582
599
  key: nullKey,
583
- value: valueNode.node ?? null,
584
- ...pendingFields
585
- }));
600
+ value: valueNode.node ?? null
601
+ }), pendingFields, void 0));
586
602
  i = valueNode.nextIdx;
587
603
  lastEnd = valueNode.node ? nodeEnd(valueNode.node) : valueSepOffset + 1;
588
604
  lastNode = valueNode.node ?? void 0;
589
605
  } else {
590
- pairs.push(new YamlPair({
606
+ pairs.push(pushPair(new YamlPair({
591
607
  key: nullKey,
592
- value: null,
593
- ...pendingFields
594
- }));
608
+ value: null
609
+ }), pendingFields, void 0));
595
610
  lastEnd = valueSepOffset + 1;
596
611
  lastNode = void 0;
597
612
  }
@@ -629,32 +644,26 @@ function buildPairs(items, pairs, text, escaped) {
629
644
  for (const c of valueResult.sameLineComments) pairTrailing = joinComments(pairTrailing, c);
630
645
  let valNode = valueResult.node;
631
646
  if (valNode !== null && valueResult.leadingComments.length > 0) valNode = withCommentFields(valNode, { commentBefore: valueResult.leadingComments.join("\n") });
632
- pairs.push(new YamlPair({
647
+ pairs.push(pushPair(new YamlPair({
633
648
  key: keyOrNull(),
634
- value: valNode,
635
- ...pendingFields,
636
- ...pairTrailing !== void 0 ? { comment: pairTrailing } : {}
637
- }));
649
+ value: valNode
650
+ }), pendingFields, pairTrailing, keyIsSimple(keyNode) && (valNode === null || !sameLineSpan(text, sepOffset, valNode.offset))));
638
651
  i = valueResult.nextIdx;
639
652
  lastEnd = valNode ? nodeEnd(valNode) : sepOffset + 1;
640
653
  lastNode = valNode ?? void 0;
641
654
  } else {
642
- pairs.push(new YamlPair({
655
+ pairs.push(pushPair(new YamlPair({
643
656
  key: keyOrNull(),
644
- value: null,
645
- ...pendingFields,
646
- ...pairTrailing !== void 0 ? { comment: pairTrailing } : {}
647
- }));
657
+ value: null
658
+ }), pendingFields, pairTrailing));
648
659
  lastEnd = sepOffset + 1;
649
660
  lastNode = void 0;
650
661
  }
651
662
  } else {
652
- pairs.push(new YamlPair({
663
+ pairs.push(pushPair(new YamlPair({
653
664
  key: keyOrNull(),
654
- value: null,
655
- ...pendingFields,
656
- ...pairTrailing !== void 0 ? { comment: pairTrailing } : {}
657
- }));
665
+ value: null
666
+ }), pendingFields, pairTrailing));
658
667
  lastEnd = keyNode ? nodeEnd(keyNode) : lastEnd;
659
668
  if (keyNode) lastNode = keyNode;
660
669
  }
@@ -679,6 +688,17 @@ function buildPairs(items, pairs, text, escaped) {
679
688
  }
680
689
  return withLeadingBlank(pending);
681
690
  }
691
+ /**
692
+ * True when a key renders in implicit `key: value` form, so the `:` sits on
693
+ * the key's own line. A multi-line or block-scalar key forces the explicit
694
+ * `? key` / `: value` spelling, where the `:` gets a line of its own.
695
+ */
696
+ function keyIsSimple(keyNode) {
697
+ if (keyNode instanceof YamlAlias) return true;
698
+ if (!(keyNode instanceof YamlScalar)) return false;
699
+ if (typeof keyNode.value === "string" && keyNode.value.includes("\n")) return false;
700
+ return keyNode.style !== "block-literal" && keyNode.style !== "block-folded";
701
+ }
682
702
  function consumeValueNode(items, startIdx, text, sepOffset) {
683
703
  let peek = startIdx;
684
704
  while (peek < items.length && items[peek]?.kind === "comment") peek++;
@@ -687,13 +707,17 @@ function consumeValueNode(items, startIdx, text, sepOffset) {
687
707
  let i = startIdx;
688
708
  const sameLineComments = [];
689
709
  const leadingComments = [];
710
+ let firstOwnLineIdx = -1;
690
711
  while (i < items.length) {
691
712
  const item = items[i];
692
713
  if (!item) break;
693
714
  if (item.kind === "comment") {
694
715
  const cText = item.comment ?? "";
695
716
  if (item.offset !== void 0 && sameLineSpan(text, sepOffset, item.offset)) sameLineComments.push(cText);
696
- else leadingComments.push(cText);
717
+ else {
718
+ if (firstOwnLineIdx < 0) firstOwnLineIdx = i;
719
+ leadingComments.push(cText);
720
+ }
697
721
  i++;
698
722
  continue;
699
723
  }
@@ -705,6 +729,12 @@ function consumeValueNode(items, startIdx, text, sepOffset) {
705
729
  };
706
730
  break;
707
731
  }
732
+ if (firstOwnLineIdx >= 0) return {
733
+ node: null,
734
+ nextIdx: firstOwnLineIdx,
735
+ sameLineComments,
736
+ leadingComments: []
737
+ };
708
738
  return i > startIdx ? {
709
739
  node: null,
710
740
  nextIdx: i,
@@ -723,10 +753,12 @@ function consumeValueNode(items, startIdx, text, sepOffset) {
723
753
  */
724
754
  function consumeValueNodeForNullKey(items, startIdx, text, valueSepOffset) {
725
755
  let i = startIdx;
756
+ let firstOwnLineIdx = -1;
726
757
  while (i < items.length) {
727
758
  const item = items[i];
728
759
  if (!item) break;
729
760
  if (item.kind === "comment") {
761
+ if (firstOwnLineIdx < 0 && item.offset !== void 0 && !sameLineSpan(text, valueSepOffset, item.offset)) firstOwnLineIdx = i;
730
762
  i++;
731
763
  continue;
732
764
  }
@@ -742,6 +774,10 @@ function consumeValueNodeForNullKey(items, startIdx, text, valueSepOffset) {
742
774
  }
743
775
  break;
744
776
  }
777
+ if (firstOwnLineIdx >= 0) return {
778
+ node: null,
779
+ nextIdx: firstOwnLineIdx
780
+ };
745
781
  return i > startIdx ? {
746
782
  node: null,
747
783
  nextIdx: i
@@ -1,4 +1,4 @@
1
- import { YamlMap, YamlScalar, YamlSeq } from "../../YamlNode.js";
1
+ import { YamlAlias, YamlMap, YamlScalar, YamlSeq } from "../../YamlNode.js";
2
2
 
3
3
  //#region src/internal/composer/comments.ts
4
4
  /** True when there is no line break between `start` and `end` in `text`. */
@@ -24,6 +24,31 @@ function isOwnLineAt(text, offset) {
24
24
  return true;
25
25
  }
26
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
+ /**
27
52
  * True when the line immediately above the line containing `offset` is blank
28
53
  * (empty or horizontal whitespace only). Purely local — see
29
54
  * {@link isOwnLineAt} for why span-based gap checks are not used.
@@ -133,8 +158,8 @@ function columnAt(text, offset) {
133
158
  /**
134
159
  * Rebuild `node` with the given comment fields merged in (existing fields are
135
160
  * 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.
161
+ * both sides are present). Every node class carries the triple, aliases
162
+ * included.
138
163
  */
139
164
  function withCommentFields(node, fields) {
140
165
  if (node instanceof YamlScalar) return new YamlScalar({
@@ -170,7 +195,12 @@ function withCommentFields(node, fields) {
170
195
  offset: node.offset,
171
196
  length: node.length
172
197
  });
173
- return node;
198
+ return new YamlAlias({
199
+ name: node.name,
200
+ ...mergedCommentFields(node, fields),
201
+ offset: node.offset,
202
+ length: node.length
203
+ });
174
204
  }
175
205
  function mergedCommentFields(existing, incoming) {
176
206
  const commentBefore = incoming.commentBefore !== void 0 ? existing.commentBefore !== void 0 ? `${existing.commentBefore}\n${incoming.commentBefore}` : incoming.commentBefore : existing.commentBefore;
@@ -184,4 +214,4 @@ function mergedCommentFields(existing, incoming) {
184
214
  }
185
215
 
186
216
  //#endregion
187
- export { blankAboveIsKeepChompContent, blankLineAboveStart, columnAt, hasBlankLineAbove, hasBlankLineBelow, isOwnLineAt, joinComments, rawCommentText, sameLineSpan, withCommentFields };
217
+ export { blankAboveIsKeepChompContent, blankLineAboveStart, columnAt, hasBlankLineAbove, hasBlankLineBelow, isAfterIndicatorOnly, isOwnLineAt, joinComments, rawCommentText, sameLineSpan, withCommentFields };
@@ -1,7 +1,7 @@
1
1
  import { YamlAlias, YamlMap, YamlPair, YamlScalar, YamlSeq } from "../../YamlNode.js";
2
2
  import { parseCSTAll } from "../cst-parser.js";
3
3
  import { checkAnchorOnAlias, getAnchorName, makeAlias, registerAnchor } from "./anchors.js";
4
- import { hasBlankLineAbove, hasBlankLineBelow, rawCommentText } from "./comments.js";
4
+ import { hasBlankLineAbove, hasBlankLineBelow, rawCommentText, withCommentFields } from "./comments.js";
5
5
  import { clearMeta, commentProps, createState, hasMeta, sameLine } from "./state.js";
6
6
  import { parseDirective, validateTagHandlesInDocument } from "./tags.js";
7
7
  import { collectMultilinePlainScalar, findNextContentChild, getScalarStyle, hasBlockMapAfterInList, hasValueSepAfter, indexOfChild, makeScalar, resolveScalar } from "./scalars.js";
@@ -134,7 +134,10 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
134
134
  let documentComment;
135
135
  let documentCommentAfter;
136
136
  let lastLeadingCommentOffset = -1;
137
+ let lastAfterMarkerCommentOffset = -1;
137
138
  let sawDocEndMarker = false;
139
+ let sawDocStartMarker = false;
140
+ let afterMarkerComment;
138
141
  const hasDocStart = children.some((c) => c.type === "whitespace" && c.source === "---");
139
142
  let i = 0;
140
143
  const meta = {};
@@ -171,6 +174,7 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
171
174
  if (child.type === "newline" && hasMeta(meta)) sawNewlineSinceMeta = true;
172
175
  if (child.type === "whitespace" || child.type === "newline") {
173
176
  if (child.type === "whitespace" && child.source === "...") sawDocEndMarker = true;
177
+ if (child.type === "whitespace" && child.source === "---") sawDocStartMarker = true;
174
178
  if (child.type === "whitespace" && (child.source === "]" || child.source === "}")) state.errors.push({
175
179
  code: "MalformedFlowCollection",
176
180
  message: `Unexpected flow indicator '${child.source}' at document level`,
@@ -182,9 +186,15 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
182
186
  }
183
187
  if (child.type === "comment" && contents === null && !sawDocEndMarker) {
184
188
  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;
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
+ }
188
198
  i++;
189
199
  continue;
190
200
  }
@@ -411,17 +421,32 @@ function composeDocument(cst, state, hasSubsequentDocuments = false, nextDocCst)
411
421
  }
412
422
  }
413
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`;
414
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}`;
415
440
  return {
416
- contents,
441
+ contents: decorated,
417
442
  errors: [...state.errors],
418
443
  warnings: [...state.warnings],
419
444
  directives,
420
445
  hasDocumentStart: hasDocStart,
421
446
  hasDocumentEnd: hasDocEnd,
422
447
  hasDocumentStartTab: hasDocStartTab,
423
- ...documentComment !== void 0 ? { comment: documentComment } : {},
424
- ...documentCommentAfter !== void 0 ? { commentAfter: documentCommentAfter } : {}
448
+ ...headerForDocument !== void 0 ? { commentBefore: headerForDocument } : {},
449
+ ...documentCommentAfter !== void 0 ? { comment: documentCommentAfter } : {}
425
450
  };
426
451
  }
427
452
  /**
@@ -598,8 +623,7 @@ function decorateSourceMultiline(node, text) {
598
623
  if (node instanceof YamlMap) {
599
624
  const newItems = node.items.map((pair) => new YamlPair({
600
625
  key: decorateSourceMultiline(pair.key, text) ?? pair.key,
601
- value: pair.value === null ? null : decorateSourceMultiline(pair.value, text),
602
- ...commentProps(pair)
626
+ value: pair.value === null ? null : decorateSourceMultiline(pair.value, text)
603
627
  }));
604
628
  const multiline = isSourceMultiline(text, node.offset, node.length);
605
629
  return new YamlMap({
@@ -637,8 +661,8 @@ function decorateDocumentSourceMultiline(doc, text) {
637
661
  errors: doc.errors,
638
662
  warnings: doc.warnings,
639
663
  directives: doc.directives,
664
+ ...doc.commentBefore !== void 0 ? { commentBefore: doc.commentBefore } : {},
640
665
  ...doc.comment !== void 0 ? { comment: doc.comment } : {},
641
- ...doc.commentAfter !== void 0 ? { commentAfter: doc.commentAfter } : {},
642
666
  hasDocumentStart: doc.hasDocumentStart,
643
667
  hasDocumentEnd: doc.hasDocumentEnd,
644
668
  hasDocumentStartTab: doc.hasDocumentStartTab
@@ -709,6 +733,72 @@ function composeAllDocuments(text, options) {
709
733
  streamErrors: crossDocState.errors
710
734
  };
711
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
+ }
712
802
 
713
803
  //#endregion
714
804
  export { EMPTY_DOCUMENT, composeAllDocuments, composeDocument, composeFirstDocument, composeFirstDocumentCounted, validateCrossDocumentDirectives };
@@ -77,7 +77,7 @@ function validateFlowContentIndent(cst, state, parentBlockColumn) {
77
77
  i++;
78
78
  continue;
79
79
  }
80
- if (i - lineStart <= parentBlockColumn) {
80
+ if (i - lineStart <= parentBlockColumn && !isClosersOnly(text, i, end)) {
81
81
  state.errors.push({
82
82
  code: "InvalidIndentation",
83
83
  message: "Flow content continuation line must be indented past the parent block",
@@ -91,6 +91,26 @@ function validateFlowContentIndent(cst, state, parentBlockColumn) {
91
91
  i++;
92
92
  }
93
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
+ }
94
114
  function composeFlowMap(cst, state, meta, parentBlockColumn) {
95
115
  if (!enterNesting(state, cst)) return new YamlMap({
96
116
  items: [],
@@ -487,10 +487,7 @@ function normalizeNodeTags(node, tagMap) {
487
487
  if (node instanceof YamlMap) return new YamlMap({
488
488
  items: node.items.map((pair) => new YamlPair({
489
489
  key: normalizeNodeTags(pair.key, tagMap),
490
- value: pair.value ? normalizeNodeTags(pair.value, tagMap) : null,
491
- ...pair.commentBefore !== void 0 ? { commentBefore: pair.commentBefore } : {},
492
- ...pair.comment !== void 0 ? { comment: pair.comment } : {},
493
- ...pair.spaceBefore !== void 0 ? { spaceBefore: pair.spaceBefore } : {}
490
+ value: pair.value ? normalizeNodeTags(pair.value, tagMap) : null
494
491
  })),
495
492
  style: node.style,
496
493
  ...node.tag ? { tag: normalizeTag(node.tag, tagMap) } : {},
@@ -643,6 +640,63 @@ function unescapeCommentLine(line) {
643
640
  function commentBlockLines(comment) {
644
641
  return comment.split("\n").map((l) => l === "" ? "" : `#${unescapeCommentLine(l)}`);
645
642
  }
643
+ /**
644
+ * Splice a trailing comment onto the FIRST line of an already-rendered node —
645
+ * for a block scalar, the header line, which is the one line of the scalar
646
+ * that legally carries a `#` comment.
647
+ */
648
+ function withHeaderComment(rendered, comment) {
649
+ const nl = rendered.indexOf("\n");
650
+ return nl === -1 ? `${rendered}${renderTrailingComment(comment)}` : `${rendered.slice(0, nl)}${renderTrailingComment(comment)}${rendered.slice(nl)}`;
651
+ }
652
+ /**
653
+ * The leading comment block of a mapping entry. Under the node-level comment
654
+ * model it lives on the entry's KEY node, not on the pair.
655
+ */
656
+ function pairLeading(pair) {
657
+ return pair.key.commentBefore;
658
+ }
659
+ /**
660
+ * The trailing comment of a mapping entry: it belongs to whichever node owns
661
+ * the entry's last line — the value when there is one, otherwise the key. For
662
+ * a block-scalar value that line is the HEADER line, which is why a block
663
+ * scalar's header comment needs no separate concept.
664
+ */
665
+ function pairTrailing(pair) {
666
+ return (pair.value ?? pair.key).comment;
667
+ }
668
+ /** True when a blank line preceded the entry (recorded on its KEY node). */
669
+ function pairSpaceBefore(pair) {
670
+ return pair.key.spaceBefore;
671
+ }
672
+ /**
673
+ * The trailing comment to place on an entry's own line, given how its value
674
+ * actually rendered.
675
+ *
676
+ * A collection's `comment` field carries its TERMINAL comment run — the
677
+ * own-line comments after its last entry — and the collection emits those
678
+ * itself, after that last entry. Only when the collection renders on a single
679
+ * line (inline flow, `a: {b: 1} # t`) is its `comment` the entry's trailing
680
+ * comment. Reading it unconditionally emits the same comment twice: once on
681
+ * the entry's line and again as the terminal run.
682
+ */
683
+ function entryTrailing(pair, ctx) {
684
+ if (pair.key.comment !== void 0) return pair.key.comment;
685
+ const owner = pair.value ?? pair.key;
686
+ if (rendersAcrossLines(owner, ctx)) return void 0;
687
+ return owner.comment;
688
+ }
689
+ /**
690
+ * True when `node` renders across more than one line, so its own `comment` is
691
+ * a terminal run rather than something that can sit on the entry's line.
692
+ */
693
+ function rendersAcrossLines(node, ctx) {
694
+ if (!(node instanceof YamlMap || node instanceof YamlSeq)) return false;
695
+ if (node.items.length === 0) return false;
696
+ if ((ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : node.style ?? ctx.defaultCollectionStyle) === "block" || node.sourceMultiline === true) return true;
697
+ if (ctx.forceDefaultStyles) return false;
698
+ return node instanceof YamlMap ? node.items.some((p) => pairLeading(p) !== void 0 || (p.value ?? p.key).comment !== void 0 || pairSpaceBefore(p) === true) : node.items.some((item) => item.commentBefore !== void 0 || item.comment !== void 0 || item.spaceBefore === true);
699
+ }
646
700
  /** Render a trailing comment for appending after content on the same line. */
647
701
  function renderTrailingComment(comment) {
648
702
  return ` #${comment.split("\n").map(unescapeCommentLine).join(" ")}`;
@@ -664,6 +718,14 @@ function pushExplicitKeyValueLines(lines, valNode, ctx, depth, pad) {
664
718
  return lines.length - 1;
665
719
  }
666
720
  const valLines = stringifyNodeLines(valNode, ctx, depth + 1);
721
+ const valueLeading = !ctx.forceDefaultStyles ? valNode.commentBefore : void 0;
722
+ if (valueLeading !== void 0) {
723
+ const colonIdx = lines.length;
724
+ lines.push(":");
725
+ for (const cl of commentBlockLines(valueLeading)) lines.push(cl === "" ? "" : `${pad}${cl}`);
726
+ for (const vl of valLines) lines.push(vl === "" ? "" : `${pad}${vl}`);
727
+ return colonIdx;
728
+ }
667
729
  if (valLines.length === 1) {
668
730
  lines.push(`: ${valLines[0]}`);
669
731
  return lines.length - 1;
@@ -674,15 +736,9 @@ function pushExplicitKeyValueLines(lines, valNode, ctx, depth, pad) {
674
736
  const isInlineQuoted = valNode instanceof YamlScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""));
675
737
  if (isBlockScalarHeader || isInlineQuoted) {
676
738
  const headerIdx = lines.length;
677
- if (isBlockScalarHeader) {
678
- const scalarComment = !ctx.forceDefaultStyles && valNode instanceof YamlScalar ? valNode.comment : void 0;
679
- lines.push(scalarComment !== void 0 ? `: ${first}${renderTrailingComment(scalarComment)}` : `: ${first}`);
680
- for (let v = 1; v < valLines.length; v++) lines.push(valLines[v]);
681
- return scalarComment !== void 0 ? -1 : headerIdx;
682
- }
683
739
  lines.push(`: ${first}`);
684
740
  for (let v = 1; v < valLines.length; v++) lines.push(valLines[v]);
685
- return lines.length - 1;
741
+ return isBlockScalarHeader ? headerIdx : lines.length - 1;
686
742
  }
687
743
  if (first.startsWith("-")) {
688
744
  lines.push(`: ${first}`);
@@ -718,17 +774,19 @@ function stringifyMapNodeLines(node, ctx, depth) {
718
774
  }
719
775
  if (style === "flow") {
720
776
  const flowPrefix = buildMetadataPrefix(node.tag, node.anchor);
721
- if (!ctx.forceDefaultStyles && (node.comment !== void 0 || items.some((p) => p.commentBefore !== void 0 || p.comment !== void 0 || p.spaceBefore === true))) {
777
+ if (!ctx.forceDefaultStyles && (items.some((p) => pairLeading(p) !== void 0 || pairTrailing(p) !== void 0 || pairSpaceBefore(p) === true) || node.comment !== void 0 && node.sourceMultiline === true)) {
722
778
  const flowPad = " ".repeat(ctx.indent);
723
779
  const flowLines = [flowPrefix ? `${flowPrefix} {` : "{"];
724
780
  for (let fi = 0; fi < items.length; fi++) {
725
781
  const pair = items[fi];
726
- if (pair.spaceBefore === true) flowLines.push("");
727
- if (pair.commentBefore !== void 0) for (const cl of commentBlockLines(pair.commentBefore)) flowLines.push(cl === "" ? "" : `${flowPad}${cl}`);
782
+ if (pairSpaceBefore(pair) === true) flowLines.push("");
783
+ const flowLeading = pairLeading(pair);
784
+ if (flowLeading !== void 0) for (const cl of commentBlockLines(flowLeading)) flowLines.push(cl === "" ? "" : `${flowPad}${cl}`);
728
785
  const keyStr = pair.key ? stringifyMappingKeyLines(pair.key, ctx, depth + 1).join(" ") : "null";
729
786
  const valStr = pair.value ? stringifyNodeLines(pair.value, ctx, depth + 1).join(" ") : "null";
730
787
  const comma = fi < items.length - 1 ? "," : "";
731
- const trailing = pair.comment !== void 0 ? renderTrailingComment(pair.comment) : "";
788
+ const flowTrailing = pairTrailing(pair);
789
+ const trailing = flowTrailing !== void 0 ? renderTrailingComment(flowTrailing) : "";
732
790
  flowLines.push(`${flowPad}${keyStr}: ${valStr}${comma}${trailing}`);
733
791
  }
734
792
  if (node.comment !== void 0) for (const cl of commentBlockLines(node.comment)) flowLines.push(cl === "" ? "" : `${flowPad}${cl}`);
@@ -750,10 +808,13 @@ function stringifyMapNodeLines(node, ctx, depth) {
750
808
  lines[idx] = `${lines[idx]}${renderTrailingComment(comment)}`;
751
809
  };
752
810
  /** A value node's own leading comment block, or undefined. */
753
- const valueLeading = (valNode) => emitComments && !(valNode instanceof YamlAlias) ? valNode.commentBefore : void 0;
811
+ const valueLeading = (valNode) => emitComments ? valNode.commentBefore : void 0;
754
812
  for (const pair of items) {
755
- if (emitComments && pair.spaceBefore === true) lines.push("");
756
- if (emitComments && pair.commentBefore !== void 0) for (const cl of commentBlockLines(pair.commentBefore)) lines.push(cl);
813
+ if (emitComments && pairSpaceBefore(pair) === true) lines.push("");
814
+ const leading = emitComments ? pairLeading(pair) : void 0;
815
+ const keyOwnComment = emitComments ? pair.key.comment : void 0;
816
+ const valueOwnComment = emitComments && pair.value !== null ? pair.value.comment : void 0;
817
+ if (leading !== void 0) for (const cl of commentBlockLines(leading)) lines.push(cl);
757
818
  const keyIsScalarWithNewline = pair.key instanceof YamlScalar && (typeof pair.key.value === "string" && pair.key.value.includes("\n") || pair.key.style === "block-literal" || pair.key.style === "block-folded");
758
819
  if ((pair.key instanceof YamlMap || pair.key instanceof YamlSeq) && pair.key.items.length > 0 || keyIsScalarWithNewline) {
759
820
  const keyLines = stringifyNodeLines(pair.key, ctx, depth + 1);
@@ -765,7 +826,7 @@ function stringifyMapNodeLines(node, ctx, depth) {
765
826
  const contPad = firstIsMetaOnly || keyIsBlockScalar ? "" : EXPLICIT_COMPACT_PAD;
766
827
  for (let k = 1; k < keyLines.length; k++) lines.push(`${contPad}${keyLines[k]}`);
767
828
  const inlineIdx = pushExplicitKeyValueLines(lines, pair.value, ctx, depth, pad);
768
- appendTrailing(inlineIdx >= 0 ? inlineIdx : keyLines.length === 1 ? explicitKeyLineIdx : -1, pair.comment);
829
+ appendTrailing(inlineIdx >= 0 ? inlineIdx : keyLines.length === 1 ? explicitKeyLineIdx : -1, entryTrailing(pair, ctx));
769
830
  continue;
770
831
  }
771
832
  let resolvedKeyStr;
@@ -776,7 +837,7 @@ function stringifyMapNodeLines(node, ctx, depth) {
776
837
  lines.push(`? ${keyStr}`);
777
838
  const spillKeyLineIdx = lines.length - 1;
778
839
  const inlineIdx = pushExplicitKeyValueLines(lines, pair.value, ctx, depth, pad);
779
- appendTrailing(inlineIdx >= 0 ? inlineIdx : spillKeyLineIdx, pair.comment);
840
+ appendTrailing(inlineIdx >= 0 ? inlineIdx : spillKeyLineIdx, entryTrailing(pair, ctx));
780
841
  continue;
781
842
  }
782
843
  const keyIsAnchoredOrTaggedEmpty = pair.key instanceof YamlScalar && pair.key.length === 0 && (pair.key.value === null || pair.key.value === void 0 || pair.key.value === "") && (pair.key.anchor !== void 0 || pair.key.tag !== void 0);
@@ -788,7 +849,7 @@ function stringifyMapNodeLines(node, ctx, depth) {
788
849
  const isRootFlowMap = ctx.parentPosition === void 0 && node.style === "flow" && node.sourceMultiline === true;
789
850
  if (ctx.forceDefaultStyles && isRootFlowMap && isPlainKey && keyIsNonEmpty) lines.push(`${keyStr}${sep} null`);
790
851
  else lines.push(`${keyStr}${sep}`);
791
- appendTrailing(lines.length - 1, pair.comment);
852
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
792
853
  continue;
793
854
  }
794
855
  const valLines = stringifyNodeLines(valNode, {
@@ -800,7 +861,7 @@ function stringifyMapNodeLines(node, ctx, depth) {
800
861
  const seqMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
801
862
  const startIdx = seqMeta ? 1 : 0;
802
863
  lines.push(seqMeta ? `${keyStr}${sep} ${seqMeta}` : `${keyStr}${sep}`);
803
- appendTrailing(lines.length - 1, pair.comment);
864
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
804
865
  if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(ctx.indentSequences ? `${pad}${cl}` : cl);
805
866
  for (let i = startIdx; i < valLines.length; i++) {
806
867
  const vl = valLines[i];
@@ -810,47 +871,57 @@ function stringifyMapNodeLines(node, ctx, depth) {
810
871
  const mapMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
811
872
  const startIdx = mapMeta ? 1 : 0;
812
873
  lines.push(mapMeta ? `${keyStr}${sep} ${mapMeta}` : `${keyStr}${sep}`);
813
- appendTrailing(lines.length - 1, pair.comment);
874
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
814
875
  if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(`${pad}${cl}`);
815
876
  for (let i = startIdx; i < valLines.length; i++) lines.push(valLines[i] === "" ? "" : `${pad}${valLines[i]}`);
816
- } else if (valLines.length === 1 && valLeading !== void 0) {
877
+ } else if (valLines.length === 1 && (valLeading !== void 0 || keyOwnComment !== void 0)) {
817
878
  lines.push(`${keyStr}${sep}`);
818
- appendTrailing(lines.length - 1, pair.comment);
819
- for (const cl of commentBlockLines(valLeading)) lines.push(`${pad}${cl}`);
879
+ appendTrailing(lines.length - 1, keyOwnComment);
880
+ if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(`${pad}${cl}`);
820
881
  lines.push(valLines[0] === "" ? "" : `${pad}${valLines[0]}`);
882
+ appendTrailing(lines.length - 1, valueOwnComment);
821
883
  } else if (valLines.length === 1) {
822
884
  lines.push(valLines[0] === "" ? `${keyStr}${sep}` : `${keyStr}${sep} ${valLines[0]}`);
823
- appendTrailing(lines.length - 1, pair.comment);
885
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
824
886
  } else {
825
887
  const first = valLines[0];
826
888
  const firstStripped = stripScalarMetadataPrefix(first);
827
889
  const isBlockScalarHeader = firstStripped.startsWith("|") || firstStripped.startsWith(">");
828
890
  const isInlineQuoted = valNode instanceof YamlScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""));
829
891
  if (isBlockScalarHeader || isInlineQuoted) {
830
- const scalarComment = isBlockScalarHeader && valNode instanceof YamlScalar ? valNode.comment : void 0;
831
- if (scalarComment !== void 0 && pair.comment !== void 0) {
892
+ if (valLeading !== void 0) {
832
893
  lines.push(`${keyStr}${sep}`);
833
- appendTrailing(lines.length - 1, pair.comment);
834
- const headerIdx = lines.length;
894
+ for (const cl of commentBlockLines(valLeading)) lines.push(cl === "" ? "" : `${pad}${cl}`);
895
+ const spilledIdx = lines.length;
835
896
  lines.push(`${pad}${first}`);
836
897
  for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
837
- appendTrailing(headerIdx, scalarComment);
838
- } else {
839
- const headerIdx = lines.length;
840
- lines.push(`${keyStr}${sep} ${first}`);
898
+ appendTrailing(isBlockScalarHeader ? spilledIdx : lines.length - 1, entryTrailing(pair, ctx));
899
+ continue;
900
+ }
901
+ const keyComment = !ctx.forceDefaultStyles ? pair.key.comment : void 0;
902
+ const scalarComment = isBlockScalarHeader && valNode instanceof YamlScalar ? valNode.comment : void 0;
903
+ if (keyComment !== void 0 && scalarComment !== void 0) {
904
+ lines.push(`${keyStr}${sep}`);
905
+ appendTrailing(lines.length - 1, keyComment);
906
+ const spilledIdx = lines.length;
907
+ lines.push(`${pad}${first}`);
841
908
  for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
842
- if (isBlockScalarHeader) appendTrailing(headerIdx, scalarComment ?? pair.comment);
843
- else appendTrailing(lines.length - 1, pair.comment);
909
+ appendTrailing(spilledIdx, scalarComment);
910
+ continue;
844
911
  }
912
+ const headerIdx = lines.length;
913
+ lines.push(`${keyStr}${sep} ${first}`);
914
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
915
+ appendTrailing(isBlockScalarHeader ? headerIdx : lines.length - 1, entryTrailing(pair, ctx));
845
916
  } else {
846
917
  const mapMeta = valNode instanceof YamlMap && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block" ? buildMetadataPrefix(valNode.tag, valNode.anchor) : void 0;
847
918
  if (mapMeta) {
848
919
  lines.push(`${keyStr}${sep} ${mapMeta}`);
849
- appendTrailing(lines.length - 1, pair.comment);
920
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
850
921
  for (let i = 1; i < valLines.length; i++) lines.push(valLines[i] === "" ? "" : `${pad}${valLines[i]}`);
851
922
  } else {
852
923
  lines.push(`${keyStr}${sep}`);
853
- appendTrailing(lines.length - 1, pair.comment);
924
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
854
925
  if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(`${pad}${cl}`);
855
926
  for (const vl of valLines) lines.push(vl === "" ? "" : `${pad}${vl}`);
856
927
  }
@@ -906,13 +977,13 @@ function stringifySeqNodeLines(node, ctx, depth) {
906
977
  }
907
978
  const emitComments = !ctx.forceDefaultStyles;
908
979
  const itemFields = (item) => {
909
- if (!emitComments || item instanceof YamlAlias) return void 0;
980
+ if (!emitComments) return void 0;
910
981
  if (item.commentBefore === void 0 && item.comment === void 0 && item.spaceBefore !== true) return void 0;
911
982
  return item;
912
983
  };
913
984
  if (style === "flow") {
914
985
  const flowPrefix = buildMetadataPrefix(node.tag, node.anchor);
915
- if (emitComments && (node.comment !== void 0 || items.some((it) => itemFields(it) !== void 0))) {
986
+ if (emitComments && (items.some((it) => itemFields(it) !== void 0) || node.comment !== void 0 && node.sourceMultiline === true)) {
916
987
  const flowPad = " ".repeat(ctx.indent);
917
988
  const flowLines = [flowPrefix ? `${flowPrefix} [` : "["];
918
989
  for (let fi = 0; fi < items.length; fi++) {
@@ -998,7 +1069,7 @@ function stringifyDocument(doc, options) {
998
1069
  const ctx = createContext(options);
999
1070
  const finalNewline = options?.finalNewline ?? true;
1000
1071
  let contents = doc.contents;
1001
- const docCommentRaw = ctx.forceDefaultStyles ? void 0 : doc.comment;
1072
+ const docCommentRaw = ctx.forceDefaultStyles ? void 0 : doc.commentBefore;
1002
1073
  const docComment = docCommentRaw !== void 0 ? commentBlockLines(docCommentRaw).join("\n") : void 0;
1003
1074
  if (ctx.forceDefaultStyles && contents) {
1004
1075
  contents = stripNodeComments(contents);
@@ -1010,28 +1081,31 @@ function stringifyDocument(doc, options) {
1010
1081
  if (doc.hasDocumentStart) return `---\n${doc.hasDocumentEnd ? "...\n" : ""}`;
1011
1082
  return "";
1012
1083
  }
1013
- if (docComment !== void 0 || doc.commentAfter !== void 0 || doc.hasDocumentStart === true || doc.hasDocumentEnd === true) {
1084
+ if (docComment !== void 0 || doc.comment !== void 0 || doc.hasDocumentStart === true || doc.hasDocumentEnd === true) {
1014
1085
  const parts = [];
1015
1086
  if (docComment !== void 0) parts.push(`${docComment}\n`);
1016
1087
  if (doc.hasDocumentStart) parts.push("---\n");
1017
1088
  if (doc.hasDocumentEnd) parts.push("...\n");
1018
- if (doc.commentAfter !== void 0) parts.push(`${commentBlockLines(doc.commentAfter).join("\n")}\n`);
1089
+ if (doc.comment !== void 0) parts.push(`${commentBlockLines(doc.comment).join("\n")}\n`);
1019
1090
  const out = parts.join("");
1020
1091
  return finalNewline ? out : out.replace(/\n$/, "");
1021
1092
  }
1022
1093
  return finalNewline ? "null\n" : "null";
1023
1094
  }
1024
1095
  const result = stringifyNodeLines(contents, ctx, 0).join("\n");
1096
+ const rootHeaderComment = !ctx.forceDefaultStyles && contents instanceof YamlScalar && (contents.style === "block-literal" || contents.style === "block-folded") ? contents.comment : void 0;
1097
+ const rendered = rootHeaderComment === void 0 ? result : withHeaderComment(result, rootHeaderComment);
1025
1098
  const needsTerminatorForKeepChomp = ctx.forceDefaultStyles && endsWithKeepChomp(result);
1026
1099
  const needsTerminatorForAnchoredPlainScalar = ctx.forceDefaultStyles && doc.hasDocumentStart && contents instanceof YamlScalar && contents.style === "plain" && contents.anchor !== void 0 && !contents.tag;
1027
1100
  const looksLikeDirectiveContinuation = contents instanceof YamlScalar && typeof contents.value === "string" && / %[A-Z]/.test(contents.value);
1028
1101
  const needsTerminatorForMultilinePlainScalar = ctx.forceDefaultStyles && doc.hasDocumentStart && contents instanceof YamlScalar && contents.style === "plain" && contents.sourceMultiline === true && looksLikeDirectiveContinuation;
1029
1102
  const needsTerminatorForDocStartTab = ctx.forceDefaultStyles && doc.hasDocumentStartTab === true;
1030
1103
  const docEndMarker = doc.hasDocumentEnd || needsTerminatorForKeepChomp || needsTerminatorForAnchoredPlainScalar || needsTerminatorForMultilinePlainScalar || needsTerminatorForDocStartTab ? "...\n" : "";
1031
- const docCommentAfter = ctx.forceDefaultStyles || doc.commentAfter === void 0 ? void 0 : commentBlockLines(doc.commentAfter).join("\n");
1104
+ const docCommentAfter = ctx.forceDefaultStyles || doc.comment === void 0 ? void 0 : commentBlockLines(doc.comment).join("\n");
1032
1105
  const docEndFull = docCommentAfter !== void 0 ? `${docEndMarker}${docCommentAfter}\n` : docEndMarker;
1033
- const body = finalNewline || docEndFull !== "" ? `${result}\n` : result;
1106
+ const body = finalNewline || docEndFull !== "" ? `${rendered}\n` : rendered;
1034
1107
  const docEnd = finalNewline || docEndFull === "" ? docEndFull : docEndFull.replace(/\n$/, "");
1108
+ const rootLeading = !ctx.forceDefaultStyles && contents !== null && contents.commentBefore !== void 0 ? `${commentBlockLines(contents.commentBefore).join("\n")}\n` : "";
1035
1109
  if (doc.hasDocumentStart) {
1036
1110
  const rootTag = contents && "tag" in contents ? contents.tag : void 0;
1037
1111
  const rootAnchor = contents && "anchor" in contents ? contents.anchor : void 0;
@@ -1048,13 +1122,15 @@ function stringifyDocument(doc, options) {
1048
1122
  if (bodyClean.startsWith(metaLinePrefix)) bodyClean = bodyClean.slice(metaLinePrefix.length);
1049
1123
  else if (bodyClean.startsWith(metaInlinePrefix)) bodyClean = bodyClean.slice(metaInlinePrefix.length);
1050
1124
  const docStart = `--- ${metaStr}`;
1051
- const sep = isCollection ? "\n" : " ";
1052
- return docComment ? `${docComment}\n${docStart}${sep}${bodyClean}${docEnd}` : `${docStart}${sep}${bodyClean}${docEnd}`;
1125
+ const metaBody = rootLeading === "" ? `${isCollection ? "\n" : " "}${bodyClean}` : `\n${rootLeading}${bodyClean}`;
1126
+ return docComment ? `${docComment}\n${docStart}${metaBody}${docEnd}` : `${docStart}${metaBody}${docEnd}`;
1053
1127
  }
1054
- if (isScalar) return docComment ? `${docComment}\n--- ${body}${docEnd}` : `--- ${body}${docEnd}`;
1055
- return docComment ? `${docComment}\n---\n${body}${docEnd}` : `---\n${body}${docEnd}`;
1128
+ if (isScalar && rootLeading === "") return docComment ? `${docComment}\n--- ${body}${docEnd}` : `--- ${body}${docEnd}`;
1129
+ const afterMarker = `---\n${rootLeading}${body}${docEnd}`;
1130
+ return docComment ? `${docComment}\n${afterMarker}` : afterMarker;
1056
1131
  }
1057
- return docComment ? `${docComment}\n${body}${docEnd}` : `${body}${docEnd}`;
1132
+ const plain = `${rootLeading}${body}${docEnd}`;
1133
+ return docComment ? `${docComment}\n${plain}` : plain;
1058
1134
  }
1059
1135
 
1060
1136
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/yaml",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Zero-dependency YAML parsing, editing, formatting and linting as Effect schemas.",
6
6
  "keywords": [
@@ -41,7 +41,7 @@
41
41
  "./package.json": "./package.json"
42
42
  },
43
43
  "peerDependencies": {
44
- "effect": "4.0.0-beta.107"
44
+ "effect": "4.0.0-rc.109"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.11.0"