use crate::gemini_parser::GeminiLine; use std::path::Path; /// Renders HTML from a vector of `GeminiLine` elements. /// /// # Arguments /// * `lines` - Vector of `GeminiLine` elements to render /// /// # Returns /// A String containing the rendered HTML. /// /// # Examples /// ``` /// use std::fs::read_to_string; /// use gema_texto::gemini_parser::parse; /// use gema_texto::html_renderer::render_html; /// /// let gemini_source = "\ /// # Hello /// This is some gemini text! /// => https://test Go to the test page. /// "; /// /// let gemini_lines = parse(&gemini_source); /// let html_source = render_html(&gemini_lines); /// println!("{html_source}"); /// ``` #[must_use] pub fn render_html(lines: &[GeminiLine]) -> String { let mut heading_stack = Vec::new(); let mut last_line: Option<&GeminiLine> = None; let mut result = String::new(); for line in lines { result.push_str(&line_preamble(line, last_line, &mut heading_stack)); result.push_str(&line_content(line)); result.push('\n'); last_line = Some(line); } result.push_str(&finalize_html(&heading_stack, last_line)); result } fn line_preamble( line: &GeminiLine, last_line: Option<&GeminiLine>, heading_stack: &mut Vec, ) -> String { let mut html = String::new(); if let Some(last_line) = last_line { match last_line { GeminiLine::ListItem(_) => match line { GeminiLine::ListItem(_) => {} _ => html.push_str("\n"), }, GeminiLine::Quote(_) => match line { GeminiLine::Quote(_) => {} _ => html.push_str("\n"), }, _ => {} } } match line { GeminiLine::Heading(level, _) => { while let Some(open_heading) = heading_stack.pop() { // You just encountered a more important heading. // Put it back. Desist. if open_heading < *level { heading_stack.push(open_heading); break; } html.push_str("\n"); if open_heading == *level { break; } } heading_stack.push(*level); html.push_str(&format!("
\n")); } GeminiLine::ListItem(_) => match last_line { Some(GeminiLine::ListItem(_)) => {} _ => html.push_str("\n"), Some(GeminiLine::Quote(_)) => html.push_str("\n"), _ => {} } for _ in heading_stack { html.push_str("
\n"); } html } #[cfg(test)] mod tests { use super::*; #[test] fn test_simple_text() { let input = vec![GeminiLine::Text("Hello world".to_string(), false)]; assert_eq!(render_html(&input), "

Hello world

\n"); } #[test] fn test_heading_nesting() { let input = vec![ GeminiLine::Heading(1, "Top".to_string()), GeminiLine::Heading(2, "Sub".to_string()), GeminiLine::Heading(3, "SubSub".to_string()), GeminiLine::Heading(2, "Another Sub".to_string()), ]; assert_eq!( render_html(&input), "
\n\

Top

\n\
\n\

Sub

\n\
\n\

SubSub

\n\
\n\
\n\
\n\

Another Sub

\n\
\n\
\n" ); } #[test] fn test_list_transitions() { let input = vec![ GeminiLine::ListItem("First".to_string()), GeminiLine::ListItem("Second".to_string()), GeminiLine::Text("Break".to_string(), false), GeminiLine::ListItem("New list".to_string()), ]; assert_eq!( render_html(&input), "\n\

Break

\n\ \n" ); } #[test] fn test_quote_transitions() { let input = vec![ GeminiLine::Quote("First quote".to_string()), GeminiLine::Quote("Still quoting".to_string()), GeminiLine::Text("Normal text".to_string(), false), GeminiLine::Quote("New quote".to_string()), ]; assert_eq!( render_html(&input), "
\n\ First quote\n\ Still quoting\n\
\n\

Normal text

\n\
\n\ New quote\n\
\n" ); } #[test] fn test_preformatted() { let input = vec![ GeminiLine::PreformattedToggle(true, "code".to_string()), GeminiLine::Text("let x = 42;".to_string(), true), GeminiLine::PreformattedToggle(false, String::new()), ]; assert_eq!( render_html(&input), "
\n\
             let x = 42;\n\
             
\n" ); } #[test] fn test_links() { let input = vec![ GeminiLine::Link("gemini://hi.gmi".to_string(), "Example".to_string()), GeminiLine::Link("/hi.gmi/kidding".to_string(), "Example".to_string()), GeminiLine::Link("/hi.gmi".to_string(), "Example".to_string()), GeminiLine::Link("https://example.com".to_string(), "Example".to_string()), GeminiLine::Link("https://rust-lang.org".to_string(), String::new()), ]; assert_eq!( render_html(&input), "

Example

\n\

Example

\n\

Example

\n\

Example

\n\

https://rust-lang.org

\n" ); } #[test] fn test_complex_nesting() { let input = vec![ GeminiLine::Heading(1, "Title".to_string()), GeminiLine::Text("Intro".to_string(), false), GeminiLine::Heading(2, "Section".to_string()), GeminiLine::ListItem("Point 1".to_string()), GeminiLine::ListItem("Point 2".to_string()), GeminiLine::Quote("Important quote".to_string()), GeminiLine::Heading(2, "Another Section".to_string()), ]; assert_eq!( render_html(&input), "
\n\

Title

\n\

Intro

\n\
\n\

Section

\n\
    \n\
  • Point 1
  • \n\
  • Point 2
  • \n\
\n\
\n\ Important quote\n\
\n\
\n\
\n\

Another Section

\n\
\n\
\n" ); } #[test] fn test_empty_input() { let input = Vec::new(); assert_eq!(render_html(&input), ""); } }