diff --git a/melib/src/lib.rs b/melib/src/lib.rs index 38ebfce2..f18e8792 100644 --- a/melib/src/lib.rs +++ b/melib/src/lib.rs @@ -105,6 +105,7 @@ pub use crate::email::*; pub use crate::thread::*; mod structs; pub use self::structs::*; +pub mod parsec; #[macro_use] extern crate serde_derive; diff --git a/melib/src/parsec.rs b/melib/src/parsec.rs new file mode 100644 index 00000000..97328a68 --- /dev/null +++ b/melib/src/parsec.rs @@ -0,0 +1,334 @@ +/* + * meli - melib crate. + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use crate::structs::StackVec; + +pub type Result<'a, Output> = std::result::Result<(&'a str, Output), &'a str>; + +pub trait Parser<'a, Output> { + fn parse(&self, input: &'a str) -> Result<'a, Output>; + + fn parse_complete(&self, input: &'a str) -> Result<'a, Output> { + match self.parse(input) { + r @ Ok(("", _)) => r, + r @ Err(_) => r, + Ok(_) => Err(input), + } + } + + fn map(self, map_fn: F) -> BoxedParser<'a, NewOutput> + where + Self: Sized + 'a, + Output: 'a, + NewOutput: 'a, + F: Fn(Output) -> NewOutput + 'a, + { + BoxedParser::new(map(self, map_fn)) + } + + fn and_then(self, f: F) -> BoxedParser<'a, NewOutput> + where + Self: Sized + 'a, + Output: 'a, + NewOutput: 'a, + NextParser: Parser<'a, NewOutput> + 'a, + F: Fn(Output) -> NextParser + 'a, + { + BoxedParser::new(and_then(self, f)) + } + + fn seq(self, p: P) -> BoxedParser<'a, NewOutput> + where + Self: Sized + 'a, + Output: 'a, + NewOutput: 'a, + P: Parser<'a, NewOutput> + 'a, + { + BoxedParser::new(seq(self, p)) + } +} + +pub fn left<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R1> +where + P1: Parser<'a, R1>, + P2: Parser<'a, R2>, +{ + map(pair(parser1, parser2), |(left, _right)| left) +} + +pub fn right<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R2> +where + P1: Parser<'a, R1>, + P2: Parser<'a, R2>, +{ + map(pair(parser1, parser2), |(_left, right)| right) +} + +impl<'a, F, Output> Parser<'a, Output> for F +where + F: Fn(&'a str) -> Result, +{ + fn parse(&self, input: &'a str) -> Result<'a, Output> { + self(input) + } +} + +pub fn map<'a, P, F, A, B>(parser: P, map_fn: F) -> impl Parser<'a, B> +where + P: Parser<'a, A>, + F: Fn(A) -> B, +{ + move |input| { + parser + .parse(input) + .map(|(next_input, result)| (next_input, map_fn(result))) + } +} + +pub fn match_literal<'a>(expected: &'static str) -> impl Parser<'a, ()> { + move |input: &'a str| match input.get(0..expected.len()) { + Some(next) if next == expected => Ok((&input[expected.len()..], ())), + _ => Err(input), + } +} + +pub fn match_literal_anycase<'a>(expected: &'static str) -> impl Parser<'a, ()> { + move |input: &'a str| match input.get(0..expected.len()) { + Some(next) if next.eq_ignore_ascii_case(expected) => Ok((&input[expected.len()..], ())), + _ => Err(input), + } +} + +pub fn one_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec> +where + P: Parser<'a, A>, +{ + move |mut input| { + let mut result = Vec::new(); + + if let Ok((next_input, first_item)) = parser.parse(input) { + input = next_input; + result.push(first_item); + } else { + return Err(input); + } + + while let Ok((next_input, next_item)) = parser.parse(input) { + input = next_input; + result.push(next_item); + } + + Ok((input, result)) + } +} + +pub fn zero_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec> +where + P: Parser<'a, A>, +{ + move |mut input| { + let mut result = Vec::new(); + + while let Ok((next_input, next_item)) = parser.parse(input) { + input = next_input; + result.push(next_item); + } + + Ok((input, result)) + } +} + +pub fn pred<'a, P, A, F>(parser: P, predicate: F) -> impl Parser<'a, A> +where + P: Parser<'a, A>, + F: Fn(&A) -> bool, +{ + move |input| { + if let Ok((next_input, value)) = parser.parse(input) { + if predicate(&value) { + return Ok((next_input, value)); + } + } + Err(input) + } +} + +pub fn whitespace_char<'a>() -> impl Parser<'a, char> { + pred(any_char, |c| c.is_whitespace()) +} + +pub fn quoted_string<'a>() -> impl Parser<'a, String> { + map( + right( + match_literal("\""), + left( + zero_or_more(pred(any_char, |c| *c != '"')), + match_literal("\""), + ), + ), + |chars| chars.into_iter().collect(), + ) +} + +pub struct BoxedParser<'a, Output> { + parser: Box + 'a>, +} + +impl<'a, Output> BoxedParser<'a, Output> { + fn new

