Add FailKinds for unopened/unclosed groups

This commit is contained in:
RumovZ 2021-01-14 19:09:31 +01:00
parent 0b73110f82
commit 8f754e1525
3 changed files with 100 additions and 77 deletions

View file

@ -2,18 +2,25 @@
search-invalid = Invalid search - please check for typing mistakes. search-invalid = Invalid search - please check for typing mistakes.
# The literal string `AND` is part of the search syntax. # The literal string `AND` is part of the search syntax.
search-misplaced-and = An `AND` was found but it is not connecting two search-misplaced-and =
An `AND` was found but it is not connecting two
search terms. If you want to search for the word itself, wrap it in search terms. If you want to search for the word itself, wrap it in
double quotes: "and". double quotes: "and".
# The literal string `OR` is part of the search syntax. # The literal string `OR` is part of the search syntax.
search-misplaced-or = An `OR` was found but it is not connecting two search-misplaced-or =
An `OR` was found but it is not connecting two
search terms. If you want to search for the word itself, wrap it in search terms. If you want to search for the word itself, wrap it in
double quotes: "or". double quotes: "or".
search-empty-group = A group was found but there was nothing between the search-empty-group =
A group was found but there was nothing between the
parentheses to search for. parentheses to search for.
search-empty-quote = A quote was found but there was nothing between the search-unopened-group = search-unopened-group
search-unclosed-group = search-unclosed-group
search-empty-quote =
A quote was found but there was nothing between the
double quotes to search for. double quotes to search for.
search-unclosed-quote = An opening double quote `"` was found but there search-unclosed-quote =
An opening double quote `"` was found but there
is no second one to close it. is no second one to close it.
search-missing-key = A colon `:` must be preceded by a key. search-missing-key = A colon `:` must be preceded by a key.
search-unknown-escape = The escape sequence `` is unknown. search-unknown-escape = The escape sequence `` is unknown.

View file

