aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlukeflo2025-10-14 14:30:56 +0200
committerlukeflo2025-10-14 14:30:56 +0200
commit9b21727bd151a3fda2133b9da12eec588068130e (patch)
tree843db53d8a266a4905d37936a606de731802094a
parent549f89c554ae70af28a9c7276673f0f77b488165 (diff)
downloadbibiman-9b21727bd151a3fda2133b9da12eec588068130e.tar.gz
bibiman-9b21727bd151a3fda2133b9da12eec588068130e.zip
use citekey formatter for adding new entries via doi
-rw-r--r--src/bibiman.rs105
-rw-r--r--src/bibiman/citekeys.rs118
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![
"entrytype;;;;:".into(),
@@ -284,9 +295,10 @@ mod tests {
old_new_keys_map: Vec::new(),
dry_run: false,
ascii_only: true,
+ ignored_chars: &IGNORED_SPECIAL_CHARS,
+ ignored_words: &IGNORED_WORDS,
};
- let _ = formatting_struct
- .do_formatting(IGNORED_SPECIAL_CHARS.as_slice(), &*IGNORED_WORDS.as_slice());
+ let formatting_struct = formatting_struct.do_formatting();
assert_eq!(
formatting_struct.old_new_keys_map.get(0).unwrap().1,
"article:bos-mccurley_lat=met=pub=wor_2023"