diff options
| author | lukeflo | 2024-10-22 21:52:36 +0200 |
|---|---|---|
| committer | lukeflo | 2024-10-22 21:52:36 +0200 |
| commit | 66402a9c23e0975a8a3d8c2707b689b9cde98ccf (patch) | |
| tree | ccba415674b13eadb6739f5a4d0cb53642dc2e62 /src/bib | |
| parent | 0a74206015e764551ec2a0ade8f6853e915b6911 (diff) | |
| download | bibiman-66402a9c23e0975a8a3d8c2707b689b9cde98ccf.tar.gz bibiman-66402a9c23e0975a8a3d8c2707b689b9cde98ccf.zip | |
rearrange code, file and folder structure
Diffstat (limited to 'src/bib')
| -rw-r--r-- | src/bib/bibmain.rs | 279 | ||||
| -rw-r--r-- | src/bib/entries.rs | 258 | ||||
| -rw-r--r-- | src/bib/keywords.rs | 55 | ||||
| -rw-r--r-- | src/bib/search.rs | 136 |
4 files changed, 728 insertions, 0 deletions
diff --git a/src/bib/bibmain.rs b/src/bib/bibmain.rs new file mode 100644 index 0000000..a7df951 --- /dev/null +++ b/src/bib/bibmain.rs @@ -0,0 +1,279 @@ +// bibiman - a TUI for managing BibLaTeX databases +// Copyright (C) 2024 lukeflo +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +///// + +use biblatex::{self, Bibliography}; +use biblatex::{ChunksExt, Type}; +use itertools::Itertools; +use std::{fs, path::PathBuf}; + +#[derive(Debug)] +pub enum FileFormat { + BibLatex, + Hayagriva, +} + +// Set necessary fields +// TODO: can surely be made more efficient/simpler +#[derive(Debug)] +pub struct BibiMain { + pub bibfile: PathBuf, // path to bibfile + pub bibfile_format: FileFormat, // Format of passed file + pub bibfilestring: String, // content of bibfile as string + pub bibliography: Bibliography, // parsed bibliography + pub citekeys: Vec<String>, // list of all citekeys + pub keyword_list: Vec<String>, // list of all available keywords + pub entry_list: Vec<BibiData>, // List of all entries +} + +#[derive(Debug, Clone)] +pub struct BibiData { + pub authors: String, + pub title: String, + pub year: String, + pub pubtype: String, + pub keywords: String, + pub citekey: String, + pub abstract_text: String, + pub doi_url: String, + pub filepath: String, +} + +impl BibiMain { + pub fn new(main_bibfile: PathBuf) -> Self { + // TODO: Needs check for config file path as soon as config file is impl + let bibfile_format = Self::check_file_format(&main_bibfile); + let bibfile = main_bibfile; + let bibfilestring = fs::read_to_string(&bibfile).unwrap(); + let bibliography = biblatex::Bibliography::parse(&bibfilestring).unwrap(); + let citekeys = Self::get_citekeys(&bibliography); + let keyword_list = Self::collect_tag_list(&citekeys, &bibliography); + let entry_list = Self::create_entry_list(&citekeys, &bibliography); + Self { + bibfile, + bibfile_format, + bibfilestring, + bibliography, + citekeys, + keyword_list, + entry_list, + } + } + + // Check which file format the passed file has + fn check_file_format(main_bibfile: &PathBuf) -> FileFormat { + let extension = main_bibfile.extension().unwrap().to_str(); + + match extension { + Some("yml") => FileFormat::Hayagriva, + Some("yaml") => FileFormat::Hayagriva, + Some("bib") => FileFormat::BibLatex, + Some(_) => panic!("The extension {:?} is no valid bibfile", extension.unwrap()), + None => panic!("The given path {:?} holds no valid file", main_bibfile), + } + } + + fn create_entry_list(citekeys: &[String], bibliography: &Bibliography) -> Vec<BibiData> { + citekeys + .into_iter() + .map(|k| BibiData { + authors: Self::get_authors(&k, &bibliography), + title: Self::get_title(&k, &bibliography), + year: Self::get_year(&k, &bibliography), + pubtype: Self::get_pubtype(&k, &bibliography), + keywords: Self::get_keywords(&k, &bibliography), + citekey: k.to_owned(), + abstract_text: Self::get_abstract(&k, &bibliography), + doi_url: Self::get_weblink(&k, &bibliography), + filepath: Self::get_filepath(&k, &bibliography), + }) + .collect() + } + + // get list of citekeys from the given bibfile + // this list is the base for further operations on the bibentries + // since it is the entry point of the biblatex crate. + pub fn get_citekeys(bibstring: &Bibliography) -> Vec<String> { + let citekeys: Vec<String> = bibstring.keys().map(|k| k.to_owned()).collect(); + citekeys + } + + // collect all keywords present in the bibliography + // sort them and remove duplicates + // this list is for fast filtering entries by topics/keyowrds + pub fn collect_tag_list(citekeys: &[String], biblio: &Bibliography) -> Vec<String> { + // Initialize vector collecting all keywords + let mut keyword_list = vec![]; + + // Loop over entries and collect all keywords + for i in citekeys { + if biblio.get(&i).unwrap().keywords().is_ok() { + let items = biblio + .get(&i) + .unwrap() + .keywords() + .unwrap() + .format_verbatim(); + // Split keyword string into slices, trim leading and trailing + // whitespaces, remove empty slices, and collect them + let mut key_vec: Vec<String> = items + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + // Append keywords to vector + keyword_list.append(&mut key_vec); + } + } + + // Sort the vector and remove duplicates + keyword_list.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + keyword_list.dedup(); + keyword_list + } + + pub fn get_authors(citekey: &str, biblio: &Bibliography) -> String { + if biblio.get(&citekey).unwrap().author().is_ok() { + let authors = biblio.get(&citekey).unwrap().author().unwrap(); + if authors.len() > 1 { + let all_authors = authors.iter().map(|a| &a.name).join(", "); + all_authors + } else if authors.len() == 1 { + let authors = authors[0].name.to_string(); + authors + } else { + let editors_authors = format!("empty"); + editors_authors + } + } else { + if !biblio.get(&citekey).unwrap().editors().unwrap().is_empty() { + let editors = biblio.get(&citekey).unwrap().editors().unwrap(); + if editors[0].0.len() > 1 { + // let editors = format!("{} (ed.) et al.", editors[0].0[0].name); + let mut editors = editors[0].0.iter().map(|e| &e.name).join(", "); + editors.push_str(" (ed.)"); + editors + } else if editors[0].0.len() == 1 { + let editors = format!("{} (ed.)", editors[0].0[0].name); + editors + } else { + let editors_authors = format!("empty"); + editors_authors + } + } else { + let editors_authors = format!("empty"); + editors_authors + } + } + } + + pub fn get_title(citekey: &str, biblio: &Bibliography) -> String { + let title = { + if biblio.get(&citekey).unwrap().title().is_ok() { + let title = biblio + .get(&citekey) + .unwrap() + .title() + .unwrap() + .format_verbatim(); + title + } else { + let title = format!("no title"); + title + } + }; + title + } + + pub fn get_year(citekey: &str, biblio: &Bibliography) -> String { + let year = biblio.get(&citekey).unwrap(); + let year = { + if year.date().is_ok() { + let year = year.date().unwrap().to_chunks().format_verbatim(); + let year = year[..4].to_string(); + year + } else { + let year = format!("n.d."); + year + } + }; + year + } + + pub fn get_pubtype(citekey: &str, biblio: &Bibliography) -> String { + let pubtype = biblio.get(&citekey).unwrap().entry_type.to_string(); + pubtype + } + + pub fn get_keywords(citekey: &str, biblio: &Bibliography) -> String { + let keywords = { + if biblio.get(&citekey).unwrap().keywords().is_ok() { + let keywords = biblio + .get(&citekey) + .unwrap() + .keywords() + .unwrap() + .format_verbatim(); + keywords + } else { + let keywords = String::from(""); + keywords + } + }; + keywords + } + + pub fn get_abstract(citekey: &str, biblio: &Bibliography) -> String { + let text = { + if biblio.get(&citekey).unwrap().abstract_().is_ok() { + let abstract_text = biblio + .get(&citekey) + .unwrap() + .abstract_() + .unwrap() + .format_verbatim(); + abstract_text + } else { + let abstract_text = format!("No abstract"); + abstract_text + } + }; + text + } + + pub fn get_weblink(citekey: &str, biblio: &Bibliography) -> String { + if let true = biblio.get(&citekey).unwrap().doi().is_ok() { + let url = biblio.get(&citekey).unwrap().doi().unwrap(); + url + } else if let true = biblio.get(&citekey).unwrap().url().is_ok() { + let url = biblio.get(&citekey).unwrap().url().unwrap(); + url + } else { + let url = "".to_string(); + url + } + } + + pub fn get_filepath(citekey: &str, biblio: &Bibliography) -> String { + if let true = biblio.get(&citekey).unwrap().file().is_ok() { + let file = biblio.get(&citekey).unwrap().file().unwrap(); + file + } else { + let file = "".to_string(); + file + } + } +} diff --git a/src/bib/entries.rs b/src/bib/entries.rs new file mode 100644 index 0000000..41edba8 --- /dev/null +++ b/src/bib/entries.rs @@ -0,0 +1,258 @@ +// bibiman - a TUI for managing BibLaTeX databases +// Copyright (C) 2024 lukeflo +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +///// + +use crate::bib::bibmain::BibiData; +use ratatui::widgets::{ScrollbarState, TableState}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EntryTableColumn { + Authors, + Title, + Year, + Pubtype, +} + +// Define list containing entries as table +#[derive(Debug, PartialEq, Eq)] +pub struct EntryTable { + pub entry_table_items: Vec<EntryTableItem>, + pub entry_table_at_search_start: Vec<EntryTableItem>, + pub entry_table_selected_column: EntryTableColumn, + pub entry_table_sorted_by_col: EntryTableColumn, + pub entry_table_reversed_sort: bool, + pub entry_table_state: TableState, + pub entry_scroll_state: ScrollbarState, + pub entry_info_scroll: u16, + pub entry_info_scroll_state: ScrollbarState, +} + +impl EntryTable { + pub fn new(entry_list: Vec<BibiData>) -> Self { + let entry_table_items = Self::set_entry_table(entry_list); + let entry_table_state = TableState::default().with_selected(0); + let entry_scroll_state = ScrollbarState::new(entry_table_items.len()); + let entry_info_scroll_state = ScrollbarState::default(); + Self { + entry_table_items, + entry_table_at_search_start: Vec::new(), + entry_table_selected_column: EntryTableColumn::Authors, + entry_table_sorted_by_col: EntryTableColumn::Authors, + entry_table_reversed_sort: false, + entry_table_state, + entry_scroll_state, + entry_info_scroll: 0, + entry_info_scroll_state, + } + } + + pub fn set_entry_table(entry_list: Vec<BibiData>) -> Vec<EntryTableItem> { + let mut entry_table: Vec<EntryTableItem> = entry_list + .into_iter() + .map(|e| EntryTableItem { + authors: e.authors, + short_author: String::new(), + title: e.title, + year: e.year, + pubtype: e.pubtype, + keywords: e.keywords, + citekey: e.citekey, + abstract_text: e.abstract_text, + doi_url: e.doi_url, + filepath: e.filepath, + }) + .collect(); + + entry_table.sort_by(|a, b| a.authors.to_lowercase().cmp(&b.authors.to_lowercase())); + entry_table + } + + // Sort entry table by specific column. + // Toggle sorting by hitting same key again + pub fn sort_entry_table(&mut self, toggle: bool) { + if toggle { + self.entry_table_reversed_sort = !self.entry_table_reversed_sort; + } + if self.entry_table_selected_column != self.entry_table_sorted_by_col { + self.entry_table_reversed_sort = false + } + self.entry_table_sorted_by_col = self.entry_table_selected_column.clone(); + if self.entry_table_reversed_sort { + match self.entry_table_selected_column { + EntryTableColumn::Authors => self + .entry_table_items + .sort_by(|a, b| b.authors.to_lowercase().cmp(&a.authors.to_lowercase())), + EntryTableColumn::Title => self + .entry_table_items + .sort_by(|a, b| b.title.to_lowercase().cmp(&a.title.to_lowercase())), + EntryTableColumn::Year => self + .entry_table_items + .sort_by(|a, b| b.year.to_lowercase().cmp(&a.year.to_lowercase())), + EntryTableColumn::Pubtype => self + .entry_table_items + .sort_by(|a, b| b.pubtype.to_lowercase().cmp(&a.pubtype.to_lowercase())), + } + } else if !self.entry_table_reversed_sort { + match self.entry_table_selected_column { + EntryTableColumn::Authors => self + .entry_table_items + .sort_by(|a, b| a.authors.to_lowercase().cmp(&b.authors.to_lowercase())), + EntryTableColumn::Title => self + .entry_table_items + .sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())), + EntryTableColumn::Year => self + .entry_table_items + .sort_by(|a, b| a.year.to_lowercase().cmp(&b.year.to_lowercase())), + EntryTableColumn::Pubtype => self + .entry_table_items + .sort_by(|a, b| a.pubtype.to_lowercase().cmp(&b.pubtype.to_lowercase())), + } + } + } +} + +// Define contents of each entry table row +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct EntryTableItem { + pub authors: String, + pub short_author: String, + pub title: String, + pub year: String, + pub pubtype: String, + pub keywords: String, + pub citekey: String, + pub abstract_text: String, + pub doi_url: String, + pub filepath: String, +} + +impl EntryTableItem { + // This functions decides which fields are rendered in the entry table + // Fields which should be usable but not visible can be left out + pub fn ref_vec(&mut self) -> Vec<&str> { + self.short_author = match self.authors.split_once(",") { + Some((first, _rest)) => { + if self.authors.contains("(ed.)") { + let first_author = format!("{} et al. (ed.)", first); + first_author + } else { + let first_author = format!("{} et al.", first); + first_author + } + } + None => String::from(""), + }; + + vec![ + { + if self.short_author.is_empty() { + &self.authors + } else { + &self.short_author + } + }, + &self.title, + &self.year, + &self.pubtype, + ] + } + + pub fn authors(&self) -> &str { + &self.authors + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn year(&self) -> &str { + &self.year + } + + pub fn pubtype(&self) -> &str { + &self.pubtype + } + + pub fn citekey(&self) -> &str { + &self.citekey + } + + pub fn doi_url(&self) -> &str { + &self.doi_url + } + + pub fn filepath(&self) -> &str { + &self.filepath + } +} + +#[cfg(test)] +mod tests { + use super::EntryTableItem; + + #[test] + fn check_os() { + let os = std::env::consts::OS; + assert_eq!( + os, + "linux", + "You're not coding on linux, but on {}... Switch to linux, now!", + std::env::consts::OS + ) + } + + #[test] + fn shorten_authors() { + let mut entry: EntryTableItem = EntryTableItem { + authors: "Miller, Schmitz, Bernard".to_string(), + short_author: "".to_string(), + title: "A title".to_string(), + year: "2000".to_string(), + pubtype: "article".to_string(), + keywords: "key1, key2".to_string(), + citekey: "miller_2000".to_string(), + abstract_text: "An abstract".to_string(), + doi_url: "www.text.org".to_string(), + filepath: "/home/test".to_string(), + }; + + let entry_vec = EntryTableItem::ref_vec(&mut entry); + + let mut entry_editors: EntryTableItem = EntryTableItem { + authors: "Miller, Schmitz, Bernard (ed.)".to_string(), + short_author: "".to_string(), + title: "A title".to_string(), + year: "2000".to_string(), + pubtype: "article".to_string(), + keywords: "key1, key2".to_string(), + citekey: "miller_2000".to_string(), + abstract_text: "An abstract".to_string(), + doi_url: "www.text.org".to_string(), + filepath: "/home/test".to_string(), + }; + + let entry_vec_editors = EntryTableItem::ref_vec(&mut entry_editors); + + assert_eq!( + entry_vec, + vec!["Miller et al.", "A title", "2000", "article"] + ); + assert_eq!( + entry_vec_editors, + vec!["Miller et al. (ed.)", "A title", "2000", "article"] + ) + } +} diff --git a/src/bib/keywords.rs b/src/bib/keywords.rs new file mode 100644 index 0000000..2668323 --- /dev/null +++ b/src/bib/keywords.rs @@ -0,0 +1,55 @@ +// bibiman - a TUI for managing BibLaTeX databases +// Copyright (C) 2024 lukeflo +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +///// + +use ratatui::widgets::{ListState, ScrollbarState}; + +#[derive(Debug)] +pub struct TagList { + pub tag_list_items: Vec<String>, + pub tag_list_state: ListState, + pub tag_scroll_state: ScrollbarState, + pub selected_keywords: Vec<String>, +} + +// Structure of the list items. +#[derive(Debug)] +pub struct TagListItem { + pub keyword: String, +} + +// Function to process inputed characters and convert them (to string, or more complex function) +impl TagListItem { + pub fn new(info: &str) -> Self { + Self { + keyword: info.to_string(), + } + } +} + +impl TagList { + pub fn new(keyword_list: Vec<String>) -> Self { + let tag_list_items = keyword_list; + let tag_list_state = ListState::default(); // for preselection: .with_selected(Some(0)); + let tag_scroll_state = ScrollbarState::new(tag_list_items.len()); + Self { + tag_list_items, + tag_list_state, + tag_scroll_state, + selected_keywords: Vec::new(), + } + } +} diff --git a/src/bib/search.rs b/src/bib/search.rs new file mode 100644 index 0000000..f6e8d14 --- /dev/null +++ b/src/bib/search.rs @@ -0,0 +1,136 @@ +use super::entries::EntryTableItem; +use nucleo_matcher::{ + pattern::{CaseMatching, Normalization, Pattern}, + Config, Matcher, +}; +use std::collections::HashMap; + +#[derive(Debug)] +pub struct BibiSearch { + pub search_string: String, // Search string show in footer, used for search + pub inner_search: bool, // True, if we trigger a search for already filtered list + pub filtered_tag_list: Vec<String>, +} + +impl Default for BibiSearch { + fn default() -> Self { + Self { + search_string: String::new(), + inner_search: false, + filtered_tag_list: Vec::new(), + } + } +} + +impl BibiSearch { + // Stringify EntryTableItem by joining/concat + fn convert_to_string(inner_vec: &EntryTableItem) -> String { + let entry_table_item_str = { + format!( + "{} {} {} {} {} {}", + &inner_vec.authors, + &inner_vec.title, + &inner_vec.year, + &inner_vec.pubtype, + &inner_vec.keywords, + &inner_vec.citekey + ) + }; + entry_table_item_str + } + + // Return a filtered entry list + pub fn search_entry_list( + search_pattern: &str, + orig_list: Vec<EntryTableItem>, + ) -> Vec<EntryTableItem> { + // Create a hashmap to connect stingified entry with entry vec + let mut entry_string_hm: HashMap<String, EntryTableItem> = HashMap::new(); + + // Convert all entries to string and insert them into the hashmap + // next to the original inner Vec<String> of the entry list + for entry in orig_list { + entry_string_hm.insert(Self::convert_to_string(&entry), entry); + } + + // Set up matcher (TODO: One time needed only, move to higher level) + let mut matcher = Matcher::new(Config::DEFAULT); + + // Filter the stringified entries and collect them into a vec + let filtered_matches: Vec<String> = { + let matches = + Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) + .match_list(entry_string_hm.keys(), &mut matcher); + matches.into_iter().map(|f| f.0.to_string()).collect() + }; + + // Create filtered entry list and push the inner entry vec's to it + // Use the filtered stringified hm-key as index + let mut filtered_list: Vec<EntryTableItem> = Vec::new(); + for m in filtered_matches { + filtered_list.push(entry_string_hm[&m].to_owned()); + } + filtered_list.sort(); + filtered_list + } + + pub fn search_tag_list(search_pattern: &str, orig_list: Vec<String>) -> Vec<String> { + // Set up matcher (TODO: One time needed only) + let mut matcher = Matcher::new(Config::DEFAULT); + + // Filter the list items by search pattern + let filtered_matches: Vec<String> = { + let matches = + Pattern::parse(search_pattern, CaseMatching::Ignore, Normalization::Smart) + .match_list(orig_list, &mut matcher); + matches.into_iter().map(|f| f.0.to_string()).collect() + }; + filtered_matches + } + + pub fn filter_entries_by_tag( + keyword: &str, + orig_list: &Vec<EntryTableItem>, + ) -> Vec<EntryTableItem> { + let mut filtered_list: Vec<EntryTableItem> = Vec::new(); + + // Loop over the whole given entry table + // Check if the selected keyword is present in the current entry + // If present, push the entry to the filtered list + for e in orig_list { + if e.keywords.contains(keyword) { + filtered_list.push(e.to_owned()); + } + } + + filtered_list + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vector_join() { + let bibvec: EntryTableItem = EntryTableItem { + authors: "Author".to_string(), + short_author: "".to_string(), + title: "Title".to_string(), + year: "1999".to_string(), + pubtype: "article".to_string(), + keywords: "hello, bye".to_string(), + citekey: "author_1999".to_string(), + abstract_text: "An abstract with multiple sentences. Here is the second".to_string(), + doi_url: "https://www.bibiman.org".to_string(), + filepath: "/home/file/path.pdf".to_string(), + }; + + let joined_vec = BibiSearch::convert_to_string(&bibvec); + + assert_eq!( + joined_vec, + "Author Title 1999 article hello, bye author_1999" + ) + } +} |
