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: Vec<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(_) => {
37 GeminiLine::ListItem(_) => {},
38 _ => html.push_str("</ul>\n"),
41 GeminiLine::Quote(_) => {
43 GeminiLine::Quote(_) => {},
44 _ => html.push_str("</blockquote>\n"),
52 GeminiLine::Heading(level, _) => {
53 while let Some(open_heading) = heading_stack.pop() {
54 // You just encountered a more important heading.
55 // Put it back. Desist.
56 if open_heading < *level {
57 heading_stack.push(open_heading);
61 html.push_str("</section>\n");
63 if open_heading == *level {
67 heading_stack.push(*level);
68 html.push_str(&format!("<section class=\"h{}\">\n", level));
70 GeminiLine::ListItem(_) => {
72 Some(GeminiLine::ListItem(_)) => {},
73 _ => html.push_str("<ul>\n"),
76 GeminiLine::Quote(_) => {
78 Some(GeminiLine::Quote(_)) => {},
79 _ => html.push_str("<blockquote>\n"),
88 fn line_content(line: &GeminiLine) -> String {
90 GeminiLine::Text(content, false) => format!("<p>{}</p>", content),
91 GeminiLine::Link(url, text) => {
92 let display = if text.is_empty() { url } else { text };
93 format!("<p class=\"a\"><a href=\"{}\">{}</a></p>", url, display)
95 GeminiLine::Heading(level, content) => format!("<h{}>{}</h{}>", level, content, level),
96 GeminiLine::ListItem(content) => format!("<li>{}</li>", content),
97 GeminiLine::PreformattedToggle(true, alt_text) => { format!("<pre aria-label=\"{}\">", alt_text) }
98 GeminiLine::PreformattedToggle(false, _) => { "</pre>".to_string() }
99 GeminiLine::Text(content, true) | GeminiLine::Quote(content) => format!("{}", content)
103 fn finalize_html(heading_stack: &[u8], last_line: Option<&GeminiLine>) -> String {
104 let mut html = String::new();
107 Some(GeminiLine::ListItem(_)) => html.push_str("</ul>\n"),
108 Some(GeminiLine::Quote(_)) => html.push_str("</blockquote>\n"),
112 for _ in heading_stack {
113 html.push_str("</section>\n");
124 fn test_simple_text() {
126 GeminiLine::Text("Hello world".to_string(), false),
130 "<p>Hello world</p>\n"
135 fn test_heading_nesting() {
137 GeminiLine::Heading(1, "Top".to_string()),
138 GeminiLine::Heading(2, "Sub".to_string()),
139 GeminiLine::Heading(3, "SubSub".to_string()),
140 GeminiLine::Heading(2, "Another Sub".to_string()),
144 "<section class=\"h1\">\n\
146 <section class=\"h2\">\n\
148 <section class=\"h3\">\n\
152 <section class=\"h2\">\n\
153 <h2>Another Sub</h2>\n\
160 fn test_list_transitions() {
162 GeminiLine::ListItem("First".to_string()),
163 GeminiLine::ListItem("Second".to_string()),
164 GeminiLine::Text("Break".to_string(), false),
165 GeminiLine::ListItem("New list".to_string()),
181 fn test_quote_transitions() {
183 GeminiLine::Quote("First quote".to_string()),
184 GeminiLine::Quote("Still quoting".to_string()),
185 GeminiLine::Text("Normal text".to_string(), false),
186 GeminiLine::Quote("New quote".to_string()),
194 <p>Normal text</p>\n\
202 fn test_preformatted() {
204 GeminiLine::PreformattedToggle(true, "code".to_string()),
205 GeminiLine::Text("let x = 42;".to_string(), true),
206 GeminiLine::PreformattedToggle(false, String::new()),
210 "<pre aria-label=\"code\">\n\
219 GeminiLine::Link("https://example.com".to_string(), "Example".to_string()),
220 GeminiLine::Link("https://rust-lang.org".to_string(), "".to_string()),
224 "<p class=\"a\"><a href=\"https://example.com\">Example</a></p>\n\
225 <p class=\"a\"><a href=\"https://rust-lang.org\">https://rust-lang.org</a></p>\n"
230 fn test_complex_nesting() {
232 GeminiLine::Heading(1, "Title".to_string()),
233 GeminiLine::Text("Intro".to_string(), false),
234 GeminiLine::Heading(2, "Section".to_string()),
235 GeminiLine::ListItem("Point 1".to_string()),
236 GeminiLine::ListItem("Point 2".to_string()),
237 GeminiLine::Quote("Important quote".to_string()),
238 GeminiLine::Heading(2, "Another Section".to_string()),
242 "<section class=\"h1\">\n\
245 <section class=\"h2\">\n\
255 <section class=\"h2\">\n\
256 <h2>Another Section</h2>\n\
263 fn test_empty_input() {
264 let input = Vec::new();
265 assert_eq!(render_html(input), "");