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.
|
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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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("!="),
|
||||||
|
|
Loading…
Reference in a new issue