diff --git a/src/markdown/mod.rs b/src/markdown/mod.rs index 3f74646..219e111 100644 --- a/src/markdown/mod.rs +++ b/src/markdown/mod.rs @@ -159,6 +159,33 @@ fn end_paragraph( lines.push(Line::from("")); } +#[allow(clippy::too_many_arguments)] +fn flush_pending_inline_if_any( + lines: &mut Vec>, + spans: &mut Vec>, + blockquote_depth: usize, + list_stack: &[ListKind], + item_stack: &mut [ItemState], + render_width: usize, + theme: &MarkdownTheme, + marker_color: Option, +) -> bool { + if spans.is_empty() { + return false; + } + flush_wrapped_spans( + lines, + spans, + blockquote_depth, + list_stack, + item_stack, + render_width, + theme, + marker_color, + ); + true +} + fn end_blockquote( lines: &mut Vec>, spans: &mut Vec>, @@ -405,17 +432,16 @@ pub(crate) fn parse_markdown_with_width( last_block = LastBlock::Paragraph; } MdEvent::Start(Tag::CodeBlock(kind)) => { - if !spans.is_empty() { - flush_wrapped_spans( - &mut lines, - &mut spans, - blockquote_depth, - &list_stack, - &mut item_stack, - render_width, - theme_colors, - blockquote_color, - ); + if flush_pending_inline_if_any( + &mut lines, + &mut spans, + blockquote_depth, + &list_stack, + &mut item_stack, + render_width, + theme_colors, + blockquote_color, + ) { wraps = true; } start_code_block( @@ -499,6 +525,18 @@ pub(crate) fn parse_markdown_with_width( push_inline_code_span(&mut spans, text.as_ref(), theme_colors); } MdEvent::Start(Tag::BlockQuote(kind)) => { + if flush_pending_inline_if_any( + &mut lines, + &mut spans, + blockquote_depth, + &list_stack, + &mut item_stack, + render_width, + theme_colors, + blockquote_color, + ) { + wraps = true; + } blockquote_depth += 1; if let Some(k) = kind { let color = alert_color(k, theme_colors); diff --git a/src/tests/markdown_lists.rs b/src/tests/markdown_lists.rs index b4eb58c..7ef08e9 100644 --- a/src/tests/markdown_lists.rs +++ b/src/tests/markdown_lists.rs @@ -309,3 +309,106 @@ fn code_block_after_continuation_line_in_tight_list_item_renders_below_it() { "expected continuation line directly after first line" ); } + +#[test] +fn code_block_in_list_item_has_same_layout_loose_and_tight() { + let (ss, theme) = test_assets(); + let loose_md = "- one\n\n ```\n three\n ```\n"; + let tight_md = "- one\n ```\n three\n ```\n"; + let (loose_lines, _, _, _) = + parse_markdown(loose_md, &ss, &theme, &test_md_theme(), false, true).into(); + let (tight_lines, _, _, _) = + parse_markdown(tight_md, &ss, &theme, &test_md_theme(), false, true).into(); + let loose = rendered_non_empty_lines(&loose_lines); + let tight = rendered_non_empty_lines(&tight_lines); + + let loose_one = loose + .iter() + .position(|l| l.contains("one")) + .expect("missing 'one' in loose rendering"); + let loose_three = loose + .iter() + .position(|l| l.contains("three")) + .expect("missing 'three' in loose rendering"); + let tight_one = tight + .iter() + .position(|l| l.contains("one")) + .expect("missing 'one' in tight rendering"); + let tight_three = tight + .iter() + .position(|l| l.contains("three")) + .expect("missing 'three' in tight rendering"); + + assert_eq!( + loose_three - loose_one, + tight_three - tight_one, + "loose and tight paths should place the code block at the same distance from the item line" + ); +} + +#[test] +fn blockquote_after_continuation_line_in_tight_list_item_renders_below_it() { + let (ss, theme) = test_assets(); + let md = "- one\n two\n > quote\n"; + let (lines, _, _, _) = parse_markdown(md, &ss, &theme, &test_md_theme(), false, true).into(); + let rendered = rendered_non_empty_lines(&lines); + + let one_idx = rendered + .iter() + .position(|line| line.contains("one")) + .expect("missing first list line"); + let two_idx = rendered + .iter() + .position(|line| line.contains("two")) + .expect("missing continuation line"); + let quote_idx = rendered + .iter() + .position(|line| line.contains("quote")) + .expect("missing blockquote line"); + + assert!( + one_idx < two_idx && two_idx < quote_idx, + "expected blockquote below continuation line, got one={one_idx}, two={two_idx}, quote={quote_idx}" + ); +} + +#[test] +fn nested_list_after_continuation_line_in_tight_list_item_renders_below_it() { + let (ss, theme) = test_assets(); + let md = "- one\n two\n - nested\n"; + let (lines, _, _, _) = parse_markdown(md, &ss, &theme, &test_md_theme(), false, true).into(); + let rendered = rendered_non_empty_lines(&lines); + + let one_idx = rendered + .iter() + .position(|line| line.contains("one")) + .expect("missing first list line"); + let two_idx = rendered + .iter() + .position(|line| line.contains("two")) + .expect("missing continuation line"); + let nested_idx = rendered + .iter() + .position(|line| line.contains("nested")) + .expect("missing nested list line"); + + assert!( + one_idx < two_idx && two_idx < nested_idx, + "expected nested list below continuation line, got one={one_idx}, two={two_idx}, nested={nested_idx}" + ); +} + +#[test] +fn html_block_after_continuation_line_in_tight_list_item_documented_behavior() { + let (ss, theme) = test_assets(); + let md = "- one\n two\n
html
\n"; + let (lines, _, _, _) = parse_markdown(md, &ss, &theme, &test_md_theme(), false, true).into(); + let rendered = rendered_non_empty_lines(&lines); + + assert!(rendered.iter().any(|line| line.contains("one"))); + assert!(rendered.iter().any(|line| line.contains("two"))); + assert!( + !rendered.iter().any(|line| line.contains("html")), + "documented behavior: raw HTML blocks are dropped from the rendered output" + ); +}