@effected/yaml 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +80 -7
  2. package/Yaml.js +80 -32
  3. package/YamlDocument.js +23 -2
  4. package/YamlFormat.js +162 -21
  5. package/YamlLint.js +239 -0
  6. package/YamlLintRule.js +36 -0
  7. package/YamlNode.js +43 -4
  8. package/YamlToken.js +139 -0
  9. package/YamlVisitor.js +52 -16
  10. package/index.d.ts +446 -12
  11. package/index.js +4 -1
  12. package/internal/composer/anchors.js +2 -27
  13. package/internal/composer/block.js +291 -67
  14. package/internal/composer/comments.js +217 -0
  15. package/internal/composer/document.js +141 -14
  16. package/internal/composer/flow.js +103 -10
  17. package/internal/composer/scalars.js +48 -11
  18. package/internal/composer/state.js +16 -2
  19. package/internal/cst-parser.js +2 -1
  20. package/internal/diagnostics.js +12 -3
  21. package/internal/fold.js +22 -10
  22. package/internal/rules/catalog.js +39 -0
  23. package/internal/rules/colon-spacing.js +77 -0
  24. package/internal/rules/comments-spacing.js +68 -0
  25. package/internal/rules/document-end.js +65 -0
  26. package/internal/rules/document-start.js +64 -0
  27. package/internal/rules/empty-lines.js +70 -0
  28. package/internal/rules/eof-newline.js +33 -0
  29. package/internal/rules/hyphen-spacing.js +52 -0
  30. package/internal/rules/indentation.js +103 -0
  31. package/internal/rules/key-duplicates.js +49 -0
  32. package/internal/rules/line-length.js +36 -0
  33. package/internal/rules/parse-validity.js +33 -0
  34. package/internal/rules/quoted-strings.js +81 -0
  35. package/internal/rules/trailing-spaces.js +39 -0
  36. package/internal/rules/truthy.js +84 -0
  37. package/internal/rules/util.js +92 -0
  38. package/internal/stringifier.js +342 -54
  39. package/package.json +3 -2
@@ -29,6 +29,27 @@ var StringifyDepthExceeded = class extends Error {
29
29
  this.name = "StringifyDepthExceeded";
30
30
  }
31
31
  };
32
+ /**
33
+ * YAML 1.2 §8.1.3 caps an implicit block-mapping key: the `:` indicator must
34
+ * appear at most 1024 characters after the start of the key. A key whose
35
+ * RENDERED form (quotes and escapes count, the source scalar does not) is
36
+ * longer than this must spill to explicit-key form (`? key` / `: value`) or
37
+ * strict parsers reject the output. Matches the reference `yaml` package
38
+ * (`stringifyPair`: `str.length > 1024`, block context only — flow contexts
39
+ * never spill): a rendered key of exactly 1024 characters stays implicit.
40
+ */
41
+ const IMPLICIT_KEY_MAX_LENGTH = 1024;
42
+ /**
43
+ * Continuation-line padding for explicit-key compact notation. Structurally
44
+ * the width of the `? ` / `: ` indicators — two columns — and deliberately
45
+ * NOT `ctx.indent`: compact continuation lines must align with the first
46
+ * item, which always starts two columns in, so any other pad silently
47
+ * re-nests (or corrupts) the value on reparse once `indent !== 2`. This is a
48
+ * recorded divergence from the reference `yaml` package, which pads these
49
+ * lines with the configured indent and emits output its own strict parser
50
+ * misreads or rejects at indent ≠ 2.
51
+ */
52
+ const EXPLICIT_COMPACT_PAD = " ";
32
53
  const NULL_RE = /^(?:null|Null|NULL|~)$/;
33
54
  const TRUE_RE = /^(?:true|True|TRUE)$/;
34
55
  const FALSE_RE = /^(?:false|False|FALSE)$/;
