mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 09:16:38 -04:00
Fix top-level search errorkinds
This commit is contained in:
parent
d00c54aacf
commit
b89381ac95
1 changed files with 101 additions and 73 deletions
|
@ -9,13 +9,13 @@ use crate::{
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::complete::{escaped, is_not, tag, tag_no_case},
|
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, map_parser, verify},
|
combinator::{all_consuming, map, verify},
|
||||||
error::{ErrorKind as NomErrorKind, ParseError as NomParseError},
|
error::{ErrorKind as NomErrorKind, ParseError as NomParseError},
|
||||||
multi::many0,
|
multi::many0,
|
||||||
sequence::{delimited, preceded, separated_pair},
|
sequence::{delimited, preceded, separated_pair},
|
||||||
Err::Failure,
|
Err::{Error, Failure},
|
||||||
};
|
};
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
@ -31,6 +31,9 @@ enum ErrorKind {
|
||||||
MisplacedAnd,
|
MisplacedAnd,
|
||||||
MisplacedOr,
|
MisplacedOr,
|
||||||
EmptyGroup,
|
EmptyGroup,
|
||||||
|
EmptyQuote,
|
||||||
|
UnclosedQuote,
|
||||||
|
MissingKey,
|
||||||
UnknownEscape(String),
|
UnknownEscape(String),
|
||||||
InvalidIdList,
|
InvalidIdList,
|
||||||
InvalidState,
|
InvalidState,
|
||||||
|
@ -147,8 +150,10 @@ 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) =
|
let (_, nodes) = all_consuming(group_inner)(input).map_err(|e| {
|
||||||
all_consuming(group_inner)(input).map_err(|e| { dbg!(e); AnkiError::SearchError(None) })?;
|
dbg!(e);
|
||||||
|
AnkiError::SearchError(None)
|
||||||
|
})?;
|
||||||
Ok(nodes)
|
Ok(nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,9 +223,7 @@ 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(')')), |nodes| {
|
map(delimited(char('('), group_inner, char(')')), Node::Group)(s)
|
||||||
Node::Group(nodes)
|
|
||||||
})(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Either quoted or unquoted text
|
/// Either quoted or unquoted text
|
||||||
|
@ -230,70 +233,98 @@ fn text(s: &str) -> IResult<Node> {
|
||||||
|
|
||||||
/// Quoted text, including the outer double quotes.
|
/// Quoted text, including the outer double quotes.
|
||||||
fn quoted_term(s: &str) -> IResult<Node> {
|
fn quoted_term(s: &str) -> IResult<Node> {
|
||||||
map_parser(quoted_term_str, map(search_node_for_text, Node::Search))(s)
|
let (remaining, term) = quoted_term_str(s)?;
|
||||||
|
Ok((remaining, Node::Search(search_node_for_text(term)?)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg deck:"foo bar" - quotes must come after the :
|
/// eg deck:"foo bar" - quotes must come after the :
|
||||||
fn partially_quoted_term(s: &str) -> IResult<Node> {
|
fn partially_quoted_term(s: &str) -> IResult<Node> {
|
||||||
let (remaining, (key, val)) = separated_pair(
|
let (remaining, (key, val)) = separated_pair(
|
||||||
verify(
|
escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(": \u{3000}")),
|
||||||
escaped(is_not("\"(): \u{3000}\\"), '\\', none_of(": \u{3000}")),
|
|
||||||
|s: &str| !s.is_empty(),
|
|
||||||
),
|
|
||||||
char(':'),
|
char(':'),
|
||||||
quoted_term_str,
|
quoted_term_str,
|
||||||
)(s)?;
|
)(s)?;
|
||||||
let (_, node) = search_node_for_text_with_argument(key, val)?;
|
if key.is_empty() {
|
||||||
|
Err(nom::Err::Failure(ParseError::Anki(
|
||||||
Ok((remaining, Node::Search(node)))
|
s,
|
||||||
|
ErrorKind::MissingKey,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
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> {
|
||||||
map_parser(
|
if let Ok((tail, term)) = verify::<_, _, _, ParseError, _, _>(
|
||||||
verify(
|
escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}")),
|
||||||
escaped(is_not("\"() \u{3000}\\"), '\\', none_of(" \u{3000}")),
|
|s: &str| !s.is_empty(),
|
||||||
|s: &str| !s.is_empty(),
|
|
||||||
),
|
|
||||||
alt((
|
|
||||||
map(all_consuming(tag_no_case("and")), |_| Node::And),
|
|
||||||
map(all_consuming(tag_no_case("or")), |_| Node::Or),
|
|
||||||
map(search_node_for_text, Node::Search),
|
|
||||||
)),
|
|
||||||
)(s)
|
)(s)
|
||||||
|
{
|
||||||
|
if tail.starts_with('\\') {
|
||||||
|
let escaped = (if tail.len() > 1 { &tail[0..2] } else { "" }).to_string();
|
||||||
|
Err(Failure(ParseError::Anki(
|
||||||
|
s,
|
||||||
|
ErrorKind::UnknownEscape(format!("\\{}", escaped)),
|
||||||
|
)))
|
||||||
|
} else if term.eq_ignore_ascii_case("and") {
|
||||||
|
Ok((tail, Node::And))
|
||||||
|
} else if term.eq_ignore_ascii_case("or") {
|
||||||
|
Ok((tail, Node::Or))
|
||||||
|
} else {
|
||||||
|
Ok((tail, Node::Search(search_node_for_text(term)?)))
|
||||||
|
}
|
||||||
|
} else if s.starts_with('\\') {
|
||||||
|
let escaped = (if s.len() > 1 { &s[0..2] } else { "" }).to_string();
|
||||||
|
Err(Failure(ParseError::Anki(
|
||||||
|
s,
|
||||||
|
ErrorKind::UnknownEscape(format!("\\{}", escaped)),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Err(Error(ParseError::Nom(s, NomErrorKind::Verify)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-empty string delimited by unescaped double quotes.
|
/// Non-empty string delimited by unescaped double quotes.
|
||||||
fn quoted_term_str(s: &str) -> IResult<&str> {
|
fn quoted_term_str(s: &str) -> IResult<&str> {
|
||||||
unempty(delimited(
|
let (opened, _) = char('"')(s)?;
|
||||||
char('"'),
|
if let Ok((tail, inner)) =
|
||||||
escaped(is_not(r#""\"#), '\\', anychar),
|
escaped::<_, ParseError, _, _, _, _>(is_not(r#""\"#), '\\', anychar)(opened)
|
||||||
char('"'),
|
{
|
||||||
)(s))
|
if tail.is_empty() {
|
||||||
}
|
Err(Failure(ParseError::Anki(s, ErrorKind::UnclosedQuote)))
|
||||||
|
} else if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) {
|
||||||
fn unempty<'a>(res: IResult<'a, &'a str>) -> IResult<'a, &'a str> {
|
Ok((remaining, inner))
|
||||||
if let Ok((_, parsed)) = res {
|
|
||||||
if parsed.is_empty() {
|
|
||||||
Err(nom::Err::Failure(ParseError::Anki(
|
|
||||||
"",
|
|
||||||
ErrorKind::EmptyGroup,
|
|
||||||
)))
|
|
||||||
} else {
|
} else {
|
||||||
res
|
Err(Failure(ParseError::Anki(s, ErrorKind::UnclosedQuote)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res
|
match opened.chars().next().unwrap() {
|
||||||
|
'"' => Err(Failure(ParseError::Anki(s, ErrorKind::EmptyQuote))),
|
||||||
|
// '\' followed by nothing
|
||||||
|
'\\' => Err(Failure(ParseError::Anki(s, ErrorKind::UnclosedQuote))),
|
||||||
|
// everything else is accepted by escaped
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine if text is a qualified search, and handle escaped chars.
|
/// Determine if text is a qualified search, and handle escaped chars.
|
||||||
fn search_node_for_text(s: &str) -> IResult<SearchNode> {
|
fn search_node_for_text(s: &str) -> ParseResult<SearchNode> {
|
||||||
let (tail, head) = escaped(is_not(r":\"), '\\', anychar)(s)?;
|
if let Ok((tail, head)) = escaped::<_, ParseError, _, _, _, _>(is_not(r":\"), '\\', anychar)(s)
|
||||||
if tail.is_empty() {
|
{
|
||||||
Ok(("", SearchNode::UnqualifiedText(unescape(head)?)))
|
if tail.is_empty() {
|
||||||
|
Ok(SearchNode::UnqualifiedText(unescape(head)?))
|
||||||
|
} else {
|
||||||
|
search_node_for_text_with_argument(head, &tail[1..])
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
search_node_for_text_with_argument(head, &tail[1..])
|
// escaped only fails on "\" and leading ':'
|
||||||
|
// "\" cannot be passed as an argument by a calling parser
|
||||||
|
Err(Failure(ParseError::Anki(s, ErrorKind::MissingKey)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,32 +332,29 @@ fn search_node_for_text(s: &str) -> IResult<SearchNode> {
|
||||||
fn search_node_for_text_with_argument<'a>(
|
fn search_node_for_text_with_argument<'a>(
|
||||||
key: &'a str,
|
key: &'a str,
|
||||||
val: &'a str,
|
val: &'a str,
|
||||||
) -> IResult<'a, SearchNode<'a>> {
|
) -> ParseResult<'a, SearchNode<'a>> {
|
||||||
Ok((
|
Ok(match key.to_ascii_lowercase().as_str() {
|
||||||
"",
|
"deck" => SearchNode::Deck(unescape(val)?),
|
||||||
match key.to_ascii_lowercase().as_str() {
|
"note" => SearchNode::NoteType(unescape(val)?),
|
||||||
"deck" => SearchNode::Deck(unescape(val)?),
|
"tag" => SearchNode::Tag(unescape(val)?),
|
||||||
"note" => SearchNode::NoteType(unescape(val)?),
|
"card" => parse_template(val)?,
|
||||||
"tag" => SearchNode::Tag(unescape(val)?),
|
"flag" => parse_flag(val)?,
|
||||||
"card" => parse_template(val)?,
|
"prop" => parse_prop(val)?,
|
||||||
"flag" => parse_flag(val)?,
|
"added" => parse_added(val)?,
|
||||||
"prop" => parse_prop(val)?,
|
"edited" => parse_edited(val)?,
|
||||||
"added" => parse_added(val)?,
|
"rated" => parse_rated(val)?,
|
||||||
"edited" => parse_edited(val)?,
|
"is" => parse_state(val)?,
|
||||||
"rated" => parse_rated(val)?,
|
"did" => parse_did(val)?,
|
||||||
"is" => parse_state(val)?,
|
"mid" => parse_mid(val)?,
|
||||||
"did" => parse_did(val)?,
|
"nid" => SearchNode::NoteIDs(check_id_list(val)?),
|
||||||
"mid" => parse_mid(val)?,
|
"cid" => SearchNode::CardIDs(check_id_list(val)?),
|
||||||
"nid" => SearchNode::NoteIDs(check_id_list(val)?),
|
"re" => SearchNode::Regex(unescape_quotes(val)),
|
||||||
"cid" => SearchNode::CardIDs(check_id_list(val)?),
|
"nc" => SearchNode::NoCombining(unescape(val)?),
|
||||||
"re" => SearchNode::Regex(unescape_quotes(val)),
|
"w" => SearchNode::WordBoundary(unescape(val)?),
|
||||||
"nc" => SearchNode::NoCombining(unescape(val)?),
|
"dupe" => parse_dupes(val)?,
|
||||||
"w" => SearchNode::WordBoundary(unescape(val)?),
|
// anything else is a field search
|
||||||
"dupe" => parse_dupes(val)?,
|
_ => parse_single_field(key, val)?,
|
||||||
// anything else is a field search
|
})
|
||||||
_ => parse_single_field(key, val)?,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_template(s: &str) -> ParseResult<SearchNode> {
|
fn parse_template(s: &str) -> ParseResult<SearchNode> {
|
||||||
|
|
Loading…
Reference in a new issue