diff --git a/ftl/core/card-templates.ftl b/ftl/core/card-templates.ftl
index 7a507fce8..50e4e61b5 100644
--- a/ftl/core/card-templates.ftl
+++ b/ftl/core/card-templates.ftl
@@ -22,6 +22,8 @@ card-templates-add-mobile-class = Add Mobile Class
card-templates-preview-settings = Options
card-templates-invalid-template-number = Card template { $number } in notetype '{ $notetype }' has a problem.
card-templates-identical-front = Its front side is identical with the one of card template { $number }.
+card-templates-missing-cloze = The 'cloze' filter must be used on both sides of a cloze template.
+card-templates-extraneous-cloze = The 'cloze' filter can only be used on cloze templates.
card-templates-see-preview = See the render preview for more information.
card-templates-changes-saved = Changes saved.
card-templates-discard-changes = Discard changes?
diff --git a/rslib/src/error/mod.rs b/rslib/src/error/mod.rs
index 914074368..da6c48375 100644
--- a/rslib/src/error/mod.rs
+++ b/rslib/src/error/mod.rs
@@ -67,6 +67,10 @@ impl AnkiError {
let details = match err.details {
TemplateSaveErrorDetails::TemplateError => tr.card_templates_see_preview(),
TemplateSaveErrorDetails::Duplicate(i) => tr.card_templates_identical_front(i),
+ TemplateSaveErrorDetails::MissingCloze => tr.card_templates_missing_cloze(),
+ TemplateSaveErrorDetails::ExtraneousCloze => {
+ tr.card_templates_extraneous_cloze()
+ }
};
format!("{}
{}", header, details)
}
@@ -146,6 +150,8 @@ pub struct TemplateSaveError {
#[derive(Debug, PartialEq)]
pub enum TemplateSaveErrorDetails {
- Duplicate(usize),
TemplateError,
+ Duplicate(usize),
+ MissingCloze,
+ ExtraneousCloze,
}
diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs
index 9e223d7af..f472f27a7 100644
--- a/rslib/src/notetype/mod.rs
+++ b/rslib/src/notetype/mod.rs
@@ -287,6 +287,28 @@ impl Notetype {
Ok(())
}
+ fn ensure_cloze_if_and_only_if_cloze_notetype(
+ &self,
+ parsed_templates: &[(Option, Option)],
+ ) -> Result<()> {
+ if self.is_cloze() {
+ if missing_cloze_filter(parsed_templates) {
+ return Err(AnkiError::TemplateSaveError(TemplateSaveError {
+ notetype: self.name.clone(),
+ ordinal: 0,
+ details: TemplateSaveErrorDetails::MissingCloze,
+ }));
+ }
+ } else if let Some(i) = find_cloze_filter(parsed_templates) {
+ return Err(AnkiError::TemplateSaveError(TemplateSaveError {
+ notetype: self.name.clone(),
+ ordinal: i,
+ details: TemplateSaveErrorDetails::ExtraneousCloze,
+ }));
+ }
+ Ok(())
+ }
+
pub(crate) fn normalize_names(&mut self) {
ensure_string_in_nfc(&mut self.name);
for f in &mut self.fields {
@@ -349,6 +371,7 @@ impl Notetype {
details: TemplateSaveErrorDetails::TemplateError,
}));
}
+ self.ensure_cloze_if_and_only_if_cloze_notetype(&parsed_templates)?;
let reqs = self.updated_requirements(&parsed_templates);
// handle renamed+deleted fields
@@ -448,6 +471,35 @@ impl Notetype {
}
}
+/// True if the slice is empty or either template of the first tuple doesn't have a cloze field.
+fn missing_cloze_filter(
+ parsed_templates: &[(Option, Option)],
+) -> bool {
+ parsed_templates
+ .get(0)
+ .map_or(true, |t| !has_cloze(&t.0) || !has_cloze(&t.1))
+}
+
+/// Return the index of the first tuple with a cloze field on either template.
+fn find_cloze_filter(
+ parsed_templates: &[(Option, Option)],
+) -> Option {
+ parsed_templates.iter().enumerate().find_map(|(i, t)| {
+ if has_cloze(&t.0) || has_cloze(&t.1) {
+ Some(i)
+ } else {
+ None
+ }
+ })
+}
+
+/// True if the template is non-empty and has a cloze field.
+fn has_cloze(template: &Option) -> bool {
+ template
+ .as_ref()
+ .map_or(false, |t| !t.cloze_fields().is_empty())
+}
+
impl From for NotetypeProto {
fn from(nt: Notetype) -> Self {
NotetypeProto {