Add checks for parsed templates

Combine existing check for unparsable templates with a check for unknown
field names and a check for front sides without any field replacement.
Updating the notetype's fields now mutates the parsed templates, so the
checks can run on the final templates.
This commit is contained in:
RumovZ 2021-05-28 10:07:31 +02:00
parent 935fbb6289
commit 2e923db6bd
4 changed files with 91 additions and 39 deletions

View file

@ -22,6 +22,7 @@ card-templates-add-mobile-class = Add Mobile Class
card-templates-preview-settings = Options card-templates-preview-settings = Options
card-templates-invalid-template-number = Card template { $number } in notetype '{ $notetype }' has a problem. card-templates-invalid-template-number = Card template { $number } in notetype '{ $notetype }' has a problem.
card-templates-identical-front = The front side is identical to card template { $number }. card-templates-identical-front = The front side is identical to card template { $number }.
card-templates-no-front-field = Expected to find a field replacement on the front of the card template.
card-templates-missing-cloze = Expected to find '{ "{{" }cloze:Text{ "}}" }' or similar on the front and back of the card template. card-templates-missing-cloze = Expected to find '{ "{{" }cloze:Text{ "}}" }' or similar on the front and back of the card template.
card-templates-extraneous-cloze = 'cloze:' can only be used on cloze notetypes. card-templates-extraneous-cloze = 'cloze:' can only be used on cloze notetypes.
card-templates-see-preview = See the preview for more information. card-templates-see-preview = See the preview for more information.

View file

