mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
deck searching
A bit more complicated than it needs to be, as we don't have the full deck manager infrastructure yet.
This commit is contained in:
parent
f559ae3ef8
commit
9f3cc0982d
6 changed files with 129 additions and 21 deletions
11
rslib/src/config.rs
Normal file
11
rslib/src/config.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::types::ObjID;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(rename = "curDeck")]
|
||||
pub(crate) current_deck_id: ObjID,
|
||||
}
|
29
rslib/src/decks.rs
Normal file
29
rslib/src/decks.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::types::ObjID;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Deck {
|
||||
pub(crate) id: ObjID,
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator<Item = ObjID> + 'a {
|
||||
let prefix = format!("{}::", name);
|
||||
decks
|
||||
.iter()
|
||||
.filter(move |d| d.name.starts_with(&prefix))
|
||||
.map(|d| d.id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_deck(decks: &[Deck], id: ObjID) -> Option<&Deck> {
|
||||
for d in decks {
|
||||
if d.id == id {
|
||||
return Some(d);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
|
@ -13,6 +13,8 @@ pub mod backend;
|
|||
pub mod card;
|
||||
pub mod cloze;
|
||||
pub mod collection;
|
||||
pub mod config;
|
||||
pub mod decks;
|
||||
pub mod err;
|
||||
pub mod i18n;
|
||||
pub mod latex;
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
|
||||
use super::parser::{Node, PropertyKind, SearchNode, StateKind, TemplateKind};
|
||||
use crate::card::CardQueue;
|
||||
use crate::decks::child_ids;
|
||||
use crate::decks::get_deck;
|
||||
use crate::err::{AnkiError, Result};
|
||||
use crate::notes::field_checksum;
|
||||
use crate::text::matches_wildcard;
|
||||
use crate::{
|
||||
collection::RequestContext, text::strip_html_preserving_image_filenames, types::ObjID,
|
||||
};
|
||||
|
@ -21,34 +25,35 @@ struct SearchContext<'a> {
|
|||
fn node_to_sql<'a>(
|
||||
ctx: &'a mut RequestContext<'a>,
|
||||
node: &'a Node,
|
||||
) -> (String, Vec<ToSqlOutput<'a>>) {
|
||||
) -> Result<(String, Vec<ToSqlOutput<'a>>)> {
|
||||
let sql = String::new();
|
||||
let args = vec![];
|
||||
let mut sctx = SearchContext { ctx, sql, args };
|
||||
write_node_to_sql(&mut sctx, node);
|
||||
(sctx.sql, sctx.args)
|
||||
write_node_to_sql(&mut sctx, node)?;
|
||||
Ok((sctx.sql, sctx.args))
|
||||
}
|
||||
|
||||
fn write_node_to_sql(ctx: &mut SearchContext, node: &Node) {
|
||||
fn write_node_to_sql(ctx: &mut SearchContext, node: &Node) -> Result<()> {
|
||||
match node {
|
||||
Node::And => write!(ctx.sql, " and ").unwrap(),
|
||||
Node::Or => write!(ctx.sql, " or ").unwrap(),
|
||||
Node::Not(node) => {
|
||||
write!(ctx.sql, "not ").unwrap();
|
||||
write_node_to_sql(ctx, node);
|
||||
write_node_to_sql(ctx, node)?;
|
||||
}
|
||||
Node::Group(nodes) => {
|
||||
write!(ctx.sql, "(").unwrap();
|
||||
for node in nodes {
|
||||
write_node_to_sql(ctx, node);
|
||||
write_node_to_sql(ctx, node)?;
|
||||
}
|
||||
write!(ctx.sql, ")").unwrap();
|
||||
}
|
||||
Node::Search(search) => write_search_node_to_sql(ctx, search),
|
||||
}
|
||||
Node::Search(search) => write_search_node_to_sql(ctx, search)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) {
|
||||
fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) -> Result<()> {
|
||||
match node {
|
||||
SearchNode::UnqualifiedText(text) => write_unqualified(ctx, text),
|
||||
SearchNode::SingleField { field, text } => {
|
||||
|
@ -58,7 +63,7 @@ fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) {
|
|||
write!(ctx.sql, "c.id > {}", days).unwrap();
|
||||
}
|
||||
SearchNode::CardTemplate(template) => write_template(ctx, template),
|
||||
SearchNode::Deck(deck) => write_deck(ctx, deck.as_ref()),
|
||||
SearchNode::Deck(deck) => write_deck(ctx, deck.as_ref())?,
|
||||
SearchNode::NoteTypeID(ntid) => {
|
||||
write!(ctx.sql, "n.mid = {}", ntid).unwrap();
|
||||
}
|
||||
|
@ -77,7 +82,8 @@ fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) {
|
|||
write!(ctx.sql, "c.id in ({})", cids).unwrap();
|
||||
}
|
||||
SearchNode::Property { operator, kind } => write_prop(ctx, operator, kind),
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_unqualified(ctx: &mut SearchContext, text: &str) {
|
||||
|
@ -180,19 +186,43 @@ fn write_state(ctx: &mut SearchContext, state: &StateKind) {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
// fixme: need deck manager
|
||||
fn write_deck(ctx: &mut SearchContext, deck: &str) {
|
||||
fn write_deck(ctx: &mut SearchContext, deck: &str) -> Result<()> {
|
||||
match deck {
|
||||
"*" => write!(ctx.sql, "true").unwrap(),
|
||||
"filtered" => write!(ctx.sql, "c.odid > 0").unwrap(),
|
||||
"current" => {
|
||||
todo!() // fixme: need current deck and child decks
|
||||
deck => {
|
||||
let all_decks = ctx.ctx.storage.all_decks()?;
|
||||
let dids_with_children = if deck == "current" {
|
||||
let config = ctx.ctx.storage.all_config()?;
|
||||
let mut dids_with_children = vec![config.current_deck_id];
|
||||
let current = get_deck(&all_decks, config.current_deck_id)
|
||||
.ok_or_else(|| AnkiError::invalid_input("invalid current deck"))?;
|
||||
for child_did in child_ids(&all_decks, ¤t.name) {
|
||||
dids_with_children.push(child_did);
|
||||
}
|
||||
dids_with_children
|
||||
} else {
|
||||
let mut dids_with_children = vec![];
|
||||
for deck in all_decks.iter().filter(|d| matches_wildcard(&d.name, deck)) {
|
||||
dids_with_children.push(deck.id);
|
||||
for child_id in child_ids(&all_decks, &deck.name) {
|
||||
dids_with_children.push(child_id);
|
||||
}
|
||||
}
|
||||
dids_with_children
|
||||
};
|
||||
|
||||
if dids_with_children.is_empty() {
|
||||
write!(ctx.sql, "false")
|
||||
} else {
|
||||
let did_strings: Vec<String> =
|
||||
dids_with_children.iter().map(ToString::to_string).collect();
|
||||
write!(ctx.sql, "c.did in ({})", did_strings.join(","))
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
_deck => {
|
||||
// fixme: narrow to dids matching possible wildcard; include children
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// fixme: need note type manager
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::collection::CollectionOp;
|
||||
use crate::config::Config;
|
||||
use crate::err::Result;
|
||||
use crate::err::{AnkiError, DBErrorKind};
|
||||
use crate::time::{i64_unix_millis, i64_unix_secs};
|
||||
use crate::types::Usn;
|
||||
use crate::{decks::Deck, types::Usn};
|
||||
use rusqlite::{params, Connection, NO_PARAMS};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -210,4 +211,18 @@ impl StorageContext<'_> {
|
|||
Ok(-1)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn all_decks(&self) -> Result<Vec<Deck>> {
|
||||
self.db
|
||||
.query_row_and_then("select decks from col", NO_PARAMS, |row| -> Result<_> {
|
||||
Ok(serde_json::from_str(row.get_raw(0).as_str()?)?)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn all_config(&self) -> Result<Config> {
|
||||
self.db
|
||||
.query_row_and_then("select conf from col", NO_PARAMS, |row| -> Result<_> {
|
||||
Ok(serde_json::from_str(row.get_raw(0).as_str()?)?)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -219,8 +219,20 @@ pub(crate) fn normalize_to_nfc(s: &str) -> Cow<str> {
|
|||
}
|
||||
}
|
||||
|
||||
/// True if search is equal to text, folding ascii case.
|
||||
/// Supports '*' to match 0 or more characters.
|
||||
pub(crate) fn matches_wildcard(text: &str, search: &str) -> bool {
|
||||
if search.contains('*') {
|
||||
let search = format!("^(?i){}$", regex::escape(search).replace(r"\*", ".*"));
|
||||
Regex::new(&search).unwrap().is_match(text)
|
||||
} else {
|
||||
text.eq_ignore_ascii_case(search)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::matches_wildcard;
|
||||
use crate::text::{
|
||||
extract_av_tags, strip_av_tags, strip_html, strip_html_preserving_image_filenames, AVTag,
|
||||
};
|
||||
|
@ -265,4 +277,13 @@ mod test {
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard() {
|
||||
assert_eq!(matches_wildcard("foo", "bar"), false);
|
||||
assert_eq!(matches_wildcard("foo", "Foo"), true);
|
||||
assert_eq!(matches_wildcard("foo", "F*"), true);
|
||||
assert_eq!(matches_wildcard("foo", "F*oo"), true);
|
||||
assert_eq!(matches_wildcard("foo", "b*"), false);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue