From a72d5a650ae24a5c9ba28d7305e8b8432e845956 Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Thu, 8 Jan 2026 20:50:26 +1100 Subject: [PATCH] Fix two markdown formatting bugs Bug 1: Inline code after list bullets not detected - After emitting a list bullet, at_line_start was not set to false - This caused the next backtick to be treated as a potential code fence - Fixed by setting at_line_start = false after emitting bullet Bug 2: Code block closing on indented backticks - Code blocks containing indented ``` (4+ spaces) were closing prematurely - The .trim() check was too permissive - Fixed by only allowing closing fence with <= 3 spaces indent (CommonMark spec) Added tests for both edge cases. --- crates/g3-cli/src/streaming_markdown.rs | 8 +- .../g3-cli/tests/streaming_markdown_test.rs | 183 ++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/crates/g3-cli/src/streaming_markdown.rs b/crates/g3-cli/src/streaming_markdown.rs index 3f97fff..80a684a 100644 --- a/crates/g3-cli/src/streaming_markdown.rs +++ b/crates/g3-cli/src/streaming_markdown.rs @@ -179,6 +179,7 @@ impl StreamingMarkdownFormatter { self.pending_output.push_back(indent); } self.pending_output.push_back("• ".to_string()); + self.at_line_start = false; return; } @@ -424,7 +425,12 @@ impl StreamingMarkdownFormatter { fn process_in_code_block(&mut self, ch: char) { if ch == '\n' { // Check if this line closes the code block - if self.current_line.trim() == "```" { + // Only close if the fence is at the start of the line with at most 3 spaces + // of indentation (per CommonMark spec). This prevents content like " ```" + // (4+ spaces, which is code indentation) from closing the block. + let trimmed = self.current_line.trim_start(); + let leading_spaces = self.current_line.len() - trimmed.len(); + if trimmed == "```" && leading_spaces <= 3 { // Emit the entire code block self.emit_code_block(); self.block_state = BlockState::None; diff --git a/crates/g3-cli/tests/streaming_markdown_test.rs b/crates/g3-cli/tests/streaming_markdown_test.rs index cf73b84..46d1573 100644 --- a/crates/g3-cli/tests/streaming_markdown_test.rs +++ b/crates/g3-cli/tests/streaming_markdown_test.rs @@ -1586,3 +1586,186 @@ fn test_language_aliases() { let full = format!("{}{}", output, remaining); assert!(full.contains("\x1b["), "Scheme should be syntax highlighted"); } + +#[test] +fn test_backticks_edge_cases() { + let mut fmt = make_formatter(); + + // Simple inline code + let input = "- `racket` / `rkt`\n"; + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + println!("Simple: {}", full); + assert!(full.contains("\x1b["), "Should have formatting"); + + // Backticks inside inline code (using double backtick delimiters) + let mut fmt = make_formatter(); + let input = "- `` `racket` `` works\n"; + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + println!("Double delim: {}", full); +} + +#[test] +fn test_inline_code_regex_directly() { + let code_re = regex::Regex::new(r"`([^`]+)`").unwrap(); + + let input = "`racket` / `rkt`"; + let matches: Vec<_> = code_re.find_iter(input).collect(); + println!("Input: {}", input); + println!("Matches: {:?}", matches); + + let result = code_re.replace_all(input, |caps: ®ex::Captures| { + let code = &caps[1]; + format!("[CODE:{}]", code) + }); + println!("Result: {}", result); +} + +#[test] +fn test_inline_code_char_by_char() { + let mut fmt = make_formatter(); + + let input = "- `racket` / `rkt`\n"; + println!("Input: {:?}", input); + + // Process char by char to see what's happening + for ch in input.chars() { + let output = fmt.process(&ch.to_string()); + if !output.is_empty() { + println!("After {:?}: output={:?}", ch, output); + } + } + + let remaining = fmt.finish(); + println!("Finish: {:?}", remaining); +} + +#[test] +fn test_inline_code_detailed_trace() { + let mut fmt = make_formatter(); + + let input = "- `racket` / `rkt`\n"; + println!("Input: {:?}", input); + + // Process char by char + for (i, ch) in input.chars().enumerate() { + let output = fmt.process(&ch.to_string()); + println!("[{}] char={:?} output={:?}", i, ch, output); + } + + let remaining = fmt.finish(); + println!("Finish: {:?}", remaining); +} + +#[test] +fn test_code_block_closing() { + let mut fmt = make_formatter(); + + let input = r#"```yaml +- type: on-load + script: | + (lock-player) +``` +"#; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); + + // Should NOT contain literal ``` in output + assert!(!full.contains("```"), "Code fence should not appear in output"); +} + +#[test] +fn test_code_block_with_trailing_fence() { + let mut fmt = make_formatter(); + + // Test case: code block followed by another code fence (malformed markdown) + let input = "```yaml\ncode here\n```\n```\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); +} + +#[test] +fn test_code_block_char_by_char() { + let mut fmt = make_formatter(); + + let input = "```yaml\ncode\n```\n"; + println!("Input: {:?}", input); + + for (i, ch) in input.chars().enumerate() { + let output = fmt.process(&ch.to_string()); + if !output.is_empty() { + println!("[{}] char={:?} output={:?}", i, ch, output); + } + } + + let remaining = fmt.finish(); + println!("Finish: {:?}", remaining); +} + +#[test] +fn test_code_fence_not_at_line_start() { + let mut fmt = make_formatter(); + + // Code fence with leading space (should NOT be treated as code block) + let input = " ```yaml\ncode\n```\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); + // With leading space, it might not be detected as a code fence +} + +#[test] +fn test_code_block_containing_backticks() { + let mut fmt = make_formatter(); + + // Code block that contains triple backticks in the content + let input = "```yaml\nscript: |\n ```\n nested\n ```\n```\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); +} + +#[test] +fn test_code_block_with_4space_indent() { + let mut fmt = make_formatter(); + + // Code block that contains triple backticks with 4-space indent (should NOT close) + let input = "```yaml\nscript: |\n ```\n nested\n ```\n```\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); + + // The 4-space indented ``` should NOT close the code block + // So "nested" should be part of the highlighted code + assert!(full.contains("nested"), "nested should be in output"); +}