mirror of
https://github.com/ankitects/anki.git
synced 2025-11-16 01:27:12 -05:00
So, this is fun. Apparently "DeckId" is considered preferable to the "DeckID" were were using until now, and the latest clippy will start warning about it. We could of course disable the warning, but probably better to bite the bullet and switch to the naming that's generally considered best.
152 lines
4.8 KiB
Rust
152 lines
4.8 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use crate::{
|
|
collection::Collection,
|
|
err::{AnkiError, Result},
|
|
notes::{NoteId, TransformNoteOutput},
|
|
prelude::*,
|
|
text::normalize_to_nfc,
|
|
};
|
|
use regex::Regex;
|
|
use std::borrow::Cow;
|
|
|
|
pub struct FindReplaceContext {
|
|
nids: Vec<NoteId>,
|
|
search: Regex,
|
|
replacement: String,
|
|
field_name: Option<String>,
|
|
}
|
|
|
|
impl FindReplaceContext {
|
|
pub fn new(
|
|
nids: Vec<NoteId>,
|
|
search_re: &str,
|
|
repl: impl Into<String>,
|
|
field_name: Option<String>,
|
|
) -> Result<Self> {
|
|
Ok(FindReplaceContext {
|
|
nids,
|
|
search: Regex::new(search_re).map_err(|_| AnkiError::invalid_input("invalid regex"))?,
|
|
replacement: repl.into(),
|
|
field_name,
|
|
})
|
|
}
|
|
|
|
fn replace_text<'a>(&self, text: &'a str) -> Cow<'a, str> {
|
|
self.search.replace_all(text, self.replacement.as_str())
|
|
}
|
|
}
|
|
|
|
impl Collection {
|
|
pub fn find_and_replace(
|
|
&mut self,
|
|
nids: Vec<NoteId>,
|
|
search_re: &str,
|
|
repl: &str,
|
|
field_name: Option<String>,
|
|
) -> Result<OpOutput<usize>> {
|
|
self.transact(Op::FindAndReplace, |col| {
|
|
let norm = col.get_bool(BoolKey::NormalizeNoteText);
|
|
let search = if norm {
|
|
normalize_to_nfc(search_re)
|
|
} else {
|
|
search_re.into()
|
|
};
|
|
let ctx = FindReplaceContext::new(nids, &search, repl, field_name)?;
|
|
col.find_and_replace_inner(ctx)
|
|
})
|
|
}
|
|
|
|
fn find_and_replace_inner(&mut self, ctx: FindReplaceContext) -> Result<usize> {
|
|
let mut last_ntid = None;
|
|
let mut field_ord = None;
|
|
self.transform_notes(&ctx.nids, |note, nt| {
|
|
if last_ntid != Some(nt.id) {
|
|
field_ord = ctx.field_name.as_ref().and_then(|n| nt.get_field_ord(n));
|
|
last_ntid = Some(nt.id);
|
|
}
|
|
|
|
let mut changed = false;
|
|
match field_ord {
|
|
None => {
|
|
// all fields
|
|
for txt in note.fields_mut() {
|
|
if let Cow::Owned(otxt) = ctx.replace_text(txt) {
|
|
changed = true;
|
|
*txt = otxt;
|
|
}
|
|
}
|
|
}
|
|
Some(ord) => {
|
|
// single field
|
|
if let Some(txt) = note.fields_mut().get_mut(ord) {
|
|
if let Cow::Owned(otxt) = ctx.replace_text(txt) {
|
|
changed = true;
|
|
*txt = otxt;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(TransformNoteOutput {
|
|
changed,
|
|
generate_cards: true,
|
|
mark_modified: true,
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use crate::{collection::open_test_collection, decks::DeckId};
|
|
|
|
#[test]
|
|
fn findreplace() -> Result<()> {
|
|
let mut col = open_test_collection();
|
|
|
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
|
let mut note = nt.new_note();
|
|
note.set_field(0, "one aaa")?;
|
|
note.set_field(1, "two aaa")?;
|
|
col.add_note(&mut note, DeckId(1))?;
|
|
|
|
let nt = col.get_notetype_by_name("Cloze")?.unwrap();
|
|
let mut note2 = nt.new_note();
|
|
note2.set_field(0, "three aaa")?;
|
|
col.add_note(&mut note2, DeckId(1))?;
|
|
|
|
let nids = col.search_notes("")?;
|
|
let out = col.find_and_replace(nids.clone(), "(?i)AAA", "BBB", None)?;
|
|
assert_eq!(out.output, 2);
|
|
|
|
let note = col.storage.get_note(note.id)?.unwrap();
|
|
// but the update should be limited to the specified field when it was available
|
|
assert_eq!(¬e.fields()[..], &["one BBB", "two BBB"]);
|
|
|
|
let note2 = col.storage.get_note(note2.id)?.unwrap();
|
|
assert_eq!(¬e2.fields()[..], &["three BBB", ""]);
|
|
|
|
assert_eq!(
|
|
col.storage.field_names_for_notes(&nids)?,
|
|
vec![
|
|
"Back".to_string(),
|
|
"Back Extra".into(),
|
|
"Front".into(),
|
|
"Text".into()
|
|
]
|
|
);
|
|
let out = col.find_and_replace(nids, "BBB", "ccc", Some("Front".into()))?;
|
|
// still 2, as the caller is expected to provide only note ids that have
|
|
// that field, and if we can't find the field we fall back on all fields
|
|
assert_eq!(out.output, 2);
|
|
|
|
let note = col.storage.get_note(note.id)?.unwrap();
|
|
// but the update should be limited to the specified field when it was available
|
|
assert_eq!(¬e.fields()[..], &["one ccc", "two BBB"]);
|
|
|
|
Ok(())
|
|
}
|
|
}
|