@@ -176,7 +197,9 @@ function renderSingleQuoted(s) {
176
197
  * Block literal and block folded styles are always accepted for single-line
177
198
  * strings even though the output is unusual.
178
199
  */
179
- function renderString(s, style, indent, ignoreType = false, canonical = false, explicitChomp, parentPosition, quoteStyle = "single") {
200
+ function renderString(s, style, indent, ignoreType = false, canonical = false, explicitChomp, parentPosition, quoteStyle = "single", explicitIndent) {
201
+ const fidelity = !canonical;
202
+ const fidelityIndent = fidelity ? explicitIndent : void 0;
180
203
  if (s.includes("\n")) {
181
204
  let hasControl = false;
182
205
  for (let i = 0; i < s.length; i++) {
@@ -192,14 +215,14 @@ function renderString(s, style, indent, ignoreType = false, canonical = false, e
192
215
  if (hasInteriorTrailingWhitespace(s)) return renderDoubleQuoted(s, canonical);
193
216
  if (hasNewlineSpacesTab(s)) return renderDoubleQuoted(s, canonical);
194
217
  }
195
- if (style === "block-literal") return renderBlockLiteral(s, indent, explicitChomp, parentPosition);
196
- if (style === "block-folded") return renderBlockFolded(s, indent);
218
+ if (style === "block-literal") return renderBlockLiteral(s, indent, explicitChomp, parentPosition, fidelity, fidelityIndent);
219
+ if (style === "block-folded") return renderBlockFolded(s, indent, fidelity ? explicitChomp : void 0, fidelityIndent);
197
220
  if (style === "plain" || style === "single-quoted") {
198
221
  if (canonical) {
199
222
  const sq = renderSingleQuotedMultiline(s, indent);
200
223
  if (sq !== null) return sq;
201
224
  }
202
- return renderBlockLiteral(s, indent, explicitChomp, parentPosition);
225
+ return renderBlockLiteral(s, indent, explicitChomp, parentPosition, fidelity, fidelityIndent);
203
226
  }
204
227
  return renderDoubleQuoted(s, canonical);
205
228
  }
@@ -215,8 +238,8 @@ function renderString(s, style, indent, ignoreType = false, canonical = false, e
215
238
  return s;
216
239
  case "single-quoted": return renderSingleQuoted(s);
217
240
  case "double-quoted": return renderDoubleQuoted(s, canonical);
218
- case "block-literal": return renderBlockLiteral(s, indent, explicitChomp, parentPosition);
219
- case "block-folded": return renderBlockFolded(s, indent);
241
+ case "block-literal": return renderBlockLiteral(s, indent, explicitChomp, parentPosition, fidelity, fidelityIndent);
242
+ case "block-folded": return renderBlockFolded(s, indent, fidelity ? explicitChomp : void 0, fidelityIndent);
220
243
  }
221
244
  }
222
245
  /**
@@ -361,6 +384,18 @@ function stringifyObjectLines(obj, ctx, depth) {
361
384
  const keyStr = renderKey(k);
362
385
  const val = obj[k];
363
386
  const valLines = stringifyLines(val, ctx, depth + 1);
387
+ if (keyStr.length > IMPLICIT_KEY_MAX_LENGTH) {
388
+ lines.push(`? ${keyStr}`);
389
+ if (valLines.length === 1) lines.push(`: ${valLines[0]}`);
390
+ else if (valLines[0].startsWith("|") || valLines[0].startsWith(">") || typeof val === "string") {
391
+ lines.push(`: ${valLines[0]}`);
392
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
393
+ } else {
394
+ lines.push(`: ${valLines[0]}`);
395
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i] === "" ? valLines[i] : `${EXPLICIT_COMPACT_PAD}${valLines[i]}`);
396
+ }
397
+ continue;
398
+ }
364
399
  if (valLines.length === 1 && !isBlockCollection(val, ctx)) lines.push(`${keyStr}: ${valLines[0]}`);
365
400
  else {
366
401
  const first = valLines[0];
@@ -439,8 +474,11 @@ function normalizeNodeTags(node, tagMap) {
439
474
  style: node.style,
440
475
  ...node.tag ? { tag: normalizeTag(node.tag, tagMap) } : {},
441
476
  ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
477
+ ...node.commentBefore !== void 0 ? { commentBefore: node.commentBefore } : {},
442
478
  ...node.comment !== void 0 ? { comment: node.comment } : {},
479
+ ...node.spaceBefore !== void 0 ? { spaceBefore: node.spaceBefore } : {},
443
480
  ...node.chomp !== void 0 ? { chomp: node.chomp } : {},
481
+ ...node.blockIndent !== void 0 ? { blockIndent: node.blockIndent } : {},
444
482
  ...node.raw !== void 0 ? { raw: node.raw } : {},
445
483
  ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
446
484
  offset: node.offset,
@@ -449,13 +487,14 @@ function normalizeNodeTags(node, tagMap) {
449
487
  if (node instanceof YamlMap) return new YamlMap({
450
488
  items: node.items.map((pair) => new YamlPair({
451
489
  key: normalizeNodeTags(pair.key, tagMap),
452
- value: pair.value ? normalizeNodeTags(pair.value, tagMap) : null,
453
- ...pair.comment !== void 0 ? { comment: pair.comment } : {}
490
+ value: pair.value ? normalizeNodeTags(pair.value, tagMap) : null
454
491
  })),
455
492
  style: node.style,
456
493
  ...node.tag ? { tag: normalizeTag(node.tag, tagMap) } : {},
457
494
  ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
495
+ ...node.commentBefore !== void 0 ? { commentBefore: node.commentBefore } : {},
458
496
  ...node.comment !== void 0 ? { comment: node.comment } : {},
497
+ ...node.spaceBefore !== void 0 ? { spaceBefore: node.spaceBefore } : {},
459
498
  ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
460
499
  offset: node.offset,
461
500
  length: node.length
@@ -465,7 +504,9 @@ function normalizeNodeTags(node, tagMap) {
465
504
  style: node.style,
466
505
  ...node.tag ? { tag: normalizeTag(node.tag, tagMap) } : {},
467
506
  ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
507
+ ...node.commentBefore !== void 0 ? { commentBefore: node.commentBefore } : {},
468
508
  ...node.comment !== void 0 ? { comment: node.comment } : {},
509
+ ...node.spaceBefore !== void 0 ? { spaceBefore: node.spaceBefore } : {},
469
510
  ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
470
511
  offset: node.offset,
471
512
  length: node.length
@@ -483,6 +524,7 @@ function stripNodeComments(node) {
483
524
  ...node.tag !== void 0 ? { tag: node.tag } : {},
484
525
  ...node.anchor !== void 0 ? { anchor: node.anchor } : {},
485
526
  ...node.chomp !== void 0 ? { chomp: node.chomp } : {},
527
+ ...node.blockIndent !== void 0 ? { blockIndent: node.blockIndent } : {},
486
528
  ...node.raw !== void 0 ? { raw: node.raw } : {},
487
529
  ...node.sourceMultiline !== void 0 ? { sourceMultiline: node.sourceMultiline } : {},
488
530
  offset: node.offset,
@@ -542,7 +584,7 @@ function stringifyScalarNodeLines(node, ctx) {
542
584
  else lines = ["null"];
543
585
  } else if (typeof val === "boolean") lines = [val ? "true" : "false"];
544
586
  else if (typeof val === "number") lines = [node.raw !== void 0 ? node.raw : renderNumber(val)];
545
- else if (typeof val === "string") lines = renderString(val, style, " ".repeat(ctx.indent), !!node.tag, ctx.forceDefaultStyles, node.chomp, ctx.parentPosition, ctx.quoteStyle).split("\n");
587
+ else if (typeof val === "string") lines = renderString(val, style, " ".repeat(ctx.indent), !!node.tag, ctx.forceDefaultStyles, node.chomp, ctx.parentPosition, ctx.quoteStyle, node.blockIndent).split("\n");
546
588
  else lines = [renderDoubleQuoted(String(val))];
547
589
  if (node.tag) lines[0] = `${node.tag} ${lines[0]}`;
548
590
  if (node.anchor) lines[0] = `&${node.anchor} ${lines[0]}`;
@@ -582,6 +624,138 @@ function stringifyMappingKeyLines(node, ctx, depth) {
582
624
  return stringifyNodeLines(node, ctx, depth);
583
625
  }
584
626
  /**
627
+ * Undo the capture-side escape on one stored comment line: a spaces-only
628
+ * slice was stored with one extra trailing space so it cannot collide with
629
+ * `""`, the embedded-blank-line encoding (see `rawCommentText`).
630
+ */
631
+ function unescapeCommentLine(line) {
632
+ return /^ +$/.test(line) ? line.slice(0, -1) : line;
633
+ }
634
+ /**
635
+ * Render a stored comment block, one output line per stored line. Stored
636
+ * text is the RAW post-`#` slice (reference parity) with spaces-only lines
637
+ * carrying one escape space: `""` is an embedded blank line; any other line
638
+ * renders as `#` + the unescaped slice (`" "` → `#`, `" "` → `# `).
639
+ */
640
+ function commentBlockLines(comment) {
641
+ return comment.split("\n").map((l) => l === "" ? "" : `#${unescapeCommentLine(l)}`);
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
+ }
700
+ /** Render a trailing comment for appending after content on the same line. */
701
+ function renderTrailingComment(comment) {
702
+ return ` #${comment.split("\n").map(unescapeCommentLine).join(" ")}`;
703
+ }
704
+ /**
705
+ * Emits the `: value` line(s) of an explicit-key block-mapping entry, in
706
+ * compact notation where the value admits it (first line on the colon line,
707
+ * continuation lines indented to align). Shared by the complex-key branch and
708
+ * the implicit-key length spill; `indentSequences` deliberately does not reach
709
+ * this branch (sequence values always align compactly under `: `).
710
+ *
711
+ * Returns the `lines` index that can safely carry the pair's trailing
712
+ * comment (the line where an inline value ends), or `-1` when no line is
713
+ * safe (a compact block collection or a block-scalar header).
714
+ */
715
+ function pushExplicitKeyValueLines(lines, valNode, ctx, depth, pad) {
716
+ if (!valNode) {
717
+ lines.push(":");
718
+ return lines.length - 1;
719
+ }
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
+ }
729
+ if (valLines.length === 1) {
730
+ lines.push(`: ${valLines[0]}`);
731
+ return lines.length - 1;
732
+ }
733
+ const first = valLines[0];
734
+ const firstStripped = stripScalarMetadataPrefix(first);
735
+ const isBlockScalarHeader = firstStripped.startsWith("|") || firstStripped.startsWith(">");
736
+ const isInlineQuoted = valNode instanceof YamlScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""));
737
+ if (isBlockScalarHeader || isInlineQuoted) {
738
+ const headerIdx = lines.length;
739
+ lines.push(`: ${first}`);
740
+ for (let v = 1; v < valLines.length; v++) lines.push(valLines[v]);
741
+ return isBlockScalarHeader ? headerIdx : lines.length - 1;
742
+ }
743
+ if (first.startsWith("-")) {
744
+ lines.push(`: ${first}`);
745
+ for (let v = 1; v < valLines.length; v++) lines.push(valLines[v] === "" ? "" : `${EXPLICIT_COMPACT_PAD}${valLines[v]}`);
746
+ return -1;
747
+ }
748
+ if (valNode instanceof YamlMap && valNode.items.length > 0 && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block") {
749
+ lines.push(`: ${first}`);
750
+ for (let v = 1; v < valLines.length; v++) lines.push(valLines[v] === "" ? "" : `${EXPLICIT_COMPACT_PAD}${valLines[v]}`);
751
+ return -1;
752
+ }
753
+ const colonIdx = lines.length;
754
+ lines.push(":");
755
+ for (const vl of valLines) lines.push(vl === "" ? "" : `${pad}${vl}`);
756
+ return colonIdx;
757
+ }
758
+ /**
585
759
  * Stringifies a YamlMap node into lines, using the node's collection style.
586
760
  */