(parser: P) -> Self + where + P: Parser<'a, Output> + 'a, + { + BoxedParser { + parser: Box::new(parser), + } + } +} + +impl<'a, Output> Parser<'a, Output> for BoxedParser<'a, Output> { + fn parse(&self, input: &'a str) -> Result<'a, Output> { + self.parser.parse(input) + } +} + +pub fn either<'a, P1, P2, A>(parser1: P1, parser2: P2) -> impl Parser<'a, A> +where + P1: Parser<'a, A>, + P2: Parser<'a, A>, +{ + move |input| match parser1.parse(input) { + ok @ Ok(_) => ok, + Err(_) => parser2.parse(input), + } +} + +pub fn whitespace_wrap<'a, P, A>(parser: P) -> impl Parser<'a, A> +where + P: Parser<'a, A>, +{ + right(space0(), left(parser, space0())) +} + +pub fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)> +where + P1: Parser<'a, R1>, + P2: Parser<'a, R2>, +{ + move |input| { + parser1.parse(input).and_then(|(next_input, result1)| { + parser2 + .parse(next_input) + .map(|(last_input, result2)| (last_input, (result1, result2))) + }) + } +} + +pub fn prefix<'a, PN, P, R, RN>(pre: PN, parser: P) -> impl Parser<'a, R> +where + PN: Parser<'a, RN>, + P: Parser<'a, R>, +{ + move |input| { + pre.parse(input).and_then(|(last_input, _)| { + parser + .parse(last_input) + .map(|(rest, result)| (rest, result)) + }) + } +} + +pub fn suffix<'a, PN, P, R, RN>(parser: P, suf: PN) -> impl Parser<'a, R> +where + PN: Parser<'a, RN>, + P: Parser<'a, R>, +{ + move |input| { + parser + .parse(input) + .and_then(|(last_input, result)| suf.parse(last_input).map(|(rest, _)| (rest, result))) + } +} + +pub fn delimited<'a, PN, RN, P, R>(lparser: PN, mid: P, rparser: PN) -> impl Parser<'a, R> +where + PN: Parser<'a, RN>, + P: Parser<'a, R>, +{ + move |input| { + lparser.parse(input).and_then(|(next_input, _)| { + mid.parse(next_input).and_then(|(last_input, result)| { + rparser.parse(last_input).map(|(rest, _)| (rest, result)) + }) + }) + } +} + +pub fn any_char(input: &str) -> Result { + match input.chars().next() { + Some(next) => Ok((&input[next.len_utf8()..], next)), + _ => Err(input), + } +} + +pub fn string<'a>() -> impl Parser<'a, String> { + one_or_more(pred(any_char, |c| c.is_alphanumeric())).map(|r| r.into_iter().collect()) +} + +pub fn space1<'a>() -> impl Parser<'a, Vec> { + one_or_more(whitespace_char()) +} + +pub fn space0<'a>() -> impl Parser<'a, Vec> { + zero_or_more(whitespace_char()) +} + +pub fn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B> +where + P: Parser<'a, A>, + NextP: Parser<'a, B>, + F: Fn(A) -> NextP, +{ + move |input| match parser.parse(input) { + Ok((next_input, result)) => f(result).parse(next_input), + Err(err) => Err(err), + } +} + +pub fn opt<'a, P, A>(opt_parser: P) -> impl Parser<'a, Option> +where + P: Parser<'a, A>, +{ + move |input| match opt_parser.parse(input) { + Ok((next_input, result)) => Ok((next_input, Some(result))), + Err(_) => Ok((input, None)), + } +} + +pub fn seq<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R2> +where + P1: Parser<'a, R1>, + P2: Parser<'a, R2>, +{ + move |input| match parser1.parse(input) { + Ok((next_input, result)) => parser2.parse(next_input), + Err(e) => Err(e), + } +} diff --git a/ui/src/cache.rs b/ui/src/cache.rs index cd0a1301..d2d64a95 100644 --- a/ui/src/cache.rs +++ b/ui/src/cache.rs @@ -27,7 +27,7 @@ use std::sync::RwLock; */ use melib::email::{Flag, UnixTimestamp}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Query { Before(UnixTimestamp), After(UnixTimestamp), @@ -47,6 +47,9 @@ pub enum Query { AllText(String), /* * * * */ Flag(Flag), + And(Box, Box), + Or(Box, Box), + Not(Box), } /* @@ -113,3 +116,212 @@ impl Cache { } } */ +impl std::ops::Not for Query { + type Output = Query; + fn not(self) -> Query { + match self { + Query::Not(q) => *q, + q => Query::Not(Box::new(q)), + } + } +} + +impl std::ops::BitAnd for Query { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self { + Query::And(Box::new(self), Box::new(rhs)) + } +} + +impl std::ops::BitOr for Query { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self { + Query::Or(Box::new(self), Box::new(rhs)) + } +} + +pub mod query_parser { + use super::Query::{self, *}; + use melib::parsec::*; + + pub fn subject<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("subject:")), + whitespace_wrap(literal()), + ) + .map(|term| Query::Subject(term)) + } + + pub fn from<'a>() -> impl Parser<'a, Query> { + prefix( + whitespace_wrap(match_literal("from:")), + whitespace_wrap(literal()), + ) + .map(|term| Query::From(term)) + } + + pub fn or<'a>() -> impl Parser<'a, Query> { + move |input| { + whitespace_wrap(match_literal_anycase("or")) + .parse(input) + .and_then(|(last_input, _)| query().parse(debug!(last_input))) + } + } + + pub fn not<'a>() -> impl Parser<'a, Query> { + move |input| { + whitespace_wrap(either( + match_literal_anycase("or"), + match_literal_anycase("!"), + )) + .parse(input) + .and_then(|(last_input, _)| query().parse(debug!(last_input))) + } + } + + pub fn and<'a>() -> impl Parser<'a, Query> { + move |input| { + whitespace_wrap(match_literal_anycase("and")) + .parse(input) + .and_then(|(last_input, _)| query().parse(debug!(last_input))) + } + } + + pub fn literal<'a>() -> impl Parser<'a, String> { + move |input| either(quoted_string(), string()).parse(input) + } + + pub fn parentheses_query<'a>() -> impl Parser<'a, Query> { + move |input| { + delimited( + whitespace_wrap(match_literal("(")), + whitespace_wrap(query()), + whitespace_wrap(match_literal(")")), + ) + .parse(input) + } + } + + pub fn query<'a>() -> impl Parser<'a, Query> { + move |input| { + let (rest, query_a): (&'a str, Query) = if let Ok(q) = parentheses_query().parse(input) + { + Ok(q) + } else if let Ok(q) = subject().parse(input) { + Ok(q) + } else if let Ok(q) = from().parse(input) { + Ok(q) + } else if let Ok(q) = not().parse(input) { + Ok(q) + } else if let Ok((rest, query_a)) = { + let result = literal().parse(input); + if result.is_ok() + && result + .as_ref() + .map(|(_, s)| s != "and" && s != "or" && s != "not") + .unwrap_or(false) + { + result.map(|(r, s)| (r, AllText(s))) + } else { + Err("") + } + } { + Ok((rest, query_a)) + } else { + Err("") + }?; + if rest.is_empty() { + return Ok((rest, query_a)); + } + + if let Ok((rest, query_b)) = and().parse(rest) { + Ok((rest, And(Box::new(query_a), Box::new(query_b)))) + } else if let Ok((rest, query_b)) = or().parse(rest) { + Ok((rest, Or(Box::new(query_a), Box::new(query_b)))) + } else if let Ok((rest, query_b)) = query().parse(rest) { + Ok((rest, Or(Box::new(query_a), Box::new(query_b)))) + } else { + Ok((rest, query_a)) + } + } + } + + #[test] + fn test_query_parsing() { + assert_eq!( + Err("subject: test and"), + query().parse_complete("subject: test and") + ); + assert_eq!( + Ok(( + "", + And( + Box::new(Subject("test".to_string())), + Box::new(AllText("i".to_string())) + ) + )), + query().parse_complete("subject: test and i") + ); + assert_eq!( + Ok(("", AllText("test".to_string()))), + query().parse_complete("test") + ); + assert_eq!( + Ok(("", Subject("test".to_string()))), + query().parse_complete("subject: test") + ); + assert_eq!( + Ok(( + "", + Or( + Box::new(Subject("wah ah ah".to_string())), + Box::new(And( + Box::new(From("Manos".to_string())), + Box::new(From("Sia".to_string())) + )) + ) + )), + query().parse_complete("subject: \"wah ah ah\" or (from: Manos and from: Sia)") + ); + assert_eq!( + Ok(( + "", + Or( + Box::new(Subject("wah".to_string())), + Box::new(And( + Box::new(From("Manos".to_string())), + Box::new(Or( + Box::new(Subject("foo".to_string())), + Box::new(Subject("bar".to_string())), + )) + )) + ) + )), + query() + .parse_complete("subject: wah or (from: Manos and (subject:foo or subject: bar))") + ); + assert_eq!( + Ok(( + "", + And( + Box::new(From("Manos".to_string())), + Box::new(And( + Box::new(Or( + Box::new(Subject("foo".to_string())), + Box::new(Subject("bar".to_string())) + )), + Box::new(Or( + Box::new(From("woo".to_string())), + Box::new(From("my".to_string())) + )) + )) + ) + )), + query().parse_complete( + "(from: Manos and (subject:foo or subject: bar) and (from:woo or from:my))" + ) + ); + } +} diff --git a/ui/src/search.rs b/ui/src/search.rs deleted file mode 100644 index abc02515..00000000 --- a/ui/src/search.rs +++ /dev/null @@ -1,90 +0,0 @@ -/* - * meli - ui crate. - * - * Copyright 2017-2018 Manos Pitsidianakis - * - * This file is part of meli. - * - * meli is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * meli is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with meli. If not, see . - */ - -use crate::state::Context; -use melib::{ - backends::FolderHash, - email::{EnvelopeHash, Flag, UnixTimestamp}, - error::Result, - thread::{SortField, SortOrder}, - StackVec, -}; - -#[derive(Debug)] -pub enum Query { - Before(UnixTimestamp), - After(UnixTimestamp), - Between(UnixTimestamp, UnixTimestamp), - On(UnixTimestamp), - /* * * * */ - From(String), - To(String), - Cc(String), - Bcc(String), - InReplyTo(String), - References(String), - AllAddresses(String), - /* * * * */ - Body(String), - Subject(String), - AllText(String), - /* * * * */ - Flag(Flag), -} - -pub fn filter( - filter_term: &str, - context: &Context, - account_idx: usize, - sort: (SortField, SortOrder), - folder_hash: FolderHash, -) -> Result> { - #[cfg(feature = "sqlite3")] - { - crate::sqlite3::search(filter_term, context, account_idx, sort, folder_hash) - } - - #[cfg(not(feature = "sqlite3"))] - { - let mut ret = StackVec::new(); - - let account = &context.accounts[account_idx]; - for env_hash in account.folders[folder_hash].as_result()?.envelopes { - let envelope = &account.collection[&env_hash]; - if envelope.subject().contains(&filter_term) { - ret.push(env_hash); - continue; - } - if envelope.field_from_to_string().contains(&filter_term) { - ret.push(env_hash); - continue; - } - let op = account.operation(env_hash); - let body = envelope.body(op)?; - let decoded = decode_rec(&body, None); - let body_text = String::from_utf8_lossy(&decoded); - if body_text.contains(&filter_term) { - ret.push(env_hash); - } - } - ret - } -}