diff --git a/rslib/src/storage/tag/mod.rs b/rslib/src/storage/tag/mod.rs index e0f1b0bd4..1f00268ab 100644 --- a/rslib/src/storage/tag/mod.rs +++ b/rslib/src/storage/tag/mod.rs @@ -2,7 +2,8 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::SqliteStorage; -use crate::{err::Result, tags::Tag, types::Usn}; +use crate::err::{AnkiError, Result}; +use crate::{tags::Tag, types::Usn}; use rusqlite::{params, Row, NO_PARAMS}; use std::collections::HashMap; @@ -15,6 +16,10 @@ fn row_to_tag(row: &Row) -> Result { }) } +fn immediate_parent_name(tag_name: &str) -> Option<&str> { + tag_name.rsplitn(2, "::").nth(1) +} + impl SqliteStorage { /// All tags in the collection, in alphabetical order. pub(crate) fn all_tags(&self) -> Result> { @@ -56,12 +61,43 @@ impl SqliteStorage { Ok(()) } - pub(crate) fn preferred_tag_case(&self, tag: &str) -> Result> { + /// If parent tag(s) exist, rewrite name to match their case. + fn match_parents(&self, tag: &str) -> Result { + let child_split: Vec<_> = tag.split("::").collect(); + let t = if let Some(parent_tag) = self.first_existing_parent(&tag)? { + let parent_count = parent_tag.matches("::").count() + 1; + format!( + "{}::{}", + parent_tag, + &child_split[parent_count..].join("::") + ) + } else { + tag.into() + }; + + Ok(t) + } + + fn first_existing_parent(&self, mut tag: &str) -> Result> { + while let Some(parent_name) = immediate_parent_name(tag) { + if let Some(parent_tag) = self.get_tag(parent_name)? { + return Ok(Some(parent_tag.name)); + } + tag = parent_name; + } + + Ok(None) + } + + // Get stored tag name or the same passed name if it doesn't exist, rewritten to match parents case. + // Returns a tuple of the preferred name and a boolean indicating if the tag exists. + pub(crate) fn preferred_tag_case(&self, tag: &str) -> Result<(Result, bool)> { self.db .prepare_cached("select tag from tags where tag = ?")? - .query_and_then(params![tag], |row| row.get(0))? - .next() - .transpose() + .query_row(params![tag], |row| { + Ok((self.match_parents(row.get_raw(0).as_str()?), true)) + }) + .or_else::(|_| Ok((self.match_parents(tag), false))) .map_err(Into::into) } diff --git a/rslib/src/tags.rs b/rslib/src/tags.rs index 0e70d77bb..ab5c74a16 100644 --- a/rslib/src/tags.rs +++ b/rslib/src/tags.rs @@ -225,13 +225,13 @@ impl Collection { if normalized_name.is_empty() { return Ok((t, false)); } - if let Some(preferred) = self.storage.preferred_tag_case(&normalized_name)? { - t.name = preferred; - Ok((t, false)) - } else { + let (preferred, exists) = self.storage.preferred_tag_case(&normalized_name)?; + t.name = preferred?; + if !exists { self.storage.register_tag(&t)?; - Ok((t, true)) } + + Ok((t, !exists)) } pub fn clear_unused_tags(&self) -> Result<()> { @@ -533,6 +533,14 @@ mod test { ) ); + // children should match the case of their parents + col.storage.clear_tags()?; + *(&mut note.tags[0]) = "FOO".into(); + *(&mut note.tags[1]) = "foo::BAR".into(); + *(&mut note.tags[2]) = "foo::bar::baz".into(); + col.update_note(&mut note)?; + assert_eq!(note.tags, vec!["FOO", "FOO::BAR", "FOO::BAR::baz"]); + Ok(()) }