587
761
  function stringifyMapNodeLines(node, ctx, depth) {
@@ -599,56 +773,73 @@ function stringifyMapNodeLines(node, ctx, depth) {
599
773
  return [line];
600
774
  }
601
775
  if (style === "flow") {
776
+ const flowPrefix = buildMetadataPrefix(node.tag, node.anchor);
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)) {
778
+ const flowPad = " ".repeat(ctx.indent);
779
+ const flowLines = [flowPrefix ? `${flowPrefix} {` : "{"];
780
+ for (let fi = 0; fi < items.length; fi++) {
781
+ const pair = items[fi];
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}`);
785
+ const keyStr = pair.key ? stringifyMappingKeyLines(pair.key, ctx, depth + 1).join(" ") : "null";
786
+ const valStr = pair.value ? stringifyNodeLines(pair.value, ctx, depth + 1).join(" ") : "null";
787
+ const comma = fi < items.length - 1 ? "," : "";
788
+ const flowTrailing = pairTrailing(pair);
789
+ const trailing = flowTrailing !== void 0 ? renderTrailingComment(flowTrailing) : "";
790
+ flowLines.push(`${flowPad}${keyStr}: ${valStr}${comma}${trailing}`);
791
+ }
792
+ if (node.comment !== void 0) for (const cl of commentBlockLines(node.comment)) flowLines.push(cl === "" ? "" : `${flowPad}${cl}`);
793
+ flowLines.push("}");
794
+ return flowLines;
795
+ }
602
796
  let line = `{${items.map((pair) => {
603
797
  return `${pair.key ? stringifyMappingKeyLines(pair.key, ctx, depth + 1).join(" ") : "null"}: ${pair.value ? stringifyNodeLines(pair.value, ctx, depth + 1).join(" ") : "null"}`;
604
798
  }).join(", ")}}`;
605
- const flowPrefix = buildMetadataPrefix(node.tag, node.anchor);
606
799
  if (flowPrefix) line = `${flowPrefix} ${line}`;
607
800
  return [line];
608
801
  }
609
802
  const pad = " ".repeat(ctx.indent);
803
+ const emitComments = !ctx.forceDefaultStyles;
610
804
  const lines = [];
805
+ /** Append a pair's trailing comment to `lines[idx]` (flattened to one line); `-1` drops it. */
806
+ const appendTrailing = (idx, comment) => {
807
+ if (!emitComments || comment === void 0 || idx < 0 || idx >= lines.length) return;
808
+ lines[idx] = `${lines[idx]}${renderTrailingComment(comment)}`;
809
+ };
810
+ /** A value node's own leading comment block, or undefined. */
811
+ const valueLeading = (valNode) => emitComments ? valNode.commentBefore : void 0;
611
812
  for (const pair of items) {
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);
612
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");
613
819
  if ((pair.key instanceof YamlMap || pair.key instanceof YamlSeq) && pair.key.items.length > 0 || keyIsScalarWithNewline) {
614
820
  const keyLines = stringifyNodeLines(pair.key, ctx, depth + 1);
615
821
  lines.push(`? ${keyLines[0]}`);
822
+ const explicitKeyLineIdx = lines.length - 1;
616
823
  const firstTokens = keyLines[0].trim().split(/\s+/).filter(Boolean);
617
824
  const firstIsMetaOnly = firstTokens.length > 0 && firstTokens.every((t) => t.startsWith("&") || t.startsWith("!"));
618
825
  const keyIsBlockScalar = pair.key instanceof YamlScalar && (pair.key.style === "block-literal" || pair.key.style === "block-folded");
619
- const contPad = firstIsMetaOnly || keyIsBlockScalar ? "" : pad;
826
+ const contPad = firstIsMetaOnly || keyIsBlockScalar ? "" : EXPLICIT_COMPACT_PAD;
620
827
  for (let k = 1; k < keyLines.length; k++) lines.push(`${contPad}${keyLines[k]}`);
621
- const valNode = pair.value;
622
- if (!valNode) lines.push(":");
623
- else {
624
- const valLines = stringifyNodeLines(valNode, ctx, depth + 1);
625
- if (valLines.length === 1) lines.push(`: ${valLines[0]}`);
626
- else {
627
- const first = valLines[0];
628
- const firstStripped = stripScalarMetadataPrefix(first);
629
- const isBlockScalarHeader = firstStripped.startsWith("|") || firstStripped.startsWith(">");
630
- const isInlineQuoted = valNode instanceof YamlScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""));
631
- if (isBlockScalarHeader || isInlineQuoted) {
632
- lines.push(`: ${first}`);
633
- for (let v = 1; v < valLines.length; v++) lines.push(valLines[v]);
634
- } else if (first.startsWith("-")) {
635
- lines.push(`: ${first}`);
636
- for (let v = 1; v < valLines.length; v++) lines.push(`${pad}${valLines[v]}`);
637
- } else if (valNode instanceof YamlMap && valNode.items.length > 0 && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block") {
638
- lines.push(`: ${first}`);
639
- for (let v = 1; v < valLines.length; v++) lines.push(`${pad}${valLines[v]}`);
640
- } else {
641
- lines.push(":");
642
- for (const vl of valLines) lines.push(`${pad}${vl}`);
643
- }
644
- }
645
- }
828
+ const inlineIdx = pushExplicitKeyValueLines(lines, pair.value, ctx, depth, pad);
829
+ appendTrailing(inlineIdx >= 0 ? inlineIdx : keyLines.length === 1 ? explicitKeyLineIdx : -1, entryTrailing(pair, ctx));
646
830
  continue;
647
831
  }
648
832
  let resolvedKeyStr;
649
833
  if (ctx.forceDefaultStyles && node.style === "flow" && node.sourceMultiline !== true && pair.key instanceof YamlScalar && (pair.key.style === "single-quoted" || pair.key.style === "double-quoted") && typeof pair.key.value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(pair.key.value)) resolvedKeyStr = pair.key.value;
650
834
  else resolvedKeyStr = pair.key ? stringifyMappingKeyLines(pair.key, ctx, depth + 1).join(" ") : "null";
651
835
  const keyStr = resolvedKeyStr;
836
+ if (keyStr.length > IMPLICIT_KEY_MAX_LENGTH) {
837
+ lines.push(`? ${keyStr}`);
838
+ const spillKeyLineIdx = lines.length - 1;
839
+ const inlineIdx = pushExplicitKeyValueLines(lines, pair.value, ctx, depth, pad);
840
+ appendTrailing(inlineIdx >= 0 ? inlineIdx : spillKeyLineIdx, entryTrailing(pair, ctx));
841
+ continue;
842
+ }
652
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);
653
844
  const sep = pair.key instanceof YamlAlias || keyIsAnchoredOrTaggedEmpty ? " :" : ":";
654
845
  const valNode = pair.value;
@@ -658,16 +849,20 @@ function stringifyMapNodeLines(node, ctx, depth) {
658
849
  const isRootFlowMap = ctx.parentPosition === void 0 && node.style === "flow" && node.sourceMultiline === true;
659
850
  if (ctx.forceDefaultStyles && isRootFlowMap && isPlainKey && keyIsNonEmpty) lines.push(`${keyStr}${sep} null`);
660
851
  else lines.push(`${keyStr}${sep}`);
852
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
661
853
  continue;
662
854
  }
663
855
  const valLines = stringifyNodeLines(valNode, {
664
856
  ...ctx,
665
857
  parentPosition: "block-map-value"
666
858
  }, depth + 1);
859
+ const valLeading = valueLeading(valNode);
667
860
  if (valNode instanceof YamlSeq && valNode.items.length > 0 && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block") {
668
861
  const seqMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
669
862
  const startIdx = seqMeta ? 1 : 0;
670
863
  lines.push(seqMeta ? `${keyStr}${sep} ${seqMeta}` : `${keyStr}${sep}`);
864
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
865
+ if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(ctx.indentSequences ? `${pad}${cl}` : cl);
671
866
  for (let i = startIdx; i < valLines.length; i++) {
672
867
  const vl = valLines[i];
673
868
  lines.push(ctx.indentSequences && vl !== "" ? `${pad}${vl}` : vl);
@@ -676,28 +871,64 @@ function stringifyMapNodeLines(node, ctx, depth) {
676
871
  const mapMeta = buildMetadataPrefix(valNode.tag, valNode.anchor);
677
872
  const startIdx = mapMeta ? 1 : 0;
678
873
  lines.push(mapMeta ? `${keyStr}${sep} ${mapMeta}` : `${keyStr}${sep}`);
679
- for (let i = startIdx; i < valLines.length; i++) lines.push(`${pad}${valLines[i]}`);
680
- } else if (valLines.length === 1) lines.push(valLines[0] === "" ? `${keyStr}${sep}` : `${keyStr}${sep} ${valLines[0]}`);
681
- else {
874
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
875
+ if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(`${pad}${cl}`);
876
+ for (let i = startIdx; i < valLines.length; i++) lines.push(valLines[i] === "" ? "" : `${pad}${valLines[i]}`);
877
+ } else if (valLines.length === 1 && (valLeading !== void 0 || keyOwnComment !== void 0)) {
878
+ lines.push(`${keyStr}${sep}`);
879
+ appendTrailing(lines.length - 1, keyOwnComment);
880
+ if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(`${pad}${cl}`);
881
+ lines.push(valLines[0] === "" ? "" : `${pad}${valLines[0]}`);
882
+ appendTrailing(lines.length - 1, valueOwnComment);
883
+ } else if (valLines.length === 1) {
884
+ lines.push(valLines[0] === "" ? `${keyStr}${sep}` : `${keyStr}${sep} ${valLines[0]}`);
885
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
886
+ } else {
682
887
  const first = valLines[0];
683
888
  const firstStripped = stripScalarMetadataPrefix(first);
684
889
  const isBlockScalarHeader = firstStripped.startsWith("|") || firstStripped.startsWith(">");
685
890
  const isInlineQuoted = valNode instanceof YamlScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""));
686
891
  if (isBlockScalarHeader || isInlineQuoted) {
892
+ if (valLeading !== void 0) {
893
+ lines.push(`${keyStr}${sep}`);
894
+ for (const cl of commentBlockLines(valLeading)) lines.push(cl === "" ? "" : `${pad}${cl}`);
895
+ const spilledIdx = lines.length;
896
+ lines.push(`${pad}${first}`);
897
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
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}`);
908
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
909
+ appendTrailing(spilledIdx, scalarComment);
910
+ continue;
911
+ }
912
+ const headerIdx = lines.length;
687
913
  lines.push(`${keyStr}${sep} ${first}`);
