+use crate::gemini_parser::GeminiLine;
+
+/// Renders HTML from a vector of GeminiLine elements.
+///
+/// # Arguments
+/// * `lines` - Vector of GeminiLine elements to render
+///
+/// # Returns
+/// A String containing the rendered HTML.
+pub fn render_html(lines: Vec<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<u8>
+) -> 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("</ul>\n"),
+ }
+ },
+ GeminiLine::Quote(_) => {
+ match line {
+ GeminiLine::Quote(_) => {},
+ _ => html.push_str("</blockquote>\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("</section>\n");
+
+ if open_heading == *level {
+ break;
+ }
+ }
+ heading_stack.push(*level);
+ html.push_str(&format!("<section class=\"h{}\">\n", level));
+ },
+ GeminiLine::ListItem(_) => {
+ match last_line {
+ Some(GeminiLine::ListItem(_)) => {},
+ _ => html.push_str("<ul>\n"),
+ }
+ },
+ GeminiLine::Quote(_) => {
+ match last_line {
+ Some(GeminiLine::Quote(_)) => {},
+ _ => html.push_str("<blockquote>\n"),
+ }
+ },
+ _ => {}
+ }
+
+ html
+}
+
+fn line_content(line: &GeminiLine) -> String {
+ match line {
+ GeminiLine::Text(content, false) => format!("<p>{}</p>", content),
+ GeminiLine::Link(url, text) => {
+ let display = if text.is_empty() { url } else { text };
+ format!("<p class=\"a\"><a href=\"{}\">{}</a></p>", url, display)
+ },
+ GeminiLine::Heading(level, content) => format!("<h{}>{}</h{}>", level, content, level),
+ GeminiLine::ListItem(content) => format!("<li>{}</li>", content),
+ GeminiLine::PreformattedToggle(true, alt_text) => { format!("<pre aria-label=\"{}\">", alt_text) }
+ GeminiLine::PreformattedToggle(false, _) => { "</pre>".to_string() }
+ GeminiLine::Text(content, true) | GeminiLine::Quote(content) => format!("{}", content)
+ }
+}
+
+fn finalize_html(heading_stack: &[u8], last_line: Option<&GeminiLine>) -> String {
+ let mut html = String::new();
+
+ match last_line {
+ Some(GeminiLine::ListItem(_)) => html.push_str("</ul>\n"),
+ Some(GeminiLine::Quote(_)) => html.push_str("</blockquote>\n"),
+ _ => {}
+ }
+
+ for _ in heading_stack {
+ html.push_str("</section>\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),
+ "<p>Hello world</p>\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),
+ "<section class=\"h1\">\n\
+ <h1>Top</h1>\n\
+ <section class=\"h2\">\n\
+ <h2>Sub</h2>\n\
+ <section class=\"h3\">\n\
+ <h3>SubSub</h3>\n\
+ </section>\n\
+ </section>\n\
+ <section class=\"h2\">\n\
+ <h2>Another Sub</h2>\n\
+ </section>\n\
+ </section>\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),
+ "<ul>\n\
+ <li>First</li>\n\
+ <li>Second</li>\n\
+ </ul>\n\
+ <p>Break</p>\n\
+ <ul>\n\
+ <li>New list</li>\n\
+ </ul>\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),
+ "<blockquote>\n\
+ First quote\n\
+ Still quoting\n\
+ </blockquote>\n\
+ <p>Normal text</p>\n\
+ <blockquote>\n\
+ New quote\n\
+ </blockquote>\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),
+ "<pre aria-label=\"code\">\n\
+ let x = 42;\n\
+ </pre>\n"
+ );
+ }
+
+ #[test]
+ fn test_links() {
+ let input = vec![
+ GeminiLine::Link("https://example.com".to_string(), "Example".to_string()),
+ GeminiLine::Link("https://rust-lang.org".to_string(), "".to_string()),
+ ];
+ assert_eq!(
+ render_html(input),
+ "<p class=\"a\"><a href=\"https://example.com\">Example</a></p>\n\
+ <p class=\"a\"><a href=\"https://rust-lang.org\">https://rust-lang.org</a></p>\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),
+ "<section class=\"h1\">\n\
+ <h1>Title</h1>\n\
+ <p>Intro</p>\n\
+ <section class=\"h2\">\n\
+ <h2>Section</h2>\n\
+ <ul>\n\
+ <li>Point 1</li>\n\
+ <li>Point 2</li>\n\
+ </ul>\n\
+ <blockquote>\n\
+ Important quote\n\
+ </blockquote>\n\
+ </section>\n\
+ <section class=\"h2\">\n\
+ <h2>Another Section</h2>\n\
+ </section>\n\
+ </section>\n"
+ );
+ }
+
+ #[test]
+ fn test_empty_input() {
+ let input = Vec::new();
+ assert_eq!(render_html(input), "");
+ }
+}