1 use crate::gemini_parser::GeminiLine;
3 /// Renders HTML from a vector of `GeminiLine` elements.
6 /// * `lines` - Vector of `GeminiLine` elements to render
9 /// A String containing the rendered HTML.
10 pub fn render_html(lines: &[GeminiLine]) -> String {
11 let mut heading_stack = Vec::new();
12 let mut last_line: Option<&GeminiLine> = None;
13 let mut result = String::new();
16 result.push_str(&line_preamble(line, last_line, &mut heading_stack));
17 result.push_str(&line_content(line));
19 last_line = Some(line);
22 result.push_str(&finalize_html(&heading_stack, last_line));
28 last_line: Option<&GeminiLine>,
29 heading_stack: &mut Vec<u8>,
31 let mut html = String::new();
33 if let Some(last_line) = last_line {
35 GeminiLine::ListItem(_) => match line {
36 GeminiLine::ListItem(_) => {}
37 _ => html.push_str("</ul>\n"),
39 GeminiLine::Quote(_) => match line {
40 GeminiLine::Quote(_) => {}
41 _ => html.push_str("</blockquote>\n"),
48 GeminiLine::Heading(level, _) => {
49 while let Some(open_heading) = heading_stack.pop() {
50 // You just encountered a more important heading.
51 // Put it back. Desist.
52 if open_heading < *level {
53 heading_stack.push(open_heading);
57 html.push_str("</section>\n");
59 if open_heading == *level {
63 heading_stack.push(*level);
64 html.push_str(&format!("<section class=\"h{level}\">\n"));
66 GeminiLine::ListItem(_) => match last_line {
67 Some(GeminiLine::ListItem(_)) => {}
68 _ => html.push_str("<ul>\n"),
70 GeminiLine::Quote(_) => match last_line {
71 Some(GeminiLine::Quote(_)) => {}
72 _ => html.push_str("<blockquote>\n"),
80 fn line_content(line: &GeminiLine) -> String {
82 GeminiLine::Text(content, false) => format!("<p>{content}</p>"),
83 GeminiLine::Link(url, text) => {
84 let display = if text.is_empty() { url } else { text };
85 format!("<p class=\"a\"><a href=\"{url}\">{display}</a></p>")
87 GeminiLine::Heading(level, content) => format!("<h{level}>{content}</h{level}>"),
88 GeminiLine::ListItem(content) => format!("<li>{content}</li>"),
89 GeminiLine::PreformattedToggle(true, alt_text) => {
90 format!("<pre aria-label=\"{alt_text}\">")
92 GeminiLine::PreformattedToggle(false, _) => "</pre>".to_string(),
93 GeminiLine::Text(content, true) | GeminiLine::Quote(content) => content.to_string(),
97 fn finalize_html(heading_stack: &[u8], last_line: Option<&GeminiLine>) -> String {
98 let mut html = String::new();
101 Some(GeminiLine::ListItem(_)) => html.push_str("</ul>\n"),
102 Some(GeminiLine::Quote(_)) => html.push_str("</blockquote>\n"),
106 for _ in heading_stack {
107 html.push_str("</section>\n");
118 fn test_simple_text() {
119 let input = vec![GeminiLine::Text("Hello world".to_string(), false)];
120 assert_eq!(render_html(&input), "<p>Hello world</p>\n");
124 fn test_heading_nesting() {
126 GeminiLine::Heading(1, "Top".to_string()),
127 GeminiLine::Heading(2, "Sub".to_string()),
128 GeminiLine::Heading(3, "SubSub".to_string()),
129 GeminiLine::Heading(2, "Another Sub".to_string()),
133 "<section class=\"h1\">\n\
135 <section class=\"h2\">\n\
137 <section class=\"h3\">\n\
141 <section class=\"h2\">\n\
142 <h2>Another Sub</h2>\n\
149 fn test_list_transitions() {
151 GeminiLine::ListItem("First".to_string()),
152 GeminiLine::ListItem("Second".to_string()),
153 GeminiLine::Text("Break".to_string(), false),
154 GeminiLine::ListItem("New list".to_string()),
170 fn test_quote_transitions() {
172 GeminiLine::Quote("First quote".to_string()),
173 GeminiLine::Quote("Still quoting".to_string()),
174 GeminiLine::Text("Normal text".to_string(), false),
175 GeminiLine::Quote("New quote".to_string()),
183 <p>Normal text</p>\n\
191 fn test_preformatted() {
193 GeminiLine::PreformattedToggle(true, "code".to_string()),
194 GeminiLine::Text("let x = 42;".to_string(), true),
195 GeminiLine::PreformattedToggle(false, String::new()),
199 "<pre aria-label=\"code\">\n\
208 GeminiLine::Link("https://example.com".to_string(), "Example".to_string()),
209 GeminiLine::Link("https://rust-lang.org".to_string(), "".to_string()),
213 "<p class=\"a\"><a href=\"https://example.com\">Example</a></p>\n\
214 <p class=\"a\"><a href=\"https://rust-lang.org\">https://rust-lang.org</a></p>\n"
219 fn test_complex_nesting() {
221 GeminiLine::Heading(1, "Title".to_string()),
222 GeminiLine::Text("Intro".to_string(), false),
223 GeminiLine::Heading(2, "Section".to_string()),
224 GeminiLine::ListItem("Point 1".to_string()),
225 GeminiLine::ListItem("Point 2".to_string()),
226 GeminiLine::Quote("Important quote".to_string()),
227 GeminiLine::Heading(2, "Another Section".to_string()),
231 "<section class=\"h1\">\n\
234 <section class=\"h2\">\n\
244 <section class=\"h2\">\n\
245 <h2>Another Section</h2>\n\
252 fn test_empty_input() {
253 let input = Vec::new();
254 assert_eq!(render_html(&input), "");