mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Add check for out-of-place/missing clozes
This commit is contained in:
parent
968bd1b27a
commit
aeedb4dc11
6 changed files with 91 additions and 33 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(¬e)
|
||||
.map(|r| pb::NoteIsDuplicateOrEmptyOut { state: r as i32 })
|
||||
col.note_fields_check(¬e)
|
||||
.map(|r| pb::NoteFieldsCheckOut { state: r as i32 })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue