From 4d7fcf585afb266566cc4c92f4cd30e67a489a8d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 28 Jul 2021 11:53:31 +0200 Subject: [PATCH 1/5] Check for invalid conditionals on templates --- ftl/core/card-template-rendering.ftl | 3 ++ rslib/src/error/mod.rs | 1 + rslib/src/template.rs | 45 +++++++++++++++++++++++----- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/ftl/core/card-template-rendering.ftl b/ftl/core/card-template-rendering.ftl index bff3327a0..7141c35f1 100644 --- a/ftl/core/card-template-rendering.ftl +++ b/ftl/core/card-template-rendering.ftl @@ -21,6 +21,9 @@ card-template-rendering-conditional-not-open = Found '{ $found }', but missing ' # when the user referenced a field that doesn't exist # eg, Found '{{Field}}', but there is not field called 'Field' card-template-rendering-no-such-field = Found '{ $found }', but there is no field called '{ $field }' +# when the user referenced a field that doesn't exist in a conditional, excepting card numbers like 'c1' +# eg, Found '{{#Field}}', but neither is it a card number nor is there a field called 'Field' +card-template-rendering-no-such-conditional = Found '{ $found }', but neither is it a card number nor is there a field called '{ $key }' # This message is shown when the front side of the card is blank, # either due to a badly-designed template, or because required fields # are missing. diff --git a/rslib/src/error/mod.rs b/rslib/src/error/mod.rs index 324976a16..907bf2492 100644 --- a/rslib/src/error/mod.rs +++ b/rslib/src/error/mod.rs @@ -119,6 +119,7 @@ pub enum TemplateError { filters: String, field: String, }, + NoSuchConditional(String), } impl From for AnkiError { diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 1bde79f3d..508e4b9f1 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -297,6 +297,12 @@ fn localized_template_error(tr: &I18n, err: TemplateError) -> String { TemplateError::FieldNotFound { field, filters } => tr .card_template_rendering_no_such_field(format!("{{{{{}{}}}}}", filters, field), field) .into(), + TemplateError::NoSuchConditional(condition) => tr + .card_template_rendering_no_such_conditional( + format!("{{{{{}}}}}", condition), + &condition[1..], + ) + .into(), } } @@ -467,12 +473,12 @@ fn render_into( } } Conditional { key, children } => { - if context.nonempty_fields.contains(key.as_str()) { + if context.evaluate_conditional(key.as_str(), false)? { render_into(rendered_nodes, children.as_ref(), context, tr)?; } } NegatedConditional { key, children } => { - if !context.nonempty_fields.contains(key.as_str()) { + if context.evaluate_conditional(key.as_str(), true)? { render_into(rendered_nodes, children.as_ref(), context, tr)?; } } @@ -482,6 +488,24 @@ fn render_into( Ok(()) } +impl<'a> RenderContext<'a> { + fn evaluate_conditional(&self, key: &str, negated: bool) -> TemplateResult { + if self.nonempty_fields.contains(key) { + Ok(true ^ negated) + } else if self.fields.contains_key(key) + || (key.starts_with('c') && key[1..].parse::().is_ok()) + { + Ok(false ^ negated) + } else { + let prefix = if negated { "^" } else { "#" }; + Err(TemplateError::NoSuchConditional(format!( + "{}{}", + prefix, key + ))) + } + } +} + /// Append to last node if last node is a string, else add new node. fn append_str_to_nodes(nodes: &mut Vec, text: &str) { if let Some(RenderedNode::Text { @@ -1015,7 +1039,7 @@ mod test { #[test] fn render_single() { - let map: HashMap<_, _> = vec![("F", "f"), ("B", "b"), ("E", " ")] + let map: HashMap<_, _> = vec![("F", "f"), ("B", "b"), ("E", " "), ("c1", "1")] .into_iter() .map(|r| (r.0, r.1.into())) .collect(); @@ -1044,10 +1068,8 @@ mod test { // missing tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap(); assert_eq!( - tmpl.render(&ctx, &tr).unwrap(), - vec![FN::Text { - text: "A".to_owned() - },] + tmpl.render(&ctx, &tr).unwrap_err(), + TemplateError::NoSuchConditional("^M".to_string()) ); // nested @@ -1059,6 +1081,15 @@ mod test { },] ); + // card conditionals + tmpl = PT::from_text("{{^c2}}1{{#c1}}2{{/c1}}{{/c2}}").unwrap(); + assert_eq!( + tmpl.render(&ctx, &tr).unwrap(), + vec![FN::Text { + text: "12".to_owned() + },] + ); + // unknown filters tmpl = PT::from_text("{{one:two:B}}").unwrap(); assert_eq!( From be1b5243968bfe54cf83505648614fa354e117ec Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 28 Jul 2021 12:13:14 +0200 Subject: [PATCH 2/5] Find template errors hidden by conditionals --- rslib/src/template.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 508e4b9f1..628bf1c26 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -475,11 +475,16 @@ fn render_into( Conditional { key, children } => { if context.evaluate_conditional(key.as_str(), false)? { render_into(rendered_nodes, children.as_ref(), context, tr)?; + } else { + // keep checking for errors, but discard rendered nodes + render_into(&mut vec![], children.as_ref(), context, tr)?; } } NegatedConditional { key, children } => { if context.evaluate_conditional(key.as_str(), true)? { render_into(rendered_nodes, children.as_ref(), context, tr)?; + } else { + render_into(&mut vec![], children.as_ref(), context, tr)?; } } }; @@ -1066,7 +1071,7 @@ mod test { assert_eq!(tmpl.render(&ctx, &tr).unwrap(), vec![]); // missing - tmpl = PT::from_text("{{^M}}A{{/M}}").unwrap(); + tmpl = PT::from_text("{{#E}}}{{^M}}A{{/M}}{{/E}}}").unwrap(); assert_eq!( tmpl.render(&ctx, &tr).unwrap_err(), TemplateError::NoSuchConditional("^M".to_string()) From c0dd769090106a52df6aa29545dd6cdd77b6979f Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 28 Jul 2021 21:46:51 +0200 Subject: [PATCH 3/5] Skip new notetype checks when importing apkg --- proto/anki/notetypes.proto | 1 + pylib/anki/importing/anki2.py | 4 +-- pylib/anki/models.py | 11 ++++-- rslib/src/backend/notetypes.rs | 14 ++++---- rslib/src/dbcheck.rs | 4 +-- rslib/src/notetype/mod.rs | 52 +++++++++++++++++++++------- rslib/src/notetype/notetypechange.rs | 2 +- rslib/src/notetype/schemachange.rs | 10 +++--- rslib/src/notetype/stock.rs | 2 +- rslib/src/sync/mod.rs | 4 +-- 10 files changed, 69 insertions(+), 35 deletions(-) diff --git a/proto/anki/notetypes.proto b/proto/anki/notetypes.proto index 4065e34ab..9bf0c7bb0 100644 --- a/proto/anki/notetypes.proto +++ b/proto/anki/notetypes.proto @@ -107,6 +107,7 @@ message Notetype { message AddOrUpdateNotetypeRequest { bytes json = 1; bool preserve_usn_and_mtime = 2; + bool skip_checks = 3; } message StockNotetype { diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index b295df974..cddacd787 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -235,7 +235,7 @@ class Anki2Importer(Importer): model = srcModel.copy() model["id"] = mid model["usn"] = self.col.usn() - self.dst.models.update(model) + self.dst.models.update(model, skip_checks=True) break # there's an existing model; do the schemas match? dstModel = self.dst.models.get(mid) @@ -246,7 +246,7 @@ class Anki2Importer(Importer): model = srcModel.copy() model["id"] = mid model["usn"] = self.col.usn() - self.dst.models.update(model) + self.dst.models.update(model, skip_checks=True) break # as they don't match, try next id mid = NotetypeId(mid + 1) diff --git a/pylib/anki/models.py b/pylib/anki/models.py index 889d749bd..932b461c1 100644 --- a/pylib/anki/models.py +++ b/pylib/anki/models.py @@ -527,12 +527,19 @@ and notes.mid = ? and cards.ord = ?""", pass # @deprecated(replaced_by=update_dict) - def update(self, notetype: NotetypeDict, preserve_usn: bool = True) -> None: + def update( + self, + notetype: NotetypeDict, + preserve_usn: bool = True, + skip_checks: bool = False, + ) -> None: "Add or update an existing model. Use .update_dict() instead." self._remove_from_cache(notetype["id"]) self.ensure_name_unique(notetype) notetype["id"] = self.col._backend.add_or_update_notetype( - json=to_json_bytes(notetype), preserve_usn_and_mtime=preserve_usn + json=to_json_bytes(notetype), + preserve_usn_and_mtime=preserve_usn, + skip_checks=skip_checks, ) self.set_current(notetype) self._mutate_after_write(notetype) diff --git a/rslib/src/backend/notetypes.rs b/rslib/src/backend/notetypes.rs index 9dfe9ab44..3e2e85047 100644 --- a/rslib/src/backend/notetypes.rs +++ b/rslib/src/backend/notetypes.rs @@ -17,7 +17,7 @@ impl NotetypesService for Backend { let mut notetype: Notetype = input.into(); self.with_col(|col| { Ok(col - .add_notetype(&mut notetype)? + .add_notetype(&mut notetype, false)? .map(|_| notetype.id.0) .into()) }) @@ -25,7 +25,7 @@ impl NotetypesService for Backend { fn update_notetype(&self, input: pb::Notetype) -> Result { let mut notetype: Notetype = input.into(); - self.with_col(|col| col.update_notetype(&mut notetype)) + self.with_col(|col| col.update_notetype(&mut notetype, false)) .map(Into::into) } @@ -34,7 +34,7 @@ impl NotetypesService for Backend { let mut notetype: Notetype = legacy.into(); self.with_col(|col| { Ok(col - .add_notetype(&mut notetype)? + .add_notetype(&mut notetype, false)? .map(|_| notetype.id.0) .into()) }) @@ -43,7 +43,7 @@ impl NotetypesService for Backend { fn update_notetype_legacy(&self, input: pb::Json) -> Result { let legacy: NotetypeSchema11 = serde_json::from_slice(&input.json)?; let mut notetype: Notetype = legacy.into(); - self.with_col(|col| col.update_notetype(&mut notetype)) + self.with_col(|col| col.update_notetype(&mut notetype, false)) .map(Into::into) } @@ -58,11 +58,11 @@ impl NotetypesService for Backend { nt.set_modified(col.usn()?); } if nt.id.0 == 0 { - col.add_notetype(&mut nt)?; + col.add_notetype(&mut nt, input.skip_checks)?; } else if !input.preserve_usn_and_mtime { - col.update_notetype(&mut nt)?; + col.update_notetype(&mut nt, input.skip_checks)?; } else { - col.add_or_update_notetype_with_existing_id(&mut nt)?; + col.add_or_update_notetype_with_existing_id(&mut nt, input.skip_checks)?; } Ok(pb::NotetypeId { ntid: nt.id.0 }) }) diff --git a/rslib/src/dbcheck.rs b/rslib/src/dbcheck.rs index 92d50e22e..46fb95c3a 100644 --- a/rslib/src/dbcheck.rs +++ b/rslib/src/dbcheck.rs @@ -273,7 +273,7 @@ impl Collection { // one note type exists if self.storage.get_all_notetype_names()?.is_empty() { let mut nt = all_stock_notetypes(&self.tr).remove(0); - self.add_notetype_inner(&mut nt, usn)?; + self.add_notetype_inner(&mut nt, usn, true)?; } if out.card_ords_duplicated > 0 @@ -367,7 +367,7 @@ impl Collection { for n in 0..extra_cards_required { basic.add_template(&format!("Card {}", n + 2), &qfmt, &afmt); } - self.add_notetype(&mut basic)?; + self.add_notetype(&mut basic, true)?; Ok(Arc::new(basic)) } diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 1220372b3..82b59d910 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -117,11 +117,15 @@ impl Notetype { impl Collection { /// Add a new notetype, and allocate it an ID. - pub fn add_notetype(&mut self, notetype: &mut Notetype) -> Result> { + pub fn add_notetype( + &mut self, + notetype: &mut Notetype, + skip_checks: bool, + ) -> Result> { self.transact(Op::AddNotetype, |col| { let usn = col.usn()?; notetype.set_modified(usn); - col.add_notetype_inner(notetype, usn) + col.add_notetype_inner(notetype, usn, skip_checks) }) } @@ -130,7 +134,11 @@ impl Collection { /// /// This does not assign ordinals to the provided notetype, so if you wish /// to make use of template_idx, the notetype must be fetched again. - pub fn update_notetype(&mut self, notetype: &mut Notetype) -> Result> { + pub fn update_notetype( + &mut self, + notetype: &mut Notetype, + skip_checks: bool, + ) -> Result> { self.transact(Op::UpdateNotetype, |col| { let original = col .storage @@ -138,7 +146,12 @@ impl Collection { .ok_or(AnkiError::NotFound)?; let usn = col.usn()?; notetype.set_modified(usn); - col.add_or_update_notetype_with_existing_id_inner(notetype, Some(original), usn) + col.add_or_update_notetype_with_existing_id_inner( + notetype, + Some(original), + usn, + skip_checks, + ) }) } @@ -147,11 +160,12 @@ impl Collection { pub fn add_or_update_notetype_with_existing_id( &mut self, notetype: &mut Notetype, + skip_checks: bool, ) -> Result<()> { self.transact_no_undo(|col| { let usn = col.usn()?; let existing = col.storage.get_notetype(notetype.id)?; - col.add_or_update_notetype_with_existing_id_inner(notetype, existing, usn) + col.add_or_update_notetype_with_existing_id_inner(notetype, existing, usn, skip_checks) }) } @@ -419,7 +433,11 @@ impl Notetype { self.templates.push(CardTemplate::new(name, qfmt, afmt)); } - pub(crate) fn prepare_for_update(&mut self, existing: Option<&Notetype>) -> Result<()> { + pub(crate) fn prepare_for_update( + &mut self, + existing: Option<&Notetype>, + skip_checks: bool, + ) -> Result<()> { if self.fields.is_empty() { return Err(AnkiError::invalid_input("1 field required")); } @@ -450,9 +468,11 @@ impl Notetype { } } self.config.reqs = reqs; - self.ensure_template_fronts_unique()?; - self.ensure_valid_parsed_templates(&parsed_templates)?; - self.ensure_cloze_if_cloze_notetype(&parsed_templates)?; + if !skip_checks { + self.ensure_template_fronts_unique()?; + self.ensure_valid_parsed_templates(&parsed_templates)?; + self.ensure_cloze_if_cloze_notetype(&parsed_templates)?; + } Ok(()) } @@ -610,8 +630,13 @@ impl Collection { } /// Caller must set notetype as modified if appropriate. - pub(crate) fn add_notetype_inner(&mut self, notetype: &mut Notetype, usn: Usn) -> Result<()> { - notetype.prepare_for_update(None)?; + pub(crate) fn add_notetype_inner( + &mut self, + notetype: &mut Notetype, + usn: Usn, + skip_checks: bool, + ) -> Result<()> { + notetype.prepare_for_update(None, skip_checks)?; self.ensure_notetype_name_unique(notetype, usn)?; self.add_notetype_undoable(notetype)?; self.set_current_notetype_id(notetype.id) @@ -624,9 +649,10 @@ impl Collection { notetype: &mut Notetype, original: Option, usn: Usn, + skip_checks: bool, ) -> Result<()> { let normalize = self.get_config_bool(BoolKey::NormalizeNoteText); - notetype.prepare_for_update(original.as_ref())?; + notetype.prepare_for_update(original.as_ref(), skip_checks)?; self.ensure_notetype_name_unique(notetype, usn)?; if let Some(original) = original { @@ -671,7 +697,7 @@ impl Collection { let all = self.storage.get_all_notetype_names()?; if all.is_empty() { let mut nt = all_stock_notetypes(&self.tr).remove(0); - self.add_notetype_inner(&mut nt, self.usn()?)?; + self.add_notetype_inner(&mut nt, self.usn()?, true)?; self.set_current_notetype_id(nt.id) } else { self.set_current_notetype_id(all[0].0) diff --git a/rslib/src/notetype/notetypechange.rs b/rslib/src/notetype/notetypechange.rs index a0904dfb9..b173af8d3 100644 --- a/rslib/src/notetype/notetypechange.rs +++ b/rslib/src/notetype/notetypechange.rs @@ -394,7 +394,7 @@ mod test { basic.add_field("Text"); // 4 basic.add_field("idx5"); // re-fetch to get ordinals - col.update_notetype(&mut basic)?; + col.update_notetype(&mut basic, false)?; let basic = col.get_notetype(basic.id)?.unwrap(); // if names match, assignments are out of order; unmatched entries diff --git a/rslib/src/notetype/schemachange.rs b/rslib/src/notetype/schemachange.rs index b940f401f..88e065005 100644 --- a/rslib/src/notetype/schemachange.rs +++ b/rslib/src/notetype/schemachange.rs @@ -258,13 +258,13 @@ mod test { col.add_note(&mut note, DeckId(1))?; nt.add_field("three"); - col.update_notetype(&mut nt)?; + col.update_notetype(&mut nt, false)?; let note = col.storage.get_note(note.id)?.unwrap(); assert_eq!(note.fields(), &["one".to_string(), "two".into(), "".into()]); nt.fields.remove(1); - col.update_notetype(&mut nt)?; + col.update_notetype(&mut nt, false)?; let note = col.storage.get_note(note.id)?.unwrap(); assert_eq!(note.fields(), &["one".to_string(), "".into()]); @@ -281,13 +281,13 @@ mod test { .unwrap(); nt.templates[0].config.q_format += "\n{{#Front}}{{some:Front}}{{Back}}{{/Front}}"; nt.fields[0].name = "Test".into(); - col.update_notetype(&mut nt)?; + col.update_notetype(&mut nt, false)?; assert_eq!( &nt.templates[0].config.q_format, "{{Test}}\n{{#Test}}{{some:Test}}{{Back}}{{/Test}}" ); nt.fields.remove(0); - col.update_notetype(&mut nt)?; + col.update_notetype(&mut nt, false)?; assert_eq!(&nt.templates[0].config.q_format, "\n{{Back}}"); Ok(()) @@ -313,7 +313,7 @@ mod test { // add an extra card template nt.add_template("card 2", "{{Front}}2", ""); - col.update_notetype(&mut nt)?; + col.update_notetype(&mut nt, false)?; assert_eq!( col.search_cards(note.id, SortMode::NoOrder).unwrap().len(), diff --git a/rslib/src/notetype/stock.rs b/rslib/src/notetype/stock.rs index 268ec485d..58619889b 100644 --- a/rslib/src/notetype/stock.rs +++ b/rslib/src/notetype/stock.rs @@ -15,7 +15,7 @@ use crate::{ impl SqliteStorage { pub(crate) fn add_stock_notetypes(&self, tr: &I18n) -> Result<()> { for (idx, mut nt) in all_stock_notetypes(tr).into_iter().enumerate() { - nt.prepare_for_update(None)?; + nt.prepare_for_update(None, true)?; self.add_notetype(&mut nt)?; if idx == Kind::Basic as usize { self.set_config_entry(&ConfigEntry::boxed( diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index c802f2bc2..e6c538326 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -1386,7 +1386,7 @@ mod test { // and a new notetype let mut nt = all_stock_notetypes(&col1.tr).remove(0); nt.name = "new".into(); - col1.add_notetype(&mut nt)?; + col1.add_notetype(&mut nt, false)?; // add another note+card+tag let mut note = nt.new_note(); @@ -1502,7 +1502,7 @@ mod test { let mut nt = col2.storage.get_notetype(nt.id)?.unwrap(); nt.name = "newer".into(); - col2.update_notetype(&mut nt)?; + col2.update_notetype(&mut nt, false)?; // sync the changes back let out = ctx.normal_sync(&mut col2).await; From d6e5f3c67c548d52ed6e501e0efbd6f0527a3579 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 28 Jul 2021 22:32:38 +0200 Subject: [PATCH 4/5] Allow duplicate templates if `{{Card}}` is used --- rslib/src/notetype/mod.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 82b59d910..f3bf11994 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -12,6 +12,7 @@ mod stock; mod templates; pub(crate) mod undo; +use regex::Regex; use std::{ collections::{HashMap, HashSet}, iter::FromIterator, @@ -332,21 +333,24 @@ impl Notetype { } fn ensure_template_fronts_unique(&self) -> Result<()> { - let mut map = HashMap::new(); - if let Some((index_1, index_2)) = - self.templates.iter().enumerate().find_map(|(index, card)| { - map.insert(&card.config.q_format, index) - .map(|old_index| (old_index, index)) - }) - { - Err(AnkiError::TemplateSaveError(TemplateSaveError { - notetype: self.name.clone(), - ordinal: index_2, - details: TemplateSaveErrorDetails::Duplicate(index_1), - })) - } else { - Ok(()) + lazy_static! { + static ref CARD_TAG: Regex = Regex::new(r"\{\{\s*Card\s*\}\}").unwrap(); } + + let mut map = HashMap::new(); + for (index, card) in self.templates.iter().enumerate() { + if let Some(old_index) = map.insert(&card.config.q_format, index) { + if !CARD_TAG.is_match(&card.config.q_format) { + return Err(AnkiError::TemplateSaveError(TemplateSaveError { + notetype: self.name.clone(), + ordinal: index, + details: TemplateSaveErrorDetails::Duplicate(old_index), + })); + } + } + } + + Ok(()) } /// Ensure no templates are None, every front template contains at least one From d090bd7e21713a136ea9caa0bc3162bde0923c73 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 29 Jul 2021 08:30:49 +0200 Subject: [PATCH 5/5] Use existing 'no such field' tr string --- ftl/core/card-template-rendering.ftl | 3 --- rslib/src/template.rs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ftl/core/card-template-rendering.ftl b/ftl/core/card-template-rendering.ftl index 7141c35f1..bff3327a0 100644 --- a/ftl/core/card-template-rendering.ftl +++ b/ftl/core/card-template-rendering.ftl @@ -21,9 +21,6 @@ card-template-rendering-conditional-not-open = Found '{ $found }', but missing ' # when the user referenced a field that doesn't exist # eg, Found '{{Field}}', but there is not field called 'Field' card-template-rendering-no-such-field = Found '{ $found }', but there is no field called '{ $field }' -# when the user referenced a field that doesn't exist in a conditional, excepting card numbers like 'c1' -# eg, Found '{{#Field}}', but neither is it a card number nor is there a field called 'Field' -card-template-rendering-no-such-conditional = Found '{ $found }', but neither is it a card number nor is there a field called '{ $key }' # This message is shown when the front side of the card is blank, # either due to a badly-designed template, or because required fields # are missing. diff --git a/rslib/src/template.rs b/rslib/src/template.rs index 628bf1c26..9628033df 100644 --- a/rslib/src/template.rs +++ b/rslib/src/template.rs @@ -298,7 +298,7 @@ fn localized_template_error(tr: &I18n, err: TemplateError) -> String { .card_template_rendering_no_such_field(format!("{{{{{}{}}}}}", filters, field), field) .into(), TemplateError::NoSuchConditional(condition) => tr - .card_template_rendering_no_such_conditional( + .card_template_rendering_no_such_field( format!("{{{{{}}}}}", condition), &condition[1..], )