688
914
  for (let i = 1; i < valLines.length; i++) lines.push(valLines[i]);
915
+ appendTrailing(isBlockScalarHeader ? headerIdx : lines.length - 1, entryTrailing(pair, ctx));
689
916
  } else {
690
917
  const mapMeta = valNode instanceof YamlMap && (ctx.forceDefaultStyles ? ctx.defaultCollectionStyle : valNode.style ?? ctx.defaultCollectionStyle) === "block" ? buildMetadataPrefix(valNode.tag, valNode.anchor) : void 0;
691
918
  if (mapMeta) {
692
919
  lines.push(`${keyStr}${sep} ${mapMeta}`);
693
- for (let i = 1; i < valLines.length; i++) lines.push(`${pad}${valLines[i]}`);
920
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
921
+ for (let i = 1; i < valLines.length; i++) lines.push(valLines[i] === "" ? "" : `${pad}${valLines[i]}`);
694
922
  } else {
695
923
  lines.push(`${keyStr}${sep}`);
696
- for (const vl of valLines) lines.push(`${pad}${vl}`);
924
+ appendTrailing(lines.length - 1, entryTrailing(pair, ctx));
925
+ if (valLeading !== void 0) for (const cl of commentBlockLines(valLeading)) lines.push(`${pad}${cl}`);
926
+ for (const vl of valLines) lines.push(vl === "" ? "" : `${pad}${vl}`);
697
927
  }
698
928
  }
