Merge pull request #927 from ankitects/simplify-props

simplify parse_prop() and associated translations
This commit is contained in:
Damien Elmes 2021-01-20 09:23:12 +10:00 committed by GitHub
commit 772f65dc7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 264 additions and 275 deletions

View file

@ -1,30 +1,28 @@
## 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"`.
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-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"`.
# Here, the ellipsis "..." may be localised. # Here, the ellipsis "..." may be localised.
search-empty-group = a group `(...)` was found, but there was nothing between the brackets to search for. If you want to search for literal brackets, wrap them in double quotes: `"( )"`. search-empty-group = a group `(...)` was found, but there was nothing between the brackets to search for. If you want to search for literal brackets, wrap them in double quotes: `"( )"`.
search-unopened-group = a closing bracket `)` was found, but there was no opening bracket `(` preceding it. If you want to search for the literal `)`, wrap it in double quotes or prepend a backslash: `")"` or `\)`. search-unopened-group = a closing bracket `)` was found, but there was no opening bracket `(` preceding it. If you want to search for a literal `)`, wrap it in double quotes or prepend a backslash: `")"` or `\)`.
search-unclosed-group = an opening bracket `(` was found, but there was no closing bracket `)` following it. If you want to search for the literal `(`, wrap it in double quotes or prepend a backslash: `"("` or `\(` . search-unclosed-group = an opening bracket `(` was found, but there was no closing bracket `)` following it. If you want to search for a literal `(`, wrap it in double quotes or prepend a backslash: `"("` or `\(` .
search-empty-quote = a pair of double quotes `""` was found but there was nothing between them to search for. If you want to search for literal double quotes, prepend backslashes: `\"\"`. search-empty-quote = a pair of double quotes `""` was found, but there was nothing between them to search for. If you want to search for literal double quotes, prepend backslashes: `\"\"`.
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 a 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 a 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 a 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-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

View file

@ -42,7 +42,7 @@ def check_missing_terms(path: str) -> bool:
def check_file(path: str, fix: bool) -> bool: def check_file(path: str, fix: bool) -> bool:
"True if file is ok." "True if file is ok."
orig_text = open(path).read() orig_text = open(path, encoding="utf8").read()
obj = parse(orig_text, with_spans=False) obj = parse(orig_text, with_spans=False)
# make sure there's no junk # make sure there's no junk
for ent in obj.body: for ent in obj.body:

View file

@ -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,32 +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::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,
@ -182,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,
@ -439,23 +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 },
InvalidDid, InvalidNegativeWholeNumber { provided: String, context: String },
InvalidMid, InvalidAnswerButton { provided: String, context: String },
Regex(String), Regex(String),
Other(Option<String>), Other(Option<String>),
} }

View file

