Fix top-level search errorkinds

This commit is contained in:
RumovZ 2021-01-12 17:32:26 +01:00
parent d00c54aacf
commit b89381ac95

View file

@ -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,81 +233,107 @@ 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() { if tail.is_empty() {
Ok(("", SearchNode::UnqualifiedText(unescape(head)?))) Ok(SearchNode::UnqualifiedText(unescape(head)?))
} else { } else {
search_node_for_text_with_argument(head, &tail[1..]) search_node_for_text_with_argument(head, &tail[1..])
} }
} else {
// escaped only fails on "\" and leading ':'
// "\" cannot be passed as an argument by a calling parser
Err(Failure(ParseError::Anki(s, ErrorKind::MissingKey)))
}
} }
/// Convert a colon-separated key/val pair into the relevant search type. /// Convert a colon-separated key/val pair into the relevant search type.
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() {
"",
match key.to_ascii_lowercase().as_str() {
"deck" => SearchNode::Deck(unescape(val)?), "deck" => SearchNode::Deck(unescape(val)?),
"note" => SearchNode::NoteType(unescape(val)?), "note" => SearchNode::NoteType(unescape(val)?),
"tag" => SearchNode::Tag(unescape(val)?), "tag" => SearchNode::Tag(unescape(val)?),
@ -325,8 +354,7 @@ fn search_node_for_text_with_argument<'a>(
"dupe" => parse_dupes(val)?, "dupe" => parse_dupes(val)?,
// anything else is a field search // anything else is a field search
_ => parse_single_field(key, val)?, _ => parse_single_field(key, val)?,
}, })
))
} }
fn parse_template(s: &str) -> ParseResult<SearchNode> { fn parse_template(s: &str) -> ParseResult<SearchNode> {