699
929
  }
700
930
  }
931
+ if (emitComments && node.comment !== void 0) for (const cl of commentBlockLines(node.comment)) lines.push(cl);
701
932
  const prefix = buildMetadataPrefix(node.tag, node.anchor);
702
933
  if (prefix) lines.unshift(prefix);
703
934
  return lines;
@@ -744,9 +975,32 @@ function stringifySeqNodeLines(node, ctx, depth) {
744
975
  if (emptyPrefix) line = `${emptyPrefix} ${line}`;
745
976
  return [line];
746
977
  }
978
+ const emitComments = !ctx.forceDefaultStyles;
979
+ const itemFields = (item) => {
980
+ if (!emitComments) return void 0;
981
+ if (item.commentBefore === void 0 && item.comment === void 0 && item.spaceBefore !== true) return void 0;
982
+ return item;
983
+ };
747
984
  if (style === "flow") {
748
- let line = `[${items.map((item) => stringifyNodeLines(item, ctx, depth + 1).join(" ")).join(", ")}]`;
749
985
  const flowPrefix = buildMetadataPrefix(node.tag, node.anchor);
986
+ if (emitComments && (items.some((it) => itemFields(it) !== void 0) || node.comment !== void 0 && node.sourceMultiline === true)) {
987
+ const flowPad = " ".repeat(ctx.indent);
988
+ const flowLines = [flowPrefix ? `${flowPrefix} [` : "["];
989
+ for (let fi = 0; fi < items.length; fi++) {
990
+ const item = items[fi];
991
+ const fields = itemFields(item);
992
+ if (fields?.spaceBefore === true) flowLines.push("");
993
+ if (fields?.commentBefore !== void 0) for (const cl of commentBlockLines(fields.commentBefore)) flowLines.push(cl === "" ? "" : `${flowPad}${cl}`);
994
+ const itemStr = stringifyNodeLines(item, ctx, depth + 1).join(" ");
995
+ const comma = fi < items.length - 1 ? "," : "";
996
+ const trailing = fields?.comment !== void 0 ? renderTrailingComment(fields.comment) : "";
997
+ flowLines.push(`${flowPad}${itemStr}${comma}${trailing}`);
998
+ }
999
+ if (node.comment !== void 0) for (const cl of commentBlockLines(node.comment)) flowLines.push(cl === "" ? "" : `${flowPad}${cl}`);
1000
+ flowLines.push("]");
1001
+ return flowLines;
1002
+ }
1003
+ let line = `[${items.map((item) => stringifyNodeLines(item, ctx, depth + 1).join(" ")).join(", ")}]`;
750
1004
  if (flowPrefix) line = `${flowPrefix} ${line}`;
751
1005
  return [line];
752
1006
  }