@ -120,38 +120,37 @@ impl AnkiError {
DBErrorKind::Locked => "Anki already open, or media currently syncing.".into(), DBErrorKind::Locked => "Anki already open, or media currently syncing.".into(),
_ => format!("{:?}", self), _ => format!("{:?}", self),
}, },
AnkiError::SearchError(kind) => { match kind { AnkiError::SearchError(kind) => match kind {
ParseErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd), ParseErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd),
ParseErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr), ParseErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr),
ParseErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup), ParseErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup),
ParseErrorKind::EmptyQuote => i18n.tr(TR::SearchEmptyQuote), ParseErrorKind::UnopenedGroup => i18n.tr(TR::SearchUnopenedGroup),
ParseErrorKind::UnclosedQuote => i18n.tr(TR::SearchUnclosedQuote), ParseErrorKind::UnclosedGroup => i18n.tr(TR::SearchUnclosedGroup),
ParseErrorKind::MissingKey => i18n.tr(TR::SearchMissingKey), ParseErrorKind::EmptyQuote => i18n.tr(TR::SearchEmptyQuote),
ParseErrorKind::UnknownEscape(_seq) => i18n.tr(TR::SearchUnknownEscape), ParseErrorKind::UnclosedQuote => i18n.tr(TR::SearchUnclosedQuote),
ParseErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList), ParseErrorKind::MissingKey => i18n.tr(TR::SearchMissingKey),
ParseErrorKind::InvalidState => i18n.tr(TR::SearchInvalidState), ParseErrorKind::UnknownEscape(_seq) => i18n.tr(TR::SearchUnknownEscape),
ParseErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag), ParseErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList),
ParseErrorKind::InvalidAdded => i18n.tr(TR::SearchInvalidAdded), ParseErrorKind::InvalidState => i18n.tr(TR::SearchInvalidState),
ParseErrorKind::InvalidEdited => i18n.tr(TR::SearchInvalidEdited), ParseErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
ParseErrorKind::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays), ParseErrorKind::InvalidAdded => i18n.tr(TR::SearchInvalidAdded),
ParseErrorKind::InvalidRatedEase => i18n.tr(TR::SearchInvalidRatedEase), ParseErrorKind::InvalidEdited => i18n.tr(TR::SearchInvalidEdited),
ParseErrorKind::InvalidDupeMid => i18n.tr(TR::SearchInvalidDupeMid), ParseErrorKind::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays),
ParseErrorKind::InvalidDupeText => i18n.tr(TR::SearchInvalidDupeText), ParseErrorKind::InvalidRatedEase => i18n.tr(TR::SearchInvalidRatedEase),
ParseErrorKind::InvalidPropProperty => i18n.tr(TR::SearchInvalidPropProperty), ParseErrorKind::InvalidDupeMid => i18n.tr(TR::SearchInvalidDupeMid),
ParseErrorKind::InvalidPropOperator => i18n.tr(TR::SearchInvalidPropOperator), ParseErrorKind::InvalidDupeText => i18n.tr(TR::SearchInvalidDupeText),
ParseErrorKind::InvalidPropFloat => i18n.tr(TR::SearchInvalidPropFloat), ParseErrorKind::InvalidPropProperty => i18n.tr(TR::SearchInvalidPropProperty),
ParseErrorKind::InvalidPropInteger => i18n.tr(TR::SearchInvalidPropInteger), ParseErrorKind::InvalidPropOperator => i18n.tr(TR::SearchInvalidPropOperator),
ParseErrorKind::InvalidPropUnsigned => i18n.tr(TR::SearchInvalidPropUnsigned), ParseErrorKind::InvalidPropFloat => i18n.tr(TR::SearchInvalidPropFloat),
ParseErrorKind::InvalidDid => i18n.tr(TR::SearchInvalidDid), ParseErrorKind::InvalidPropInteger => i18n.tr(TR::SearchInvalidPropInteger),
ParseErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid), ParseErrorKind::InvalidPropUnsigned => i18n.tr(TR::SearchInvalidPropUnsigned),
ParseErrorKind::Regex(text) => text.into(), ParseErrorKind::InvalidDid => i18n.tr(TR::SearchInvalidDid),
ParseErrorKind::Other(opt) => if let Some(info) = opt { ParseErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid),
info.into() ParseErrorKind::Regex(text) => text.into(),
} else { ParseErrorKind::Other(Some(info)) => info.into(),
i18n.tr(TR::SearchInvalid) ParseErrorKind::Other(None) => i18n.tr(TR::SearchInvalid),
} }
}.into() .into(),
},
_ => format!("{:?}", self), _ => format!("{:?}", self),
} }
} }
@ -365,6 +364,8 @@ pub enum ParseErrorKind {
MisplacedAnd, MisplacedAnd,
MisplacedOr, MisplacedOr,
EmptyGroup, EmptyGroup,
UnopenedGroup,
UnclosedGroup,
EmptyQuote, EmptyQuote,
UnclosedQuote, UnclosedQuote,
MissingKey, MissingKey,

View file

@ -11,10 +11,10 @@ use nom::{
branch::alt, branch::alt,
bytes::complete::{escaped, is_not, tag}, bytes::complete::{escaped, is_not, tag},
character::complete::{anychar, char, none_of, one_of}, character::complete::{anychar, char, none_of, one_of},
combinator::{all_consuming, map}, combinator::{map, verify},
error::ErrorKind as NomErrorKind, error::ErrorKind as NomErrorKind,
multi::many0, multi::many0,
sequence::{delimited, preceded, separated_pair}, sequence::{preceded, separated_pair},
}; };
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use std::borrow::Cow; use std::borrow::Cow;
@ -26,6 +26,10 @@ fn parse_failure(input: &str, kind: FailKind) -> nom::Err<ParseError<'_>> {
nom::Err::Failure(ParseError::Anki(input, kind)) nom::Err::Failure(ParseError::Anki(input, kind))
} }
fn parse_error(input: &str) -> nom::Err<ParseError<'_>> {
nom::Err::Error(ParseError::Anki(input, FailKind::Other(None)))
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Node<'a> { pub enum Node<'a> {
And, And,
@ -110,8 +114,11 @@ pub(super) fn parse(input: &str) -> Result<Vec<Node>> {
return Ok(vec![Node::Search(SearchNode::WholeCollection)]); return Ok(vec![Node::Search(SearchNode::WholeCollection)]);
} }
let (_, nodes) = all_consuming(group_inner)(input)?; match group_inner(input) {
Ok(nodes) Ok(("", nodes)) => Ok(nodes),
Ok((remaining, _)) => Err(parse_failure(remaining, FailKind::UnopenedGroup).into()),
Err(err) => Err(err.into()),
}
} }
/// One or more nodes inside brackets, er 'one OR two -three' /// One or more nodes inside brackets, er 'one OR two -three'
@ -143,24 +150,21 @@ fn group_inner(input: &str) -> IResult<Vec<Node>> {
} }
Err(e) => match e { Err(e) => match e {
nom::Err::Error(_) => break, nom::Err::Error(_) => break,
// fixme: add context to failure
_ => return Err(e), _ => return Err(e),
}, },
}; };
} }
if nodes.is_empty() { if let Some(last) = nodes.last() {
Err(parse_failure(input, FailKind::EmptyGroup)) match last {
} else if nodes.last().unwrap() == &Node::And { Node::And => return Err(parse_failure(input, FailKind::MisplacedAnd)),
Err(parse_failure(input, FailKind::MisplacedAnd)) Node::Or => return Err(parse_failure(input, FailKind::MisplacedOr)),
} else if nodes.last().unwrap() == &Node::Or { _ => (),
Err(parse_failure(input, FailKind::MisplacedOr)) }
} else {
// chomp any trailing whitespace
let (remaining, _) = whitespace0(remaining)?;
Ok((remaining, nodes))
} }
let (remaining, _) = whitespace0(remaining)?;
Ok((remaining, nodes))
} }
fn whitespace0(s: &str) -> IResult<Vec<char>> { fn whitespace0(s: &str) -> IResult<Vec<char>> {
@ -180,7 +184,17 @@ fn negated_node(s: &str) -> IResult<Node> {
/// One or more nodes surrounded by brackets, eg (one OR two) /// One or more nodes surrounded by brackets, eg (one OR two)
fn group(s: &str) -> IResult<Node> { fn group(s: &str) -> IResult<Node> {
map(delimited(char('('), group_inner, char(')')), Node::Group)(s) let (opened, _) = char('(')(s)?;
let (tail, inner) = group_inner(opened)?;
if let Some(remaining) = tail.strip_prefix(')') {
if inner.is_empty() {
Err(parse_failure(s, FailKind::EmptyGroup))
} else {
Ok((remaining, Node::Group(inner)))
}
} else {
Err(parse_failure(s, FailKind::UnclosedGroup))
}
} }
/// Either quoted or unquoted text /// Either quoted or unquoted text
@ -201,16 +215,18 @@ fn partially_quoted_term(s: &str) -> IResult<Node> {
char(':'), char(':'),
quoted_term_str, quoted_term_str,
)(s)?; )(s)?;
Ok((remaining, Node::Search(search_node_for_text_with_argument(key, val)?))) Ok((
remaining,
Node::Search(search_node_for_text_with_argument(key, val)?),
))
} }
/// Unquoted text, terminated by whitespace or unescaped ", ( or ) /// Unquoted text, terminated by whitespace or unescaped ", ( or )
fn unquoted_term(s: &str) -> IResult<Node> { fn unquoted_term(s: &str) -> IResult<Node> {
match escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}"))(s) match escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}"))(s) {
{
Ok((tail, term)) => { Ok((tail, term)) => {
if term.is_empty() { if term.is_empty() {
Err(nom::Err::Error(ParseError::Nom(s, NomErrorKind::Verify))) Err(parse_error(s))
} else if term.eq_ignore_ascii_case("and") { } else if term.eq_ignore_ascii_case("and") {
Ok((tail, Node::And)) Ok((tail, Node::And))
} else if term.eq_ignore_ascii_case("or") { } else if term.eq_ignore_ascii_case("or") {
@ -218,17 +234,20 @@ fn unquoted_term(s: &str) -> IResult<Node> {
} else { } else {
Ok((tail, Node::Search(search_node_for_text(term)?))) Ok((tail, Node::Search(search_node_for_text(term)?)))
} }
}, }
Err(err) => { Err(err) => {
if let nom::Err::Error((c, NomErrorKind::NoneOf)) = err { if let nom::Err::Error((c, NomErrorKind::NoneOf)) = err {
Err(parse_failure(s, FailKind::UnknownEscape(format!("\\{}", c)))) Err(parse_failure(
s,
FailKind::UnknownEscape(format!("\\{}", c)),
))
} else if "\"() \u{3000}".contains(s.chars().next().unwrap()) { } else if "\"() \u{3000}".contains(s.chars().next().unwrap()) {
Err(nom::Err::Error(ParseError::Nom(s, NomErrorKind::IsNot))) Err(parse_error(s))
} else { } else {
// input ends in an odd number of backslashes // input ends in an odd number of backslashes
Err(parse_failure(s, FailKind::UnknownEscape('\\'.to_string()))) Err(parse_failure(s, FailKind::UnknownEscape('\\'.to_string())))
} }
}, }
} }
} }
@ -258,19 +277,15 @@ fn quoted_term_str(s: &str) -> IResult<&str> {
/// Determine if text is a qualified search, and handle escaped chars. /// Determine if text is a qualified search, and handle escaped chars.
/// Expect well-formed input: unempty and no trailing \. /// Expect well-formed input: unempty and no trailing \.
fn search_node_for_text(s: &str) -> ParseResult<SearchNode> { fn search_node_for_text(s: &str) -> ParseResult<SearchNode> {
if s.is_empty() { // leading : is only possible error for well-formed input
return Err(parse_failure(s, FailKind::Other(Some("Unexpected search error.".to_string())))); let (tail, head) = verify(escaped(is_not(r":\"), '\\', anychar), |t: &str| {
} !t.is_empty()
if let Ok((tail, head)) = escaped::<_, ParseError, _, _, _, _>(is_not(r":\"), '\\', anychar)(s) })(s)
{ .map_err(|_: nom::Err<ParseError>| parse_failure(s, FailKind::MissingKey))?;
if tail.is_empty() { if tail.is_empty() {
Ok(SearchNode::UnqualifiedText(unescape(head)?)) Ok(SearchNode::UnqualifiedText(unescape(head)?))
} else {
search_node_for_text_with_argument(head, &tail[1..])
}
} else { } else {
// trailing \ should not be passed, so error must be leading ':' search_node_for_text_with_argument(head, &tail[1..])
Err(parse_failure(s, FailKind::MissingKey))
} }
} }
@ -319,13 +334,13 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode> {
Ok(SearchNode::Flag(flag)) Ok(SearchNode::Flag(flag))
} }
} else { } else {
Err(parse_failure(s, FailKind::InvalidEdited)) Err(parse_failure(s, FailKind::InvalidFlag))
} }
} }
/// eg prop:ivl>3, prop:ease!=2.5 /// eg prop:ivl>3, prop:ease!=2.5
fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> { fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> {
let (tail, prop) = alt::<&str, &str, ParseError, _>(( let (tail, prop) = alt::<_, _, ParseError, _>((
tag("ivl"), tag("ivl"),
tag("due"), tag("due"),
tag("reps"), tag("reps"),
@ -335,7 +350,7 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> {
))(s) ))(s)
.map_err(|_| parse_failure(s, FailKind::InvalidPropProperty))?; .map_err(|_| parse_failure(s, FailKind::InvalidPropProperty))?;
let (num, operator) = alt::<&str, &str, ParseError, _>(( let (num, operator) = alt::<_, _, ParseError, _>((
tag("<="), tag("<="),
tag(">="), tag(">="),
tag("!="), tag("!="),