244 lines
7.3 KiB
Rust
244 lines
7.3 KiB
Rust
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>, 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<String>,
|
|
lines: Vec<GemtextNode>,
|
|
}
|
|
|
|
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<GemtextNode> {
|
|
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);
|
|
}
|
|
}
|
|
}
|