@@ -757,21 +1011,37 @@ function stringifySeqNodeLines(node, ctx, depth) {
757
1011
  parentPosition: "block-seq-item"
758
1012
  };
759
1013
  for (const item of items) {
1014
+ const fields = itemFields(item);
1015
+ if (fields?.spaceBefore === true) lines.push("");
1016
+ if (fields?.commentBefore !== void 0) for (const cl of commentBlockLines(fields.commentBefore)) lines.push(cl);
1017
+ /** Append the item's trailing comment to `lines[idx]`; `-1` drops it. */
1018
+ const appendItemTrailing = (idx) => {
1019
+ if (fields?.comment === void 0 || idx < 0 || idx >= lines.length) return;
1020
+ lines[idx] = `${lines[idx]}${renderTrailingComment(fields.comment)}`;
1021
+ };
760
1022
  const itemLines = stringifyNodeLines(item, itemCtx, depth + 1);
761
- if (itemLines.length === 1) lines.push(itemLines[0] === "" ? "-" : `- ${itemLines[0]}`);
762
- else {
1023
+ if (itemLines.length === 1) {
1024
+ lines.push(itemLines[0] === "" ? "-" : `- ${itemLines[0]}`);
1025
+ appendItemTrailing(lines.length - 1);
1026
+ } else {
763
1027
  const first = itemLines[0];
764
1028
  const firstStripped = stripScalarMetadataPrefix(first);
765
1029
  const itemIsScalar = item instanceof YamlScalar;
766
- if (firstStripped.startsWith("|") || firstStripped.startsWith(">") || itemIsScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""))) {
1030
+ const isBlockScalarHeader = firstStripped.startsWith("|") || firstStripped.startsWith(">");
1031
+ if (isBlockScalarHeader || itemIsScalar && (firstStripped.startsWith("'") || firstStripped.startsWith("\""))) {
1032
+ const headerIdx = lines.length;
767
1033
  lines.push(`- ${first}`);
768
1034
  for (let i = 1; i < itemLines.length; i++) lines.push(itemLines[i]);
1035
+ appendItemTrailing(isBlockScalarHeader ? headerIdx : lines.length - 1);
769
1036
  } else {
770
1037
  lines.push(first === "" ? "-" : `- ${first}`);
771
- for (let i = 1; i < itemLines.length; i++) lines.push(`${pad}${itemLines[i]}`);
1038
+ const dashLineIdx = lines.length - 1;
1039
+ for (let i = 1; i < itemLines.length; i++) lines.push(itemLines[i] === "" ? "" : `${pad}${itemLines[i]}`);
1040
+ appendItemTrailing(dashLineIdx);
772
1041
  }
773
1042
  }
774
1043
  }
