diff --git a/crates/dotrain/src/parser/raindocument/logic.rs b/crates/dotrain/src/parser/raindocument/logic.rs index af694236..0e319488 100644 --- a/crates/dotrain/src/parser/raindocument/logic.rs +++ b/crates/dotrain/src/parser/raindocument/logic.rs @@ -784,24 +784,28 @@ impl RainDocument { name = slices.0[..slices.0.len() - 1].to_owned(); name_position = [parsed_binding.1[0], parsed_binding.1[0] + boundry_offset]; - let slices = content_text.split_at(boundry_offset + 1); - let trimmed_content = tracked_trim(slices.1); - content_position = if trimmed_content.0.is_empty() { + let orig_slices = content_text.split_at(boundry_offset + 1); + let trimmed_orig = tracked_trim(orig_slices.1); + // For content_position start: use leading trim from original text so comments + // at the start of a binding's content are preserved (not skipped). + // For content_position end: use trailing trim from modified doc (raw_trimmed.2) + // so comments before the next #-binding are excluded from this binding. + content_position = if trimmed_orig.0.is_empty() { [ parsed_binding.1[0] + boundry_offset + 1, parsed_binding.1[1], ] } else { [ - parsed_binding.1[0] + boundry_offset + 1 + trimmed_content.1, - parsed_binding.1[1] - trimmed_content.2, + parsed_binding.1[0] + boundry_offset + 1 + trimmed_orig.1, + parsed_binding.1[1] - raw_trimmed.2, ] }; - content = if trimmed_content.0.is_empty() { - slices.1.to_owned() - } else { - trimmed_content.0.to_owned() - }; + content = self + .text + .get(content_position[0]..content_position[1]) + .unwrap_or("") + .to_owned(); } else { name = parsed_binding.0.clone(); name_position = parsed_binding.1; diff --git a/crates/dotrain/src/parser/raindocument/mod.rs b/crates/dotrain/src/parser/raindocument/mod.rs index eed0956e..4172f073 100644 --- a/crates/dotrain/src/parser/raindocument/mod.rs +++ b/crates/dotrain/src/parser/raindocument/mod.rs @@ -947,4 +947,48 @@ _: opcode-1(0xabcd 456); assert_eq!(inner.hash, inner_hash_hex); assert_eq!(inner.unwrap_constant_binding(), "0x1111"); } + + // Mutation-validated: revert the process_binding fix (restore content_text path) and + // this test fails because b1's content_position extends through the comment. + #[test] + fn test_comment_before_binding_excluded_from_previous_content_position() { + let store = Store::new(); + let meta_store = Arc::new(RwLock::new(store)); + + // Comment before #b2 must not appear in b1's content or content_position. + let text = "---\n#b1\n! elided\n\n/* comment for b2 */\n#b2\n! elided2\n"; + let rain_document = + RainDocument::create(text.to_owned(), Some(meta_store.clone()), None, None); + + let b1 = rain_document + .bindings() + .iter() + .find(|b| b.name == "b1") + .expect("binding b1 not found"); + + // b1 content must be the elision text only, not including the comment. + assert_eq!(b1.content, "! elided"); + + // b1 content_position must end at the last char of "! elided", not at the comment. + let content_end = b1.content_position[1]; + let text_at_end = text.get(b1.content_position[0]..content_end).unwrap(); + assert_eq!(text_at_end, "! elided"); + + // The comment text must not appear anywhere inside b1's content_position range. + let b1_content_range = text + .get(b1.content_position[0]..b1.content_position[1]) + .unwrap(); + assert!( + !b1_content_range.contains("/*"), + "comment leaked into b1 content_position: {b1_content_range:?}" + ); + + // b2 must also parse correctly. + let b2 = rain_document + .bindings() + .iter() + .find(|b| b.name == "b2") + .expect("binding b2 not found"); + assert_eq!(b2.content, "! elided2"); + } }