From f1a2f745f589902f996865bda0a048cdeaee1f4d Mon Sep 17 00:00:00 2001 From: DavidWells Date: Thu, 26 Jun 2025 10:52:28 -0700 Subject: [PATCH 1/2] Implement inline comment support after key colons --- src/compose/compose-node.ts | 12 ++++-- src/compose/resolve-block-map.ts | 56 ++++++++++++++++++++++++- src/nodes/Node.ts | 3 ++ src/parse/cst.ts | 5 +++ src/parse/parser.ts | 70 ++++++++++++++++++++++++++++++++ tests/doc/comments.ts | 16 +++++--- tests/doc/stringify.ts | 6 +-- 7 files changed, 154 insertions(+), 14 deletions(-) diff --git a/src/compose/compose-node.ts b/src/compose/compose-node.ts index 88dab4ec..12cb185e 100644 --- a/src/compose/compose-node.ts +++ b/src/compose/compose-node.ts @@ -95,8 +95,11 @@ export function composeNode( } if (spaceBefore) node.spaceBefore = true if (comment) { - if (token.type === 'scalar' && token.source === '') node.comment = comment - else node.commentBefore = comment + if (token.type === 'scalar' && token.source === '') { + node.comment = comment + } else { + node.commentBefore = comment + } } // @ts-expect-error Type checking misses meaning of isSrcToken if (ctx.options.keepSourceTokens && isSrcToken) node.srcToken = token @@ -124,9 +127,12 @@ export function composeEmptyNode( onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string') } if (spaceBefore) node.spaceBefore = true + // Extend the range to include any trailing content (like comments) if it's beyond the current range end + if (end > node.range[2]) { + node.range[2] = end + } if (comment) { node.comment = comment - node.range[2] = end } return node } diff --git a/src/compose/resolve-block-map.ts b/src/compose/resolve-block-map.ts index d01c7a66..cf650cd3 100644 --- a/src/compose/resolve-block-map.ts +++ b/src/compose/resolve-block-map.ts @@ -1,7 +1,7 @@ import type { ParsedNode } from '../nodes/Node.ts' import { Pair } from '../nodes/Pair.ts' import { YAMLMap } from '../nodes/YAMLMap.ts' -import type { BlockMap } from '../parse/cst.ts' +import type { BlockMap, SourceToken } from '../parse/cst.ts' import type { CollectionTag } from '../schema/types.ts' import type { ComposeContext, ComposeNode } from './compose-node.ts' import type { ComposeErrorHandler } from './composer.ts' @@ -12,6 +12,49 @@ import { mapIncludes } from './util-map-includes.ts' const startColMsg = 'All mapping items must start at the same column' +/** + * Extracts key comments from separator tokens. + * Key comments are single-line comments that appear immediately after the colon. + * Multi-line comments (where there are additional comments after a newline) should + * be left as value comments. Returns the extracted comment and modified separator tokens. + */ +function extractKeyComment(sep: SourceToken[]): { keyComment: string, modifiedSep: SourceToken[] } { + if (!sep.length) return { keyComment: '', modifiedSep: [] } + + let mapValueIndIndex = -1 + let keyComment = '' + let modifiedSep = [...sep] + + // Find the map-value-ind (colon) + for (let i = 0; i < sep.length; i++) { + if (sep[i].type === 'map-value-ind') { + mapValueIndIndex = i + break + } + } + if (mapValueIndIndex === -1) return { keyComment: '', modifiedSep: sep } + + // Only treat comments as key comments if they are immediately after the colon, with only spaces in between (no newline) + let i = mapValueIndIndex + 1 + while (i < sep.length && sep[i].type === 'space') i++ + if (i < sep.length && sep[i].type === 'comment') { + // Check for any newline between colon and comment + let hasNewline = false + for (let j = mapValueIndIndex + 1; j < i; j++) { + if (sep[j].type === 'newline') { + hasNewline = true + break + } + } + if (!hasNewline) { + // This comment is inline after the colon + keyComment = sep[i].source.substring(1) || ' ' + modifiedSep = sep.slice(0, i).concat(sep.slice(i + 1)) + } + } + return { keyComment, modifiedSep } +} + export function resolveBlockMap( { composeNode, composeEmptyNode }: ComposeNode, ctx: ComposeContext, @@ -80,8 +123,17 @@ export function resolveBlockMap( if (mapIncludes(ctx, map.items, keyNode)) onError(keyStart, 'DUPLICATE_KEY', 'Map keys must be unique') + // Extract key comments from separator tokens and modify tokens for value processing + const { keyComment, modifiedSep } = extractKeyComment(sep ?? []) + + // Apply key comment to the key node if found + if (keyComment) { + if (keyNode.comment) keyNode.comment += '\n' + keyComment + else keyNode.comment = keyComment + } + // value properties - const valueProps = resolveProps(sep ?? [], { + const valueProps = resolveProps(modifiedSep, { indicator: 'map-value-ind', next: value, offset: keyNode.range[2], diff --git a/src/nodes/Node.ts b/src/nodes/Node.ts index 0ecfbe8a..60966f00 100644 --- a/src/nodes/Node.ts +++ b/src/nodes/Node.ts @@ -54,6 +54,9 @@ export abstract class NodeBase { /** A comment before this */ declare commentBefore?: string | null + /** A comment after key: */ + declare commentAfterKey?: string | null + /** * The `[start, value-end, node-end]` character offsets for the part of the * source parsed into this node (undefined if not parsed). The `value-end` diff --git a/src/parse/cst.ts b/src/parse/cst.ts index a945ea03..1267bf4a 100644 --- a/src/parse/cst.ts +++ b/src/parse/cst.ts @@ -31,6 +31,11 @@ export interface SourceToken { offset: number indent: number source: string + commentAfterKey?: boolean + commentOnParentKey?: string + parentComment?: string + commentIsAfterKey?: boolean + context?: any } export interface ErrorToken { diff --git a/src/parse/parser.ts b/src/parse/parser.ts index 39718af5..e61b85a2 100644 --- a/src/parse/parser.ts +++ b/src/parse/parser.ts @@ -14,6 +14,8 @@ import type { import { prettyToken, tokenType } from './cst.ts' import { Lexer } from './lexer.ts' +const DEBUG = false + function includesToken(list: SourceToken[], type: SourceToken['type']) { for (let i = 0; i < list.length; ++i) if (list[i].type === type) return true return false @@ -241,6 +243,36 @@ export class Parser { while (this.stack.length > 0) yield* this.pop() } + private getCurrentContext(parent: Token) { + const top = parent + if (!top) return { context: 'stream' } + + switch (top.type) { + case 'block-map': { + const it = top.items[top.items.length - 1] + if (!it) return { context: 'map-start' } + if (it.value) return { context: 'map-value', key: it.key } + if (it.sep) return { context: 'map-separator', sep: it.sep } + return { context: 'map-key', key: it.key } + } + case 'block-seq': { + const it = top.items[top.items.length - 1] + if (!it) return { context: 'seq-start' } + if (it.value) return { context: 'seq-value' } + return { context: 'seq-item' } + } + case 'flow-collection': { + const it = top.items[top.items.length - 1] + if (!it) return { context: 'flow-start' } + if (it.value) return { context: 'flow-value', key: it.key } + if (it.sep) return { context: 'flow-separator' } + return { context: 'flow-key', key: it.key } + } + default: + return { context: 'other', parentType: top.type } + } + } + private get sourceToken() { const st: SourceToken = { type: this.type as SourceToken['type'], @@ -248,6 +280,44 @@ export class Parser { indent: this.indent, source: this.source } + + if (this.type === 'comment') { + const parent = this.peek(1) + const currentContext = this.getCurrentContext(parent) + + if (DEBUG) { + st.context = currentContext + } + + if (parent && 'items' in parent && parent.items && currentContext.context === 'map-separator') { + const it = parent.items[parent.items.length - 1] + if (it?.sep) { + if (DEBUG) st.parentComment = (it?.sep.find(st => st.type === 'comment') ?? {})?.source + // Check if this comment appears right after a map-value-ind token + const mapValueIndIndex = it.sep.findIndex(token => token.type === 'map-value-ind') + if (mapValueIndIndex !== -1) { + // Check if all tokens after map-value-ind are spaces (no newlines) followed by this comment + let allSpacesAfterMapValue = true + for (let i = mapValueIndIndex + 1; i < it.sep.length; i++) { + const token = it.sep[i] + if (token.type === 'newline') { + allSpacesAfterMapValue = false + break + } else if (token.type !== 'space') { + allSpacesAfterMapValue = false + break + } + } + + // If all tokens after map-value-ind are spaces (no newlines), this comment is commentIsAfterKey + if (allSpacesAfterMapValue) { + st.commentIsAfterKey = true + } + } + } + } + } + return st } diff --git a/tests/doc/comments.ts b/tests/doc/comments.ts index 630afbdc..7fddb73a 100644 --- a/tests/doc/comments.ts +++ b/tests/doc/comments.ts @@ -237,7 +237,6 @@ describe('parse comments', () => { { key: { commentBefore: 'c0', value: 'k1' }, value: { - commentBefore: 'c1', items: [{ value: 'v1' }, { commentBefore: 'c2', value: 'v2' }], comment: 'c3' } @@ -252,8 +251,7 @@ describe('parse comments', () => { }) expect(String(doc)).toBe(source` #c0 - k1: - #c1 + k1: #c1 - v1 #c2 - v2 @@ -1148,9 +1146,17 @@ entryB: }) test('collection end comment', () => { - const src = `a: b #c\n#d\n` + const src = source` + a: b #c + + #d + ` const doc = YAML.parseDocument(src) - expect(String(doc)).toBe(`a: b #c\n\n#d\n`) + expect(String(doc)).toBe(source` + a: b #c + + #d + `) }) test('blank line after seq in map', () => { diff --git a/tests/doc/stringify.ts b/tests/doc/stringify.ts index f263f28c..e606756c 100644 --- a/tests/doc/stringify.ts +++ b/tests/doc/stringify.ts @@ -414,8 +414,7 @@ z: ` const doc = YAML.parseDocument(src) expect(String(doc)).toBe(source` - key: - #comment + key: #comment - one - two `) @@ -429,8 +428,7 @@ z: ` const doc = YAML.parseDocument(src) expect(String(doc)).toBe(source` - key: - #comment + key: #comment !tag - one - two From 42e3f927f10aa67d11a969f1ba07175a16329879 Mon Sep 17 00:00:00 2001 From: DavidWells Date: Thu, 26 Jun 2025 10:54:56 -0700 Subject: [PATCH 2/2] Add comprehensive tests for trailing key comment functionality --- tests/trailing-key-comments.test.ts | 704 ++++++++++++++++++++++++++++ 1 file changed, 704 insertions(+) create mode 100644 tests/trailing-key-comments.test.ts diff --git a/tests/trailing-key-comments.test.ts b/tests/trailing-key-comments.test.ts new file mode 100644 index 00000000..a233e441 --- /dev/null +++ b/tests/trailing-key-comments.test.ts @@ -0,0 +1,704 @@ +import { source } from './_utils.ts' +import * as YAML from 'yaml' + +describe('trailing key comments in block collections', () => { + test('kitchen sink', () => { + const src = source` + foo: bar + # test 1 + tutorial: # test 1 level 1 + # test 1 leading comment for array line 1 + # test 1 leading comment for array line 2 + - yaml: # test 1 nesting level 2 + name: value # comment on value + # Two + two: # comment on two key + # comment before keyTwo + keyTwo: value # comment on keyTwo value + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('test 1 output') + console.log(output) + + // The key comments should stay inline with their keys + expect(output).toContain('tutorial: # test 1 level 1') + expect(output).toContain('yaml: # test 1 nesting level 2') + expect(output).toContain('two: # comment on two key') + + // Ensure round-trip works + const reparsed = YAML.parseDocument(output) + expect(reparsed.toString()).toBe(output) + }) + test('key comment before block sequence', () => { + const src = source` + tutorial: # test 1 level 1 + - yaml: # test 1 nesting level 2 + name: value + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + + // The key comments should stay inline with their keys + expect(output).toContain('tutorial: # test 1 level 1') + expect(output).toContain('yaml: # test 1 nesting level 2') + + // Ensure round-trip works + const reparsed = YAML.parseDocument(output) + expect(reparsed.toString()).toBe(output) + }) + + test('key comment before block map', () => { + const src = source` + tutorial: #comment + key: value + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + + expect(output).toContain('tutorial: #comment') + }) + + test('key comment before block map with comment before key', () => { + const src = source` + tutorial: # comment on key + # comment before key + key: value + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + + expect(output).toContain('tutorial: # comment on key') + // expect(output).toStrictEqual(src) + expect(String(doc)).toBe(src) + }) + + test('original issue reproduction', () => { + const originalString = `tutorial: #nesting level 1 + - yaml: #nesting level 2 (2 spaces used for indentation) + name: YAML Ain't Markup Language #string [literal] #nesting level 3 (4 spaces used for indentation) + type: awesome #string [literal] + born: 2001 #number [literal]` + + const originalYamlDoc = YAML.parseDocument(originalString) + const outputTest = new YAML.Document(originalYamlDoc) + const outputTestStr = outputTest.toString() + + // Key comments should stay inline + expect(outputTestStr).toContain('tutorial: #nesting level 1') + expect(outputTestStr).toContain('yaml: #nesting level 2 (2 spaces used for indentation)') + }) + + test('multiline key comments should still move to separate lines', () => { + const src = source` + key: # line 1 + # line 2 + - item + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('multiline key comments output') + console.log(output) + + // Multiline comments should still be moved to separate lines + expect(output).not.toContain('key: #line 1') + }) + + test('Handles arrays with comments', () => { + const src = source` + key: #comment + !tag + - one + - two + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('arrays with comments output') + console.log(output) + expect(output).toBe(source` + key: #comment + !tag + - one + - two + `) + }) + + test('preserves block scalar comments', () => { + const src = source` + yaml: |2 + # Strip + # Comments: + strip: |- + # text + ␣␣ + # Clip + # comments: + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('block scalar test output') + console.log(output) + + // Verify that the "Clip" comments are preserved + expect(output).toContain('# Clip') + expect(output).toContain('# comments:') + + // Ensure round-trip works + const reparsed = YAML.parseDocument(output) + expect(reparsed.toString()).toBe(output) + }) + + test('array with inline comment on key', () => { + const src = source` + items: # array of items + - first + - second + - third + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('array key comment output') + console.log(output) + + expect(output).toContain('items: # array of items') + expect(String(doc)).toBe(src) + }) + + test('array items with inline comments', () => { + const src = source` + colors: + - red # primary + - green # secondary + - blue # tertiary + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('array items with comments output') + console.log(output) + + expect(output).toContain('- red # primary') + expect(output).toContain('- green # secondary') + expect(output).toContain('- blue # tertiary') + expect(String(doc)).toBe(src) + }) + + test('nested arrays with comments', () => { + const src = source` + matrix: # 2D array + # row 1 + - - 1 # first element + - 2 # second element + # row 2 + - - 3 # first element + - 4 # second element + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('nested arrays output') + console.log(output) + + expect(output).toContain('matrix: # 2D array') + // expect(output).toContain('- # row 1') + expect(output).toContain('- 1 # first element') + expect(String(doc)).toBe(src) + }) + + test.skip('nested arrays with comments on -', () => { + const src = source` + matrix: # 2D array + - # row 1 + - 1 # first element + - 2 # second element + - # row 2 + - 3 # first element + - 4 # second element + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('nested arrays output') + console.log(output) + + expect(output).toContain('matrix: # 2D array') + expect(output).toContain('- # row 1') + expect(output).toContain('- 1 # first element') + expect(String(doc)).toBe(src) + }) + + test('array with leading comments before items', () => { + const src = source` + fruits: + # tropical fruits + - mango + - pineapple + # citrus fruits + - orange + - lemon + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('array with leading comments output') + console.log(output) + + expect(output).toContain('# tropical fruits') + expect(output).toContain('# citrus fruits') + expect(String(doc)).toBe(src) + }) + + test('array with mixed comment types', () => { + const src = source` + config: # configuration array + # server settings + - host: localhost # default host + port: 8080 # default port + # client settings + - timeout: 30 # seconds + retries: 3 # attempts + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('mixed comment types output') + console.log(output) + + expect(output).toContain('config: # configuration array') + expect(output).toContain('# server settings') + expect(output).toContain('host: localhost # default host') + expect(String(doc)).toBe(src) + }) + + test('flow array with comments', () => { + const src = source` + simple: [one, two, three] # simple array + with_comments: [a, b, c] # array with comments + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('flow array output') + console.log(output) + + expect(output).toContain('simple: [ one, two, three ] # simple array') + expect(output).toContain('with_comments: [ a, b, c ] # array with comments') + expect(String(doc)).toBe( source` + simple: [ one, two, three ] # simple array + with_comments: [ a, b, c ] # array with comments + `) + }) + + test('array with tagged items and comments', () => { + const src = source` + tagged: # tagged array + - !str hello # string tag + - !!int 42 # integer tag + - !!float 3.14 # float tag + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('tagged array output') + console.log(output) + + expect(output).toContain('tagged: # tagged array') + expect(output).toContain('- !str hello # string tag') + expect(output).toContain('- !!int 42 # integer tag') + expect(output).toContain('- !!float 3.14 # float tag') + expect(String(doc)).toBe(src) + }) + + test('array with anchor and alias comments', () => { + const src = source` + anchors: # anchor definitions + # base anchor + - &base + name: base + value: 1 + - *base # alias reference + # derived anchor + - &derived + <<: *base # merge key + value: 2 # override + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('anchor alias output') + console.log(output) + + expect(output).toContain('anchors: # anchor definitions') + expect(output).toContain('- *base # alias reference') + expect(String(doc)).toBe(src) + }) + + // TODO: This is not working as expected. # derived anchor is hoisted + test.skip('array with anchor and alias comments - inline', () => { + const src = source` + anchors: # anchor definitions + - &base # base anchor + name: base + value: 1 + - *base # alias reference + - &derived # derived anchor + <<: *base # merge key + value: 2 # override + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('anchor alias output') + console.log(output) + + expect(output).toContain('anchors: # anchor definitions') + expect(output).toContain('- &base # base anchor') + expect(output).toContain('- *base # alias reference') + expect(String(doc)).toBe(src) + }) + + test('array with scalar comments and block scalars', () => { + const src = source` + mixed: # mixed content array + - plain # plain scalar + - | # literal block + block + content + - > # folded block + folded content + - quoted # quoted scalar + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('mixed scalar types output') + console.log(output) + + expect(output).toContain('mixed: # mixed content array') + expect(output).toContain('- plain # plain scalar') + expect(output).toContain('- | # literal block') + expect(output).toContain('- > # folded block') + expect(String(doc)).toBe(src) + }) + + // TODO: This is not working as expected. `- # empty item 1` has 2 spaces `- # empty item 1` + test.skip('array with empty items and comments', () => { + const src = source` + empty: # array with empty items + - # empty item 1 + - # empty item 2 + - value # non-empty item + - # empty item 3 + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('empty array items output') + console.log(output) + + expect(output).toContain('empty: # array with empty items') + expect(output).toContain('- # empty item 1') + expect(output).toContain('- value # non-empty item') + expect(String(doc)).toBe(src) + }) + + test('array with multiline comments', () => { + const src = source` + multiline: # array with + # multiline comments + - item1 # first item + - item2 # second item + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('multiline comments output') + console.log(output) + + expect(output).toContain('multiline: # array with') + expect(output).toContain('# multiline comments') + expect(output).toContain('- item1 # first item') + expect(String(doc)).toBe(src) + }) + + // Indentation is not preserved + test.skip('array with multiline comments with indentation', () => { + const src = source` + multiline: # array with + # multiline comments + - item1 # first item + - item2 # second item + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('multiline comments output') + console.log(output) + + expect(output).toContain('multiline: # array with') + expect(output).toContain('# multiline comments') + expect(output).toContain('- item1 # first item') + expect(String(doc)).toBe(src) + }) + + test('array with complex nested structure and comments', () => { + const src = source` + complex: # complex nested structure + - name: outer # outer object + items: # inner array + - inner1 # inner item 1 + - inner2 # inner item 2 + config: # nested object + enabled: true # flag + count: 5 # number + - simple: value # simple item + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('complex nested structure output') + console.log(output) + + expect(output).toContain('complex: # complex nested structure') + expect(output).toContain('- name: outer # outer object') + expect(output).toContain('items: # inner array') + expect(output).toContain('- inner1 # inner item 1') + expect(output).toContain('config: # nested object') + expect(output).toContain('enabled: true # flag') + expect(String(doc)).toBe(src) + }) + + test.skip('array with directive and document comments', () => { + const src = source` + %YAML 1.2 # YAML version + --- # document start + array: # main array + - item1 # first item + - item2 # second item + ... # document end + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('directive comments output') + console.log(output) + + expect(output).toContain('%YAML 1.2 # YAML version') + expect(output).toContain('--- # document start') + expect(output).toContain('array: # main array') + expect(output).toContain('... # document end') + expect(String(doc)).toBe(src) + }) + + test('array with special characters in comments', () => { + const src = source` + special: # array with special chars + - normal # normal comment + - quoted # "quoted" comment + - escaped # \\n escaped \\t chars + - unicode # 🚀 emoji comment + - symbols # @#$%^&* symbols + ` + const doc = YAML.parseDocument(src) + console.log('Document errors:', doc.errors) + console.log('Document warnings:', doc.warnings) + + if (doc.errors.length > 0) { + console.log('First error:', doc.errors[0]) + } + + const output = doc.toString() + console.log('special characters output') + console.log(output) + + expect(output).toContain('special: # array with special chars') + expect(output).toContain('- normal # normal comment') + expect(output).toContain('- quoted # "quoted" comment') + expect(output).toContain('- unicode # 🚀 emoji comment') + expect(String(doc)).toBe(src) + }) + + test('key with big 3-line comment', () => { + const src = source` + important_config: + # This is a very important configuration + # that controls the core behavior of the system + # and should be handled with extreme care + setting1: value1 + # This is a very important configuration + # that controls the core behavior of the system + # and should be handled with extreme care + setting2: value2 + ` + const doc = YAML.parseDocument(src) + const output = doc.toString() + console.log('big 3-line comment output') + console.log(output) + + expect(String(doc)).toBe(src) + }) + + + test('Test manipulation of comments', () => { + // Test programmatic comment manipulation + const src = source` + key1: value1 + key2: value2 + array: + - item1 + - item2 + ` + const doc = YAML.parseDocument(src) + + // Add comments to existing nodes + const key1 = doc.contents.items[0] + key1.key.comment = ' key comment' + key1.value.comment = ' value comment' + + const key2 = doc.contents.items[1] + key2.value.commentBefore = ' comment before value' + // key2.value.spaceBefore = true + + const arrayNode = doc.contents.items[2] + arrayNode.key.comment = ' array comment' + arrayNode.value.items[0].comment = ' first item comment' + arrayNode.value.comment = ' end of array comment' + + // Document-level comments + doc.commentBefore = ' Document comment' + doc.comment = ' End document comment' + + const output = doc.toString() + console.log('comment manipulation output') + console.log(output) + + // Verify the exact expected output format + const expectedOutput = source` + # Document comment + + key1: # key comment + value1 # value comment + key2: + # comment before value + value2 + array: # array comment + - item1 # first item comment + - item2 + # end of array comment + + # End document comment + ` + expect(output).toBe(expectedOutput) + }) + + test('Test comment removal and modification', () => { + const src = source` + # Original comment + key: value # inline comment + # Another comment + array: + - item # item comment + ` + const doc = YAML.parseDocument(src) + + // Remove the original comment (attached to the first key) + const keyPair = doc.contents.items[0] + keyPair.key.commentBefore = null + + // Modify inline comment + keyPair.value.comment = ' modified comment' + + // Remove array comment and add new one + const arrayPair = doc.contents.items[1] + arrayPair.key.commentBefore = null // This removes "Another comment" + arrayPair.key.comment = ' new array comment' + + // Modify item comment + arrayPair.value.items[0].comment = ' modified item comment' + + const output = doc.toString() + console.log('comment modification output') + console.log(output) + + // Verify modifications + expect(output).not.toContain('# Original comment') + expect(output).toContain('key: value # modified comment') + expect(output).not.toContain('# Another comment') + expect(output).toContain('array: # new array comment') + expect(output).toContain('- item # modified item comment') + }) + + test('Test adding comments to programmatically created nodes', () => { + // Create a new document from scratch + const doc = new YAML.Document() + + // Create a map + const map = doc.createNode({ + name: 'test', + version: '1.0', + features: ['parsing', 'stringifying'] + }) + + doc.contents = map + + // Add comments to the programmatically created nodes + map.items[0].key.comment = ' project name' + map.items[0].value.commentBefore = ' semantic version' + map.items[0].value.spaceBefore = true + + map.items[1].key.comment = ' version number' + map.items[1].value.comment = ' follows semver' + + map.items[2].key.comment = ' available features' + map.items[2].value.items[0].comment = ' read YAML' + map.items[2].value.items[1].comment = ' write YAML' + map.items[2].value.comment = ' core functionality' + + // Document-level comments + doc.commentBefore = ' Generated configuration' + doc.comment = ' End of config' + + const output = doc.toString() + console.log('programmatic creation output') + console.log(output) + + // Verify all programmatically added comments + expect(output).toContain('# Generated configuration') + expect(output).toContain('name: # project name') + expect(output).toContain('# semantic version') + expect(output).toContain('version: # version number') + expect(output).toContain('"1.0" # follows semver') + expect(output).toContain('features: # available features') + expect(output).toContain('- parsing # read YAML') + expect(output).toContain('- stringifying # write YAML') + expect(output).toContain('# core functionality') + expect(output).toContain('# End of config') + }) + + test('Test comment inheritance and round-trip', () => { + const src = source` + # Top level comment + config: + # Database settings + database: + host: localhost # server address + port: 5432 # default postgres port + # Cache settings + cache: + enabled: true # feature flag + ttl: 300 # seconds + ` + + const doc = YAML.parseDocument(src) + const configNode = doc.contents.items[0] + + // Add additional comments + configNode.value.items[0].value.items[1].value.comment = ' updated port comment' + configNode.value.items[1].value.items[1].value.commentBefore = ' time to live' + configNode.value.items[1].value.items[1].value.spaceBefore = true + + const output = doc.toString() + console.log('inheritance and round-trip output') + console.log(output) + + // Parse the output again to test round-trip + const reparsed = YAML.parseDocument(output) + const roundTripOutput = reparsed.toString() + + // Verify original and added comments are preserved + expect(output).toContain('# Top level comment') + expect(output).toContain('# Database settings') + expect(output).toContain('port: 5432 # updated port comment') + expect(output).toContain('# time to live') + expect(output).toContain('# Cache settings') + + // Verify round-trip stability + expect(roundTripOutput).toBe(output) + }) + +}) \ No newline at end of file