//! Heading rendering with theme-based styling. use crate::inline::render_inline_content; use crate::style::{HeadingStyler, InlineStyler}; use crate::utils::simple_wrap_preserving_spaces; /// Render a heading with appropriate styling. pub fn render_heading( level: u8, content: &str, width: usize, margin: &str, styler: &S, ) -> Vec { // Create the heading prefix (e.g., "# ", "## ", etc.) let prefix = "#".repeat(level as usize); // For h1, uppercase the content before rendering inline elements let content_to_render = if level == 1 { content.to_uppercase() } else { content.to_string() }; // First render inline elements (bold, italic, etc.) in the content let rendered_content = render_inline_content(&content_to_render, styler); // Adjust width to account for the prefix (e.g., "# " = 2 chars, "## " = 3 // chars, etc.) let prefix_display_width = level as usize + 1; let content_width = width.saturating_sub(prefix_display_width); let lines = simple_wrap_preserving_spaces(&rendered_content, content_width); let mut result = Vec::new(); for line in lines { let formatted = match level { 1 => { // H1: Bold, left-aligned, uppercase, with dimmed prefix format!( "{}\n{}{} {}", margin, margin, styler.dimmed(&styler.h1(&prefix)), styler.h1(&line) ) } 2 => { // H2: Bold, bright color, left-aligned, with dimmed prefix format!( "{}\n{}{} {}", margin, margin, styler.dimmed(&styler.h2(&prefix)), styler.h2(&line) ) } 3 => { format!( "{}{} {}", margin, styler.dimmed(&styler.h3(&prefix)), styler.h3(&line) ) } 4 => { format!( "{}{} {}", margin, styler.dimmed(&styler.h4(&prefix)), styler.h4(&line) ) } 5 => { format!( "{}{} {}", margin, styler.dimmed(&styler.h5(&prefix)), styler.h5(&line) ) } _ => { format!( "{}{} {}", margin, styler.dimmed(&styler.h6(&prefix)), styler.h6(&line) ) } }; result.push(formatted); } result } #[cfg(test)] mod tests { use super::*; use crate::theme::TagStyler; fn render(level: u8, content: &str) -> String { render_heading(level, content, 80, " ", &TagStyler).join("\n") } fn render_with_width(level: u8, content: &str, width: usize) -> String { render_heading(level, content, width, " ", &TagStyler).join("\n") } fn render_with_margin(level: u8, content: &str, margin: &str) -> String { render_heading(level, content, 80, margin, &TagStyler).join("\n") } #[test] fn test_h1_simple() { insta::assert_snapshot!(render(1, "Hello World"), @"

#

HELLO WORLD

"); } #[test] fn test_h2_simple() { insta::assert_snapshot!(render(2, "Chapter One"), @"

##

Chapter One

"); } #[test] fn test_h3_simple() { insta::assert_snapshot!(render(3, "Section Title"), @"

###

Section Title

"); } #[test] fn test_h4_simple() { insta::assert_snapshot!(render(4, "Subsection"), @"

####

Subsection

"); } #[test] fn test_h5_simple() { insta::assert_snapshot!(render(5, "Minor Heading"), @"
#####
Minor Heading
"); } #[test] fn test_h6_simple() { insta::assert_snapshot!(render(6, "Smallest Heading"), @"
######
Smallest Heading
"); } #[test] fn test_h1_with_inline_bold() { insta::assert_snapshot!(render(1, "Hello **bold** world"), @"

#

HELLO BOLD WORLD

"); } #[test] fn test_h2_with_inline_italic() { insta::assert_snapshot!(render(2, "Hello *italic* text"), @"

##

Hello italic text

"); } #[test] fn test_h3_with_code() { insta::assert_snapshot!(render(3, "Using `code` here"), @"

###

Using code here

"); } #[test] fn test_heading_level_beyond_6() { // Level 7+ should fall through to h6 styling insta::assert_snapshot!(render(7, "Level Seven"), @"
#######
Level Seven
"); insta::assert_snapshot!(render(10, "Level Ten"), @"
##########
Level Ten
"); } #[test] fn test_empty_content() { insta::assert_snapshot!(render(1, ""), @"

#

"); } #[test] fn test_custom_margin() { insta::assert_snapshot!(render_with_margin(1, "Title", " "), @"

#

TITLE

"); insta::assert_snapshot!(render_with_margin(3, "Section", ">>> "), @">>>

###

Section

"); } #[test] fn test_no_margin() { insta::assert_snapshot!(render_with_margin(1, "Title", ""), @"

#

TITLE

"); insta::assert_snapshot!(render_with_margin(3, "Section", ""), @"

###

Section

"); } #[test] fn test_wrapping_narrow_width() { insta::assert_snapshot!(render_with_width(1, "This is a very long heading that should wrap", 20), @"

#

THIS IS A VERY

#

LONG HEADING THAT

#

SHOULD WRAP

"); } #[test] fn test_h3_wrapping() { insta::assert_snapshot!(render_with_width(3, "A long section title that wraps", 15), @"

###

A long

###

section

###

title that

###

wraps

"); } #[test] fn test_h3_wrapping_preserves_korean_word_spaces() { let actual = render_with_width(3, "한글 공백 보존 확인", 12); insta::assert_snapshot!(actual, @r"

###

한글

###

공백

###

보존

###

확인

"); } #[test] fn test_h3_wrapping_splits_long_tokens() { let actual = render_with_width(3, "supercalifragilistic", 12); insta::assert_snapshot!(actual, @r"

###

supercal

###

ifragili

###

stic

"); } #[test] fn test_special_characters() { insta::assert_snapshot!(render(2, "Hello & Goodbye < World >"), @"

##

Hello & Goodbye < World >

"); } #[test] fn test_heading_with_link() { insta::assert_snapshot!(render(3, "See [documentation](https://example.com)"), @r#"

###

See documentation

"#); } #[test] fn test_mixed_inline_styles() { insta::assert_snapshot!(render(2, "**Bold** and *italic* and `code`"), @"

##

Bold and italic and code

"); } #[test] fn test_all_levels_structure() { // H1 and H2 have extra newline prefix let h1 = render(1, "H1"); let h2 = render(2, "H2"); let h3 = render(3, "H3"); assert!(h1.contains("\n"), "H1 should have newline"); assert!(h2.contains("\n"), "H2 should have newline"); assert!(!h3.starts_with("\n"), "H3 should not start with newline"); } }