From 46669a0ffa0e6f679649f9af82c6e0454a4f3b5e Mon Sep 17 00:00:00 2001 From: sou1118 Date: Wed, 5 Aug 2026 02:01:01 +0900 Subject: [PATCH] fix: keep paragraphs joined across indented line comments Generalize the Space-node guard from #9: when the next Space starts with a newline and the paragraph buffer already ends in a newline plus optional indent, drop the trailing indent and the duplicate newline. This prevents whitespace-only lines in the output, which Typst treats as paragraph breaks. Closes #16 --- src/parse.rs | 71 ++++++++++++++++++++++++++++++++++++++++---- tests/integration.rs | 11 +++++++ 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index 2273196..072a5ab 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -68,12 +68,28 @@ pub fn parse(source: &str) -> Vec { // ---- inline elements (accumulate into paragraph) ---- Expr::Space(_) => { let text = node_text(&expr); - // Two consecutive Space("\n") nodes arise only when a LineComment - // was dropped between them (bare \n\n is always a Parbreak token, - // never two Space nodes). Skip the second \n to avoid producing - // \n\n in the reconstructed source, which Typst would treat as a - // paragraph break. - if !(text == "\n" && paragraph_buf.ends_with('\n')) { + // Two consecutive Space nodes spanning a newline arise only when + // a comment was dropped between them (bare \n\n is always a + // Parbreak token, never two Space nodes). Reconstructing them + // verbatim would leave a blank or whitespace-only line, which + // Typst treats as a paragraph break. Drop the buffer's trailing + // indent and the duplicate newline so the two lines stay in one + // paragraph. The buffer may end in \n plus indent when the + // dropped comment was itself indented. + let joined = match text.strip_prefix('\n') { + Some(rest) => { + let line_start = paragraph_buf.trim_end_matches([' ', '\t']); + if line_start.ends_with('\n') { + paragraph_buf.truncate(line_start.len()); + paragraph_buf.push_str(rest); + true + } else { + false + } + } + None => false, + }; + if !joined { paragraph_buf.push_str(&text); } } @@ -240,6 +256,49 @@ mod tests { } } + #[test] + fn test_parse_line_comment_keeps_single_paragraph() { + let blocks = parse("First.\n// a comment\nSecond.\n"); + assert_eq!(blocks.len(), 1, "expected 1 block, got: {blocks:?}"); + assert!( + matches!(&blocks[0], Block::Paragraph { source_text } if source_text == "First.\nSecond.") + ); + } + + #[test] + fn test_parse_indented_line_comment_keeps_single_paragraph() { + let blocks = parse("First.\n // indented comment\nSecond.\n"); + assert_eq!(blocks.len(), 1, "expected 1 block, got: {blocks:?}"); + assert!( + matches!(&blocks[0], Block::Paragraph { source_text } if source_text == "First.\nSecond.") + ); + } + + #[test] + fn test_parse_consecutive_indented_comments_keep_single_paragraph() { + let blocks = parse("First.\n // c1\n // c2\nSecond.\n"); + assert_eq!(blocks.len(), 1, "expected 1 block, got: {blocks:?}"); + assert!( + matches!(&blocks[0], Block::Paragraph { source_text } if source_text == "First.\nSecond.") + ); + } + + #[test] + fn test_parse_blank_line_before_comment_keeps_paragraph_break() { + let blocks = parse("First.\n\n// a comment\nSecond.\n"); + assert!( + blocks + .iter() + .any(|b| matches!(b, Block::Paragraph { source_text } if source_text == "First.")) + ); + assert!(blocks.iter().any(|b| matches!(b, Block::Parbreak))); + assert!( + blocks + .iter() + .any(|b| matches!(b, Block::Paragraph { source_text } if source_text == "Second.")) + ); + } + #[test] fn test_parse_label_at_paragraph_start_as_own_block() { let blocks = parse("= Sample\n\n\nAlpha beta.\n"); diff --git a/tests/integration.rs b/tests/integration.rs index f52c925..dacad43 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -198,3 +198,14 @@ after the field log was reconciled. assert!(output.contains("#ref(, supplement: [])")); assert!(!output.contains("#ref(\\")); } + +#[test] +fn test_indented_line_comment_does_not_split_paragraph() { + let old = "First sentence.\n // indented comment\nSecond sentence.\n"; + let new = "First sentence.\n // indented comment\nSecond sentence changed.\n"; + let output = run_diff(old, new); + + assert!(output.contains("First sentence.\nSecond sentence")); + // A whitespace-only line would be treated as a paragraph break by Typst. + assert!(!output.contains("\n \n")); +}