#[derive(PartialEq, Eq, Debug)] pub enum GeminiLine { Text(String, bool), PreformattedToggle(bool, String), Heading(u8, String), Link(String, String), Quote(String), ListItem(String), } /// Parses gemtext source code into a vector of `GeminiLine` elements. /// /// # Arguments /// * `source` - A string slice that contains the gemtext /// /// # Returns /// A `Vec` containing the rendered HTML. pub fn parse(source: &str) -> Vec { source .lines() .fold((Vec::new(), false), |(mut lines, is_preformatted), line| { let parsed = if is_preformatted { parse_preformatted_line(line) } else { parse_line(line) }; let new_is_preformatted = match parsed { GeminiLine::PreformattedToggle(x, _) => x, _ => is_preformatted, }; lines.push(parsed); (lines, new_is_preformatted) }) .0 } fn parse_preformatted_line(line: &str) -> GeminiLine { match line { s if s.starts_with("```") => GeminiLine::PreformattedToggle(false, String::new()), _ => GeminiLine::Text(line.to_string(), true), } } fn parse_line(line: &str) -> GeminiLine { match line { s if s.starts_with("###") => GeminiLine::Heading(3, s[3..].to_string()), s if s.starts_with("##") => GeminiLine::Heading(2, s[2..].to_string()), s if s.starts_with('#') => GeminiLine::Heading(1, s[1..].to_string()), s if s.starts_with("=>") => { let content = s[2..].trim(); match content.split_once(char::is_whitespace) { Some((url, text)) => { GeminiLine::Link(url.trim().to_string(), text.trim().to_string()) } None => GeminiLine::Link(content.trim().to_string(), String::new()), } } s if s.starts_with("* ") => GeminiLine::ListItem(s[2..].to_string()), s if s.starts_with('>') => GeminiLine::Quote(s[1..].to_string()), s if s.starts_with("```") => GeminiLine::PreformattedToggle(true, s[3..].to_string()), _ => GeminiLine::Text(line.to_string(), false), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_headings() { assert_eq!( parse_line("### Heading"), GeminiLine::Heading(3, " Heading".to_string()) ); assert_eq!( parse_line("## Heading"), GeminiLine::Heading(2, " Heading".to_string()) ); assert_eq!( parse_line("# Heading"), GeminiLine::Heading(1, " Heading".to_string()) ); assert_eq!(parse_line("###"), GeminiLine::Heading(3, "".to_string())); assert_eq!( parse_line("#####"), GeminiLine::Heading(3, "##".to_string()) ); assert_eq!(parse_line("# "), GeminiLine::Heading(1, " ".to_string())); assert_eq!( parse_preformatted_line("### Heading"), GeminiLine::Text("### Heading".to_string(), true) ); assert_eq!( parse_preformatted_line("## Heading"), GeminiLine::Text("## Heading".to_string(), true) ); assert_eq!( parse_preformatted_line("# Heading"), GeminiLine::Text("# Heading".to_string(), true) ); } #[test] fn test_links() { assert_eq!( parse_line("=> https://example.com Link text"), GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()) ); assert_eq!( parse_line("=> /local/path"), GeminiLine::Link("/local/path".to_string(), "".to_string()) ); assert_eq!( parse_line("=>"), GeminiLine::Link("".to_string(), "".to_string()) ); assert_eq!( parse_line("=> "), GeminiLine::Link("".to_string(), "".to_string()) ); assert_eq!( parse_line("=> multiple spaces in text"), GeminiLine::Link("multiple".to_string(), "spaces in text".to_string()) ); assert_eq!( parse_preformatted_line("=> https://example.com Link text"), GeminiLine::Text("=> https://example.com Link text".to_string(), true) ); } #[test] fn test_list_items() { assert_eq!( parse_line("* List item"), GeminiLine::ListItem("List item".to_string()) ); assert_eq!(parse_line("* "), GeminiLine::ListItem("".to_string())); assert_eq!(parse_line("*"), GeminiLine::Text("*".to_string(), false)); assert_eq!( parse_line("*WithText"), GeminiLine::Text("*WithText".to_string(), false) ); assert_eq!( parse_line("* Multiple spaces"), GeminiLine::ListItem(" Multiple spaces".to_string()) ); } #[test] fn test_quotes() { assert_eq!( parse_line(">Quote text"), GeminiLine::Quote("Quote text".to_string()) ); assert_eq!(parse_line(">"), GeminiLine::Quote("".to_string())); assert_eq!(parse_line("> "), GeminiLine::Quote(" ".to_string())); assert_eq!( parse_line(">>Nested"), GeminiLine::Quote(">Nested".to_string()) ); } #[test] fn test_preformatted() { assert_eq!( parse_line("```alt-text"), GeminiLine::PreformattedToggle(true, "alt-text".to_string()) ); assert_eq!( parse_line("```"), GeminiLine::PreformattedToggle(true, "".to_string()) ); assert_eq!( parse_line("``` "), GeminiLine::PreformattedToggle(true, " ".to_string()) ); assert_eq!( parse_line("````"), GeminiLine::PreformattedToggle(true, "`".to_string()) ); assert_eq!( parse_preformatted_line("```alt-text"), GeminiLine::PreformattedToggle(false, "".to_string()) ); assert_eq!( parse_preformatted_line("```"), GeminiLine::PreformattedToggle(false, "".to_string()) ); } #[test] fn test_text() { // Normal case assert_eq!( parse_line("Regular text"), GeminiLine::Text("Regular text".to_string(), false) ); // Edge cases assert_eq!(parse_line(""), GeminiLine::Text("".to_string(), false)); assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); assert_eq!(parse_line(" "), GeminiLine::Text(" ".to_string(), false)); } #[test] fn test_malformed_input() { assert_eq!( parse_line("= >Not a link"), GeminiLine::Text("= >Not a link".to_string(), false) ); assert_eq!( parse_line("``Not preformatted"), GeminiLine::Text("``Not preformatted".to_string(), false) ); assert_eq!( parse_line("** Not a list"), GeminiLine::Text("** Not a list".to_string(), false) ); } #[test] fn test_full_document() { let input = "\ # Heading 1 ## Heading 2 ### Heading 3 Regular text => https://example.com Link text * List item >Quote ```alt code # Heading 1 ## Heading 2 ### Heading 3 => https://example.com Link text * List item >Quote ```trailing alt"; let result = parse(input); assert_eq!( result, vec![ GeminiLine::Heading(1, " Heading 1".to_string()), GeminiLine::Heading(2, " Heading 2".to_string()), GeminiLine::Heading(3, " Heading 3".to_string()), GeminiLine::Text("Regular text".to_string(), false), GeminiLine::Link("https://example.com".to_string(), "Link text".to_string()), GeminiLine::ListItem("List item".to_string()), GeminiLine::Quote("Quote".to_string()), GeminiLine::PreformattedToggle(true, "alt".to_string()), GeminiLine::Text("code".to_string(), true), GeminiLine::Text("# Heading 1".to_string(), true), GeminiLine::Text("## Heading 2".to_string(), true), GeminiLine::Text("### Heading 3".to_string(), true), GeminiLine::Text("=> https://example.com Link text".to_string(), true), GeminiLine::Text("* List item".to_string(), true), GeminiLine::Text(">Quote".to_string(), true), GeminiLine::PreformattedToggle(false, "".to_string()), ] ); } }