diff options
| author | lukeflo | 2025-10-14 14:30:56 +0200 |
|---|---|---|
| committer | lukeflo | 2025-10-14 14:30:56 +0200 |
| commit | 9b21727bd151a3fda2133b9da12eec588068130e (patch) | |
| tree | 843db53d8a266a4905d37936a606de731802094a | |
| parent | 549f89c554ae70af28a9c7276673f0f77b488165 (diff) | |
| download | bibiman-9b21727bd151a3fda2133b9da12eec588068130e.tar.gz bibiman-9b21727bd151a3fda2133b9da12eec588068130e.zip | |
use citekey formatter for adding new entries via doi
| -rw-r--r-- | src/bibiman.rs | 105 | ||||
| -rw-r--r-- | src/bibiman/citekeys.rs | 118 |
2 files changed, 97 insertions, 126 deletions
diff --git a/src/bibiman.rs b/src/bibiman.rs index 3158d73..392ae95 100644 --- a/src/bibiman.rs +++ b/src/bibiman.rs @@ -16,22 +16,23 @@ ///// use crate::app::expand_home; +use crate::bibiman::citekeys::CitekeyFormatting; use crate::bibiman::entries::EntryTableColumn; use crate::bibiman::{bibisetup::*, search::BibiSearch}; use crate::cliargs::CLIArgs; use crate::config::BibiConfig; -use crate::tui::popup::{PopupArea, PopupItem, PopupKind}; use crate::tui::Tui; +use crate::tui::popup::{PopupArea, PopupItem, PopupKind}; use crate::{app, cliargs}; use crate::{bibiman::entries::EntryTable, bibiman::keywords::TagList}; use arboard::Clipboard; -use color_eyre::eyre::{Context, Error, Result}; +use biblatex::Bibliography; +use color_eyre::eyre::{Context, Error, Result, eyre}; use crossterm::event::KeyCode; use editor_command::EditorBuilder; use ratatui::widgets::ScrollbarState; -use regex::Regex; use std::ffi::OsStr; -use std::fs::{self, read_to_string}; +use std::fs::{self}; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; @@ -190,7 +191,9 @@ impl Bibiman { self.popup_area.popup_message = message.unwrap().to_owned(); Ok(()) } else { - Err(Error::msg("You need to past at least a message via Some(&str) to create a message popup")) + Err(Error::msg( + "You need to past at least a message via Some(&str) to create a message popup", + )) } } PopupKind::MessageError => { @@ -202,7 +205,9 @@ impl Bibiman { self.popup_area.popup_message = message.unwrap().to_owned(); Ok(()) } else { - Err(Error::msg("You need to past at least a message via Some(&str) to create a message popup")) + Err(Error::msg( + "You need to past at least a message via Some(&str) to create a message popup", + )) } } PopupKind::OpenRes => { @@ -680,23 +685,32 @@ impl Bibiman { // Index of selected popup field let popup_idx = self.popup_area.popup_state.selected().unwrap(); - // regex pattern to match citekey in fetched bibtexstring - let pattern = Regex::new(r"\{([^\{\},]*),").unwrap(); + let new_bib_entry = Bibliography::parse(&self.popup_area.popup_sel_item) + .map_err(|e| eyre!("Couldn't parse downloaded bib entry: {}", e.to_string()))?; - let citekey = pattern - .captures(&self.popup_area.popup_sel_item) - .unwrap() - .get(1) - .unwrap() - .as_str() - .to_string(); + let formatted_struct = + if let Some(formatter) = CitekeyFormatting::new(cfg, new_bib_entry.clone()) { + Some(formatter.do_formatting()) + } else { + None + }; + + let (new_citekey, entry_string) = if let Some(mut formatter) = formatted_struct { + ( + formatter.get_citekey_pair(0).unwrap().1, + formatter.print_updated_bib_as_string(), + ) + } else { + let keys = new_bib_entry.keys().collect::<Vec<&str>>(); + (keys[0].to_string(), new_bib_entry.to_biblatex_string()) + }; // Check if new file or existing file was choosen let mut file = if self.popup_area.popup_list[popup_idx] .0 .contains("Create new file") { - let citekey = PathBuf::from(&citekey); + let citekey = PathBuf::from(&new_citekey); // Get path of current files let path: PathBuf = if self.main_bibfiles[0].is_file() { self.main_bibfiles[0].parent().unwrap().to_owned() @@ -714,45 +728,18 @@ impl Bibiman { } else { let file_path = &self.main_bibfiles[popup_idx - 1]; - // Check if similar citekey already exists - let file_string = read_to_string(&file_path).unwrap(); - - // If choosen file contains entry with fetched citekey, append an - // char to the citekey so no dublettes are created - if file_string.contains(&citekey) { - let mut new_citekey = String::new(); - - // Loop over ASCII alpabetic chars and check again if citekey with - // appended char exists. If yes, move to next char and test again. - // If the citekey is free, use it and break the loop - for c in b'a'..=b'z' { - let append_char = (c as char).to_string(); - new_citekey = citekey.clone() + &append_char; - if !file_string.contains(&new_citekey) { - break; - } - } - - let new_entry_string_clone = self.popup_area.popup_sel_item.clone(); - - // Replace the double citekey with newly created - self.popup_area.popup_sel_item = pattern - .replace(&new_entry_string_clone, format!("{{{},", &new_citekey)) - .to_string(); - } - OpenOptions::new().append(true).open(file_path).unwrap() }; // Optionally, add a newline before the content file.write_all(b"\n")?; // Write content to file - file.write_all(self.popup_area.popup_sel_item.as_bytes())?; + file.write_all(entry_string.as_bytes())?; // Update the database and the lists to reflect the new content self.update_lists(cfg); self.close_popup(); // Select newly created entry - self.select_entry_by_citekey(&citekey); + self.select_entry_by_citekey(&new_citekey); Ok(()) } @@ -1285,38 +1272,10 @@ impl Bibiman { #[cfg(test)] mod tests { - use regex::Captures; - - use super::*; - #[test] fn citekey_pattern() { let citekey = format!("{{{},", "a_key_2001"); assert_eq!(citekey, "{a_key_2001,") } - - #[test] - fn regex_capture_citekey() { - let re = Regex::new(r"\{([^\{\},]*),").unwrap(); - - let bibstring = String::from("@article{citekey77_2001:!?, author = {Hanks, Tom}, title = {A great book}, year = {2001}}"); - - let citekey = re.captures(&bibstring).unwrap().get(1).unwrap().as_str(); - - assert_eq!(citekey, "citekey77_2001:!?"); - - if bibstring.contains(&citekey) { - let append_char = "a"; - let new_entry_string_clone = bibstring.clone(); - - let updated_bibstring = re - .replace(&new_entry_string_clone, |caps: &Captures| { - format!("{{{}{},", &caps[1], &append_char) - }) - .to_string(); - - assert_eq!(updated_bibstring, "@article{citekey77_2001:!?a, author = {Hanks, Tom}, title = {A great book}, year = {2001}}") - } - } } diff --git a/src/bibiman/citekeys.rs b/src/bibiman/citekeys.rs index 999c6cb..4516b28 100644 --- a/src/bibiman/citekeys.rs +++ b/src/bibiman/citekeys.rs @@ -51,24 +51,27 @@ pub enum CitekeyCase { } #[derive(Debug, Default, Clone)] -pub(crate) struct CitekeyFormatting { +pub(crate) struct CitekeyFormatting<'a> { /// bibfile to replace keys at. The optional fields defines a differing /// output file to write to, otherwise original file will be overwritten. - bibfile_path: (PathBuf, Option<PathBuf>), bib_entries: Bibliography, fields: Vec<String>, case: Option<CitekeyCase>, old_new_keys_map: Vec<(String, String)>, dry_run: bool, ascii_only: bool, + ignored_chars: &'a [char], + ignored_words: &'a [String], } -impl CitekeyFormatting { +impl<'a> CitekeyFormatting<'a> { pub(crate) fn parse_citekey_cli( parser: &mut lexopt::Parser, cfg: &BibiConfig, ) -> color_eyre::Result<()> { let mut formatter = CitekeyFormatting::default(); + let mut source_file = PathBuf::new(); + let mut target_file: Option<PathBuf> = None; formatter.fields = cfg.citekey_formatter.fields.clone().ok_or_eyre(format!( "Need to define {} correctly in config file", @@ -93,78 +96,73 @@ impl CitekeyFormatting { } Short('d') | Long("dry-run") => formatter.dry_run = true, Short('s') | Short('f') | Long("source") | Long("file") => { - formatter.bibfile_path.0 = parser.value()?.into() + source_file = parser.value()?.into() } Short('t') | Short('o') | Long("target") | Long("output") => { - formatter.bibfile_path.1 = Some(parser.value()?.into()) + target_file = Some(parser.value()?.into()) } _ => return Err(arg.unexpected().into()), } } - let bibstring = std::fs::read_to_string(&formatter.bibfile_path.0)?; + let bibstring = std::fs::read_to_string(&source_file)?; formatter.bib_entries = Bibliography::parse(&bibstring) .map_err(|e| eyre!("Couldn't parse bibfile due to {}", e.kind))?; - let ignored_chars = if let Some(chars) = &cfg.citekey_formatter.ignored_chars { + formatter.ignored_chars = if let Some(chars) = &cfg.citekey_formatter.ignored_chars { chars.as_slice() } else { IGNORED_SPECIAL_CHARS.as_slice() }; - let ignored_words = if let Some(words) = &cfg.citekey_formatter.ignored_words { + formatter.ignored_words = if let Some(words) = &cfg.citekey_formatter.ignored_words { words.as_slice() } else { &*IGNORED_WORDS.as_slice() }; formatter - .do_formatting(ignored_chars, ignored_words) + .do_formatting() .rev_sort_new_keys_by_len() - .update_file()?; + .update_file(source_file, target_file)?; Ok(()) } /// Start Citekey formatting with building a new instance of `CitekeyFormatting` - /// Formatting is processed file by file, because `bibman` can handle - /// multi-file setups. - /// The `Bibliography` inserted will be edited in place with the new citekeys. - /// Thus, in the end the `bib_entries` field will hold the updated `Bibliography` - pub fn new<P: AsRef<Path>>( - cfg: &BibiConfig, - path: P, - target: Option<P>, - bib_entries: Bibliography, - ) -> color_eyre::Result<Self> { - let fields = cfg - .citekey_formatter - .fields - .clone() - .expect("Need to define fields in config to format citekeys"); + pub fn new(cfg: &'a BibiConfig, bib_entries: Bibliography) -> Option<Self> { + let fields = cfg.citekey_formatter.fields.clone().unwrap_or(Vec::new()); if fields.is_empty() { - return Err(eyre!( - "To format all citekeys, you need to provide {} values in the config file", - "fields".bold() - )); + return None; } - Ok(Self { - bibfile_path: ( - path.as_ref().to_path_buf(), - target.map(|p| p.as_ref().to_path_buf()), - ), + let ignored_chars = if let Some(chars) = &cfg.citekey_formatter.ignored_chars { + chars.as_slice() + } else { + IGNORED_SPECIAL_CHARS.as_slice() + }; + + let ignored_words = if let Some(words) = &cfg.citekey_formatter.ignored_words { + words.as_slice() + } else { + &*IGNORED_WORDS.as_slice() + }; + + Some(Self { bib_entries, fields, case: cfg.citekey_formatter.case.clone(), old_new_keys_map: Vec::new(), dry_run: false, ascii_only: cfg.citekey_formatter.ascii_only, + ignored_chars, + ignored_words, }) } - /// Process the actual formatting. The citekey of every entry will be updated. - pub fn do_formatting(&mut self, ignored_chars: &[char], ignored_words: &[String]) -> &mut Self { + /// Process the actual formatting. Updated citekeys will be stored in a the + /// `self.old_new_keys_map` vector consisting of pairs (old key, new key). + pub fn do_formatting(mut self) -> Self { let mut old_new_keys: Vec<(String, String)> = Vec::new(); for entry in self.bib_entries.iter() { // Skip specific entries @@ -178,8 +176,8 @@ impl CitekeyFormatting { &self.fields, self.case.as_ref(), self.ascii_only, - ignored_chars, - ignored_words, + self.ignored_chars, + self.ignored_words, ), )); } @@ -189,8 +187,12 @@ impl CitekeyFormatting { self } - /// Write entries with updated citekeys to bibfile - pub fn update_file(&mut self) -> color_eyre::Result<()> { + /// Write formatted citekeys to bibfile replacing the old keys in all fields + pub fn update_file<P: AsRef<Path>>( + &mut self, + source_file: P, + target_file: Option<P>, + ) -> color_eyre::Result<()> { if self.dry_run { println!("Following citekeys would be formatted: old => new\n"); self.old_new_keys_map.sort_by(|a, b| a.0.cmp(&b.0)); @@ -198,11 +200,10 @@ impl CitekeyFormatting { println!("{} => {}", old.italic(), new.bold()) } } else { - let source_file = self.bibfile_path.0.as_path(); - let target_file = if let Some(path) = &self.bibfile_path.1 { - path + let target_file = if let Some(path) = target_file { + path.as_ref().to_path_buf() } else { - source_file + source_file.as_ref().to_path_buf() }; let mut content = std::fs::read_to_string(source_file)?; @@ -228,23 +229,34 @@ impl CitekeyFormatting { /// You are **very encouraged** to call this method before `update_file()` to /// prevent replacing citekeys partly which afterwards wont match the pattern /// anymore. - pub fn rev_sort_new_keys_by_len(&mut self) -> &mut Self { + pub fn rev_sort_new_keys_by_len(mut self) -> Self { self.old_new_keys_map .sort_by(|a, b| b.0.len().cmp(&a.0.len())); self } + + /// Update the `Bibliography` of the `CitekeyFormatting` struct and return + /// it as `String`. + pub fn print_updated_bib_as_string(&mut self) -> String { + let mut content = self.bib_entries.to_biblatex_string(); + for (old_key, new_key) in self.old_new_keys_map.iter() { + content = content.replace(old_key, new_key); + } + content + } + + pub fn get_citekey_pair(&self, idx: usize) -> Option<(String, String)> { + self.old_new_keys_map.get(idx).map(|pair| pair.to_owned()) + } } #[cfg(test)] mod tests { - use std::path::PathBuf; - - use biblatex::Bibliography; - use crate::{ bibiman::citekeys::{CitekeyCase, CitekeyFormatting}, config::{IGNORED_SPECIAL_CHARS, IGNORED_WORDS}, }; + use biblatex::Bibliography; #[test] fn format_citekey_test() { @@ -270,8 +282,7 @@ mod tests { } "; let bibliography = Bibliography::parse(src).unwrap(); - let mut formatting_struct = CitekeyFormatting { - bibfile_path: (PathBuf::new(), None), + let formatting_struct = CitekeyFormatting { bib_entries: bibliography, fields: vec