1044
+ if (emitComments && node.comment !== void 0) for (const cl of commentBlockLines(node.comment)) lines.push(cl);
775
1045
  const prefix = buildMetadataPrefix(node.tag, node.anchor);
776
1046
  if (prefix) lines.unshift(prefix);
777
1047
  return lines;
@@ -799,7 +1069,8 @@ function stringifyDocument(doc, options) {
799
1069
  const ctx = createContext(options);
800
1070
  const finalNewline = options?.finalNewline ?? true;
801
1071
  let contents = doc.contents;
802
- const docComment = ctx.forceDefaultStyles ? void 0 : doc.comment;
1072
+ const docCommentRaw = ctx.forceDefaultStyles ? void 0 : doc.commentBefore;
1073
+ const docComment = docCommentRaw !== void 0 ? commentBlockLines(docCommentRaw).join("\n") : void 0;
803
1074
  if (ctx.forceDefaultStyles && contents) {
804
1075
  contents = stripNodeComments(contents);
805
1076
  const tagMap = buildTagMap(doc.directives);
@@ -810,16 +1081,31 @@ function stringifyDocument(doc, options) {
810
1081
  if (doc.hasDocumentStart) return `---\n${doc.hasDocumentEnd ? "...\n" : ""}`;
811
1082
  return "";
812
1083
  }
1084
+ if (docComment !== void 0 || doc.comment !== void 0 || doc.hasDocumentStart === true || doc.hasDocumentEnd === true) {
1085
+ const parts = [];
1086
+ if (docComment !== void 0) parts.push(`${docComment}\n`);
1087
+ if (doc.hasDocumentStart) parts.push("---\n");
1088
+ if (doc.hasDocumentEnd) parts.push("...\n");
1089
+ if (doc.comment !== void 0) parts.push(`${commentBlockLines(doc.comment).join("\n")}\n`);
1090
+ const out = parts.join("");
1091
+ return finalNewline ? out : out.replace(/\n$/, "");
1092
+ }
813
1093
  return finalNewline ? "null\n" : "null";
814
1094
  }
815
1095
  const result = stringifyNodeLines(contents, ctx, 0).join("\n");
816
- const body = finalNewline ? `${result}\n` : result;
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);
817
1098
  const needsTerminatorForKeepChomp = ctx.forceDefaultStyles && endsWithKeepChomp(result);
