2020-02-10 05:19:39 +01:00
|
|
|
// Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
2020-04-17 06:36:45 +02:00
|
|
|
use crate::{
|
|
|
|
collection::Collection,
|
|
|
|
define_newtype,
|
|
|
|
err::{AnkiError, Result},
|
|
|
|
notetype::{CardGenContext, NoteField, NoteType, NoteTypeID},
|
|
|
|
text::strip_html_preserving_image_filenames,
|
|
|
|
timestamp::TimestampSecs,
|
|
|
|
types::Usn,
|
|
|
|
};
|
2020-04-16 09:02:59 +02:00
|
|
|
use num_integer::Integer;
|
|
|
|
use std::{collections::HashSet, convert::TryInto};
|
2020-02-10 05:19:39 +01:00
|
|
|
|
2020-03-26 05:42:43 +01:00
|
|
|
define_newtype!(NoteID, i64);
|
|
|
|
|
2020-04-14 07:41:01 +02:00
|
|
|
// fixme: ensure nulls and x1f not in field contents
|
|
|
|
|
2020-02-10 05:19:39 +01:00
|
|
|
#[derive(Debug)]
|
2020-04-14 07:41:01 +02:00
|
|
|
pub struct Note {
|
2020-03-26 05:42:43 +01:00
|
|
|
pub id: NoteID,
|
2020-04-14 07:41:01 +02:00
|
|
|
pub guid: String,
|
2020-03-26 06:00:24 +01:00
|
|
|
pub ntid: NoteTypeID,
|
2020-03-26 03:59:51 +01:00
|
|
|
pub mtime: TimestampSecs,
|
2020-02-10 05:19:39 +01:00
|
|
|
pub usn: Usn,
|
2020-04-14 07:41:01 +02:00
|
|
|
pub tags: Vec<String>,
|
2020-04-16 09:02:59 +02:00
|
|
|
pub(crate) fields: Vec<String>,
|
2020-04-14 07:41:01 +02:00
|
|
|
pub(crate) sort_field: Option<String>,
|
|
|
|
pub(crate) checksum: Option<u32>,
|
2020-02-10 05:19:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Note {
|
2020-04-17 06:36:45 +02:00
|
|
|
pub(crate) fn new(notetype: &NoteType) -> Self {
|
2020-04-16 09:02:59 +02:00
|
|
|
Note {
|
|
|
|
id: NoteID(0),
|
|
|
|
guid: guid(),
|
2020-04-17 06:36:45 +02:00
|
|
|
ntid: notetype.id,
|
2020-04-16 09:02:59 +02:00
|
|
|
mtime: TimestampSecs(0),
|
|
|
|
usn: Usn(0),
|
|
|
|
tags: vec![],
|
2020-04-17 06:36:45 +02:00
|
|
|
fields: vec!["".to_string(); notetype.fields.len()],
|
2020-04-16 09:02:59 +02:00
|
|
|
sort_field: None,
|
|
|
|
checksum: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-10 05:19:39 +01:00
|
|
|
pub fn fields(&self) -> &Vec<String> {
|
|
|
|
&self.fields
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_field(&mut self, idx: usize, text: impl Into<String>) -> Result<()> {
|
|
|
|
if idx >= self.fields.len() {
|
|
|
|
return Err(AnkiError::invalid_input(
|
|
|
|
"field idx out of range".to_string(),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
self.fields[idx] = text.into();
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2020-04-14 07:41:01 +02:00
|
|
|
|
2020-04-17 06:36:45 +02:00
|
|
|
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()
|
|
|
|
)));
|
|
|
|
}
|
|
|
|
|
2020-04-14 07:41:01 +02:00
|
|
|
let field1_nohtml = strip_html_preserving_image_filenames(&self.fields()[0]);
|
|
|
|
let checksum = field_checksum(field1_nohtml.as_ref());
|
2020-04-17 06:36:45 +02:00
|
|
|
let sort_field = if nt.config.sort_field_idx == 0 {
|
2020-04-14 07:41:01 +02:00
|
|
|
field1_nohtml
|
|
|
|
} else {
|
|
|
|
strip_html_preserving_image_filenames(
|
|
|
|
self.fields
|
2020-04-17 06:36:45 +02:00
|
|
|
.get(nt.config.sort_field_idx as usize)
|
2020-04-14 07:41:01 +02:00
|
|
|
.map(AsRef::as_ref)
|
|
|
|
.unwrap_or(""),
|
|
|
|
)
|
|
|
|
};
|
|
|
|
self.sort_field = Some(sort_field.into());
|
|
|
|
self.checksum = Some(checksum);
|
|
|
|
self.mtime = TimestampSecs::now();
|
|
|
|
self.usn = usn;
|
2020-04-17 06:36:45 +02:00
|
|
|
Ok(())
|
2020-04-14 07:41:01 +02:00
|
|
|
}
|
2020-04-16 09:02:59 +02:00
|
|
|
|
2020-04-17 06:36:45 +02:00
|
|
|
pub(crate) fn nonempty_fields<'a>(&self, fields: &'a [NoteField]) -> HashSet<&'a str> {
|
2020-04-16 09:02:59 +02:00
|
|
|
self.fields
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.filter_map(|(ord, s)| {
|
|
|
|
if s.trim().is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
2020-04-17 06:36:45 +02:00
|
|
|
fields.get(ord).map(|f| f.name.as_str())
|
2020-04-16 09:02:59 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
2020-02-10 05:19:39 +01:00
|
|
|
}
|
|
|
|
|
2020-03-17 05:52:55 +01:00
|
|
|
/// Text must be passed to strip_html_preserving_image_filenames() by
|
|
|
|
/// caller prior to passing in here.
|
2020-03-17 03:31:54 +01:00
|
|
|
pub(crate) fn field_checksum(text: &str) -> u32 {
|
2020-03-17 05:52:55 +01:00
|
|
|
let digest = sha1::Sha1::from(text).digest().bytes();
|
2020-02-10 05:19:39 +01:00
|
|
|
u32::from_be_bytes(digest[..4].try_into().unwrap())
|
|
|
|
}
|
2020-04-16 09:02:59 +02:00
|
|
|
|
|
|
|
fn guid() -> String {
|
|
|
|
anki_base91(rand::random())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn anki_base91(mut n: u64) -> String {
|
|
|
|
let table = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\
|
2020-04-17 06:36:45 +02:00
|
|
|
0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~";
|
2020-04-16 09:02:59 +02:00
|
|
|
let mut buf = String::new();
|
|
|
|
while n > 0 {
|
|
|
|
let (q, r) = n.div_rem(&(table.len() as u64));
|
|
|
|
buf.push(table[r as usize] as char);
|
|
|
|
n = q;
|
|
|
|
}
|
|
|
|
|
|
|
|
buf.chars().rev().collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Collection {
|
|
|
|
pub fn add_note(&mut self, note: &mut Note) -> Result<()> {
|
|
|
|
self.transact(None, |col| {
|
2020-04-17 06:36:45 +02:00
|
|
|
let nt = col
|
2020-04-18 00:48:10 +02:00
|
|
|
.get_notetype(note.ntid)?
|
2020-04-17 06:36:45 +02:00
|
|
|
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
2020-04-20 13:32:55 +02:00
|
|
|
let ctx = CardGenContext::new(&nt, col.usn()?);
|
|
|
|
col.add_note_inner(&ctx, note)
|
2020-04-16 09:02:59 +02:00
|
|
|
})
|
|
|
|
}
|
2020-04-17 06:36:45 +02:00
|
|
|
|
2020-04-20 13:32:55 +02:00
|
|
|
pub(crate) fn add_note_inner(&mut self, ctx: &CardGenContext, note: &mut Note) -> Result<()> {
|
|
|
|
note.prepare_for_update(&ctx.notetype, ctx.usn)?;
|
|
|
|
let cards = ctx.new_cards_required(note, Default::default());
|
2020-04-17 06:36:45 +02:00
|
|
|
if cards.is_empty() {
|
|
|
|
return Err(AnkiError::NoCardsGenerated);
|
|
|
|
}
|
2020-04-20 13:32:55 +02:00
|
|
|
// add note first, as we need the allocated ID for the cards
|
2020-04-17 06:36:45 +02:00
|
|
|
self.storage.add_note(note)?;
|
2020-04-20 13:32:55 +02:00
|
|
|
self.add_generated_cards(ctx, note.id, &cards)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn update_note(&mut self, note: &mut Note) -> Result<()> {
|
|
|
|
self.transact(None, |col| {
|
|
|
|
let nt = col
|
|
|
|
.get_notetype(note.ntid)?
|
|
|
|
.ok_or_else(|| AnkiError::invalid_input("missing note type"))?;
|
|
|
|
let ctx = CardGenContext::new(&nt, col.usn()?);
|
|
|
|
col.update_note_inner(&ctx, note)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn update_note_inner(
|
|
|
|
&mut self,
|
|
|
|
ctx: &CardGenContext,
|
|
|
|
note: &mut Note,
|
|
|
|
) -> Result<()> {
|
|
|
|
note.prepare_for_update(ctx.notetype, ctx.usn)?;
|
|
|
|
self.generate_cards_for_existing_note(ctx, note)?;
|
|
|
|
self.storage.update_note(note)?;
|
2020-04-17 06:36:45 +02:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2020-04-16 09:02:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::{anki_base91, field_checksum};
|
2020-04-17 06:36:45 +02:00
|
|
|
use crate::{
|
|
|
|
collection::open_test_collection,
|
|
|
|
err::{AnkiError, Result},
|
|
|
|
search::SortMode,
|
|
|
|
};
|
2020-04-16 09:02:59 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_base91() {
|
|
|
|
// match the python implementation for now
|
|
|
|
assert_eq!(anki_base91(0), "");
|
|
|
|
assert_eq!(anki_base91(1), "b");
|
|
|
|
assert_eq!(anki_base91(u64::max_value()), "Rj&Z5m[>Zp");
|
|
|
|
assert_eq!(anki_base91(1234567890), "saAKk");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_field_checksum() {
|
|
|
|
assert_eq!(field_checksum("test"), 2840236005);
|
|
|
|
assert_eq!(field_checksum("今日"), 1464653051);
|
|
|
|
}
|
2020-04-17 06:36:45 +02:00
|
|
|
|
|
|
|
#[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
|
|
|
|
);
|
|
|
|
|
2020-04-21 05:49:40 +02:00
|
|
|
let nt = col.get_notetype_by_name("cloze")?.unwrap();
|
|
|
|
let mut note = nt.new_note();
|
|
|
|
// cloze cards without any cloze deletions are allowed
|
|
|
|
col.add_note(&mut note).unwrap();
|
|
|
|
let existing = col.storage.existing_cards_for_note(note.id)?;
|
|
|
|
assert_eq!(existing.len(), 1);
|
|
|
|
assert_eq!(existing[0].ord, 0);
|
|
|
|
|
|
|
|
// fixme
|
|
|
|
// assert_eq!(existing[0].original_deck_id, DeckID(1));
|
|
|
|
|
|
|
|
note.fields[0] = "{{c1::foo}} {{c2::bar}} {{c3::baz}} {{c0::quux}} {{c501::over}}".into();
|
|
|
|
col.update_note(&mut note)?;
|
|
|
|
let existing = col.storage.existing_cards_for_note(note.id)?;
|
|
|
|
let mut ords = existing.iter().map(|a| a.ord).collect::<Vec<_>>();
|
|
|
|
ords.sort();
|
|
|
|
assert_eq!(ords, vec![0, 1, 2, 499]);
|
|
|
|
|
2020-04-17 06:36:45 +02:00
|
|
|
Ok(())
|
|
|
|
}
|
2020-04-16 09:02:59 +02:00
|
|
|
}
|