mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 00:36:38 -04:00
Add FailKinds for unopened/unclosed groups
This commit is contained in:
parent
0b73110f82
commit
8f754e1525
3 changed files with 100 additions and 77 deletions
|
@ -2,18 +2,25 @@
|
|||
|
||||
search-invalid = Invalid search - please check for typing mistakes.
|
||||
# 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
|
||||
double quotes: "and".
|
||||
# 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
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
search-missing-key = A colon `:` must be preceded by a key.
|
||||
search-unknown-escape = The escape sequence `` is unknown.
|
||||
|
|
|
@ -120,38 +120,37 @@ impl AnkiError {
|
|||
DBErrorKind::Locked => "Anki already open, or media currently syncing.".into(),
|
||||
_ => format!("{:?}", self),
|
||||
},
|
||||
AnkiError::SearchError(kind) => { match kind {
|
||||
ParseErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd),
|
||||
ParseErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr),
|
||||
ParseErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup),
|
||||
ParseErrorKind::EmptyQuote => i18n.tr(TR::SearchEmptyQuote),
|
||||
ParseErrorKind::UnclosedQuote => i18n.tr(TR::SearchUnclosedQuote),
|
||||
ParseErrorKind::MissingKey => i18n.tr(TR::SearchMissingKey),
|
||||
ParseErrorKind::UnknownEscape(_seq) => i18n.tr(TR::SearchUnknownEscape),
|
||||
ParseErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList),
|
||||
ParseErrorKind::InvalidState => i18n.tr(TR::SearchInvalidState),
|
||||
ParseErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
|
||||
ParseErrorKind::InvalidAdded => i18n.tr(TR::SearchInvalidAdded),
|
||||
ParseErrorKind::InvalidEdited => i18n.tr(TR::SearchInvalidEdited),
|
||||
ParseErrorKind::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays),
|
||||
ParseErrorKind::InvalidRatedEase => i18n.tr(TR::SearchInvalidRatedEase),
|
||||
ParseErrorKind::InvalidDupeMid => i18n.tr(TR::SearchInvalidDupeMid),
|
||||
ParseErrorKind::InvalidDupeText => i18n.tr(TR::SearchInvalidDupeText),
|
||||
ParseErrorKind::InvalidPropProperty => i18n.tr(TR::SearchInvalidPropProperty),
|
||||
ParseErrorKind::InvalidPropOperator => i18n.tr(TR::SearchInvalidPropOperator),
|
||||
ParseErrorKind::InvalidPropFloat => i18n.tr(TR::SearchInvalidPropFloat),
|
||||
ParseErrorKind::InvalidPropInteger => i18n.tr(TR::SearchInvalidPropInteger),
|
||||
ParseErrorKind::InvalidPropUnsigned => i18n.tr(TR::SearchInvalidPropUnsigned),
|
||||
ParseErrorKind::InvalidDid => i18n.tr(TR::SearchInvalidDid),
|
||||
ParseErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid),
|
||||
ParseErrorKind::Regex(text) => text.into(),
|
||||
ParseErrorKind::Other(opt) => if let Some(info) = opt {
|
||||
info.into()
|
||||
} else {
|
||||
i18n.tr(TR::SearchInvalid)
|
||||
}
|
||||
}.into()
|
||||
},
|
||||
AnkiError::SearchError(kind) => match kind {
|
||||
ParseErrorKind::MisplacedAnd => i18n.tr(TR::SearchMisplacedAnd),
|
||||
ParseErrorKind::MisplacedOr => i18n.tr(TR::SearchMisplacedOr),
|
||||
ParseErrorKind::EmptyGroup => i18n.tr(TR::SearchEmptyGroup),
|
||||
ParseErrorKind::UnopenedGroup => i18n.tr(TR::SearchUnopenedGroup),
|
||||
ParseErrorKind::UnclosedGroup => i18n.tr(TR::SearchUnclosedGroup),
|
||||
ParseErrorKind::EmptyQuote => i18n.tr(TR::SearchEmptyQuote),
|
||||
ParseErrorKind::UnclosedQuote => i18n.tr(TR::SearchUnclosedQuote),
|
||||
ParseErrorKind::MissingKey => i18n.tr(TR::SearchMissingKey),
|
||||
ParseErrorKind::UnknownEscape(_seq) => i18n.tr(TR::SearchUnknownEscape),
|
||||
ParseErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList),
|
||||
ParseErrorKind::InvalidState => i18n.tr(TR::SearchInvalidState),
|
||||
ParseErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
|
||||
ParseErrorKind::InvalidAdded => i18n.tr(TR::SearchInvalidAdded),
|
||||
ParseErrorKind::InvalidEdited => i18n.tr(TR::SearchInvalidEdited),
|
||||
ParseErrorKind::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays),
|
||||
ParseErrorKind::InvalidRatedEase => i18n.tr(TR::SearchInvalidRatedEase),
|
||||
ParseErrorKind::InvalidDupeMid => i18n.tr(TR::SearchInvalidDupeMid),
|
||||
ParseErrorKind::InvalidDupeText => i18n.tr(TR::SearchInvalidDupeText),
|
||||
ParseErrorKind::InvalidPropProperty => i18n.tr(TR::SearchInvalidPropProperty),
|
||||
ParseErrorKind::InvalidPropOperator => i18n.tr(TR::SearchInvalidPropOperator),
|
||||
ParseErrorKind::InvalidPropFloat => i18n.tr(TR::SearchInvalidPropFloat),
|
||||
ParseErrorKind::InvalidPropInteger => i18n.tr(TR::SearchInvalidPropInteger),
|
||||
ParseErrorKind::InvalidPropUnsigned => i18n.tr(TR::SearchInvalidPropUnsigned),
|
||||
ParseErrorKind::InvalidDid => i18n.tr(TR::SearchInvalidDid),
|
||||
ParseErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid),
|
||||
ParseErrorKind::Regex(text) => text.into(),
|
||||
ParseErrorKind::Other(Some(info)) => info.into(),
|
||||
ParseErrorKind::Other(None) => i18n.tr(TR::SearchInvalid),
|
||||
}
|
||||
.into(),
|
||||
_ => format!("{:?}", self),
|
||||
}
|
||||
}
|
||||
|
@ -365,6 +364,8 @@ pub enum ParseErrorKind {
|
|||
MisplacedAnd,
|
||||
MisplacedOr,
|
||||
EmptyGroup,
|
||||
UnopenedGroup,
|
||||
UnclosedGroup,
|
||||
EmptyQuote,
|
||||
UnclosedQuote,
|
||||
MissingKey,
|
||||
|
|
|
@ -11,10 +11,10 @@ use nom::{
|
|||
branch::alt,
|
||||
bytes::complete::{escaped, is_not, tag},
|
||||
character::complete::{anychar, char, none_of, one_of},
|
||||
combinator::{all_consuming, map},
|
||||
combinator::{map, verify},
|
||||
error::ErrorKind as NomErrorKind,
|
||||
multi::many0,
|
||||
sequence::{delimited, preceded, separated_pair},
|
||||
sequence::{preceded, separated_pair},
|
||||
};
|
||||
use regex::{Captures, Regex};
|
||||
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))
|
||||
}
|
||||
|
||||
fn parse_error(input: &str) -> nom::Err<ParseError<'_>> {
|
||||
nom::Err::Error(ParseError::Anki(input, FailKind::Other(None)))
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Node<'a> {
|
||||
And,
|
||||
|
@ -110,8 +114,11 @@ pub(super) fn parse(input: &str) -> Result<Vec<Node>> {
|
|||
return Ok(vec![Node::Search(SearchNode::WholeCollection)]);
|
||||
}
|
||||
|
||||
let (_, nodes) = all_consuming(group_inner)(input)?;
|
||||
Ok(nodes)
|
||||
match group_inner(input) {
|
||||
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'
|
||||
|
@ -143,24 +150,21 @@ fn group_inner(input: &str) -> IResult<Vec<Node>> {
|
|||
}
|
||||
Err(e) => match e {
|
||||
nom::Err::Error(_) => break,
|
||||
// fixme: add context to failure
|
||||
_ => return Err(e),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if nodes.is_empty() {
|
||||
Err(parse_failure(input, FailKind::EmptyGroup))
|
||||
} else if nodes.last().unwrap() == &Node::And {
|
||||
Err(parse_failure(input, FailKind::MisplacedAnd))
|
||||
} 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))
|
||||
if let Some(last) = nodes.last() {
|
||||
match last {
|
||||
Node::And => return Err(parse_failure(input, FailKind::MisplacedAnd)),
|
||||
Node::Or => return Err(parse_failure(input, FailKind::MisplacedOr)),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let (remaining, _) = whitespace0(remaining)?;
|
||||
|
||||
Ok((remaining, nodes))
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
|
@ -201,16 +215,18 @@ fn partially_quoted_term(s: &str) -> IResult<Node> {
|
|||
char(':'),
|
||||
quoted_term_str,
|
||||
)(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 )
|
||||
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)) => {
|
||||
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") {
|
||||
Ok((tail, Node::And))
|
||||
} else if term.eq_ignore_ascii_case("or") {
|
||||
|
@ -218,17 +234,20 @@ fn unquoted_term(s: &str) -> IResult<Node> {
|
|||
} else {
|
||||
Ok((tail, Node::Search(search_node_for_text(term)?)))
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(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()) {
|
||||
Err(nom::Err::Error(ParseError::Nom(s, NomErrorKind::IsNot)))
|
||||
Err(parse_error(s))
|
||||
} else {
|
||||
// input ends in an odd number of backslashes
|
||||
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.
|
||||
/// Expect well-formed input: unempty and no trailing \.
|
||||
fn search_node_for_text(s: &str) -> ParseResult<SearchNode> {
|
||||
if s.is_empty() {
|
||||
return Err(parse_failure(s, FailKind::Other(Some("Unexpected search error.".to_string()))));
|
||||
}
|
||||
if let Ok((tail, head)) = escaped::<_, ParseError, _, _, _, _>(is_not(r":\"), '\\', anychar)(s)
|
||||
{
|
||||
if tail.is_empty() {
|
||||
Ok(SearchNode::UnqualifiedText(unescape(head)?))
|
||||
} else {
|
||||
search_node_for_text_with_argument(head, &tail[1..])
|
||||
}
|
||||
// leading : is only possible error for well-formed input
|
||||
let (tail, head) = verify(escaped(is_not(r":\"), '\\', anychar), |t: &str| {
|
||||
!t.is_empty()
|
||||
})(s)
|
||||
.map_err(|_: nom::Err<ParseError>| parse_failure(s, FailKind::MissingKey))?;
|
||||
if tail.is_empty() {
|
||||
Ok(SearchNode::UnqualifiedText(unescape(head)?))
|
||||
} else {
|
||||
// trailing \ should not be passed, so error must be leading ':'
|
||||
Err(parse_failure(s, FailKind::MissingKey))
|
||||
search_node_for_text_with_argument(head, &tail[1..])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,13 +334,13 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode> {
|
|||
Ok(SearchNode::Flag(flag))
|
||||
}
|
||||
} else {
|
||||
Err(parse_failure(s, FailKind::InvalidEdited))
|
||||
Err(parse_failure(s, FailKind::InvalidFlag))
|
||||
}
|
||||
}
|
||||
|
||||
/// eg prop:ivl>3, prop:ease!=2.5
|
||||
fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> {
|
||||
let (tail, prop) = alt::<&str, &str, ParseError, _>((
|
||||
let (tail, prop) = alt::<_, _, ParseError, _>((
|
||||
tag("ivl"),
|
||||
tag("due"),
|
||||
tag("reps"),
|
||||
|
@ -335,7 +350,7 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> {
|
|||
))(s)
|
||||
.map_err(|_| parse_failure(s, FailKind::InvalidPropProperty))?;
|
||||
|
||||
let (num, operator) = alt::<&str, &str, ParseError, _>((
|
||||
let (num, operator) = alt::<_, _, ParseError, _>((
|
||||
tag("<="),
|
||||
tag(">="),
|
||||
tag("!="),
|
||||
|
|
Loading…
Reference in a new issue