From 540892639feebb02e09256988fa2c7bc3b37948e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 13 Apr 2020 19:14:30 +1000 Subject: [PATCH] add stock note types in backend --- proto/backend.proto | 15 ++- pylib/anki/storage.py | 15 +-- rslib/src/backend/mod.rs | 2 +- rslib/src/config.rs | 27 +++- rslib/src/notetype/mod.rs | 96 +++++++++++++-- rslib/src/notetype/schema11.rs | 51 ++++++-- rslib/src/notetype/stock.rs | 130 ++++++++++++++++++++ rslib/src/storage/notetype/add_notetype.sql | 21 +++- rslib/src/storage/notetype/mod.rs | 23 ++++ rslib/src/storage/sqlite.rs | 4 + 10 files changed, 342 insertions(+), 42 deletions(-) create mode 100644 rslib/src/notetype/stock.rs diff --git a/proto/backend.proto b/proto/backend.proto index 9eb9dd558..623c944b6 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -533,8 +533,13 @@ message NoteTypeConfig { } message CardRequirement { + enum CardRequirementKind { + None = 0; + Any = 1; + All = 2; + } uint32 card_ord = 1; - uint32 kind = 2; + CardRequirementKind kind = 2; repeated uint32 field_ords = 3; } @@ -547,3 +552,11 @@ message NoteType { repeated NoteField fields = 8; repeated CardTemplate templates = 9; } + +enum StockNoteType { + StockNoteTypeBasic = 0; + StockNoteTypeBasicAndReversed = 1; + StockNoteTypeBasicOptionalReversed = 2; + StockNoteTypeBasicTyping = 3; + StockNoteTypeCloze = 4; +} diff --git a/pylib/anki/storage.py b/pylib/anki/storage.py index b8a0bb8f6..82da0dc0c 100644 --- a/pylib/anki/storage.py +++ b/pylib/anki/storage.py @@ -13,13 +13,6 @@ from anki.dbproxy import DBProxy from anki.lang import _ from anki.media import media_paths_from_col_path from anki.rsbackend import RustBackend -from anki.stdmodels import ( - addBasicModel, - addBasicTypingModel, - addClozeModel, - addForwardOptionalReverse, - addForwardReverse, -) from anki.utils import intTime @@ -51,19 +44,13 @@ def Collection( db = DBProxy(weakref.proxy(backend), path) # initial setup required? - create = db.scalar("select models = '{}' from col") + create = db.scalar("select decks = '{}' from col") if create: initial_db_setup(db) # add db to col and do any remaining upgrades col = _Collection(db, backend=backend, server=server, log=should_log) if create: - # add in reverse order so basic is default - addClozeModel(col) - addBasicTypingModel(col) - addForwardOptionalReverse(col) - addForwardReverse(col) - addBasicModel(col) col.save() else: db.begin() diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 286e0f067..33336b5ed 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -827,7 +827,7 @@ impl Backend { pb::set_config_json::Op::Val(val) => { // ensure it's a well-formed object let val: JsonValue = serde_json::from_slice(&val)?; - col.set_config(&input.key, &val) + col.set_config(input.key.as_str(), &val) } pb::set_config_json::Op::Remove(_) => col.remove_config(&input.key), } diff --git a/rslib/src/config.rs b/rslib/src/config.rs index aede6bd7e..01a1244b1 100644 --- a/rslib/src/config.rs +++ b/rslib/src/config.rs @@ -1,10 +1,10 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::collection::Collection; -use crate::decks::DeckID; -use crate::err::Result; -use crate::timestamp::TimestampSecs; +use crate::{ + collection::Collection, decks::DeckID, err::Result, notetype::NoteTypeID, + timestamp::TimestampSecs, +}; use serde::{de::DeserializeOwned, Serialize}; use serde_aux::field_attributes::deserialize_bool_from_anything; use serde_derive::Deserialize; @@ -38,6 +38,7 @@ pub(crate) enum ConfigKey { CreationOffset, Rollover, LocalOffset, + CurrentNoteTypeID, } impl From for &'static str { @@ -49,6 +50,7 @@ impl From for &'static str { ConfigKey::CreationOffset => "creationOffset", ConfigKey::Rollover => "rollover", ConfigKey::LocalOffset => "localOffset", + ConfigKey::CurrentNoteTypeID => "curModel", } } } @@ -83,9 +85,12 @@ impl Collection { self.get_config_optional(key).unwrap_or_default() } - pub(crate) fn set_config(&self, key: &str, val: &T) -> Result<()> { + pub(crate) fn set_config<'a, T: Serialize, K>(&self, key: K, val: &T) -> Result<()> + where + K: Into<&'a str>, + { self.storage - .set_config_value(key, val, self.usn()?, TimestampSecs::now()) + .set_config_value(key.into(), val, self.usn()?, TimestampSecs::now()) } pub(crate) fn remove_config(&self, key: &str) -> Result<()> { @@ -117,6 +122,16 @@ impl Collection { pub(crate) fn get_rollover(&self) -> Option { self.get_config_optional(ConfigKey::Rollover) } + + #[allow(dead_code)] + pub(crate) fn get_current_notetype_id(&self) -> Option { + self.get_config_optional(ConfigKey::CurrentNoteTypeID) + } + + #[allow(dead_code)] + pub(crate) fn set_current_notetype_id(&self, id: NoteTypeID) -> Result<()> { + self.set_config(ConfigKey::CurrentNoteTypeID, &id) + } } #[derive(Deserialize, PartialEq, Debug)] diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 3767bdedc..df1b0bf00 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -2,15 +2,20 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod schema11; +mod stock; pub use crate::backend_proto::{ - CardRequirement, CardTemplate, CardTemplateConfig, NoteField, NoteFieldConfig, NoteType, - NoteTypeConfig, NoteTypeKind, + card_requirement::CardRequirementKind, CardRequirement, CardTemplate, CardTemplateConfig, + NoteField, NoteFieldConfig, NoteType, NoteTypeConfig, NoteTypeKind, }; pub use schema11::{CardTemplateSchema11, NoteFieldSchema11, NoteTypeSchema11}; -use crate::{define_newtype, text::ensure_string_in_nfc}; -use std::collections::HashSet; +use crate::{ + define_newtype, + template::{without_legacy_template_directives, FieldRequirements, ParsedTemplate}, + text::ensure_string_in_nfc, +}; +use std::collections::{HashMap, HashSet}; use unicase::UniCase; define_newtype!(NoteTypeID, i64); @@ -36,9 +41,6 @@ pub(crate) const DEFAULT_LATEX_HEADER: &str = r#"\documentclass[12pt]{article} pub(crate) const DEFAULT_LATEX_FOOTER: &str = r#"\end{document}"#; -// other: vec![], // fixme: ensure empty map converted to empty bytes -// fixme: rollback savepoint when tags not changed - impl NoteType { pub fn new() -> Self { let mut nt = Self::default(); @@ -87,6 +89,53 @@ impl NoteType { } } + fn update_requirements(&mut self) { + let field_map: HashMap<&str, u16> = self + .fields + .iter() + .enumerate() + .map(|(idx, field)| (field.name.as_str(), idx as u16)) + .collect(); + let reqs: Vec<_> = self + .templates + .iter() + .enumerate() + .map(|(ord, tmpl)| { + let conf = tmpl.config.as_ref().unwrap(); + let normalized = without_legacy_template_directives(&conf.q_format); + if let Ok(tmpl) = ParsedTemplate::from_text(normalized.as_ref()) { + let mut req = match tmpl.requirements(&field_map) { + FieldRequirements::Any(ords) => CardRequirement { + card_ord: ord as u32, + kind: CardRequirementKind::Any as i32, + field_ords: ords.into_iter().map(|n| n as u32).collect(), + }, + FieldRequirements::All(ords) => CardRequirement { + card_ord: ord as u32, + kind: CardRequirementKind::All as i32, + field_ords: ords.into_iter().map(|n| n as u32).collect(), + }, + FieldRequirements::None => CardRequirement { + card_ord: ord as u32, + kind: CardRequirementKind::None as i32, + field_ords: vec![], + }, + }; + req.field_ords.sort_unstable(); + req + } else { + // template parsing failures make card unsatisfiable + CardRequirement { + card_ord: ord as u32, + kind: CardRequirementKind::None as i32, + field_ords: vec![], + } + } + }) + .collect(); + self.config.as_mut().unwrap().reqs = reqs; + } + pub(crate) fn normalize_names(&mut self) { ensure_string_in_nfc(&mut self.name); for f in &mut self.fields { @@ -96,4 +145,37 @@ impl NoteType { ensure_string_in_nfc(&mut t.name); } } + + pub(crate) fn add_field>(&mut self, name: S) { + let mut config = NoteFieldConfig::default(); + config.font_name = "Arial".to_string(); + config.font_size = 20; + let mut field = NoteField::default(); + field.name = name.into(); + field.config = Some(config); + self.fields.push(field); + } + + pub(crate) fn add_template(&mut self, name: S1, qfmt: S2, afmt: S3) + where + S1: Into, + S2: Into, + S3: Into, + { + let mut config = CardTemplateConfig::default(); + config.q_format = qfmt.into(); + config.a_format = afmt.into(); + + let mut tmpl = CardTemplate::default(); + tmpl.name = name.into(); + tmpl.config = Some(config); + + self.templates.push(tmpl); + } + + pub(crate) fn prepare_for_adding(&mut self) { + self.normalize_names(); + self.ensure_names_unique(); + self.update_requirements(); + } } diff --git a/rslib/src/notetype/schema11.rs b/rslib/src/notetype/schema11.rs index 753a4fe7d..56b7b019d 100644 --- a/rslib/src/notetype/schema11.rs +++ b/rslib/src/notetype/schema11.rs @@ -16,7 +16,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_tuple::Serialize_tuple; use std::collections::HashMap; -use super::NoteTypeID; +use super::{CardRequirementKind, NoteTypeID}; #[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Clone)] #[repr(u8)] @@ -101,7 +101,7 @@ impl From for NoteType { latex_post: nt.latex_post, latex_svg: nt.latexsvg, reqs: nt.req.0.into_iter().map(Into::into).collect(), - other: serde_json::to_vec(&nt.other).unwrap(), + other: other_to_bytes(&nt.other), }), fields: nt.flds.into_iter().map(Into::into).collect(), templates: nt.tmpls.into_iter().map(Into::into).collect(), @@ -109,6 +109,29 @@ impl From for NoteType { } } +fn other_to_bytes(other: &HashMap) -> Vec { + if other.is_empty() { + vec![] + } else { + serde_json::to_vec(other).unwrap_or_else(|e| { + // theoretically should never happen + println!("serialization failed for {:?}: {}", other, e); + vec![] + }) + } +} + +fn bytes_to_other(bytes: &[u8]) -> HashMap { + if bytes.is_empty() { + Default::default() + } else { + serde_json::from_slice(bytes).unwrap_or_else(|e| { + println!("deserialization failed for other: {}", e); + Default::default() + }) + } +} + impl From for NoteTypeSchema11 { fn from(p: NoteType) -> Self { let c = p.config.unwrap(); @@ -135,7 +158,7 @@ impl From for NoteTypeSchema11 { latex_post: c.latex_post, latexsvg: c.latex_svg, req: CardRequirementsSchema11(c.reqs.into_iter().map(Into::into).collect()), - other: serde_json::from_slice(&c.other).unwrap_or_default(), + other: bytes_to_other(&c.other), } } } @@ -144,7 +167,11 @@ impl From for CardRequirement { fn from(r: CardRequirementSchema11) -> Self { CardRequirement { card_ord: r.card_ord as u32, - kind: r.kind as u32, + kind: match r.kind { + FieldRequirementKindSchema11::Any => CardRequirementKind::Any, + FieldRequirementKindSchema11::All => CardRequirementKind::All, + FieldRequirementKindSchema11::None => CardRequirementKind::None, + } as i32, field_ords: r.field_ords.into_iter().map(|n| n as u32).collect(), } } @@ -154,10 +181,10 @@ impl From for CardRequirementSchema11 { fn from(p: CardRequirement) -> Self { CardRequirementSchema11 { card_ord: p.card_ord as u16, - kind: match p.kind { - 0 => FieldRequirementKindSchema11::Any, - 1 => FieldRequirementKindSchema11::All, - _ => FieldRequirementKindSchema11::None, + kind: match p.kind() { + CardRequirementKind::Any => FieldRequirementKindSchema11::Any, + CardRequirementKind::All => FieldRequirementKindSchema11::All, + CardRequirementKind::None => FieldRequirementKindSchema11::None, }, field_ords: p.field_ords.into_iter().map(|n| n as u16).collect(), } @@ -202,7 +229,7 @@ impl From for NoteField { rtl: f.rtl, font_name: f.font, font_size: f.size as u32, - other: serde_json::to_vec(&f.other).unwrap(), + other: other_to_bytes(&f.other), }), } } @@ -218,7 +245,7 @@ impl From for NoteFieldSchema11 { rtl: conf.rtl, font: conf.font_name, size: conf.font_size as u16, - other: serde_json::from_slice(&conf.other).unwrap(), + other: bytes_to_other(&conf.other), } } } @@ -259,7 +286,7 @@ impl From for CardTemplate { target_deck_id: t.did.unwrap_or(DeckID(0)).0, browser_font_name: t.bfont, browser_font_size: t.bsize as u32, - other: serde_json::to_vec(&t.other).unwrap(), + other: other_to_bytes(&t.other), }), } } @@ -282,7 +309,7 @@ impl From for CardTemplateSchema11 { }, bfont: conf.browser_font_name, bsize: conf.browser_font_size as u8, - other: serde_json::from_slice(&conf.other).unwrap(), + other: bytes_to_other(&conf.other), } } } diff --git a/rslib/src/notetype/stock.rs b/rslib/src/notetype/stock.rs new file mode 100644 index 000000000..8a254d07a --- /dev/null +++ b/rslib/src/notetype/stock.rs @@ -0,0 +1,130 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::NoteTypeKind; +use crate::{ + config::ConfigKey, err::Result, i18n::I18n, i18n::TR, notetype::NoteType, + storage::SqliteStorage, timestamp::TimestampSecs, +}; + +pub use crate::backend_proto::StockNoteType; + +impl SqliteStorage { + pub(crate) fn add_stock_notetypes(&self, i18n: &I18n) -> Result<()> { + for (idx, mut nt) in all_stock_notetypes(i18n).into_iter().enumerate() { + self.add_new_notetype(&mut nt)?; + if idx == StockNoteType::Basic as usize { + self.set_config_value( + ConfigKey::CurrentNoteTypeID.into(), + &nt.id(), + self.usn(false)?, + TimestampSecs::now(), + )?; + } + } + Ok(()) + } +} + +// if changing this, make sure to update StockNoteType enum +pub fn all_stock_notetypes(i18n: &I18n) -> Vec { + vec![ + basic(i18n), + basic_forward_reverse(i18n), + basic_optional_reverse(i18n), + basic_typing(i18n), + cloze(i18n), + ] +} + +/// returns {{name}} +fn fieldref>(name: S) -> String { + format!("{{{{{}}}}}", name.as_ref()) +} + +pub(crate) fn basic(i18n: &I18n) -> NoteType { + let mut nt = NoteType::new(); + nt.name = i18n.tr(TR::NotetypesBasicName).into(); + let front = i18n.tr(TR::NotetypesFrontField); + let back = i18n.tr(TR::NotetypesBackField); + nt.add_field(front.as_ref()); + nt.add_field(back.as_ref()); + nt.add_template( + i18n.tr(TR::NotetypesCard1Name), + fieldref(front), + format!( + "{}\n\n
\n\n{}", + fieldref("FrontSide"), + fieldref(back), + ), + ); + nt.prepare_for_adding(); + nt +} + +pub(crate) fn basic_typing(i18n: &I18n) -> NoteType { + let mut nt = basic(i18n); + nt.name = i18n.tr(TR::NotetypesBasicTypeAnswerName).into(); + let front = i18n.tr(TR::NotetypesFrontField); + let back = i18n.tr(TR::NotetypesBackField); + let tmpl = nt.templates[0].config.as_mut().unwrap(); + tmpl.q_format = format!("{}\n\n{{{{type:{}}}}}", fieldref(front.as_ref()), back); + tmpl.a_format = format!( + "{}\n\n
\n\n{{{{type:{}}}}}", + fieldref(front), + back + ); + nt.prepare_for_adding(); + nt +} + +pub(crate) fn basic_forward_reverse(i18n: &I18n) -> NoteType { + let mut nt = basic(i18n); + nt.name = i18n.tr(TR::NotetypesBasicReversedName).into(); + let front = i18n.tr(TR::NotetypesFrontField); + let back = i18n.tr(TR::NotetypesBackField); + nt.add_template( + i18n.tr(TR::NotetypesCard2Name), + fieldref(back), + format!( + "{}\n\n
\n\n{}", + fieldref("FrontSide"), + fieldref(front), + ), + ); + nt.prepare_for_adding(); + nt +} + +pub(crate) fn basic_optional_reverse(i18n: &I18n) -> NoteType { + let mut nt = basic_forward_reverse(i18n); + nt.name = i18n.tr(TR::NotetypesBasicOptionalReversedName).into(); + let addrev = i18n.tr(TR::NotetypesAddReverseField); + nt.add_field(addrev.as_ref()); + let tmpl = nt.templates[1].config.as_mut().unwrap(); + tmpl.q_format = format!("{{{{#{}}}}}{}{{{{/{}}}}}", addrev, tmpl.q_format, addrev); + nt.prepare_for_adding(); + nt +} + +pub(crate) fn cloze(i18n: &I18n) -> NoteType { + let mut nt = NoteType::new(); + nt.name = i18n.tr(TR::NotetypesClozeName).into(); + let text = i18n.tr(TR::NotetypesTextField); + nt.add_field(text.as_ref()); + let fmt = format!("{{{{cloze:{}}}}}", text); + nt.add_template(nt.name.clone(), fmt.clone(), fmt); + let mut config = nt.config.as_mut().unwrap(); + config.kind = NoteTypeKind::Cloze as i32; + config.css += " +.cloze { + font-weight: bold; + color: blue; +} +.nightMode .cloze { + color: lightblue; +} +"; + nt.prepare_for_adding(); + nt +} diff --git a/rslib/src/storage/notetype/add_notetype.sql b/rslib/src/storage/notetype/add_notetype.sql index 9f4cc7168..4578ab3b4 100644 --- a/rslib/src/storage/notetype/add_notetype.sql +++ b/rslib/src/storage/notetype/add_notetype.sql @@ -1,3 +1,22 @@ insert into notetypes (id, name, mtime_secs, usn, config) values - (?, ?, ?, ?, ?) \ No newline at end of file + ( + ( + case + when ?1 in ( + select + id + from notetypes + ) then ( + select + max(id) + 1 + from notetypes + ) + else ?1 + end + ), + ?, + ?, + ?, + ? + ); \ No newline at end of file diff --git a/rslib/src/storage/notetype/mod.rs b/rslib/src/storage/notetype/mod.rs index b7b6fdd0b..9a591418d 100644 --- a/rslib/src/storage/notetype/mod.rs +++ b/rslib/src/storage/notetype/mod.rs @@ -8,6 +8,7 @@ use crate::{ }, err::{AnkiError, DBErrorKind, Result}, notetype::{NoteTypeID, NoteTypeSchema11}, + timestamp::TimestampMillis, }; use prost::Message; use rusqlite::{params, Row, NO_PARAMS}; @@ -160,6 +161,27 @@ impl SqliteStorage { Ok(()) } + pub(crate) fn add_new_notetype(&self, nt: &mut NoteType) -> Result<()> { + assert!(nt.id == 0); + + let mut stmt = self.db.prepare_cached(include_str!("add_notetype.sql"))?; + let mut config_bytes = vec![]; + nt.config.as_ref().unwrap().encode(&mut config_bytes)?; + stmt.execute(params![ + TimestampMillis::now(), + nt.name, + nt.mtime_secs, + nt.usn, + config_bytes + ])?; + nt.id = self.db.last_insert_rowid(); + + self.update_notetype_fields(nt.id(), &nt.fields)?; + self.update_notetype_templates(nt.id(), &nt.templates)?; + + Ok(()) + } + // Upgrading/downgrading pub(crate) fn upgrade_notetypes_to_schema15(&self) -> Result<()> { @@ -181,6 +203,7 @@ impl SqliteStorage { self.update_notetype_fields(ntid, &nt.fields)?; self.update_notetype_templates(ntid, &nt.templates)?; } + self.db.execute("update col set models = ''", NO_PARAMS)?; Ok(()) } diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 860e7fb41..d0dffe88e 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -20,6 +20,9 @@ fn unicase_compare(s1: &str, s2: &str) -> Ordering { UniCase::new(s1).cmp(&UniCase::new(s2)) } +// fixme: rollback savepoint when tags not changed +// fixme: switch away from proto for top level struct + // currently public for dbproxy #[derive(Debug)] pub struct SqliteStorage { @@ -195,6 +198,7 @@ impl SqliteStorage { if create { storage.add_default_deck_config(i18n)?; + storage.add_stock_notetypes(i18n)?; } if create || upgrade {