818
1099
  const needsTerminatorForAnchoredPlainScalar = ctx.forceDefaultStyles && doc.hasDocumentStart && contents instanceof YamlScalar && contents.style === "plain" && contents.anchor !== void 0 && !contents.tag;
819
1100
  const looksLikeDirectiveContinuation = contents instanceof YamlScalar && typeof contents.value === "string" && / %[A-Z]/.test(contents.value);
820
1101
  const needsTerminatorForMultilinePlainScalar = ctx.forceDefaultStyles && doc.hasDocumentStart && contents instanceof YamlScalar && contents.style === "plain" && contents.sourceMultiline === true && looksLikeDirectiveContinuation;
821
1102
  const needsTerminatorForDocStartTab = ctx.forceDefaultStyles && doc.hasDocumentStartTab === true;
822
- const docEnd = doc.hasDocumentEnd || needsTerminatorForKeepChomp || needsTerminatorForAnchoredPlainScalar || needsTerminatorForMultilinePlainScalar || needsTerminatorForDocStartTab ? "...\n" : "";
1103
+ const docEndMarker = doc.hasDocumentEnd || needsTerminatorForKeepChomp || needsTerminatorForAnchoredPlainScalar || needsTerminatorForMultilinePlainScalar || needsTerminatorForDocStartTab ? "...\n" : "";
1104
+ const docCommentAfter = ctx.forceDefaultStyles || doc.comment === void 0 ? void 0 : commentBlockLines(doc.comment).join("\n");
1105
+ const docEndFull = docCommentAfter !== void 0 ? `${docEndMarker}${docCommentAfter}\n` : docEndMarker;
1106
+ const body = finalNewline || docEndFull !== "" ? `${rendered}\n` : rendered;
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` : "";
823
1109
  if (doc.hasDocumentStart) {
824
1110
  const rootTag = contents && "tag" in contents ? contents.tag : void 0;
825
1111
  const rootAnchor = contents && "anchor" in contents ? contents.anchor : void 0;
@@ -836,13 +1122,15 @@ function stringifyDocument(doc, options) {
836
1122
  if (bodyClean.startsWith(metaLinePrefix)) bodyClean = bodyClean.slice(metaLinePrefix.length);
837
1123
  else if (bodyClean.startsWith(metaInlinePrefix)) bodyClean = bodyClean.slice(metaInlinePrefix.length);
838
1124
  const docStart = `--- ${metaStr}`;
839
- const sep = isCollection ? "\n" : " ";
840
- 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}`;
841
1127
  }
842
- if (isScalar) return docComment ? `# ${docComment}\n--- ${body}${docEnd}` : `--- ${body}${docEnd}`;
843
- 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;
844
1131
  }
845
- return docComment ? `# ${docComment}\n${body}${docEnd}` : `${body}${docEnd}`;
1132
+ const plain = `${rootLeading}${body}${docEnd}`;
1133
+ return docComment ? `${docComment}\n${plain}` : plain;
846
1134
  }
847
1135
 
848
1136
  //#endregion
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@effected/yaml",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
- "description": "Zero-dependency YAML parsing, editing and formatting as Effect schemas.",
5
+ "description": "Zero-dependency YAML parsing, editing, formatting and linting as Effect schemas.",
6
6
  "keywords": [
7
7
  "yaml",
8
8
  "parser",
@@ -12,6 +12,7 @@
12
12
  "cst",
13
13
  "editing",
14
14
  "formatting",
15
+ "linting",
15
16
  "effected"
16
17
  ],
17
18
  "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/yaml#readme",