]> git.r.bdr.sh - rbdr/page/blobdiff - src/gemini_parser.rs
Update layout tests
[rbdr/page] / src / gemini_parser.rs
index 36abb169ed5788bfc5c1a4cfc82a8cd642ff5d78..418b758b2206bfc294373e0f4e18e9e8d5d25662 100644 (file)
-pub fn parse(source: &str) -> String {
-
-    let lines = source.split("\n");
-    let mut is_preformatted = false;
-
-    let mut html:String = "".to_owned();
-    let mut current_line_type: Option<LineType> = None;
-
-    for line in lines {
-        let line_type = identify_line(&(line[..3]), is_preformatted);
-        match line_type {
-            LineType::PreformattedToggle => is_preformatted = !is_preformatted,
-            _ => {
-                // Close previous block if needed
-                if let Some(line) = &current_line_type {
-                    if line != &line_type && is_block(line) {
-                        html.push_str(get_line_closer(line));
-                    }
-                }
-
-                // Blocks
-                if is_block(&line_type) {
-                    if let Some(line) = &current_line_type {
-                        if line != &line_type  {
-                            html.push_str(get_line_opener(&line_type));
-                        }
-                    } else {
-                        html.push_str(get_line_opener(&line_type));
-                    }
-
-                    let line_content = get_partial_line_content(&line_type, line);
-                    html.push_str(&line_content);
-                } else {
-                    let line_content = get_full_line_content(&line_type, line);
-                    html.push_str(&line_content);
-                }
-                current_line_type = Some(line_type);
-            },
-        }
-    }
-    if let Some(line) = &current_line_type {
-        if is_block(line) {
-            html.push_str(get_line_closer(line));
-        }
-    }
-    html
+#[derive(PartialEq, Eq, Debug)]
+pub enum GeminiLine {
+    Text(String, bool),
+    PreformattedToggle(bool, String),
+    Heading(u8, String),
+    Link(String, String),
+    Quote(String),
+    ListItem(String)
 }
 
-fn is_block(line_type: &LineType) -> bool {
-    return match line_type {
-        LineType::PreformattedText => true,
-        LineType::ListItem => true,
-        LineType::Quote => true,
-        _ => false,
-    }
-}
+/// Parses gemtext source code into a vector of GeminiLine elements.
+/// 
+/// # Arguments
+/// * `source` - A string slice that contains the gemtext
+/// 
+/// # Returns
+/// A `Vec<GeminiLine>` containing the rendered HTML.
+pub fn parse(source: &str) -> Vec<GeminiLine> {
+    source.lines()
+        .fold(
+            (Vec::new(), false),
+            |(mut lines, is_preformatted), line| {
+                let parsed = if is_preformatted {
+                    parse_preformatted_line(line)
+                } else {
+                    parse_line(line)
+                };
 
-fn get_partial_line_content(line_type: &LineType, line: &str) -> String {
-    return match line_type {
-        LineType::ListItem => format!("<li>{}</li>", line[2..].trim()),
-        LineType::Quote => line[1..].trim().to_string(),
-        LineType::PreformattedText => format!("{}\n", line),
-        _ => "".to_string(),
-    }
-}
+                let new_is_preformatted = match parsed {
+                    GeminiLine::PreformattedToggle(x, _) => x,
+                    _ => is_preformatted
+                };
 
-fn get_full_line_content(line_type: &LineType, line: &str) -> String {
-     match line_type {
-        LineType::Text => format!("<p>{}</p>\n", line.trim()),
-        LineType::Blank => "<br/>\n".to_string(),
-        LineType::Link => format!("<div><a href=\"{}\">{}</a></div>\n", get_link_address(line), get_link_content(line)),
-        LineType::Heading1 => format!("<h1>{}</h1>\n", line[1..].trim()),
-        LineType::Heading2 => format!("<h2>{}</h2>\n", line[2..].trim()),
-        LineType::Heading3 => format!("<h3>{}</h3>\n", line[3..].trim()),
-        _ => "".to_string(),
-    }
+                lines.push(parsed);
+                (lines, new_is_preformatted)
+            }
+        )
+        .0
 }
 
-fn get_line_opener(line_type: &LineType) -> &'static str {
-    match line_type {
-        LineType::ListItem => "<ul>",
-        LineType::Quote => "<blockquote>",
-        LineType::PreformattedText => "<pre>",
-        _ => "",
+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 get_line_closer(line_type: &LineType) -> &'static str {
-    match line_type {
-        LineType::ListItem => "</ul>\n",
-        LineType::Quote => "</blockquote>\n",
-        LineType::PreformattedText => "</pre>\n",
-        _ => "",
+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),
     }
 }
 
-fn get_link_content(line: &str) -> &str {
-    let components: Vec<&str> = line[2..].trim().splitn(2, " ").collect();
-    if components.len() > 1 {
-        return components[1].trim()
-    }
-    components[0].trim()
-}
+#[cfg(test)]
+mod tests {
+    use super::*;
 
-fn get_link_address(line: &str) -> &str {
-    let components: Vec<&str> = line[2..].trim().splitn(2, " ").collect();
-    components[0].trim()
-}
+    #[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()));
 
-fn identify_line(line: &str, is_preformatted: bool) -> LineType {
-    if line.starts_with("```") {
-        return LineType::PreformattedToggle;
+        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));
     }
-    if is_preformatted {
-        return LineType::PreformattedText;
-    }
-    if line.is_empty() {
-        return LineType::Blank;
-    }
-    if line.starts_with("=>") {
-        return LineType::Link;
-    }
-    if line.starts_with("* ") {
-        return LineType::ListItem;
+
+    #[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)
+        );
     }
-    if line.starts_with(">") {
-        return LineType::Quote;
+
+    #[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()));
     }
-    if line.starts_with("###") {
-        return LineType::Heading3;
+
+    #[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()));
     }
-    if line.starts_with("##") {
-        return LineType::Heading2;
+
+    #[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())
+        );
+
     }
-    if line.starts_with("#") {
-        return LineType::Heading1;
+
+    #[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));
     }
 
-    LineType::Text
-}
+    #[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));
+    }
 
-#[derive(PartialEq, Eq)]
-enum LineType {
-    Text,
-    Blank,
-    Link,
-    PreformattedToggle,
-    PreformattedText,
-    Heading1,
-    Heading2,
-    Heading3,
-    ListItem,
-    Quote
+    #[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()),
+        ]);
+    }
 }