mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -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.models import NotetypeDict, NotetypeId, TemplateDict
|
||||||
from anki.utils import joinFields
|
from anki.utils import joinFields
|
||||||
|
|
||||||
DuplicateOrEmptyResult = _pb.NoteIsDuplicateOrEmptyOut.State
|
NoteFieldsCheckResult = DuplicateOrEmptyResult = _pb.NoteFieldsCheckOut.State
|
||||||
|
|
||||||
# types
|
# types
|
||||||
NoteId = NewType("NoteId", int)
|
NoteId = NewType("NoteId", int)
|
||||||
|
@ -190,12 +190,10 @@ class Note:
|
||||||
addTag = add_tag
|
addTag = add_tag
|
||||||
delTag = remove_tag
|
delTag = remove_tag
|
||||||
|
|
||||||
# Unique/duplicate check
|
# Unique/duplicate/cloze check
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def duplicate_or_empty(self) -> DuplicateOrEmptyResult.V:
|
def fields_check(self) -> NoteFieldsCheckResult.V:
|
||||||
return self.col._backend.note_is_duplicate_or_empty(
|
return self.col._backend.note_fields_check(self._to_backend_note()).state
|
||||||
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 ClozeNumbersInNote(Note) returns (ClozeNumbersInNoteOut);
|
||||||
rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (OpChangesWithCount);
|
rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (OpChangesWithCount);
|
||||||
rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
|
rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
|
||||||
rpc NoteIsDuplicateOrEmpty(Note) returns (NoteIsDuplicateOrEmptyOut);
|
rpc NoteFieldsCheck(Note) returns (NoteFieldsCheckOut);
|
||||||
rpc CardsOfNote(NoteId) returns (CardIds);
|
rpc CardsOfNote(NoteId) returns (CardIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1231,11 +1231,14 @@ message ReparentDecksIn {
|
||||||
int64 new_parent = 2;
|
int64 new_parent = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NoteIsDuplicateOrEmptyOut {
|
message NoteFieldsCheckOut {
|
||||||
enum State {
|
enum State {
|
||||||
NORMAL = 0;
|
NORMAL = 0;
|
||||||
EMPTY = 1;
|
EMPTY = 1;
|
||||||
DUPLICATE = 2;
|
DUPLICATE = 2;
|
||||||
|
MISSING_CLOZE = 3;
|
||||||
|
NOTETYPE_NOT_CLOZE = 4;
|
||||||
|
FIELD_NOT_CLOZE = 5;
|
||||||
}
|
}
|
||||||
State state = 1;
|
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();
|
let note: Note = input.into();
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.note_is_duplicate_or_empty(¬e)
|
col.note_fields_check(¬e)
|
||||||
.map(|r| pb::NoteIsDuplicateOrEmptyOut { state: r as i32 })
|
.map(|r| pb::NoteFieldsCheckOut { state: r as i32 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,10 @@ pub fn expand_clozes_to_reveal_latex(text: &str) -> String {
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn contains_cloze(text: &str) -> bool {
|
||||||
|
CLOZE.is_match(text)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cloze_numbers_in_string(html: &str) -> HashSet<u16> {
|
pub fn cloze_numbers_in_string(html: &str) -> HashSet<u16> {
|
||||||
let mut set = HashSet::with_capacity(4);
|
let mut set = HashSet::with_capacity(4);
|
||||||
add_cloze_numbers_in_string(html, &mut set);
|
add_cloze_numbers_in_string(html, &mut set);
|
||||||
|
|
|
@ -14,7 +14,8 @@ use num_integer::Integer;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backend_proto as pb,
|
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,
|
decks::DeckId,
|
||||||
define_newtype,
|
define_newtype,
|
||||||
error::{AnkiError, Result},
|
error::{AnkiError, Result},
|
||||||
|
@ -529,35 +530,71 @@ impl Collection {
|
||||||
Ok(changed_notes)
|
Ok(changed_notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn note_is_duplicate_or_empty(&self, note: &Note) -> Result<DuplicateState> {
|
/// Check if the note's first field is empty or a duplicate. Then for cloze
|
||||||
if let Some(field1) = note.fields.get(0) {
|
/// 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) {
|
let field1 = if self.get_config_bool(BoolKey::NormalizeNoteText) {
|
||||||
normalize_to_nfc(field1)
|
normalize_to_nfc(text)
|
||||||
} else {
|
} else {
|
||||||
field1.into()
|
text.into()
|
||||||
};
|
};
|
||||||
let stripped = strip_html_preserving_media_filenames(&field1);
|
let stripped = strip_html_preserving_media_filenames(&field1);
|
||||||
if stripped.trim().is_empty() {
|
if stripped.trim().is_empty() {
|
||||||
Ok(DuplicateState::Empty)
|
Ok(NoteFieldsState::Empty)
|
||||||
|
} else if self.is_duplicate(&stripped, note)? {
|
||||||
|
Ok(NoteFieldsState::Duplicate)
|
||||||
} else {
|
} else {
|
||||||
let csum = field_checksum(&stripped);
|
self.field_cloze_check(note)
|
||||||
let have_dupe = self
|
}
|
||||||
|
} else {
|
||||||
|
Ok(NoteFieldsState::Empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_duplicate(&self, first_field: &str, note: &Note) -> Result<bool> {
|
||||||
|
let csum = field_checksum(&first_field);
|
||||||
|
Ok(self
|
||||||
.storage
|
.storage
|
||||||
.note_fields_by_checksum(note.notetype_id, csum)?
|
.note_fields_by_checksum(note.notetype_id, csum)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|(nid, field)| {
|
.any(|(nid, field)| {
|
||||||
nid != note.id && strip_html_preserving_media_filenames(&field) == stripped
|
nid != note.id && strip_html_preserving_media_filenames(&field) == first_field
|
||||||
});
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
if have_dupe {
|
fn field_cloze_check(&mut self, note: &Note) -> Result<NoteFieldsState> {
|
||||||
Ok(DuplicateState::Duplicate)
|
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 {
|
} else {
|
||||||
Ok(DuplicateState::Normal)
|
Some(NoteFieldsState::FieldNotCloze)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(DuplicateState::Empty)
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -548,6 +548,22 @@ impl Notetype {
|
||||||
pub(crate) fn is_cloze(&self) -> bool {
|
pub(crate) fn is_cloze(&self) -> bool {
|
||||||
matches!(self.config.kind(), NotetypeKind::Cloze)
|
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.
|
/// 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