diff --git a/qt/aqt/clayout.py b/qt/aqt/clayout.py index 48c6e0661..88adc5060 100644 --- a/qt/aqt/clayout.py +++ b/qt/aqt/clayout.py @@ -747,7 +747,7 @@ Enter deck to place new %s cards in, or leave blank:""" try: fut.result() except TemplateError as e: - showWarning("Unable to save changes: " + str(e)) + showWarning(str(e)) return self.mw.reset() tooltip("Changes saved.", parent=self.parent()) diff --git a/rslib/ftl/card-templates.ftl b/rslib/ftl/card-templates.ftl index addf38156..7a0a13672 100644 --- a/rslib/ftl/card-templates.ftl +++ b/rslib/ftl/card-templates.ftl @@ -13,3 +13,6 @@ card-templates-preview-box = Preview card-templates-template-box = Template card-templates-sample-cloze = This is a {"{{c1::"}sample{"}}"} cloze deletion. card-templates-fill-empty = Fill Empty Fields +card-templates-invalid-template-number = Please correct the problems on card template { $number } first. + + diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 540e2184d..6dab3ee07 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -146,6 +146,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { AnkiError::Existing => V::Exists(Empty {}), AnkiError::DeckIsFiltered => V::DeckIsFiltered(Empty {}), AnkiError::SearchError(_) => V::InvalidInput(pb::Empty {}), + AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}), }; pb::BackendError { diff --git a/rslib/src/cloze.rs b/rslib/src/cloze.rs index 47ea27e54..759873e3a 100644 --- a/rslib/src/cloze.rs +++ b/rslib/src/cloze.rs @@ -93,15 +93,19 @@ pub fn reveal_cloze_text(text: &str, cloze_ord: u16, question: bool) -> Cow } pub fn reveal_cloze_text_only(text: &str, cloze_ord: u16, question: bool) -> Cow { - for caps in CLOZE.captures_iter(text) { - let captured_ord = caps - .get(cloze_caps::ORD) - .unwrap() - .as_str() - .parse() - .unwrap_or(0); + CLOZE + .captures_iter(text) + .filter(|caps| { + let captured_ord = caps + .get(cloze_caps::ORD) + .unwrap() + .as_str() + .parse() + .unwrap_or(0); - if captured_ord == cloze_ord { + captured_ord == cloze_ord + }) + .map(|caps| { let cloze = if question { // hint provided? if let Some(hint) = caps.get(cloze_caps::HINT) { @@ -112,11 +116,12 @@ pub fn reveal_cloze_text_only(text: &str, cloze_ord: u16, question: bool) -> Cow } else { caps.get(cloze_caps::TEXT).unwrap().as_str() }; - return cloze.to_owned().into(); - } - } - "".into() + cloze + }) + .collect::>() + .join(", ") + .into() } /// If text contains any LaTeX tags, render the front and back @@ -214,6 +219,10 @@ mod test { ); assert_eq!(reveal_cloze_text_only("foo {{c1::bar}}", 1, false), "bar"); assert_eq!(reveal_cloze_text_only("foo {{c1::bar}}", 2, false), ""); + assert_eq!( + reveal_cloze_text_only("{{c1::foo}} {{c1::bar}}", 1, false), + "foo, bar" + ); } #[test] diff --git a/rslib/src/err.rs b/rslib/src/err.rs index fe243278e..6650ceacf 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::i18n::{tr_strs, I18n, TR}; +use crate::i18n::{tr_args, tr_strs, I18n, TR}; pub use failure::{Error, Fail}; use reqwest::StatusCode; use std::io; @@ -16,6 +16,9 @@ pub enum AnkiError { #[fail(display = "invalid card template: {}", info)] TemplateError { info: String }, + #[fail(display = "unable to save template {}", ordinal)] + TemplateSaveError { ordinal: usize }, + #[fail(display = "I/O error: {}", info)] IOError { info: String }, @@ -107,6 +110,10 @@ impl AnkiError { // already localized info.into() } + AnkiError::TemplateSaveError { ordinal } => i18n.trn( + TR::CardTemplatesInvalidTemplateNumber, + tr_args!["number"=>ordinal+1], + ), AnkiError::DBError { info, kind } => match kind { DBErrorKind::Corrupt => info.clone(), DBErrorKind::Locked => "Anki already open, or media currently syncing.".into(), diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 234c20869..57ce56d2d 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -229,9 +229,7 @@ impl NoteType { } }); if let Some(idx) = invalid_card_idx { - return Err(AnkiError::TemplateError { - info: format!("invalid card {}", idx + 1), - }); + return Err(AnkiError::TemplateSaveError { ordinal: idx }); } let reqs = self.updated_requirements(&parsed_templates); diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 8fe5412f5..a450f26f8 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -5,15 +5,16 @@ use crate::{ err::{AnkiError, Result}, notetype::NoteTypeID, }; +use lazy_static::lazy_static; use nom::{ branch::alt, bytes::complete::{escaped, is_not, tag, take_while1}, character::complete::{anychar, char, one_of}, - character::is_digit, combinator::{all_consuming, map, map_res}, sequence::{delimited, preceded, tuple}, {multi::many0, IResult}, }; +use regex::Regex; use std::{borrow::Cow, num}; // fixme: need to preserve \ when used twice in string @@ -294,10 +295,13 @@ fn search_node_for_text_with_argument<'a>( /// ensure a list of ids contains only numbers and commas, returning unchanged if true /// used by nid: and cid: fn check_id_list(s: Cow) -> ParseResult> { - if s.is_empty() || s.as_bytes().iter().any(|&c| !is_digit(c) && c != b',') { - Err(ParseError {}) - } else { + lazy_static! { + static ref RE: Regex = Regex::new(r"^(\d+,)*\d+$").unwrap(); + } + if RE.is_match(s.as_ref()) { Ok(s) + } else { + Err(ParseError {}) } } diff --git a/ts/package.json b/ts/package.json index e0ef94a25..ee3564634 100644 --- a/ts/package.json +++ b/ts/package.json @@ -34,7 +34,7 @@ "sass-loader": "^8.0.2", "style-loader": "^1.2.1", "svelte": "^3.23.2", - "svelte-check": "^0.1.58", + "svelte-check": "^0.1.59", "svelte-loader": "^2.13.6", "svelte-preprocess": "^3.9.9", "ts-loader": "^7.0.5", diff --git a/ts/src/stats/CardCounts.svelte b/ts/src/stats/CardCounts.svelte index 923286bec..0f67877a1 100644 --- a/ts/src/stats/CardCounts.svelte +++ b/ts/src/stats/CardCounts.svelte @@ -36,7 +36,7 @@ + style="opacity: {graphData.totalCards ? 1 : 0}">