From 39918cf28118a4af23ededc7733f193a2c56eb2e Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Sun, 11 Jan 2026 08:00:34 +0800 Subject: [PATCH] fix: process bold/italic/code formatting inside markdown headers The format_header() function was not calling format_inline_content() to process inline formatting like **bold**, *italic*, and `code` within headers. This caused raw markdown markers to appear in output. Added 4 tests to verify the fix: - test_bold_inside_header - test_italic_inside_header - test_code_inside_header - test_mixed_formatting_inside_header --- crates/g3-cli/src/streaming_markdown.rs | 12 +- .../g3-cli/tests/streaming_markdown_test.rs | 109 ++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/crates/g3-cli/src/streaming_markdown.rs b/crates/g3-cli/src/streaming_markdown.rs index 1cda99c..d46541b 100644 --- a/crates/g3-cli/src/streaming_markdown.rs +++ b/crates/g3-cli/src/streaming_markdown.rs @@ -557,11 +557,17 @@ impl StreamingMarkdownFormatter { let content: String = chars.collect(); let content = content.trim_end(); + // Process inline formatting (bold, italic, code, etc.) within the header + let formatted_content = self.format_inline_content(content); + // Remove trailing newline from format_inline_content since we add our own + let formatted_content = formatted_content.trim_end(); + // Format based on level (magenta, bold for h1/h2) + // We wrap the already-formatted content in header color, then reset at the end match level { - 1 => format!("\x1b[1;35m{}\x1b[0m\n", content), // Bold magenta - 2 => format!("\x1b[35m{}\x1b[0m\n", content), // Magenta - _ => format!("\x1b[35m{}\x1b[0m\n", content), // Magenta for h3+ + 1 => format!("\x1b[1;35m{}\x1b[0m\n", formatted_content), // Bold magenta + 2 => format!("\x1b[35m{}\x1b[0m\n", formatted_content), // Magenta + _ => format!("\x1b[35m{}\x1b[0m\n", formatted_content), // Magenta for h3+ } } diff --git a/crates/g3-cli/tests/streaming_markdown_test.rs b/crates/g3-cli/tests/streaming_markdown_test.rs index 36586a8..d7ba295 100644 --- a/crates/g3-cli/tests/streaming_markdown_test.rs +++ b/crates/g3-cli/tests/streaming_markdown_test.rs @@ -1770,3 +1770,112 @@ fn test_code_block_with_4space_indent() { // So "nested" should be part of the highlighted code assert!(full.contains("nested"), "nested should be in output"); } + +#[test] +fn test_bold_inside_header() { + let mut fmt = make_formatter(); + + // Bold inside header - valid per CommonMark spec + let input = "# **Bold Header**\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); + + // Should NOT contain raw ** in output + assert!(!full.contains("**"), "Should not contain raw ** markers, got: {}", full); + + // Should have header formatting (magenta) + assert!(full.contains("\x1b[1;35m"), "Should have bold magenta header formatting"); + + // Should have bold formatting (green) for the bold text inside + assert!(full.contains("\x1b[1;32m"), "Should have green bold formatting for **Bold Header**"); +} + +#[test] +fn test_italic_inside_header() { + let mut fmt = make_formatter(); + + // Italic inside header - valid per CommonMark spec + let input = "## *Italic Header*\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); + + // Should NOT contain raw * in output (except as part of ANSI codes) + // Count asterisks that are NOT part of ANSI escape sequences + let without_ansi = strip_ansi(&full); + assert!(!without_ansi.contains('*'), "Should not contain raw * markers, got: {}", without_ansi); + + // Should have header formatting (magenta) + assert!(full.contains("\x1b[35m"), "Should have magenta header formatting"); + + // Should have italic formatting (cyan) for the italic text inside + assert!(full.contains("\x1b[3;36m"), "Should have cyan italic formatting for *Italic Header*"); +} + +#[test] +fn test_code_inside_header() { + let mut fmt = make_formatter(); + + // Inline code inside header - valid per CommonMark spec + let input = "### Header with `code`\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); + + // Should NOT contain raw backticks in output + let without_ansi = strip_ansi(&full); + assert!(!without_ansi.contains('`'), "Should not contain raw backticks, got: {}", without_ansi); + + // Should have header formatting (magenta) + assert!(full.contains("\x1b[35m"), "Should have magenta header formatting"); + + // Should have code formatting (orange) for the inline code + assert!(full.contains("\x1b[38;2;216;177;114m"), "Should have orange code formatting"); +} + +#[test] +fn test_mixed_formatting_inside_header() { + let mut fmt = make_formatter(); + + // Mixed formatting inside header + let input = "# **Bold** and *italic* header\n"; + + println!("Input: {:?}", input); + + let output = fmt.process(input); + let remaining = fmt.finish(); + let full = format!("{}{}", output, remaining); + + println!("Output: {:?}", full); + + // Should NOT contain raw markdown markers + let without_ansi = strip_ansi(&full); + assert!(!without_ansi.contains("**"), "Should not contain raw ** markers"); + assert!(!without_ansi.contains("*italic*"), "Should not contain raw *italic* markers"); + + // Should have both bold and italic formatting + assert!(full.contains("\x1b[1;32m"), "Should have green bold formatting"); + assert!(full.contains("\x1b[3;36m"), "Should have cyan italic formatting"); +} + +/// Helper to strip ANSI escape codes for easier assertion +fn strip_ansi(s: &str) -> String { + let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + re.replace_all(s, "").to_string() +}