more parsing error tweaks
The majority of the error checking can now be delegated to routines like parse_f32(), parse_negative_i32() and so on, instead of creating specific error messages for each type.
This commit is contained in:
parent
0b83efb63e
commit
d273ebb756
@ -1,5 +1,7 @@
|
|||||||
## Errors shown when invalid search input is encountered.
|
## Errors shown when invalid search input is encountered.
|
||||||
## Text wrapped in `backticks` is literal search input, and should generally not to be altered.
|
## Text wrapped in `backticks` is literal search input, and should generally not to be altered.
|
||||||
|
## It's ok to change quotes however, eg:
|
||||||
|
## "`{ $context }`" => 「`{ $context }`」
|
||||||
|
|
||||||
search-invalid-search = Invalid search: { $reason }
|
search-invalid-search = Invalid search: { $reason }
|
||||||
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"`.
|
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"`.
|
||||||
@ -12,20 +14,15 @@ search-empty-quote = a pair of double quotes `""` was found but there was nothin
|
|||||||
search-unclosed-quote = an opening double quote `"` was found but there was no second one to close it. If you want to search for the literal `"`, prepend a backslash: `\"`.
|
search-unclosed-quote = an opening double quote `"` was found but there was no second one to close it. If you want to search for the literal `"`, prepend a backslash: `\"`.
|
||||||
search-missing-key = a colon `:` was found but there was no keyword preceding it. If you want to search for the literal `:`, prepend a backslash: `\:`.
|
search-missing-key = a colon `:` was found but there was no keyword preceding it. If you want to search for the literal `:`, prepend a backslash: `\:`.
|
||||||
search-unknown-escape = the escape sequence `{ $val }` is not defined. If you want to search for the literal backslash `\`, prepend another one: `\\`.
|
search-unknown-escape = the escape sequence `{ $val }` is not defined. If you want to search for the literal backslash `\`, prepend another one: `\\`.
|
||||||
search-invalid-id-list = note or card id lists must be comma-separated numbers.
|
|
||||||
search-invalid-argument = `{ $term }` was given an invalid argument '`{ $argument }`'.
|
search-invalid-argument = `{ $term }` was given an invalid argument '`{ $argument }`'.
|
||||||
search-invalid-flag = `flag:` must be followed by a valid flag number: `1` (red), `2` (orange), `3` (green), `4` (blue) or `0` (no flag).
|
search-invalid-flag = `flag:` must be followed by a valid flag number: `1` (red), `2` (orange), `3` (green), `4` (blue) or `0` (no flag).
|
||||||
search-invalid-followed-by-positive-days = `{ $term }` must be followed by a positive number of days.
|
search-invalid-prop-operator = `prop:{ $val }` must be followed by one of the following comparison operators: `=`, `!=`, `<`, `>`, `<=` or `>=`.
|
||||||
search-invalid-rated-days = `rated:` must be followed by a positive number of days.
|
|
||||||
search-invalid-rated-ease = `{ $val }:` must be followed by `1` (again), `2` (hard), `3` (good) or `4` (easy).
|
|
||||||
search-invalid-prop-operator = `prop:{ $val }` must be followed by one of the comparison operators: `=`, `!=`, `<`, `>`, `<=` or `>=`.
|
|
||||||
search-invalid-prop-float = `prop:{ $val }` must be followed by a decimal number.
|
|
||||||
search-invalid-prop-integer = `prop:{ $val }` must be followed by a whole number.
|
|
||||||
search-invalid-prop-unsigned = `prop:{ $val }` must be followed by a non-negative whole number.
|
|
||||||
search-invalid-number = `{ $val }`: invalid number provided
|
|
||||||
search-invalid-did = `did:` must be followed by a valid deck id.
|
|
||||||
search-invalid-mid = `mid:` must be followed by a note type id.
|
|
||||||
search-invalid-other = please check for typing mistakes.
|
search-invalid-other = please check for typing mistakes.
|
||||||
|
search-invalid-number = expected a number in "`{ $context }`", but found "`{ $provided }`".
|
||||||
|
search-invalid-whole-number = expected a whole number in "`{ $context }`", but found "`{ $provided }`".
|
||||||
|
search-invalid-positive-whole-number = expected a positive whole number in "`{ $context }`", but found "`{ $provided }`".
|
||||||
|
search-invalid-negative-whole-number = expected a whole number less than or equal to 0 in "`{ $context }`", but found "`{ $provided }`".
|
||||||
|
search-invalid-answer-button = expected an answer button between 1-4 in "`{ $context }`", but found "`{ $provided }`"
|
||||||
|
|
||||||
## Column labels in browse screen
|
## Column labels in browse screen
|
||||||
|
|
||||||
|
@ -139,7 +139,6 @@ impl AnkiError {
|
|||||||
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
|
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
SearchErrorKind::InvalidIdList => i18n.tr(TR::SearchInvalidIdList),
|
|
||||||
SearchErrorKind::InvalidState(state) => i18n
|
SearchErrorKind::InvalidState(state) => i18n
|
||||||
.trn(
|
.trn(
|
||||||
TR::SearchInvalidArgument,
|
TR::SearchInvalidArgument,
|
||||||
@ -147,35 +146,6 @@ impl AnkiError {
|
|||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
SearchErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
|
SearchErrorKind::InvalidFlag => i18n.tr(TR::SearchInvalidFlag),
|
||||||
SearchErrorKind::InvalidAdded => i18n
|
|
||||||
.trn(
|
|
||||||
TR::SearchInvalidFollowedByPositiveDays,
|
|
||||||
tr_strs!("term" => "added:"),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidEdited => i18n
|
|
||||||
.trn(
|
|
||||||
TR::SearchInvalidFollowedByPositiveDays,
|
|
||||||
tr_strs!("term" => "edited:"),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidRatedDays => i18n.tr(TR::SearchInvalidRatedDays),
|
|
||||||
SearchErrorKind::InvalidRatedEase(ctx) => i18n
|
|
||||||
.trn(TR::SearchInvalidRatedEase, tr_strs!["val"=>(ctx)])
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidNumber(ctx) => i18n
|
|
||||||
.trn(TR::SearchInvalidNumber, tr_strs!["val"=>(ctx)])
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidResched => i18n
|
|
||||||
.trn(
|
|
||||||
TR::SearchInvalidFollowedByPositiveDays,
|
|
||||||
tr_strs!("term" => "resched:"),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidDupeMid | SearchErrorKind::InvalidDupeText => {
|
|
||||||
// this is an undocumented search keyword, so no translation
|
|
||||||
"`dupe:` arguments were invalid".into()
|
|
||||||
}
|
|
||||||
SearchErrorKind::InvalidPropProperty(prop) => i18n
|
SearchErrorKind::InvalidPropProperty(prop) => i18n
|
||||||
.trn(
|
.trn(
|
||||||
TR::SearchInvalidArgument,
|
TR::SearchInvalidArgument,
|
||||||
@ -185,29 +155,39 @@ impl AnkiError {
|
|||||||
SearchErrorKind::InvalidPropOperator(ctx) => i18n
|
SearchErrorKind::InvalidPropOperator(ctx) => i18n
|
||||||
.trn(TR::SearchInvalidPropOperator, tr_strs!["val"=>(ctx)])
|
.trn(TR::SearchInvalidPropOperator, tr_strs!["val"=>(ctx)])
|
||||||
.into(),
|
.into(),
|
||||||
SearchErrorKind::InvalidPropFloat(ctx) => i18n
|
|
||||||
.trn(
|
|
||||||
TR::SearchInvalidPropFloat,
|
|
||||||
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidPropInteger(ctx) => i18n
|
|
||||||
.trn(
|
|
||||||
TR::SearchInvalidPropInteger,
|
|
||||||
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidPropUnsigned(ctx) => i18n
|
|
||||||
.trn(
|
|
||||||
TR::SearchInvalidPropUnsigned,
|
|
||||||
tr_strs!["val"=>(htmlescape::encode_minimal(ctx))],
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
SearchErrorKind::InvalidDid => i18n.tr(TR::SearchInvalidDid),
|
|
||||||
SearchErrorKind::InvalidMid => i18n.tr(TR::SearchInvalidMid),
|
|
||||||
SearchErrorKind::Regex(text) => text.into(),
|
SearchErrorKind::Regex(text) => text.into(),
|
||||||
SearchErrorKind::Other(Some(info)) => info.into(),
|
SearchErrorKind::Other(Some(info)) => info.into(),
|
||||||
SearchErrorKind::Other(None) => i18n.tr(TR::SearchInvalidOther),
|
SearchErrorKind::Other(None) => i18n.tr(TR::SearchInvalidOther),
|
||||||
|
SearchErrorKind::InvalidNumber { provided, context } => i18n
|
||||||
|
.trn(
|
||||||
|
TR::SearchInvalidNumber,
|
||||||
|
tr_strs!["provided"=>provided, "context"=>context],
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
SearchErrorKind::InvalidWholeNumber { provided, context } => i18n
|
||||||
|
.trn(
|
||||||
|
TR::SearchInvalidWholeNumber,
|
||||||
|
tr_strs!["provided"=>provided, "context"=>context],
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
SearchErrorKind::InvalidPositiveWholeNumber { provided, context } => i18n
|
||||||
|
.trn(
|
||||||
|
TR::SearchInvalidPositiveWholeNumber,
|
||||||
|
tr_strs!["provided"=>provided, "context"=>context],
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
SearchErrorKind::InvalidNegativeWholeNumber { provided, context } => i18n
|
||||||
|
.trn(
|
||||||
|
TR::SearchInvalidNegativeWholeNumber,
|
||||||
|
tr_strs!["provided"=>provided, "context"=>context],
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
SearchErrorKind::InvalidAnswerButton { provided, context } => i18n
|
||||||
|
.trn(
|
||||||
|
TR::SearchInvalidAnswerButton,
|
||||||
|
tr_strs!["provided"=>provided, "context"=>context],
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
};
|
};
|
||||||
i18n.trn(
|
i18n.trn(
|
||||||
TR::SearchInvalidSearch,
|
TR::SearchInvalidSearch,
|
||||||
@ -442,24 +422,15 @@ pub enum SearchErrorKind {
|
|||||||
UnclosedQuote,
|
UnclosedQuote,
|
||||||
MissingKey,
|
MissingKey,
|
||||||
UnknownEscape(String),
|
UnknownEscape(String),
|
||||||
InvalidIdList,
|
|
||||||
InvalidState(String),
|
InvalidState(String),
|
||||||
InvalidFlag,
|
InvalidFlag,
|
||||||
InvalidAdded,
|
|
||||||
InvalidEdited,
|
|
||||||
InvalidRatedDays,
|
|
||||||
InvalidRatedEase(String),
|
|
||||||
InvalidDupeMid,
|
|
||||||
InvalidDupeText,
|
|
||||||
InvalidResched,
|
|
||||||
InvalidPropProperty(String),
|
InvalidPropProperty(String),
|
||||||
InvalidPropOperator(String),
|
InvalidPropOperator(String),
|
||||||
InvalidPropFloat(String),
|
InvalidNumber { provided: String, context: String },
|
||||||
InvalidPropInteger(String),
|
InvalidWholeNumber { provided: String, context: String },
|
||||||
InvalidPropUnsigned(String),
|
InvalidPositiveWholeNumber { provided: String, context: String },
|
||||||
InvalidNumber(String),
|
InvalidNegativeWholeNumber { provided: String, context: String },
|
||||||
InvalidDid,
|
InvalidAnswerButton { provided: String, context: String },
|
||||||
InvalidMid,
|
|
||||||
Regex(String),
|
Regex(String),
|
||||||
Other(Option<String>),
|
Other(Option<String>),
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,7 @@ use nom::{
|
|||||||
sequence::{preceded, separated_pair},
|
sequence::{preceded, separated_pair},
|
||||||
};
|
};
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use std::{
|
use std::borrow::Cow;
|
||||||
borrow::Cow,
|
|
||||||
num::{ParseFloatError, ParseIntError},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err<ParseError<'a>>>;
|
type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err<ParseError<'a>>>;
|
||||||
type ParseResult<'a, O> = std::result::Result<O, nom::Err<ParseError<'a>>>;
|
type ParseResult<'a, O> = std::result::Result<O, nom::Err<ParseError<'a>>>;
|
||||||
@ -322,8 +318,8 @@ fn search_node_for_text_with_argument<'a>(
|
|||||||
"is" => parse_state(val)?,
|
"is" => parse_state(val)?,
|
||||||
"did" => parse_did(val)?,
|
"did" => parse_did(val)?,
|
||||||
"mid" => parse_mid(val)?,
|
"mid" => parse_mid(val)?,
|
||||||
"nid" => SearchNode::NoteIDs(check_id_list(val)?),
|
"nid" => SearchNode::NoteIDs(check_id_list(val, key)?),
|
||||||
"cid" => SearchNode::CardIDs(check_id_list(val)?),
|
"cid" => SearchNode::CardIDs(check_id_list(val, key)?),
|
||||||
"re" => SearchNode::Regex(unescape_quotes(val)),
|
"re" => SearchNode::Regex(unescape_quotes(val)),
|
||||||
"nc" => SearchNode::NoCombining(unescape(val)?),
|
"nc" => SearchNode::NoCombining(unescape(val)?),
|
||||||
"w" => SearchNode::WordBoundary(unescape(val)?),
|
"w" => SearchNode::WordBoundary(unescape(val)?),
|
||||||
@ -355,14 +351,10 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode> {
|
|||||||
|
|
||||||
/// eg resched:3
|
/// eg resched:3
|
||||||
fn parse_resched(s: &str) -> ParseResult<SearchNode> {
|
fn parse_resched(s: &str) -> ParseResult<SearchNode> {
|
||||||
if let Ok(days) = s.parse::<u32>() {
|
parse_u32(s, "resched:").map(|days| SearchNode::Rated {
|
||||||
Ok(SearchNode::Rated {
|
days,
|
||||||
days,
|
ease: EaseKind::ManualReschedule,
|
||||||
ease: EaseKind::ManualReschedule,
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(parse_failure(s, FailKind::InvalidResched))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg prop:ivl>3, prop:ease!=2.5
|
/// eg prop:ivl>3, prop:ease!=2.5
|
||||||
@ -395,17 +387,17 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
|||||||
.map_err(|_| parse_failure(prop_clause, FailKind::InvalidPropOperator(prop.to_string())))?;
|
.map_err(|_| parse_failure(prop_clause, FailKind::InvalidPropOperator(prop.to_string())))?;
|
||||||
|
|
||||||
let kind = match prop {
|
let kind = match prop {
|
||||||
"ease" => PropertyKind::Ease(parse_prop_float(num, prop_clause)?),
|
"ease" => PropertyKind::Ease(parse_f32(num, prop_clause)?),
|
||||||
"due" => PropertyKind::Due(parse_prop_integer(num, prop_clause)?),
|
"due" => PropertyKind::Due(parse_i32(num, prop_clause)?),
|
||||||
"rated" => parse_prop_rated(num, prop_clause)?,
|
"rated" => parse_prop_rated(num, prop_clause)?,
|
||||||
"resched" => PropertyKind::Rated(
|
"resched" => PropertyKind::Rated(
|
||||||
parse_prop_integer::<i32>(num, prop_clause)?.min(0),
|
parse_negative_i32(num, prop_clause)?,
|
||||||
EaseKind::ManualReschedule,
|
EaseKind::ManualReschedule,
|
||||||
),
|
),
|
||||||
"ivl" => PropertyKind::Interval(parse_prop_integer(num, prop_clause)?),
|
"ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?),
|
||||||
"reps" => PropertyKind::Reps(parse_prop_integer(num, prop_clause)?),
|
"reps" => PropertyKind::Reps(parse_u32(num, prop_clause)?),
|
||||||
"lapses" => PropertyKind::Lapses(parse_prop_integer(num, prop_clause)?),
|
"lapses" => PropertyKind::Lapses(parse_u32(num, prop_clause)?),
|
||||||
"pos" => PropertyKind::Position(parse_prop_integer(num, prop_clause)?),
|
"pos" => PropertyKind::Position(parse_u32(num, prop_clause)?),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -415,90 +407,114 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_prop_float<'a, N>(num: &str, prop_clause: &'a str) -> ParseResult<'a, N>
|
fn parse_u32<'a>(num: &str, context: &'a str) -> ParseResult<'a, u32> {
|
||||||
where
|
num.parse().map_err(|_e| {
|
||||||
N: FromStr,
|
parse_failure(
|
||||||
<N as FromStr>::Err: PartialEq<ParseFloatError>,
|
context,
|
||||||
{
|
FailKind::InvalidPositiveWholeNumber {
|
||||||
num.parse()
|
context: context.into(),
|
||||||
.map_err(|_e| parse_failure(prop_clause, FailKind::InvalidNumber(prop_clause.to_owned())))
|
provided: num.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_prop_integer<'a, N>(num: &str, prop_clause: &'a str) -> ParseResult<'a, N>
|
fn parse_i32<'a>(num: &str, context: &'a str) -> ParseResult<'a, i32> {
|
||||||
where
|
num.parse().map_err(|_e| {
|
||||||
N: FromStr,
|
parse_failure(
|
||||||
<N as FromStr>::Err: PartialEq<ParseIntError>,
|
context,
|
||||||
{
|
FailKind::InvalidWholeNumber {
|
||||||
num.parse()
|
context: context.into(),
|
||||||
.map_err(|_e| parse_failure(prop_clause, FailKind::InvalidNumber(prop_clause.to_owned())))
|
provided: num.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_prop_rated<'a>(num: &str, prop_clause: &'a str) -> ParseResult<'a, PropertyKind> {
|
fn parse_negative_i32<'a>(num: &str, context: &'a str) -> ParseResult<'a, i32> {
|
||||||
|
num.parse()
|
||||||
|
.map_err(|_| ())
|
||||||
|
.and_then(|n| if n > 0 { Err(()) } else { Ok(n) })
|
||||||
|
.map_err(|_| {
|
||||||
|
parse_failure(
|
||||||
|
context,
|
||||||
|
FailKind::InvalidNegativeWholeNumber {
|
||||||
|
context: context.into(),
|
||||||
|
provided: num.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_f32<'a>(num: &str, context: &'a str) -> ParseResult<'a, f32> {
|
||||||
|
num.parse().map_err(|_e| {
|
||||||
|
parse_failure(
|
||||||
|
context,
|
||||||
|
FailKind::InvalidNumber {
|
||||||
|
context: context.into(),
|
||||||
|
provided: num.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_i64<'a>(num: &str, context: &'a str) -> ParseResult<'a, i64> {
|
||||||
|
num.parse().map_err(|_e| {
|
||||||
|
parse_failure(
|
||||||
|
context,
|
||||||
|
FailKind::InvalidWholeNumber {
|
||||||
|
context: context.into(),
|
||||||
|
provided: num.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, EaseKind> {
|
||||||
|
Ok(if let Some(num) = num {
|
||||||
|
EaseKind::AnswerButton(
|
||||||
|
num.parse()
|
||||||
|
.map_err(|_| ())
|
||||||
|
.and_then(|n| if matches!(n, 1..=4) { Ok(n) } else { Err(()) })
|
||||||
|
.map_err(|_| {
|
||||||
|
parse_failure(
|
||||||
|
context,
|
||||||
|
FailKind::InvalidAnswerButton {
|
||||||
|
context: context.into(),
|
||||||
|
provided: num.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EaseKind::AnyAnswerButton
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_prop_rated<'a>(num: &str, context: &'a str) -> ParseResult<'a, PropertyKind> {
|
||||||
let mut it = num.splitn(2, ':');
|
let mut it = num.splitn(2, ':');
|
||||||
let days = parse_prop_integer::<i32>(it.next().unwrap(), prop_clause)?.min(0);
|
let days = parse_negative_i32(it.next().unwrap(), context)?;
|
||||||
let ease = match it.next() {
|
let button = parse_answer_button(it.next(), context)?;
|
||||||
Some(v) => match parse_prop_integer(v, prop_clause)? {
|
Ok(PropertyKind::Rated(days, button))
|
||||||
u @ 1..=5 => EaseKind::AnswerButton(u),
|
|
||||||
_ => {
|
|
||||||
return Err(parse_failure(
|
|
||||||
prop_clause,
|
|
||||||
FailKind::InvalidRatedEase(prop_clause.to_owned()),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => EaseKind::AnyAnswerButton,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(PropertyKind::Rated(days, ease))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg added:1
|
/// eg added:1
|
||||||
fn parse_added(s: &str) -> ParseResult<SearchNode> {
|
fn parse_added(s: &str) -> ParseResult<SearchNode> {
|
||||||
if let Ok(days) = s.parse::<u32>() {
|
parse_u32(s, "added:").map(|n| SearchNode::AddedInDays(n.max(1)))
|
||||||
Ok(SearchNode::AddedInDays(days.max(1)))
|
|
||||||
} else {
|
|
||||||
Err(parse_failure(s, FailKind::InvalidAdded))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg edited:1
|
/// eg edited:1
|
||||||
fn parse_edited(s: &str) -> ParseResult<SearchNode> {
|
fn parse_edited(s: &str) -> ParseResult<SearchNode> {
|
||||||
if let Ok(days) = s.parse::<u32>() {
|
parse_u32(s, "edited:").map(|n| SearchNode::EditedInDays(n.max(1)))
|
||||||
Ok(SearchNode::EditedInDays(days.max(1)))
|
|
||||||
} else {
|
|
||||||
Err(parse_failure(s, FailKind::InvalidEdited))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg rated:3 or rated:10:2
|
/// eg rated:3 or rated:10:2
|
||||||
/// second arg must be between 1-4
|
/// second arg must be between 1-4
|
||||||
fn parse_rated(s: &str) -> ParseResult<SearchNode> {
|
fn parse_rated(s: &str) -> ParseResult<SearchNode> {
|
||||||
let mut it = s.splitn(2, ':');
|
let mut it = s.splitn(2, ':');
|
||||||
if let Ok(days) = it.next().unwrap().parse::<u32>() {
|
let days = parse_u32(it.next().unwrap(), s)?.max(1);
|
||||||
let days = days.max(1);
|
let button = parse_answer_button(it.next(), s)?;
|
||||||
let ease = if let Some(tail) = it.next() {
|
Ok(SearchNode::Rated { days, ease: button })
|
||||||
if let Ok(u) = tail.parse::<u8>() {
|
|
||||||
if u > 0 && u < 5 {
|
|
||||||
EaseKind::AnswerButton(u)
|
|
||||||
} else {
|
|
||||||
return Err(parse_failure(
|
|
||||||
s,
|
|
||||||
FailKind::InvalidRatedEase(format!("rated:{}", days.to_string())),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(parse_failure(
|
|
||||||
s,
|
|
||||||
FailKind::InvalidRatedEase(format!("rated:{}", days.to_string())),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
EaseKind::AnyAnswerButton
|
|
||||||
};
|
|
||||||
Ok(SearchNode::Rated { days, ease })
|
|
||||||
} else {
|
|
||||||
Err(parse_failure(s, FailKind::InvalidRatedDays))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg is:due
|
/// eg is:due
|
||||||
@ -518,48 +534,48 @@ fn parse_state(s: &str) -> ParseResult<SearchNode> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_did(s: &str) -> ParseResult<SearchNode> {
|
fn parse_did(s: &str) -> ParseResult<SearchNode> {
|
||||||
if let Ok(did) = s.parse() {
|
parse_i64(s, "did:").map(|n| SearchNode::DeckID(n.into()))
|
||||||
Ok(SearchNode::DeckID(did))
|
|
||||||
} else {
|
|
||||||
Err(parse_failure(s, FailKind::InvalidDid))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_mid(s: &str) -> ParseResult<SearchNode> {
|
fn parse_mid(s: &str) -> ParseResult<SearchNode> {
|
||||||
if let Ok(mid) = s.parse() {
|
parse_i64(s, "mid:").map(|n| SearchNode::NoteTypeID(n.into()))
|
||||||
Ok(SearchNode::NoteTypeID(mid))
|
|
||||||
} else {
|
|
||||||
Err(parse_failure(s, FailKind::InvalidMid))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ensure a list of ids contains only numbers and commas, returning unchanged if true
|
/// ensure a list of ids contains only numbers and commas, returning unchanged if true
|
||||||
/// used by nid: and cid:
|
/// used by nid: and cid:
|
||||||
fn check_id_list(s: &str) -> ParseResult<&str> {
|
fn check_id_list<'a, 'b>(s: &'a str, context: &'b str) -> ParseResult<'a, &'a str> {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref RE: Regex = Regex::new(r"^(\d+,)*\d+$").unwrap();
|
static ref RE: Regex = Regex::new(r"^(\d+,)*\d+$").unwrap();
|
||||||
}
|
}
|
||||||
if RE.is_match(s) {
|
if RE.is_match(s) {
|
||||||
Ok(s)
|
Ok(s)
|
||||||
} else {
|
} else {
|
||||||
Err(parse_failure(s, FailKind::InvalidIdList))
|
Err(parse_failure(
|
||||||
|
s,
|
||||||
|
// id lists are undocumented, so no translation
|
||||||
|
FailKind::Other(Some(format!(
|
||||||
|
"expected only digits and commas in {}:",
|
||||||
|
context
|
||||||
|
))),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// eg dupe:1231,hello
|
/// eg dupe:1231,hello
|
||||||
fn parse_dupe(s: &str) -> ParseResult<SearchNode> {
|
fn parse_dupe(s: &str) -> ParseResult<SearchNode> {
|
||||||
let mut it = s.splitn(2, ',');
|
let mut it = s.splitn(2, ',');
|
||||||
if let Ok(mid) = it.next().unwrap().parse::<NoteTypeID>() {
|
let ntid = parse_i64(it.next().unwrap(), s)?;
|
||||||
if let Some(text) = it.next() {
|
if let Some(text) = it.next() {
|
||||||
Ok(SearchNode::Duplicates {
|
Ok(SearchNode::Duplicates {
|
||||||
note_type_id: mid,
|
note_type_id: ntid.into(),
|
||||||
text: unescape_quotes_and_backslashes(text),
|
text: unescape_quotes_and_backslashes(text),
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
Err(parse_failure(s, FailKind::InvalidDupeText))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Err(parse_failure(s, FailKind::InvalidDupeMid))
|
// this is an undocumented keyword, so no translation/help
|
||||||
|
Err(parse_failure(
|
||||||
|
s,
|
||||||
|
FailKind::Other(Some("invalid 'dupe:' search".into())),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,6 +676,8 @@ fn is_parser_escape(txt: &str) -> bool {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use crate::err::SearchErrorKind;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -827,6 +845,14 @@ mod test {
|
|||||||
assert_eq!(parse(input), Err(AnkiError::SearchError(kind)));
|
assert_eq!(parse(input), Err(AnkiError::SearchError(kind)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn failkind(input: &str) -> SearchErrorKind {
|
||||||
|
if let Err(AnkiError::SearchError(err)) = parse(input) {
|
||||||
|
err
|
||||||
|
} else {
|
||||||
|
panic!("expected search error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert_err_kind("foo and", MisplacedAnd);
|
assert_err_kind("foo and", MisplacedAnd);
|
||||||
assert_err_kind("and foo", MisplacedAnd);
|
assert_err_kind("and foo", MisplacedAnd);
|
||||||
assert_err_kind("and", MisplacedAnd);
|
assert_err_kind("and", MisplacedAnd);
|
||||||
@ -867,14 +893,18 @@ mod test {
|
|||||||
assert_err_kind(r"\ ", UnknownEscape(r"\".to_string()));
|
assert_err_kind(r"\ ", UnknownEscape(r"\".to_string()));
|
||||||
assert_err_kind(r#""\ ""#, UnknownEscape(r"\ ".to_string()));
|
assert_err_kind(r#""\ ""#, UnknownEscape(r"\ ".to_string()));
|
||||||
|
|
||||||
assert_err_kind("nid:1_2,3", InvalidIdList);
|
for term in &[
|
||||||
assert_err_kind("nid:1,2,x", InvalidIdList);
|
"nid:1_2,3",
|
||||||
assert_err_kind("nid:,2,3", InvalidIdList);
|
"nid:1,2,x",
|
||||||
assert_err_kind("nid:1,2,", InvalidIdList);
|
"nid:,2,3",
|
||||||
assert_err_kind("cid:1_2,3", InvalidIdList);
|
"nid:1,2,",
|
||||||
assert_err_kind("cid:1,2,x", InvalidIdList);
|
"cid:1_2,3",
|
||||||
assert_err_kind("cid:,2,3", InvalidIdList);
|
"cid:1,2,x",
|
||||||
assert_err_kind("cid:1,2,", InvalidIdList);
|
"cid:,2,3",
|
||||||
|
"cid:1,2,",
|
||||||
|
] {
|
||||||
|
assert!(matches!(failkind(term), SearchErrorKind::Other(_)));
|
||||||
|
}
|
||||||
|
|
||||||
assert_err_kind("is:foo", InvalidState("foo".into()));
|
assert_err_kind("is:foo", InvalidState("foo".into()));
|
||||||
assert_err_kind("is:DUE", InvalidState("DUE".into()));
|
assert_err_kind("is:DUE", InvalidState("DUE".into()));
|
||||||
@ -888,36 +918,29 @@ mod test {
|
|||||||
assert_err_kind("flag:5", InvalidFlag);
|
assert_err_kind("flag:5", InvalidFlag);
|
||||||
assert_err_kind("flag:1.1", InvalidFlag);
|
assert_err_kind("flag:1.1", InvalidFlag);
|
||||||
|
|
||||||
assert_err_kind("added:1.1", InvalidAdded);
|
for term in &["added", "edited", "rated", "resched"] {
|
||||||
assert_err_kind("added:-1", InvalidAdded);
|
assert!(
|
||||||
assert_err_kind("added:", InvalidAdded);
|
matches!(failkind(&format!("{}:1.1", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
assert_err_kind("added:foo", InvalidAdded);
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(failkind(&format!("{}:-1", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(failkind(&format!("{}:", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(failkind(&format!("{}:foo", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert_err_kind("edited:1.1", InvalidEdited);
|
assert!(matches!(failkind("rated:1:"), SearchErrorKind::InvalidAnswerButton { .. }));
|
||||||
assert_err_kind("edited:-1", InvalidEdited);
|
assert!(matches!(failkind("rated:2:-1"), SearchErrorKind::InvalidAnswerButton { .. }));
|
||||||
assert_err_kind("edited:", InvalidEdited);
|
assert!(matches!(failkind("rated:3:1.1"), SearchErrorKind::InvalidAnswerButton { .. }));
|
||||||
assert_err_kind("edited:foo", InvalidEdited);
|
assert!(matches!(failkind("rated:0:foo"), SearchErrorKind::InvalidAnswerButton { .. }));
|
||||||
|
|
||||||
assert_err_kind("rated:1.1", InvalidRatedDays);
|
assert!(matches!(failkind("dupe:"), SearchErrorKind::InvalidWholeNumber { .. }));
|
||||||
assert_err_kind("rated:-1", InvalidRatedDays);
|
assert!(matches!(failkind("dupe:1.1"), SearchErrorKind::InvalidWholeNumber { .. }));
|
||||||
assert_err_kind("rated:", InvalidRatedDays);
|
assert!(matches!(failkind("dupe:foo"), SearchErrorKind::InvalidWholeNumber { .. }));
|
||||||
assert_err_kind("rated:foo", InvalidRatedDays);
|
|
||||||
|
|
||||||
assert_err_kind("rated:1:", InvalidRatedEase("rated:1".to_string()));
|
|
||||||
assert_err_kind("rated:2:-1", InvalidRatedEase("rated:2".to_string()));
|
|
||||||
assert_err_kind("rated:3:1.1", InvalidRatedEase("rated:3".to_string()));
|
|
||||||
assert_err_kind("rated:0:foo", InvalidRatedEase("rated:1".to_string()));
|
|
||||||
|
|
||||||
assert_err_kind("resched:", FailKind::InvalidResched);
|
|
||||||
assert_err_kind("resched:-1", FailKind::InvalidResched);
|
|
||||||
assert_err_kind("resched:1:1", FailKind::InvalidResched);
|
|
||||||
assert_err_kind("resched:foo", FailKind::InvalidResched);
|
|
||||||
|
|
||||||
assert_err_kind("dupe:", InvalidDupeMid);
|
|
||||||
assert_err_kind("dupe:1.1", InvalidDupeMid);
|
|
||||||
assert_err_kind("dupe:foo", InvalidDupeMid);
|
|
||||||
|
|
||||||
assert_err_kind("dupe:123", InvalidDupeText);
|
|
||||||
|
|
||||||
assert_err_kind("prop:", InvalidPropProperty("".into()));
|
assert_err_kind("prop:", InvalidPropProperty("".into()));
|
||||||
assert_err_kind("prop:=1", InvalidPropProperty("=1".into()));
|
assert_err_kind("prop:=1", InvalidPropProperty("=1".into()));
|
||||||
@ -927,17 +950,33 @@ mod test {
|
|||||||
assert_err_kind("prop:pos~1", InvalidPropOperator("pos".to_string()));
|
assert_err_kind("prop:pos~1", InvalidPropOperator("pos".to_string()));
|
||||||
assert_err_kind("prop:reps10", InvalidPropOperator("reps".to_string()));
|
assert_err_kind("prop:reps10", InvalidPropOperator("reps".to_string()));
|
||||||
|
|
||||||
assert_err_kind("prop:ease>", InvalidNumber("ease>".to_string()));
|
// unsigned
|
||||||
assert_err_kind("prop:ease!=one", InvalidNumber("ease!=one".to_string()));
|
|
||||||
assert_err_kind("prop:ease<1,3", InvalidNumber("ease<1,3".to_string()));
|
|
||||||
|
|
||||||
assert_err_kind("prop:due>", InvalidNumber("due>".to_string()));
|
for term in &["ivl", "reps", "lapses", "pos"] {
|
||||||
assert_err_kind("prop:due=0.5", InvalidNumber("due=0.5".to_string()));
|
assert!(
|
||||||
assert_err_kind("prop:due<foo", InvalidNumber("due<foo".to_string()));
|
matches!(failkind(&format!("prop:{}>", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(failkind(&format!("prop:{}=0.5", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(failkind(&format!("prop:{}!=-1", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(failkind(&format!("prop:{}<foo", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert_err_kind("prop:ivl>", InvalidNumber("ivl>".to_string()));
|
// signed
|
||||||
assert_err_kind("prop:reps=1.1", InvalidNumber("reps=1.1".to_string()));
|
|
||||||
assert_err_kind("prop:lapses!=-1", InvalidNumber("lapses!=-1".to_string()));
|
assert!(matches!(failkind("prop:due>"), SearchErrorKind::InvalidWholeNumber { .. }));
|
||||||
|
assert!(matches!(failkind("prop:due=0.5"), SearchErrorKind::InvalidWholeNumber { .. }));
|
||||||
|
|
||||||
|
// float
|
||||||
|
|
||||||
|
assert!(matches!(failkind("prop:ease>"), SearchErrorKind::InvalidNumber { .. }));
|
||||||
|
assert!(matches!(failkind("prop:ease!=one"), SearchErrorKind::InvalidNumber { .. }));
|
||||||
|
assert!(matches!(failkind("prop:ease<1,3"), SearchErrorKind::InvalidNumber { .. }));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user