mirror of
https://github.com/ankitects/anki.git
synced 2025-11-06 20:57:13 -05:00
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:
parent
935fbb6289
commit
2e923db6bd
4 changed files with 91 additions and 39 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue