@@ -336,18 +336,36 @@ def test_keymap_without_comments_unaffected():
336336]
337337
338338
339+ def _coalesce (comments ):
340+ """Merge adjacent same-indent Comments into a single (joined-text,
341+ indent) tuple. The dumper emits adjacent same-indent Comments
342+ without a separating blank, so on re-load they fuse into one
343+ Comment -- text content and slot assignment are preserved, only
344+ the Comment-object granularity is. This helper normalises that
345+ so summaries can be compared across load/dump/load cycles."""
346+ out = []
347+ for c in comments :
348+ if out and out [- 1 ][1 ] == c .indent :
349+ out [- 1 ] = (out [- 1 ][0 ] + "\n " + c .text , c .indent )
350+ else :
351+ out .append ((c .text , c .indent ))
352+ return out
353+
354+
339355def _keymap_summary (km ):
340356 """Reduce a keymap to its semantically-significant content for
341- comparison: comment texts and indents on each key, plus header/footer."""
357+ comparison: comment texts and indents on each key, plus header/footer.
358+ Adjacent same-indent Comments within a slot are coalesced so the
359+ comparison is stable across load → dump → load."""
342360 summary = {}
343361 for key , loc in km .items ():
344362 summary [key ] = {
345- "key_leading" : [( c . text , c . indent ) for c in loc .get_key_leading_comments ()] ,
346- "key_trailing" : [( c . text , c . indent ) for c in loc .get_key_trailing_comments ()] ,
347- "value_leading" : [( c . text , c . indent ) for c in loc .get_value_leading_comments ()] ,
348- "value_trailing" : [( c . text , c . indent ) for c in loc .get_value_trailing_comments ()] ,
349- "header" : [( c . text , c . indent ) for c in loc .get_header_comments ()] ,
350- "footer" : [( c . text , c . indent ) for c in loc .get_footer_comments ()] ,
363+ "key_leading" : _coalesce ( loc .get_key_leading_comments ()) ,
364+ "key_trailing" : _coalesce ( loc .get_key_trailing_comments ()) ,
365+ "value_leading" : _coalesce ( loc .get_value_leading_comments ()) ,
366+ "value_trailing" : _coalesce ( loc .get_value_trailing_comments ()) ,
367+ "header" : _coalesce ( loc .get_header_comments ()) ,
368+ "footer" : _coalesce ( loc .get_footer_comments ()) ,
351369 }
352370 return summary
353371
@@ -775,7 +793,10 @@ def test_dumps_empty_data_with_header():
775793def test_s11_inline_converted_to_trailing_on_dump ():
776794 """s11 does not round-trip exactly: per the rules, the inline comment is
777795 captured as trailing on the value at load time, and the dumper emits it
778- after the value rather than between `>` lines."""
796+ after the value rather than between `>` lines. Its indent is also
797+ bumped to one tabstop past the value's column so that a re-load will
798+ still classify it as value_trailing rather than as a leading comment
799+ on the next sibling (or a footer at EOF)."""
779800 text = (EXAMPLES / "s11_gapG_comment_inside_multiline.nt" ).read_text ()
780801 keymap = {}
781802 data = nt .loads (text , top = "any" , keymap = keymap )
@@ -784,8 +805,8 @@ def test_s11_inline_converted_to_trailing_on_dump():
784805 "notice:\n "
785806 " > the cache layer is being decommissioned\n "
786807 " > use the new metrics service instead\n "
787- " # a comment inside a multi-line string value\n "
788- " # (per Gap G: attached as a trailing comment for 'notice'; position within the value is lost on dump)"
808+ " # a comment inside a multi-line string value\n "
809+ " # (per Gap G: attached as a trailing comment for 'notice'; position within the value is lost on dump)"
789810 )
790811
791812
@@ -1001,21 +1022,21 @@ def test_annotate_blanks_before_after():
10011022 assert lines [intro_idx + 1 ] == "" # blank after
10021023
10031024
1004- def test_annotate_tab_mode_suppresses_auto_blank ():
1005- """User-built (tab-mode) same-indent Comments do NOT get the
1006- auto-blank that loader-built Comments get; the user controls
1007- surrounding blanks explicitly via before/after."""
1025+ def test_same_indent_comments_emit_contiguously ():
1026+ """Two same-indent Comments in a single slot are emitted
1027+ contiguously, with no separating blank line. (On re-load they will
1028+ merge into one Comment; the dumper does not insert any auto-blank
1029+ to preserve Comment-object granularity.)"""
10081030 keymap = {}
10091031 annotate (("section" ,), keymap , key_leading = [
1010- Comment ("first" ), # tab-mode, before=0
1011- Comment ("second" ), # tab-mode, before=0
1032+ Comment ("first" ),
1033+ Comment ("second" ),
10121034 ])
10131035 data = {"section" : "x" }
10141036 out = nt .dumps (data , map_keys = keymap )
10151037 lines = out .split ("\n " )
10161038 first_idx = lines .index ("# first" )
10171039 second_idx = lines .index ("# second" )
1018- # adjacent — no auto-blank between them
10191040 assert second_idx == first_idx + 1
10201041
10211042
@@ -1122,6 +1143,62 @@ def test_multi_line_key_round_trip_preserves_all_comment_slots():
11221143 assert [c .text for c in loc .get_value_trailing_comments ()] == ["trailing on value" ]
11231144
11241145
1146+ def test_inline_in_multi_line_key_indent_is_bumped_past_value ():
1147+ """A comment between fragments of a multi-line key, originally at a
1148+ shallow indent, has its indent bumped to value-depth+4 at load time
1149+ so it stays in key_trailing on a re-load."""
1150+ src = (
1151+ ": key1a\n "
1152+ "# inline\n " # at indent 0
1153+ ": key1b\n "
1154+ " > value\n "
1155+ )
1156+ keymap = {}
1157+ nt .loads (src , top = "dict" , keymap = keymap )
1158+ loc = keymap [("key1a\n key1b" ,)]
1159+ kts = loc .get_key_trailing_comments ()
1160+ assert any (c .text == "inline" and c .indent == 8 for c in kts )
1161+
1162+
1163+ def test_inline_in_multi_line_string_indent_is_bumped_past_value ():
1164+ """A comment between '>' lines of a multi-line string, originally at
1165+ the value's column, has its indent bumped to value-depth+4 at load
1166+ time so it stays in value_trailing on a re-load."""
1167+ src = (
1168+ "key:\n "
1169+ " > a\n "
1170+ " # inline\n " # at the value column (indent 4)
1171+ " > b\n "
1172+ )
1173+ keymap = {}
1174+ nt .loads (src , top = "dict" , keymap = keymap )
1175+ loc = keymap [("key" ,)]
1176+ vts = loc .get_value_trailing_comments ()
1177+ assert any (c .text == "inline" and c .indent == 8 for c in vts )
1178+
1179+
1180+ def test_inline_indent_bump_makes_round_trip_stable ():
1181+ """After bumping inline indents, a load → dump → load yields the
1182+ same keymap structure (comments stay in their original slots)."""
1183+ src = (
1184+ ": k1\n "
1185+ "# inline-k\n "
1186+ ": k2\n "
1187+ " > a\n "
1188+ " # inline-v\n "
1189+ " > b\n "
1190+ )
1191+ keymap1 = {}
1192+ nt .loads (src , top = "dict" , keymap = keymap1 )
1193+ out = nt .dumps ({"k1\n k2" : "a\n b" }, map_keys = keymap1 )
1194+ keymap2 = {}
1195+ nt .loads (out , top = "dict" , keymap = keymap2 )
1196+ loc1 = keymap1 [("k1\n k2" ,)]
1197+ loc2 = keymap2 [("k1\n k2" ,)]
1198+ assert [c .text for c in loc1 .get_key_trailing_comments ()] == [c .text for c in loc2 .get_key_trailing_comments ()]
1199+ assert [c .text for c in loc1 .get_value_trailing_comments ()] == [c .text for c in loc2 .get_value_trailing_comments ()]
1200+
1201+
11251202def test_multi_line_key_inline_comment_collected_as_key_trailing ():
11261203 """A comment between fragments of a multi-line key is collected and
11271204 emitted at the key_trailing position (similar to the inline-in-
@@ -1263,19 +1340,24 @@ def kt(k):
12631340 assert "> no" in out
12641341
12651342
1266- def test_loader_auto_blank_still_fires ():
1267- """Two same-indent loader-built Comments still get an auto-blank
1268- between them, so the boundary survives a re-load."""
1269- src = (
1270- "# block one\n "
1271- "\n "
1272- "# block two\n "
1273- "key: value\n "
1274- )
1343+ def test_loader_same_slot_same_indent_comments_merge_on_dump_reload ():
1344+ """Two same-indent Comments in a single slot (e.g., key_leading)
1345+ are emitted contiguously by the dumper. On re-load they merge into
1346+ a single Comment. Text and slot are preserved; only Comment-object
1347+ granularity may shift."""
12751348 keymap = {}
1276- nt .loads (src , top = "dict" , keymap = keymap )
1277- out = nt .dumps ({"key" : "value" }, map_keys = keymap )
1278- assert "# block one\n \n # block two" in out
1349+ annotate (("section" ,), keymap , key_leading = [
1350+ Comment ("first" ),
1351+ Comment ("second" ),
1352+ ])
1353+ data = {"section" : "x" }
1354+ out = nt .dumps (data , map_keys = keymap )
1355+ assert "# first\n # second" in out
1356+ keymap2 = {}
1357+ nt .loads (out , top = "dict" , keymap = keymap2 )
1358+ leading = keymap2 [("section" ,)].get_key_leading_comments ()
1359+ assert len (leading ) == 1
1360+ assert "first" in leading [0 ].text and "second" in leading [0 ].text
12791361
12801362
12811363def test_annotate_sets_spacing ():
0 commit comments