@ -318,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)?),
@ -351,18 +351,14 @@ 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
fn parse_prop(s: &str) -> ParseResult<SearchNode> { fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
let (tail, prop) = alt::<_, _, ParseError, _>(( let (tail, prop) = alt::<_, _, ParseError, _>((
tag("ivl"), tag("ivl"),
tag("due"), tag("due"),
@ -372,8 +368,13 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode> {
tag("pos"), tag("pos"),
tag("rated"), tag("rated"),
tag("resched"), tag("resched"),
))(s) ))(prop_clause)
.map_err(|_| parse_failure(s, FailKind::InvalidPropProperty(s.into())))?; .map_err(|_| {
parse_failure(
prop_clause,
FailKind::InvalidPropProperty(prop_clause.into()),
)
})?;
let (num, operator) = alt::<_, _, ParseError, _>(( let (num, operator) = alt::<_, _, ParseError, _>((
tag("<="), tag("<="),
@ -383,87 +384,21 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode> {
tag("<"), tag("<"),
tag(">"), tag(">"),
))(tail) ))(tail)
.map_err(|_| parse_failure(s, FailKind::InvalidPropOperator(prop.to_string())))?; .map_err(|_| parse_failure(prop_clause, FailKind::InvalidPropOperator(prop.to_string())))?;
let kind = if prop == "ease" { let kind = match prop {
if let Ok(f) = num.parse::<f32>() { "ease" => PropertyKind::Ease(parse_f32(num, prop_clause)?),
PropertyKind::Ease(f) "due" => PropertyKind::Due(parse_i32(num, prop_clause)?),
} else { "rated" => parse_prop_rated(num, prop_clause)?,
return Err(parse_failure( "resched" => PropertyKind::Rated(
s, parse_negative_i32(num, prop_clause)?,
FailKind::InvalidPropFloat(format!("{}{}", prop, operator)), EaseKind::ManualReschedule,
)); ),
} "ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?),
} else if prop == "due" { "reps" => PropertyKind::Reps(parse_u32(num, prop_clause)?),
if let Ok(i) = num.parse::<i32>() { "lapses" => PropertyKind::Lapses(parse_u32(num, prop_clause)?),
PropertyKind::Due(i) "pos" => PropertyKind::Position(parse_u32(num, prop_clause)?),
} else {
return Err(parse_failure(
s,
FailKind::InvalidPropInteger(format!("{}{}", prop, operator)),
));
}
} else if prop == "rated" {
let mut it = num.splitn(2, ':');
let days: i32 = if let Ok(i) = it.next().unwrap().parse::<i32>() {
i.min(0)
} else {
return Err(parse_failure(
s,
FailKind::InvalidPropInteger(format!("{}{}", prop, operator)),
));
};
let ease = match it.next() {
Some(v) => {
if let Ok(u) = v.parse::<u8>() {
if (1..5).contains(&u) {
EaseKind::AnswerButton(u)
} else {
return Err(parse_failure(
s,
FailKind::InvalidRatedEase(format!(
"prop:{}{}{}",
prop,
operator,
days.to_string()
)),
));
}
} else {
return Err(parse_failure(
s,
FailKind::InvalidPropInteger(format!("{}{}", prop, operator)),
));
}
}
None => EaseKind::AnyAnswerButton,
};
PropertyKind::Rated(days, ease)
} else if prop == "resched" {
if let Ok(days) = num.parse::<i32>() {
PropertyKind::Rated(days.min(0), EaseKind::ManualReschedule)
} else {
return Err(parse_failure(
s,
FailKind::InvalidPropInteger(format!("{}{}", prop, operator)),
));
}
} else if let Ok(u) = num.parse::<u32>() {
match prop {
"ivl" => PropertyKind::Interval(u),
"reps" => PropertyKind::Reps(u),
"lapses" => PropertyKind::Lapses(u),
"pos" => PropertyKind::Position(u),
_ => unreachable!(), _ => unreachable!(),
}
} else {
return Err(parse_failure(
s,
FailKind::InvalidPropUnsigned(format!("{}{}", prop, operator)),
));
}; };
Ok(SearchNode::Property { Ok(SearchNode::Property {
@ -472,53 +407,114 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode> {
}) })
} }
fn parse_u32<'a>(num: &str, context: &'a str) -> ParseResult<'a, u32> {
num.parse().map_err(|_e| {
parse_failure(
context,
FailKind::InvalidPositiveWholeNumber {
context: context.into(),
provided: num.into(),
},
)
})
}
fn parse_i32<'a>(num: &str, context: &'a str) -> ParseResult<'a, i32> {
num.parse().map_err(|_e| {
parse_failure(
context,
FailKind::InvalidWholeNumber {
context: context.into(),
provided: num.into(),
},
)
})
}
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 days = parse_negative_i32(it.next().unwrap(), context)?;
let button = parse_answer_button(it.next(), context)?;
Ok(PropertyKind::Rated(days, button))
}
/// 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(), "rated:")?.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
@ -538,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 { } else {
Err(parse_failure(s, FailKind::InvalidDupeText)) // this is an undocumented keyword, so no translation/help
} Err(parse_failure(
} else { s,
Err(parse_failure(s, FailKind::InvalidDupeMid)) FailKind::Other(Some("invalid 'dupe:' search".into())),
))
} }
} }
@ -680,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]
@ -847,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);
@ -887,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()));
@ -908,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()));
@ -947,20 +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>", InvalidPropFloat("ease>".to_string())); // unsigned
assert_err_kind("prop:ease!=one", InvalidPropFloat("ease!=".to_string()));
assert_err_kind("prop:ease<1,3", InvalidPropFloat("ease<".to_string()));
assert_err_kind("prop:due>", InvalidPropInteger("due>".to_string())); for term in &["ivl", "reps", "lapses", "pos"] {
assert_err_kind("prop:due=0.5", InvalidPropInteger("due=".to_string())); assert!(
assert_err_kind("prop:due<foo", InvalidPropInteger("due<".to_string())); matches!(failkind(&format!("prop:{}>", term)), SearchErrorKind::InvalidPositiveWholeNumber { .. })
assert_err_kind("prop:ivl>", InvalidPropUnsigned("ivl>".to_string()));
assert_err_kind("prop:reps=1.1", InvalidPropUnsigned("reps=".to_string()));
assert_err_kind(
"prop:lapses!=-1",
InvalidPropUnsigned("lapses!=".to_string()),
); );
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 { .. })
);
}
// signed
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(())
} }