Add check for out-of-place/missing clozes

This commit is contained in:
RumovZ 2021-06-12 10:02:21 +02:00
parent 968bd1b27a
commit aeedb4dc11
6 changed files with 91 additions and 33 deletions

View file

@ -14,7 +14,7 @@ from anki.consts import MODEL_STD
from anki.models import NotetypeDict, NotetypeId, TemplateDict
from anki.utils import joinFields
DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State
NoteFieldsCheckResult = DuplicateOrEmptyResult = _pb.NoteFieldsCheckOut.State
# types
NoteId = NewType("NoteId", int)
@ -190,12 +190,10 @@ class Note:
addTag = add_tag
delTag = remove_tag
# Unique/duplicate check
# Unique/duplicate/cloze check
##################################################
def duplicate_or_empty(self) -> DuplicateOrEmptyResult.V:
return self.col._backend.note_is_duplicate_or_empty(
self._to_backend_note()
).state
def fields_check(self) -> NoteFieldsCheckResult.V:
return self.col._backend.note_fields_check(self._to_backend_note()).state
dupeOrEmpty = duplicate_or_empty
dupeOrEmpty = duplicate_or_empty = fields_check

View file

@ -174,7 +174,7 @@ service NotesService {
rpc ClozeNumbersInNote(Note) returns (ClozeNumbersInNoteOut);
rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (OpChangesWithCount);
rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
rpc NoteIsDuplicateOrEmpty(Note) returns (NoteIsDuplicateOrEmptyOut);
rpc NoteFieldsCheck(Note) returns (NoteFieldsCheckOut);
rpc CardsOfNote(NoteId) returns (CardIds);
}
@ -1231,11 +1231,14 @@ message ReparentDecksIn {
int64 new_parent = 2;
}
message NoteIsDuplicateOrEmptyOut {
message NoteFieldsCheckOut {
enum State {
NORMAL = 0;
EMPTY = 1;
DUPLICATE = 2;
MISSING_CLOZE = 3;
NOTETYPE_NOT_CLOZE = 4;
FIELD_NOT_CLOZE = 5;
}
State state = 1;
}

View file

@ -120,11 +120,11 @@ impl NotesService for Backend {
})
}
fn note_is_duplicate_or_empty(&self, input: pb::Note) -> Result<pb::NoteIsDuplicateOrEmptyOut> {
fn note_fields_check(&self, input: pb::Note) -> Result<pb::NoteFieldsCheckOut> {
let note: Note = input.into();
self.with_col(|col| {
col.note_is_duplicate_or_empty(&note)
.map(|r| pb::NoteIsDuplicateOrEmptyOut { state: r as i32 })
col.note_fields_check(&note)
.map(|r| pb::NoteFieldsCheckOut { state: r as i32 })
})
}

View file

@ -139,6 +139,10 @@ pub fn expand_clozes_to_reveal_latex(text: &str) -> String {
buf
}
pub(crate) fn contains_cloze(text: &str) -> bool {
CLOZE.is_match(text)
}
pub fn cloze_numbers_in_string(html: &str) -> HashSet<u16> {
let mut set = HashSet::with_capacity(4);
add_cloze_numbers_in_string(html, &mut set);

View file

@ -14,7 +14,8 @@ use num_integer::Integer;
use crate::{
backend_proto as pb,
backend_proto::note_is_duplicate_or_empty_out::State as DuplicateState,
backend_proto::note_fields_check_out::State as NoteFieldsState,
cloze::contains_cloze,
decks::DeckId,
define_newtype,
error::{AnkiError, Result},
@ -529,36 +530,72 @@ impl Collection {
Ok(changed_notes)
}
pub(crate) fn note_is_duplicate_or_empty(&self, note: &Note) -> Result<DuplicateState> {
if let Some(field1) = note.fields.get(0) {
/// Check if the note's first field is empty or a duplicate. Then for cloze
/// notetypes, check if there is a cloze in a non-cloze field or if there's
/// no cloze at all. For other notetypes, just check if there's a cloze.
pub(crate) fn note_fields_check(&mut self, note: &Note) -> Result<NoteFieldsState> {
if let Some(text) = note.fields.get(0) {
let field1 = if self.get_config_bool(BoolKey::NormalizeNoteText) {
normalize_to_nfc(field1)
normalize_to_nfc(text)
} else {
field1.into()
text.into()
};
let stripped = strip_html_preserving_media_filenames(&field1);
if stripped.trim().is_empty() {
Ok(DuplicateState::Empty)
Ok(NoteFieldsState::Empty)
} else if self.is_duplicate(&stripped, note)? {
Ok(NoteFieldsState::Duplicate)
} else {
let csum = field_checksum(&stripped);
let have_dupe = self
.storage
.note_fields_by_checksum(note.notetype_id, csum)?
.into_iter()
.any(|(nid, field)| {
nid != note.id && strip_html_preserving_media_filenames(&field) == stripped
});
if have_dupe {
Ok(DuplicateState::Duplicate)
} else {
Ok(DuplicateState::Normal)
}
self.field_cloze_check(note)
}
} else {
Ok(DuplicateState::Empty)
Ok(NoteFieldsState::Empty)
}
}
fn is_duplicate(&self, first_field: &str, note: &Note) -> Result<bool> {
let csum = field_checksum(&first_field);
Ok(self
.storage
.note_fields_by_checksum(note.notetype_id, csum)?
.into_iter()
.any(|(nid, field)| {
nid != note.id && strip_html_preserving_media_filenames(&field) == first_field
}))
}
fn field_cloze_check(&mut self, note: &Note) -> Result<NoteFieldsState> {
let notetype = self
.get_notetype(note.notetype_id)?
.ok_or(AnkiError::NotFound)?;
let cloze_fields = notetype.cloze_fields();
let mut has_cloze = false;
let extraneous_cloze = note.fields.iter().enumerate().find_map(|(i, field)| {
if notetype.is_cloze() {
if contains_cloze(field) {
if cloze_fields.contains(&i) {
has_cloze = true;
None
} else {
Some(NoteFieldsState::FieldNotCloze)
}
} else {
None
}
} else if contains_cloze(field) {
Some(NoteFieldsState::NotetypeNotCloze)
} else {
None
}
});
Ok(if let Some(state) = extraneous_cloze {
state
} else if notetype.is_cloze() && !has_cloze {
NoteFieldsState::MissingCloze
} else {
NoteFieldsState::Normal
})
}
}
/// The existing note pulled from the DB will have sfld and csum set, but the

View file

@ -548,6 +548,22 @@ impl Notetype {
pub(crate) fn is_cloze(&self) -> bool {
matches!(self.config.kind(), NotetypeKind::Cloze)
}
/// Return all clozable fields. A field is clozable when it belongs to a cloze
/// notetype and a 'cloze' filter is applied to it in the template.
pub(crate) fn cloze_fields(&self) -> HashSet<usize> {
if !self.is_cloze() {
HashSet::new()
} else if let Some((Some(front), _)) = self.parsed_templates().get(0) {
front
.cloze_fields()
.iter()
.filter_map(|name| self.get_field_ord(name))
.collect()
} else {
HashSet::new()
}
}
}
/// True if the slice is empty or either template of the first tuple doesn't have a cloze field.