use crate::prelude::{Link, Mailbox, Recipients}; use std::fmt; #[derive(Debug)] pub struct PreBlk<'a> { alt: Option<&'a str>, lines: Vec<&'a str>, } #[derive(Debug)] enum State<'a> { Normal, Preformatted(PreBlk<'a>), Quote(Vec<&'a str>), } #[derive(Clone, Debug, PartialEq)] pub enum GemtextNode { Sender(Mailbox), Recipients(Recipients), Timestamp(String), Text(String), Heading1(String), Heading2(String), Heading3(String), ListItem(String), Quote(String), Preformatted(Option, String), Link(Link), } impl fmt::Display for GemtextNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Recipients(r) => writeln!(f, "{r}"), Self::Sender(m) => writeln!(f, "{m}"), Self::Timestamp(t) => writeln!(f, "@ {t}"), Self::Text(t) => writeln!(f, "{t}"), Self::Heading1(h) => writeln!(f, "# {h}"), Self::Heading2(h) => writeln!(f, "## {h}"), Self::Heading3(h) => writeln!(f, "### {h}"), Self::ListItem(l) => writeln!(f, "* {l}"), Self::Quote(q) => writeln!(f, "> {q}"), Self::Preformatted(a, p) => match a { None => writeln!(f, "```\n{}\n```", p), Some(alt) => writeln!(f, "```{alt}\n{}\n```", p), }, Self::Link(l) => writeln!(f, "=> {l}"), } } } impl<'a> GemtextNode { fn parse_link(text: &'a str) -> Self { if let Ok(link) = text.parse() { Self::Link(link) } else { Self::Text(text.to_string()) } } fn parse_heading(text: &'a str) -> Self { if let Some((h, s)) = text.split_once(char::is_whitespace) { match h { "#" => Self::Heading1(s.to_string()), "##" => Self::Heading2(s.to_string()), "###" => Self::Heading3(s.to_string()), _ => Self::Text(text.to_string()), } } else { Self::Text(text.to_string()) } } fn parse_list_item(text: &'a str) -> Self { match text.split_once(char::is_whitespace) { Some((pre, s)) if pre == "*" => GemtextNode::ListItem(s.to_string()), _ => GemtextNode::Text(text.to_string()), } } fn parse_senders(text: &'a str) -> Self { let Some(line) = text.strip_prefix('<') else { return Self::Text(text.to_string()); }; if let Ok(user) = line.parse() { Self::Sender(user) } else { Self::Text(text.to_string()) } } fn parse_recipients(text: &'a str) -> Self { let Some(line) = text.strip_prefix(':') else { return Self::Text(text.to_string()); }; let split = line.split_whitespace(); let mut recipients: Recipients = Recipients { boxes: vec![] }; for s in split { if let Ok(m) = s.parse() { recipients.boxes.push(m); } else { return Self::Text(text.to_string()); } } Self::Recipients(recipients) } fn parse_timestamp(text: &'a str) -> Self { todo!() } } #[derive(Debug)] pub struct Parser<'a> { state: State<'a>, title: Option, lines: Vec, } impl<'a> Parser<'a> { pub fn new() -> Self { Self { state: State::Normal, title: None, lines: vec![], } } pub fn parse(mut self, raw: &'a str) -> Vec { for line in raw.lines() { match self.state { State::Normal => self.parse_normal(line), State::Preformatted(_) => self.parse_preformatted(line), State::Quote(_) => self.parse_quote(line), } } self.lines } fn link(&mut self, line: &'a str) { self.lines.push(GemtextNode::parse_link(line)); } fn heading(&mut self, line: &'a str) { let line = GemtextNode::parse_heading(line); if self.title.is_none() { match &line { GemtextNode::Heading1(t) => self.title = Some(t.clone()), _ => {} } } self.lines.push(line); } fn list_item(&mut self, line: &'a str) { self.lines.push(GemtextNode::parse_list_item(line)); } fn enter_quote(&mut self, line: &'a str) { match line.split_once(char::is_whitespace) { Some((prefix, suffix)) if prefix == ">" => self.state = State::Quote(vec![suffix]), _ => self.lines.push(GemtextNode::Text(line.to_string())), } } fn leave_quote(&mut self, line: &'a str) { match &mut self.state { State::Quote(q) => { let quote = q.join("\n").to_string(); self.lines.push(GemtextNode::Quote(quote)); } _ => panic!("Attempt to parse as quote when not in quote mode"), } self.state = State::Normal; self.lines.push(GemtextNode::Text(line.to_string())); } fn enter_preformatted(&mut self, line: &'a str) { let alt = if line.len() > 3 { Some(line[3..].trim()) } else { None }; let preblk = PreBlk { alt, lines: vec![] }; self.state = State::Preformatted(preblk); } fn leave_preformatted(&mut self) { match &self.state { State::Preformatted(v) => { let s = v.lines.join("\n").to_string(); self.lines .push(GemtextNode::Preformatted(v.alt.map(str::to_string), s)); self.state = State::Normal; } _ => panic!("Attempted to leave preformatted mode when not in preformatted mode"), } } fn senders(&mut self, line: &'a str) { self.lines.push(GemtextNode::parse_senders(line)); } fn recipients(&mut self, line: &'a str) { self.lines.push(GemtextNode::parse_recipients(line)); } fn parse_normal(&mut self, line: &'a str) { match line { s if s.starts_with("=>") => self.link(s), s if s.starts_with('#') => self.heading(s), s if s.starts_with('*') => self.list_item(s), s if s.starts_with('>') => self.enter_quote(s), s if s.starts_with("```") => self.enter_preformatted(s), s if s.starts_with('<') => self.senders(s), s if s.starts_with(':') => self.recipients(s), s if s.starts_with('@') => {} s => self.lines.push(GemtextNode::Text(s.to_string())), } } fn parse_preformatted(&mut self, line: &'a str) { if line.starts_with("```") { self.leave_preformatted(); } else { match &mut self.state { State::Preformatted(p) => p.lines.push(line), _ => panic!("Attempt to parse as preformatted when not in preformatted mode"), } } } fn parse_quote(&mut self, line: &'a str) { if let Some(suffix) = line.strip_prefix('>') { match &mut self.state { State::Quote(q) => q.push(suffix.trim()), _ => panic!("Attempt to parse as quote when not in quote mode"), } } else { self.leave_quote(line); } } }