@ -65,7 +65,9 @@ impl AnkiError {
let header = let header =
tr.card_templates_invalid_template_number(err.ordinal + 1, &err.notetype); tr.card_templates_invalid_template_number(err.ordinal + 1, &err.notetype);
let details = match err.details { let details = match err.details {
TemplateSaveErrorDetails::TemplateError => tr.card_templates_see_preview(), TemplateSaveErrorDetails::TemplateError
| TemplateSaveErrorDetails::NoSuchField => tr.card_templates_see_preview(),
TemplateSaveErrorDetails::NoFrontField => tr.card_templates_no_front_field(),
TemplateSaveErrorDetails::Duplicate(i) => { TemplateSaveErrorDetails::Duplicate(i) => {
tr.card_templates_identical_front(i + 1) tr.card_templates_identical_front(i + 1)
} }
@ -154,6 +156,8 @@ pub struct TemplateSaveError {
pub enum TemplateSaveErrorDetails { pub enum TemplateSaveErrorDetails {
TemplateError, TemplateError,
Duplicate(usize), Duplicate(usize),
NoFrontField,
NoSuchField,
MissingCloze, MissingCloze,
ExtraneousCloze, ExtraneousCloze,
} }

View file

@ -290,6 +290,63 @@ impl Notetype {
} }
} }
/// Ensure no templates are None, every front template contains at least one
/// field, and all used field names belong to a field of this notetype.
fn ensure_valid_parsed_templates(
&self,
templates: &[(Option<ParsedTemplate>, Option<ParsedTemplate>)],
) -> Result<()> {
if let Some((invalid_index, details)) =
templates.iter().enumerate().find_map(|(index, sides)| {
if let (Some(q), Some(a)) = sides {
let q_fields = q.fields();
if q_fields.is_empty() {
Some((index, TemplateSaveErrorDetails::NoFrontField))
} else if self.unknown_field_name(q_fields.union(&a.fields())) {
Some((index, TemplateSaveErrorDetails::NoSuchField))
} else {
None
}
} else {
Some((index, TemplateSaveErrorDetails::TemplateError))
}
})
{
Err(AnkiError::TemplateSaveError(TemplateSaveError {
notetype: self.name.clone(),
ordinal: invalid_index,
details,
}))
} else {
Ok(())
}
}
/// True if any name in names does not belong to a field of this notetype.
fn unknown_field_name<T, I>(&self, names: T) -> bool
where
T: IntoIterator<Item = I>,
I: AsRef<str>,
{
let special_fields = [
"FrontSide",
"Card",
"CardFlag",
"Deck",
"Subdeck",
"Tags",
"Type",
];
names.into_iter().any(|name| {
!special_fields.contains(&name.as_ref())
&& self
.fields
.iter()
.map(|field| &field.name)
.all(|field_name| field_name != name.as_ref())
})
}
fn ensure_cloze_if_and_only_if_cloze_notetype( fn ensure_cloze_if_and_only_if_cloze_notetype(
&self, &self,
parsed_templates: &[(Option<ParsedTemplate>, Option<ParsedTemplate>)], parsed_templates: &[(Option<ParsedTemplate>, Option<ParsedTemplate>)],
@ -354,37 +411,21 @@ impl Notetype {
self.fix_template_names()?; self.fix_template_names()?;
self.ensure_names_unique(); self.ensure_names_unique();
self.reposition_sort_idx(); self.reposition_sort_idx();
self.ensure_template_fronts_unique()?;
let parsed_templates = self.parsed_templates(); let mut parsed_templates = self.parsed_templates();
let invalid_card_idx = parsed_templates
.iter()
.enumerate()
.find_map(|(idx, (q, a))| {
if q.is_none() || a.is_none() {
Some(idx)
} else {
None
}
});
if let Some(idx) = invalid_card_idx {
return Err(AnkiError::TemplateSaveError(TemplateSaveError {
notetype: self.name.clone(),
ordinal: idx,
details: TemplateSaveErrorDetails::TemplateError,
}));
}
self.ensure_cloze_if_and_only_if_cloze_notetype(&parsed_templates)?;
let reqs = self.updated_requirements(&parsed_templates); let reqs = self.updated_requirements(&parsed_templates);
// handle renamed+deleted fields // handle renamed+deleted fields
if let Some(existing) = existing { if let Some(existing) = existing {
let fields = self.renamed_and_removed_fields(existing); let fields = self.renamed_and_removed_fields(existing);
if !fields.is_empty() { if !fields.is_empty() {
self.update_templates_for_renamed_and_removed_fields(fields, parsed_templates); self.update_templates_for_renamed_and_removed_fields(fields, &mut parsed_templates);
} }
} }
self.config.reqs = reqs; self.config.reqs = reqs;
self.ensure_template_fronts_unique()?;
self.ensure_valid_parsed_templates(&parsed_templates)?;
self.ensure_cloze_if_and_only_if_cloze_notetype(&parsed_templates)?;
Ok(()) Ok(())
} }
@ -422,16 +463,16 @@ impl Notetype {
fn update_templates_for_renamed_and_removed_fields( fn update_templates_for_renamed_and_removed_fields(
&mut self, &mut self,
fields: HashMap<String, Option<String>>, fields: HashMap<String, Option<String>>,
parsed: Vec<(Option<ParsedTemplate>, Option<ParsedTemplate>)>, parsed: &mut [(Option<ParsedTemplate>, Option<ParsedTemplate>)],
) { ) {
for (idx, (q, a)) in parsed.into_iter().enumerate() { for (idx, (q_opt, a_opt)) in parsed.iter_mut().enumerate() {
if let Some(q) = q { if let Some(q) = q_opt {
let updated = q.rename_and_remove_fields(&fields); q.rename_and_remove_fields(&fields);
self.templates[idx].config.q_format = updated.template_to_string(); self.templates[idx].config.q_format = q.template_to_string();
} }
if let Some(a) = a { if let Some(a) = a_opt {
let updated = a.rename_and_remove_fields(&fields); a.rename_and_remove_fields(&fields);
self.templates[idx].config.a_format = updated.template_to_string(); self.templates[idx].config.a_format = a.template_to_string();
} }
} }
} }

View file

@ -655,12 +655,9 @@ impl ParsedTemplate {
impl ParsedTemplate { impl ParsedTemplate {
/// Given a map of old to new field names, update references to the new names. /// Given a map of old to new field names, update references to the new names.
/// Returns true if any changes made. /// Returns true if any changes made.
pub(crate) fn rename_and_remove_fields( pub(crate) fn rename_and_remove_fields(&mut self, fields: &HashMap<String, Option<String>>) {
self, let old_nodes = std::mem::replace(&mut self.0, vec![]);
fields: &HashMap<String, Option<String>>, self.0 = rename_and_remove_fields(old_nodes, fields);
) -> ParsedTemplate {
let out = rename_and_remove_fields(self.0, fields);
ParsedTemplate(out)
} }
} }
@ -765,25 +762,34 @@ fn nodes_to_string(buf: &mut String, nodes: &[ParsedNode]) {
//---------------------------------------- //----------------------------------------
impl ParsedTemplate { impl ParsedTemplate {
/// A set of all field names. Field names may not be valid.
pub(crate) fn fields(&self) -> HashSet<&str> {
let mut set = HashSet::new();
find_fields_with_filter(&self.0, &mut set, None);
set
}
/// A set of field names with a cloze filter attached. /// A set of field names with a cloze filter attached.
/// Field names may not be valid. /// Field names may not be valid.
pub(crate) fn cloze_fields(&self) -> HashSet<&str> { pub(crate) fn cloze_fields(&self) -> HashSet<&str> {
let mut set = HashSet::new(); let mut set = HashSet::new();
find_fields_with_filter(&self.0, &mut set, "cloze"); find_fields_with_filter(&self.0, &mut set, Some("cloze"));
set set
} }
} }
/// Insert all fields in 'nodes' with 'filter' into 'fields'. If 'filter' is None,
/// all fields are collected.
fn find_fields_with_filter<'a>( fn find_fields_with_filter<'a>(
nodes: &'a [ParsedNode], nodes: &'a [ParsedNode],
fields: &mut HashSet<&'a str>, fields: &mut HashSet<&'a str>,
filter: &str, filter: Option<&str>,
) { ) {
for node in nodes { for node in nodes {
match node { match node {
ParsedNode::Text(_) => {} ParsedNode::Text(_) => {}
ParsedNode::Replacement { key, filters } => { ParsedNode::Replacement { key, filters } => {
if filters.iter().any(|f| f == filter) { if filter.is_none() || filters.iter().any(|f| f == filter.unwrap()) {
fields.insert(key); fields.insert(key);
} }
} }