mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -04:00
more work towards note adding
Still a prototype at this stage - we'll likely want a caching layer for note types, and I'm not sure of the merit of having fields in a separate table, since they're almost always required.
This commit is contained in:
parent
5d4f9dc3c0
commit
f75fd5335d
11 changed files with 247 additions and 27 deletions
|
@ -78,6 +78,7 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError {
|
|||
AnkiError::SchemaChange => V::InvalidInput(pb::Empty {}),
|
||||
AnkiError::JSONError { info } => V::JsonError(info),
|
||||
AnkiError::ProtoError { info } => V::ProtoError(info),
|
||||
AnkiError::NoCardsGenerated => todo!(),
|
||||
};
|
||||
|
||||
pb::BackendError {
|
||||
|
|
|
@ -99,6 +99,15 @@ impl Undoable for UpdateCardUndo {
|
|||
}
|
||||
}
|
||||
|
||||
impl Card {
|
||||
pub fn new(nid: NoteID, ord: u16, deck_id: DeckID) -> Self {
|
||||
let mut card = Card::default();
|
||||
card.nid = nid;
|
||||
card.ord = ord;
|
||||
card.did = deck_id;
|
||||
card
|
||||
}
|
||||
}
|
||||
impl Collection {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn get_and_update_card<F, T>(&mut self, cid: CardID, func: F) -> Result<Card>
|
||||
|
@ -127,7 +136,6 @@ impl Collection {
|
|||
self.storage.update_card(card)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn add_card(&mut self, card: &mut Card) -> Result<()> {
|
||||
if card.id.0 != 0 {
|
||||
return Err(AnkiError::invalid_input("card id already set"));
|
||||
|
|
|
@ -8,7 +8,7 @@ use std::io;
|
|||
|
||||
pub type Result<T> = std::result::Result<T, AnkiError>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
#[derive(Debug, Fail, PartialEq)]
|
||||
pub enum AnkiError {
|
||||
#[fail(display = "invalid input: {}", info)]
|
||||
InvalidInput { info: String },
|
||||
|
@ -48,6 +48,9 @@ pub enum AnkiError {
|
|||
|
||||
#[fail(display = "Operation modifies schema, but schema not marked modified.")]
|
||||
SchemaChange,
|
||||
|
||||
#[fail(display = "No cards generated.")]
|
||||
NoCardsGenerated,
|
||||
}
|
||||
|
||||
// error helpers
|
||||
|
|
|
@ -404,7 +404,7 @@ where
|
|||
&self.mgr.media_folder,
|
||||
)? {
|
||||
// note was modified, needs saving
|
||||
note.prepare_for_update(nt.config.sort_field_idx as usize, usn);
|
||||
note.prepare_for_update(nt, usn)?;
|
||||
self.ctx.storage.update_note(¬e)?;
|
||||
collection_modified = true;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::err::{AnkiError, Result};
|
||||
use crate::notetype::NoteTypeID;
|
||||
use crate::text::strip_html_preserving_image_filenames;
|
||||
use crate::timestamp::TimestampSecs;
|
||||
use crate::{collection::Collection, define_newtype, types::Usn};
|
||||
use crate::{
|
||||
card::Card,
|
||||
collection::Collection,
|
||||
define_newtype,
|
||||
err::{AnkiError, Result},
|
||||
notetype::{CardGenContext, NoteField, NoteType, NoteTypeID},
|
||||
text::strip_html_preserving_image_filenames,
|
||||
timestamp::TimestampSecs,
|
||||
types::Usn,
|
||||
};
|
||||
use num_integer::Integer;
|
||||
use std::{collections::HashSet, convert::TryInto};
|
||||
|
||||
|
@ -27,15 +32,15 @@ pub struct Note {
|
|||
}
|
||||
|
||||
impl Note {
|
||||
pub(crate) fn new(ntid: NoteTypeID, field_count: usize) -> Self {
|
||||
pub(crate) fn new(notetype: &NoteType) -> Self {
|
||||
Note {
|
||||
id: NoteID(0),
|
||||
guid: guid(),
|
||||
ntid,
|
||||
ntid: notetype.id,
|
||||
mtime: TimestampSecs(0),
|
||||
usn: Usn(0),
|
||||
tags: vec![],
|
||||
fields: vec!["".to_string(); field_count],
|
||||
fields: vec!["".to_string(); notetype.fields.len()],
|
||||
sort_field: None,
|
||||
checksum: None,
|
||||
}
|
||||
|
@ -57,15 +62,24 @@ impl Note {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepare_for_update(&mut self, sort_field_idx: usize, usn: Usn) {
|
||||
pub fn prepare_for_update(&mut self, nt: &NoteType, usn: Usn) -> Result<()> {
|
||||
assert!(nt.id == self.ntid);
|
||||
if nt.fields.len() != self.fields.len() {
|
||||
return Err(AnkiError::invalid_input(format!(
|
||||
"note has {} fields, expected {}",
|
||||
self.fields.len(),
|
||||
nt.fields.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let field1_nohtml = strip_html_preserving_image_filenames(&self.fields()[0]);
|
||||
let checksum = field_checksum(field1_nohtml.as_ref());
|
||||
let sort_field = if sort_field_idx == 0 {
|
||||
let sort_field = if nt.config.sort_field_idx == 0 {
|
||||
field1_nohtml
|
||||
} else {
|
||||
strip_html_preserving_image_filenames(
|
||||
self.fields
|
||||
.get(sort_field_idx)
|
||||
.get(nt.config.sort_field_idx as usize)
|
||||
.map(AsRef::as_ref)
|
||||
.unwrap_or(""),
|
||||
)
|
||||
|
@ -74,10 +88,10 @@ impl Note {
|
|||
self.checksum = Some(checksum);
|
||||
self.mtime = TimestampSecs::now();
|
||||
self.usn = usn;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn nonempty_fields(&self) -> HashSet<u16> {
|
||||
pub(crate) fn nonempty_fields<'a>(&self, fields: &'a [NoteField]) -> HashSet<&'a str> {
|
||||
self.fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
@ -85,7 +99,7 @@ impl Note {
|
|||
if s.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ord as u16)
|
||||
fields.get(ord).map(|f| f.name.as_str())
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
@ -105,7 +119,7 @@ fn guid() -> String {
|
|||
|
||||
fn anki_base91(mut n: u64) -> String {
|
||||
let table = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\
|
||||
0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~";
|
||||
0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~";
|
||||
let mut buf = String::new();
|
||||
while n > 0 {
|
||||
let (q, r) = n.div_rem(&(table.len() as u64));
|
||||
|
@ -119,17 +133,50 @@ fn anki_base91(mut n: u64) -> String {
|
|||
impl Collection {
|
||||
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
|
||||
self.transact(None, |col| {
|
||||
println!("fixme: need to add cards, abort if no cards generated, etc");
|
||||
// fixme: proper index
|
||||
note.prepare_for_update(0, col.usn()?);
|
||||
col.storage.add_note(note)
|
||||
let nt = col
|
||||
.storage
|
||||
.get_full_notetype(note.ntid)?
|
||||
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
||||
|
||||
let cardgen = CardGenContext::new(&nt, col.usn()?);
|
||||
col.add_note_inner(note, &cardgen)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn add_note_inner(
|
||||
&mut self,
|
||||
note: &mut Note,
|
||||
cardgen: &CardGenContext,
|
||||
) -> Result<()> {
|
||||
let nt = cardgen.notetype;
|
||||
note.prepare_for_update(nt, cardgen.usn)?;
|
||||
let nonempty_fields = note.nonempty_fields(&cardgen.notetype.fields);
|
||||
let cards =
|
||||
cardgen.new_cards_required(&nonempty_fields, nt.target_deck_id(), &Default::default());
|
||||
if cards.is_empty() {
|
||||
return Err(AnkiError::NoCardsGenerated);
|
||||
}
|
||||
|
||||
// add the note
|
||||
self.storage.add_note(note)?;
|
||||
// and its associated cards
|
||||
for (card_ord, target_deck_id) in cards {
|
||||
let mut card = Card::new(note.id, card_ord as u16, target_deck_id);
|
||||
self.add_card(&mut card)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{anki_base91, field_checksum};
|
||||
use crate::{
|
||||
collection::open_test_collection,
|
||||
err::{AnkiError, Result},
|
||||
search::SortMode,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_base91() {
|
||||
|
@ -145,4 +192,29 @@ mod test {
|
|||
assert_eq!(field_checksum("test"), 2840236005);
|
||||
assert_eq!(field_checksum("今日"), 1464653051);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding() -> Result<()> {
|
||||
let mut col = open_test_collection();
|
||||
let nt = col.get_notetype_by_name("basic")?.unwrap();
|
||||
|
||||
let mut note = nt.new_note();
|
||||
assert_eq!(col.add_note(&mut note), Err(AnkiError::NoCardsGenerated));
|
||||
|
||||
note.fields[1] = "foo".into();
|
||||
assert_eq!(col.add_note(&mut note), Err(AnkiError::NoCardsGenerated));
|
||||
|
||||
note.fields[0] = "bar".into();
|
||||
col.add_note(&mut note).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
col.search_cards(&format!("nid:{}", note.id), SortMode::NoOrder)
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
// fixme: add nt cache, refcount
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
85
rslib/src/notetype/cardgeninfo.rs
Normal file
85
rslib/src/notetype/cardgeninfo.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::NoteType;
|
||||
use crate::{decks::DeckID, template::ParsedTemplate, types::Usn};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Info required to determine whether a particular card ordinal should exist,
|
||||
/// and which deck it should be placed in.
|
||||
pub(crate) struct SingleCardGenContext<'a> {
|
||||
template: Option<ParsedTemplate<'a>>,
|
||||
target_deck_id: Option<DeckID>,
|
||||
}
|
||||
|
||||
/// Info required to determine which cards should be generated when note added/updated,
|
||||
/// and where they should be placed.
|
||||
pub(crate) struct CardGenContext<'a> {
|
||||
pub usn: Usn,
|
||||
pub notetype: &'a NoteType,
|
||||
cards: Vec<SingleCardGenContext<'a>>,
|
||||
}
|
||||
|
||||
impl CardGenContext<'_> {
|
||||
pub(crate) fn new(nt: &NoteType, usn: Usn) -> CardGenContext<'_> {
|
||||
CardGenContext {
|
||||
usn,
|
||||
notetype: &nt,
|
||||
cards: nt
|
||||
.templates
|
||||
.iter()
|
||||
.map(|tmpl| SingleCardGenContext {
|
||||
template: tmpl.parsed_question(),
|
||||
target_deck_id: tmpl.target_deck_id(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// If template[ord] generates a non-empty question given nonempty_fields, return the provided
|
||||
/// deck id, or an overriden one. If question is empty, return None.
|
||||
pub fn deck_id_if_nonempty(
|
||||
&self,
|
||||
card_ord: usize,
|
||||
nonempty_fields: &HashSet<&str>,
|
||||
target_deck_id: DeckID,
|
||||
) -> Option<DeckID> {
|
||||
let card = &self.cards[card_ord];
|
||||
let template = match card.template {
|
||||
Some(ref template) => template,
|
||||
None => {
|
||||
// template failed to parse; card can not be generated
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if template.renders_with_fields(&nonempty_fields) {
|
||||
Some(card.target_deck_id.unwrap_or(target_deck_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a list of (ordinal, deck id) for any new cards not in existing_ords
|
||||
/// that are non-empty, and thus need to be added.
|
||||
pub fn new_cards_required(
|
||||
&self,
|
||||
nonempty_fields: &HashSet<&str>,
|
||||
target_deck_id: DeckID,
|
||||
existing_ords: &HashSet<u16>,
|
||||
) -> Vec<(usize, DeckID)> {
|
||||
self.cards
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ord, card)| {
|
||||
let deck_id = card.target_deck_id.unwrap_or(target_deck_id);
|
||||
if existing_ords.contains(&(ord as u16)) {
|
||||
None
|
||||
} else {
|
||||
self.deck_id_if_nonempty(ord, nonempty_fields, deck_id)
|
||||
.map(|did| (ord, did))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use crate::backend_proto::{NoteField as NoteFieldProto, NoteFieldConfig, OptionalUInt32};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoteField {
|
||||
pub ord: Option<u32>,
|
||||
pub name: String,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
mod cardgeninfo;
|
||||
mod fields;
|
||||
mod schema11;
|
||||
mod schemachange;
|
||||
|
@ -11,6 +12,7 @@ pub use crate::backend_proto::{
|
|||
card_requirement::CardRequirementKind, CardRequirement, CardTemplateConfig, NoteFieldConfig,
|
||||
NoteType as NoteTypeProto, NoteTypeConfig, NoteTypeKind,
|
||||
};
|
||||
pub(crate) use cardgeninfo::CardGenContext;
|
||||
pub use fields::NoteField;
|
||||
pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11};
|
||||
pub use stock::all_stock_notetypes;
|
||||
|
@ -18,6 +20,7 @@ pub use templates::CardTemplate;
|
|||
|
||||
use crate::{
|
||||
collection::Collection,
|
||||
decks::DeckID,
|
||||
define_newtype,
|
||||
err::{AnkiError, Result},
|
||||
notes::Note,
|
||||
|
@ -35,6 +38,7 @@ pub(crate) const DEFAULT_CSS: &str = include_str!("styling.css");
|
|||
pub(crate) const DEFAULT_LATEX_HEADER: &str = include_str!("header.tex");
|
||||
pub(crate) const DEFAULT_LATEX_FOOTER: &str = r"\end{document}";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NoteType {
|
||||
pub id: NoteTypeID,
|
||||
pub name: String,
|
||||
|
@ -165,7 +169,11 @@ impl NoteType {
|
|||
}
|
||||
|
||||
pub fn new_note(&self) -> Note {
|
||||
Note::new(self.id, self.fields.len())
|
||||
Note::new(&self)
|
||||
}
|
||||
|
||||
pub fn target_deck_id(&self) -> DeckID {
|
||||
DeckID(self.config.target_deck_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,4 +202,8 @@ impl Collection {
|
|||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_notetype_by_name(&mut self, name: &str) -> Result<Option<NoteType>> {
|
||||
self.storage.get_notetype_by_name(name)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ impl Collection {
|
|||
})
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
note.prepare_for_update(nt.config.sort_field_idx as usize, usn);
|
||||
note.prepare_for_update(nt, usn)?;
|
||||
self.storage.update_note(¬e)?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -3,10 +3,13 @@
|
|||
|
||||
use crate::{
|
||||
backend_proto::{CardTemplate as CardTemplateProto, CardTemplateConfig, OptionalUInt32},
|
||||
decks::DeckID,
|
||||
template::ParsedTemplate,
|
||||
timestamp::TimestampSecs,
|
||||
types::Usn,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CardTemplate {
|
||||
pub ord: Option<u32>,
|
||||
pub mtime_secs: TimestampSecs,
|
||||
|
@ -15,6 +18,20 @@ pub struct CardTemplate {
|
|||
pub config: CardTemplateConfig,
|
||||
}
|
||||
|
||||
impl CardTemplate {
|
||||
pub(crate) fn parsed_question(&self) -> Option<ParsedTemplate<'_>> {
|
||||
ParsedTemplate::from_text(&self.config.q_format).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn target_deck_id(&self) -> Option<DeckID> {
|
||||
if self.config.target_deck_id > 0 {
|
||||
Some(DeckID(self.config.target_deck_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardTemplate> for CardTemplateProto {
|
||||
fn from(t: CardTemplate) -> Self {
|
||||
CardTemplateProto {
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
timestamp::TimestampMillis,
|
||||
};
|
||||
use prost::Message;
|
||||
use rusqlite::{params, Row, NO_PARAMS};
|
||||
use rusqlite::{params, OptionalExtension, Row, NO_PARAMS};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use unicase::UniCase;
|
||||
|
||||
|
@ -36,11 +36,16 @@ impl SqliteStorage {
|
|||
}
|
||||
|
||||
pub(crate) fn get_all_notetype_core(&self) -> Result<HashMap<NoteTypeID, NoteType>> {
|
||||
self.db
|
||||
let mut nts: HashMap<NoteTypeID, NoteType> = self
|
||||
.db
|
||||
.prepare_cached(include_str!("get_notetype.sql"))?
|
||||
.query_and_then(NO_PARAMS, row_to_notetype_core)?
|
||||
.map(|ntres| ntres.map(|nt| (nt.id, nt)))
|
||||
.collect()
|
||||
.collect::<Result<_>>()?;
|
||||
for nt in nts.values_mut() {
|
||||
nt.fields = self.get_notetype_fields(nt.id)?;
|
||||
}
|
||||
Ok(nts)
|
||||
}
|
||||
|
||||
pub(crate) fn get_notetype_fields(&self, ntid: NoteTypeID) -> Result<Vec<NoteField>> {
|
||||
|
@ -84,6 +89,22 @@ impl SqliteStorage {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_notetype_id(&self, name: &str) -> Result<Option<NoteTypeID>> {
|
||||
self.db
|
||||
.prepare_cached("select id from notetypes where name = ?")?
|
||||
.query_row(params![name], |row| row.get(0))
|
||||
.optional()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn get_notetype_by_name(&mut self, name: &str) -> Result<Option<NoteType>> {
|
||||
if let Some(id) = self.get_notetype_id(name)? {
|
||||
self.get_full_notetype(id)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get_all_notetype_names(&self) -> Result<Vec<(NoteTypeID, String)>> {
|
||||
self.db
|
||||
|
|
Loading…
Reference in a new issue