use crate::error::{OptimError, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationManager {
pub citations: HashMap<String, Citation>,
pub styles: HashMap<String, CitationStyle>,
pub default_style: String,
pub groups: HashMap<String, CitationGroup>,
pub settings: CitationSettings,
pub modified_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Citation {
pub key: String,
pub publication_type: PublicationType,
pub title: String,
pub authors: Vec<Author>,
pub year: Option<u32>,
pub venue: Option<String>,
pub volume: Option<String>,
pub issue: Option<String>,
pub pages: Option<String>,
pub doi: Option<String>,
pub url: Option<String>,
pub abstracttext: Option<String>,
pub keywords: Vec<String>,
pub notes: Option<String>,
pub custom_fields: HashMap<String, String>,
pub attachments: Vec<String>,
pub groups: Vec<String>,
pub import_source: Option<String>,
pub created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PublicationType {
Article,
InProceedings,
Book,
InCollection,
PhDThesis,
MastersThesis,
TechReport,
Manual,
Misc,
Unpublished,
Preprint,
Patent,
Software,
Dataset,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Author {
pub first_name: String,
pub last_name: String,
pub middle_name: Option<String>,
pub suffix: Option<String>,
pub orcid: Option<String>,
pub affiliation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationStyle {
pub name: String,
pub description: String,
pub intext_format: InTextFormat,
pub bibliography_format: BibliographyFormat,
pub formatting_rules: FormattingRules,
pub sorting_rules: SortingRules,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum InTextFormat {
AuthorYear,
Numbered,
Superscript,
AuthorNumber,
Footnote,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BibliographyFormat {
pub entry_separator: String,
pub field_separators: HashMap<String, String>,
pub name_format: NameFormat,
pub title_format: TitleFormat,
pub date_format: DateFormat,
pub punctuation: PunctuationRules,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum NameFormat {
LastFirstMiddle,
FirstMiddleLast,
LastFirstInitial,
FirstInitialLast,
LastFirstInitialNoSpace,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TitleFormat {
TitleCase,
SentenceCase,
Uppercase,
Lowercase,
AsEntered,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DateFormat {
Year,
MonthYear,
MonthAbbrevYear,
FullDate,
ISODate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PunctuationRules {
pub periods_after_abbreviations: bool,
pub commas_between_fields: bool,
pub parentheses_around_year: bool,
pub quote_titles: bool,
pub italicize_journals: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormattingRules {
pub max_authors: Option<usize>,
pub et_altext: String,
pub et_al_threshold: usize,
pub title_case: bool,
pub abbreviate_journals: bool,
pub include_doi: bool,
pub include_url: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SortingRules {
pub primary_sort: SortField,
pub secondary_sort: Option<SortField>,
pub sort_direction: SortDirection,
pub group_by_type: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SortField {
Author,
Year,
Title,
Venue,
Key,
DateAdded,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SortDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationGroup {
pub name: String,
pub description: String,
pub color: Option<String>,
pub citation_keys: Vec<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationSettings {
pub auto_generate_keys: bool,
pub key_pattern: String,
pub auto_import_doi: bool,
pub auto_import_url: bool,
pub duplicate_detection: bool,
pub backup_enabled: bool,
pub export_formats: Vec<ExportFormat>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ExportFormat {
BibTeX,
RIS,
EndNote,
JSON,
CSV,
Word,
}
#[derive(Debug)]
pub struct BibTeXProcessor {
settings: BibTeXSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BibTeXSettings {
pub preserve_case: bool,
pub utf8_conversion: bool,
pub cleanup_formatting: bool,
pub validate_entries: bool,
}
#[derive(Debug)]
pub struct CitationDiscovery {
search_engines: Vec<SearchEngine>,
api_keys: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchEngine {
pub name: String,
pub endpoint: String,
pub rate_limit: f64,
pub query_types: Vec<QueryType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum QueryType {
DOI,
Title,
Author,
ArXiv,
PubMed,
ISBN,
FreeText,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationNetwork {
pub citations: Vec<String>,
pub relationships: Vec<CitationRelationship>,
pub metrics: NetworkMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CitationRelationship {
pub citing: String,
pub cited: String,
pub relationship_type: RelationshipType,
pub strength: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RelationshipType {
DirectCitation,
CoCitation,
BibliographicCoupling,
SameAuthor,
SameVenue,
SimilarTopic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkMetrics {
pub total_nodes: usize,
pub total_edges: usize,
pub density: f64,
pub clustering_coefficient: f64,
pub most_cited: Vec<(String, usize)>,
pub most_influential_authors: Vec<(String, f64)>,
}
impl Default for CitationManager {
fn default() -> Self {
Self::new()
}
}
impl CitationManager {
pub fn new() -> Self {
let mut styles = HashMap::new();
styles.insert("APA".to_string(), Self::create_apa_style());
styles.insert("IEEE".to_string(), Self::create_ieee_style());
styles.insert("ACM".to_string(), Self::create_acm_style());
Self {
citations: HashMap::new(),
styles,
default_style: "APA".to_string(),
groups: HashMap::new(),
settings: CitationSettings::default(),
modified_at: Utc::now(),
}
}
pub fn add_citation(&mut self, citation: Citation) -> Result<()> {
if self.citations.contains_key(&citation.key) {
return Err(OptimError::InvalidConfig(format!(
"Citation with key '{}' already exists",
citation.key
)));
}
self.citations.insert(citation.key.clone(), citation);
self.modified_at = Utc::now();
Ok(())
}
pub fn get_citation(&self, key: &str) -> Option<&Citation> {
self.citations.get(key)
}
pub fn update_citation(&mut self, key: &str, citation: Citation) -> Result<()> {
if !self.citations.contains_key(key) {
return Err(OptimError::InvalidConfig(format!(
"Citation with key '{}' not found",
key
)));
}
self.citations.insert(key.to_string(), citation);
self.modified_at = Utc::now();
Ok(())
}
pub fn remove_citation(&mut self, key: &str) -> Result<()> {
if self.citations.remove(key).is_none() {
return Err(OptimError::InvalidConfig(format!(
"Citation with key '{}' not found",
key
)));
}
self.modified_at = Utc::now();
Ok(())
}
pub fn search_citations(&self, query: &str) -> Vec<&Citation> {
let query_lower = query.to_lowercase();
self.citations
.values()
.filter(|citation| {
citation.title.to_lowercase().contains(&query_lower)
|| citation.authors.iter().any(|author| {
author.last_name.to_lowercase().contains(&query_lower)
|| author.first_name.to_lowercase().contains(&query_lower)
})
|| citation
.keywords
.iter()
.any(|keyword| keyword.to_lowercase().contains(&query_lower))
|| citation
.venue
.as_ref()
.is_some_and(|venue| venue.to_lowercase().contains(&query_lower))
})
.collect()
}
pub fn format_citation(&self, key: &str, style: Option<&str>) -> Result<String> {
let citation = self
.get_citation(key)
.ok_or_else(|| OptimError::InvalidConfig(format!("Citation '{}' not found", key)))?;
let style_name = style.unwrap_or(&self.default_style);
let citation_style = self.styles.get(style_name).ok_or_else(|| {
OptimError::InvalidConfig(format!("Style '{}' not found", style_name))
})?;
self.format_citation_with_style(citation, citation_style)
}
pub fn generate_bibliography(
&self,
citation_keys: &[String],
style: Option<&str>,
) -> Result<String> {
let style_name = style.unwrap_or(&self.default_style);
let citation_style = self.styles.get(style_name).ok_or_else(|| {
OptimError::InvalidConfig(format!("Style '{}' not found", style_name))
})?;
let mut citations: Vec<&Citation> = citation_keys
.iter()
.filter_map(|key| self.citations.get(key))
.collect();
self.sort_citations(&mut citations, &citation_style.sorting_rules);
let mut bibliography = String::new();
for citation in citations {
let formatted = self.format_citation_with_style(citation, citation_style)?;
bibliography.push_str(&formatted);
bibliography.push('\n');
}
Ok(bibliography)
}
pub fn export_bibtex(&self, citation_keys: Option<&[String]>) -> String {
let citations: Vec<&Citation> = if let Some(_keys) = citation_keys {
_keys
.iter()
.filter_map(|key| self.citations.get(key))
.collect()
} else {
self.citations.values().collect()
};
let mut bibtex = String::new();
for citation in citations {
bibtex.push_str(&self.citation_to_bibtex(citation));
bibtex.push('\n');
}
bibtex
}
pub fn import_bibtex(&mut self, bibtex_content: &str) -> Result<usize> {
let processor = BibTeXProcessor::new(BibTeXSettings::default());
let citations = processor.parse_bibtex(bibtex_content)?;
let mut imported_count = 0;
for citation in citations {
if !self.citations.contains_key(&citation.key) {
self.citations.insert(citation.key.clone(), citation);
imported_count += 1;
}
}
self.modified_at = Utc::now();
Ok(imported_count)
}
pub fn create_group(&mut self, name: &str, description: &str) -> String {
let group_id = uuid::Uuid::new_v4().to_string();
let group = CitationGroup {
name: name.to_string(),
description: description.to_string(),
color: None,
citation_keys: Vec::new(),
created_at: Utc::now(),
};
self.groups.insert(group_id.clone(), group);
group_id
}
pub fn add_to_group(&mut self, group_id: &str, citation_key: &str) -> Result<()> {
let group = self
.groups
.get_mut(group_id)
.ok_or_else(|| OptimError::InvalidConfig(format!("Group '{}' not found", group_id)))?;
if !group.citation_keys.contains(&citation_key.to_string()) {
group.citation_keys.push(citation_key.to_string());
}
Ok(())
}
fn format_citation_with_style(
&self,
citation: &Citation,
style: &CitationStyle,
) -> Result<String> {
match style.intext_format {
InTextFormat::AuthorYear => self.format_author_year(citation, style),
InTextFormat::Numbered => self.format_numbered(citation, style),
InTextFormat::Superscript => self.format_superscript(citation, style),
InTextFormat::AuthorNumber => self.format_author_number(citation, style),
InTextFormat::Footnote => self.format_footnote(citation, style),
}
}
fn format_author_year(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
let authors = self.format_authors(&citation.authors, &style.formatting_rules);
let year = citation
.year
.map(|y| y.to_string())
.unwrap_or_else(|| "n.d.".to_string());
Ok(format!("({}, {})", authors, year))
}
fn format_numbered(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
Ok(format!("[{}]", 1)) }
fn format_superscript(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
Ok("¹".to_string()) }
fn format_author_number(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
let authors = self.format_authors(&citation.authors, &style.formatting_rules);
Ok(format!("{} [1]", authors)) }
fn format_footnote(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
self.format_full_citation(citation, style)
}
fn format_full_citation(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
let mut formatted = String::new();
let authors = self.format_authors(&citation.authors, &style.formatting_rules);
formatted.push_str(&authors);
let title = self.format_title(&citation.title, &style.bibliography_format.title_format);
formatted.push_str(&format!(". {}.", title));
if let Some(venue) = &citation.venue {
let venue_formatted = if style.bibliography_format.punctuation.italicize_journals {
format!(" *{}*", venue)
} else {
format!(" {venue}")
};
formatted.push_str(&venue_formatted);
}
if let Some(year) = citation.year {
if style
.bibliography_format
.punctuation
.parentheses_around_year
{
formatted.push_str(&format!(" ({})", year));
} else {
formatted.push_str(&format!(" {year}"));
}
}
if style.formatting_rules.include_doi {
if let Some(doi) = &citation.doi {
formatted.push_str(&format!(". DOI: {doi}"));
}
}
Ok(formatted)
}
fn format_authors(&self, authors: &[Author], rules: &FormattingRules) -> String {
if authors.is_empty() {
return "Anonymous".to_string();
}
let max_authors = rules.max_authors.unwrap_or(authors.len());
let display_authors = if authors.len() > max_authors && max_authors > 0 {
&authors[..max_authors]
} else {
authors
};
let mut formatted_authors = Vec::new();
for author in display_authors {
let formatted = format!("{}, {}", author.last_name, author.first_name);
formatted_authors.push(formatted);
}
let mut result = formatted_authors.join(", ");
if authors.len() > max_authors {
result.push_str(&format!(", {}", rules.et_altext));
}
result
}
fn format_title(&self, title: &str, format: &TitleFormat) -> String {
match format {
TitleFormat::TitleCase => self.to_title_case(title),
TitleFormat::SentenceCase => self.to_sentence_case(title),
TitleFormat::Uppercase => title.to_uppercase(),
TitleFormat::Lowercase => title.to_lowercase(),
TitleFormat::AsEntered => title.to_string(),
}
}
fn to_title_case(&self, s: &str) -> String {
s.split_whitespace()
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => {
first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
}
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn to_sentence_case(&self, s: &str) -> String {
if s.is_empty() {
return String::new();
}
let mut chars = s.chars();
let first = chars
.next()
.expect("unwrap failed")
.to_uppercase()
.collect::<String>();
first + &chars.as_str().to_lowercase()
}
fn sort_citations(&self, citations: &mut Vec<&Citation>, rules: &SortingRules) {
citations.sort_by(|a, b| {
let primary_cmp = self.compare_by_field(a, b, &rules.primary_sort);
if primary_cmp == std::cmp::Ordering::Equal {
if let Some(secondary) = &rules.secondary_sort {
self.compare_by_field(a, b, secondary)
} else {
std::cmp::Ordering::Equal
}
} else {
primary_cmp
}
});
if rules.sort_direction == SortDirection::Descending {
citations.reverse();
}
}
fn compare_by_field(
&self,
a: &Citation,
b: &Citation,
field: &SortField,
) -> std::cmp::Ordering {
match field {
SortField::Author => {
let a_author = a
.authors
.first()
.map(|au| au.last_name.as_str())
.unwrap_or("");
let b_author = b
.authors
.first()
.map(|au| au.last_name.as_str())
.unwrap_or("");
a_author.cmp(b_author)
}
SortField::Year => a.year.cmp(&b.year),
SortField::Title => a.title.cmp(&b.title),
SortField::Venue => a.venue.cmp(&b.venue),
SortField::Key => a.key.cmp(&b.key),
SortField::DateAdded => a.created_at.cmp(&b.created_at),
}
}
fn citation_to_bibtex(&self, citation: &Citation) -> String {
let mut bibtex = format!(
"@{}{{{},\n",
self.publication_type_to_bibtex(&citation.publication_type),
citation.key
);
bibtex.push_str(&format!(" title = {{{}}},\n", citation.title));
if !citation.authors.is_empty() {
let authors = citation
.authors
.iter()
.map(|a| format!("{} {}", a.first_name, a.last_name))
.collect::<Vec<_>>()
.join(" and ");
bibtex.push_str(&format!(" author = {{{}}},\n", authors));
}
if let Some(year) = citation.year {
bibtex.push_str(&format!(" year = {{{}}},\n", year));
}
if let Some(venue) = &citation.venue {
let field_name = match citation.publication_type {
PublicationType::Article => "journal",
PublicationType::InProceedings => "booktitle",
PublicationType::Book => "publisher",
PublicationType::InCollection => "booktitle",
PublicationType::PhDThesis => "school",
PublicationType::MastersThesis => "school",
PublicationType::TechReport => "institution",
PublicationType::Manual => "organization",
PublicationType::Misc => "howpublished",
PublicationType::Unpublished => "note",
PublicationType::Preprint => "archivePrefix",
PublicationType::Patent => "assignee",
PublicationType::Software => "url",
PublicationType::Dataset => "url",
};
bibtex.push_str(&format!(" {} = {{{}}},\n", field_name, venue));
}
if let Some(volume) = &citation.volume {
bibtex.push_str(&format!(" volume = {{{}}},\n", volume));
}
if let Some(pages) = &citation.pages {
bibtex.push_str(&format!(" pages = {{{}}},\n", pages));
}
if let Some(doi) = &citation.doi {
bibtex.push_str(&format!(" doi = {{{}}},\n", doi));
}
bibtex.push_str("}\n");
bibtex
}
fn publication_type_to_bibtex(&self, pub_type: &PublicationType) -> &'static str {
match pub_type {
PublicationType::Article => "article",
PublicationType::InProceedings => "inproceedings",
PublicationType::Book => "book",
PublicationType::InCollection => "incollection",
PublicationType::PhDThesis => "phdthesis",
PublicationType::MastersThesis => "mastersthesis",
PublicationType::TechReport => "techreport",
PublicationType::Manual => "manual",
PublicationType::Misc => "misc",
PublicationType::Unpublished => "unpublished",
PublicationType::Preprint => "misc",
PublicationType::Patent => "misc",
PublicationType::Software => "misc",
PublicationType::Dataset => "misc",
}
}
fn create_apa_style() -> CitationStyle {
CitationStyle {
name: "APA".to_string(),
description: "American Psychological Association style".to_string(),
intext_format: InTextFormat::AuthorYear,
bibliography_format: BibliographyFormat {
entry_separator: "\n".to_string(),
field_separators: {
let mut separators = HashMap::new();
separators.insert("author_title".to_string(), ". ".to_string());
separators.insert("title_venue".to_string(), ". ".to_string());
separators
},
name_format: NameFormat::LastFirstInitial,
title_format: TitleFormat::SentenceCase,
date_format: DateFormat::Year,
punctuation: PunctuationRules {
periods_after_abbreviations: true,
commas_between_fields: true,
parentheses_around_year: true,
quote_titles: false,
italicize_journals: true,
},
},
formatting_rules: FormattingRules {
max_authors: Some(7),
et_altext: "et al.".to_string(),
et_al_threshold: 8,
title_case: false,
abbreviate_journals: false,
include_doi: true,
include_url: false,
},
sorting_rules: SortingRules {
primary_sort: SortField::Author,
secondary_sort: Some(SortField::Year),
sort_direction: SortDirection::Ascending,
group_by_type: false,
},
}
}
fn create_ieee_style() -> CitationStyle {
CitationStyle {
name: "IEEE".to_string(),
description: "Institute of Electrical and Electronics Engineers style".to_string(),
intext_format: InTextFormat::Numbered,
bibliography_format: BibliographyFormat {
entry_separator: "\n".to_string(),
field_separators: HashMap::new(),
name_format: NameFormat::FirstInitialLast,
title_format: TitleFormat::AsEntered,
date_format: DateFormat::Year,
punctuation: PunctuationRules {
periods_after_abbreviations: true,
commas_between_fields: true,
parentheses_around_year: false,
quote_titles: true,
italicize_journals: true,
},
},
formatting_rules: FormattingRules {
max_authors: None,
et_altext: "et al.".to_string(),
et_al_threshold: 7,
title_case: false,
abbreviate_journals: true,
include_doi: true,
include_url: false,
},
sorting_rules: SortingRules {
primary_sort: SortField::Year,
secondary_sort: Some(SortField::Author),
sort_direction: SortDirection::Ascending,
group_by_type: false,
},
}
}
fn create_acm_style() -> CitationStyle {
CitationStyle {
name: "ACM".to_string(),
description: "Association for Computing Machinery style".to_string(),
intext_format: InTextFormat::Numbered,
bibliography_format: BibliographyFormat {
entry_separator: "\n".to_string(),
field_separators: HashMap::new(),
name_format: NameFormat::FirstMiddleLast,
title_format: TitleFormat::TitleCase,
date_format: DateFormat::Year,
punctuation: PunctuationRules {
periods_after_abbreviations: true,
commas_between_fields: true,
parentheses_around_year: false,
quote_titles: false,
italicize_journals: true,
},
},
formatting_rules: FormattingRules {
max_authors: None,
et_altext: "et al.".to_string(),
et_al_threshold: 3,
title_case: true,
abbreviate_journals: false,
include_doi: true,
include_url: true,
},
sorting_rules: SortingRules {
primary_sort: SortField::Author,
secondary_sort: Some(SortField::Year),
sort_direction: SortDirection::Ascending,
group_by_type: false,
},
}
}
}
impl BibTeXProcessor {
pub fn new(settings: BibTeXSettings) -> Self {
Self { settings }
}
pub fn parse_bibtex(&self, content: &str) -> Result<Vec<Citation>> {
let mut citations = Vec::new();
let lines: Vec<&str> = content.lines().collect();
let mut current_entry: Option<(String, PublicationType, HashMap<String, String>)> = None;
for line in lines {
let line = line.trim();
if line.starts_with('@') {
if let Some((key, pub_type, fields)) = current_entry.take() {
if let Ok(citation) = self.fields_to_citation(key, pub_type, fields) {
citations.push(citation);
}
}
if let Some(pos) = line.find('{') {
let entry_type = line[1..pos].to_lowercase();
let pub_type = self.bibtex_type_to_publication_type(&entry_type);
let key_part = &line[pos + 1..];
if let Some(comma_pos) = key_part.find(',') {
let key = key_part[..comma_pos].trim().to_string();
current_entry = Some((key, pub_type, HashMap::new()));
}
}
} else if line.contains('=') && current_entry.is_some() {
if let Some(eq_pos) = line.find('=') {
let field_name = line[..eq_pos].trim().to_lowercase();
let field_value = line[eq_pos + 1..]
.trim()
.trim_start_matches('{')
.trim_end_matches("},")
.trim_start_matches('"')
.trim_end_matches("\",")
.to_string();
if let Some((_, _, ref mut fields)) = current_entry {
fields.insert(field_name, field_value);
}
}
}
}
if let Some((key, pub_type, fields)) = current_entry {
if let Ok(citation) = self.fields_to_citation(key, pub_type, fields) {
citations.push(citation);
}
}
Ok(citations)
}
fn bibtex_type_to_publication_type(&self, bibtex_type: &str) -> PublicationType {
match bibtex_type {
"article" => PublicationType::Article,
"inproceedings" | "conference" => PublicationType::InProceedings,
"book" => PublicationType::Book,
"incollection" | "inbook" => PublicationType::InCollection,
"phdthesis" => PublicationType::PhDThesis,
"mastersthesis" => PublicationType::MastersThesis,
"techreport" => PublicationType::TechReport,
"manual" => PublicationType::Manual,
"unpublished" => PublicationType::Unpublished,
_ => PublicationType::Misc,
}
}
fn fields_to_citation(
&self,
key: String,
pub_type: PublicationType,
fields: HashMap<String, String>,
) -> Result<Citation> {
let title = fields.get("title").cloned().unwrap_or_default();
let authors = if let Some(author_str) = fields.get("author") {
self.parse_authors(author_str)
} else {
Vec::new()
};
let year = fields.get("year").and_then(|y| y.parse().ok());
let venue = match pub_type {
PublicationType::Article => fields.get("journal").cloned(),
PublicationType::InProceedings => fields.get("booktitle").cloned(),
PublicationType::Book => fields.get("publisher").cloned(),
PublicationType::InCollection => fields.get("booktitle").cloned(),
PublicationType::PhDThesis => fields.get("school").cloned(),
PublicationType::MastersThesis => fields.get("school").cloned(),
PublicationType::TechReport => fields.get("institution").cloned(),
PublicationType::Manual => fields.get("organization").cloned(),
PublicationType::Misc => fields.get("howpublished").cloned(),
PublicationType::Unpublished => fields.get("note").cloned(),
PublicationType::Preprint => fields.get("archivePrefix").cloned(),
PublicationType::Patent => fields.get("assignee").cloned(),
PublicationType::Software => fields.get("url").cloned(),
PublicationType::Dataset => fields.get("url").cloned(),
};
let now = Utc::now();
Ok(Citation {
key,
publication_type: pub_type,
title,
authors,
year,
venue,
volume: fields.get("volume").cloned(),
issue: fields.get("number").cloned(),
pages: fields.get("pages").cloned(),
doi: fields.get("doi").cloned(),
url: fields.get("url").cloned(),
abstracttext: fields.get("abstract").cloned(),
keywords: Vec::new(),
notes: fields.get("note").cloned(),
custom_fields: HashMap::new(),
attachments: Vec::new(),
groups: Vec::new(),
import_source: Some("BibTeX".to_string()),
created_at: now,
modified_at: now,
})
}
fn parse_authors(&self, author_str: &str) -> Vec<Author> {
author_str
.split(" and ")
.map(|author_part| {
let author_part = author_part.trim();
if let Some(comma_pos) = author_part.find(',') {
let last_name = author_part[..comma_pos].trim().to_string();
let first_name = author_part[comma_pos + 1..].trim().to_string();
Author {
first_name,
last_name,
middle_name: None,
suffix: None,
orcid: None,
affiliation: None,
}
} else {
let parts: Vec<&str> = author_part.split_whitespace().collect();
if parts.len() >= 2 {
let first_name = parts[0].to_string();
let last_name = parts[parts.len() - 1].to_string();
let middle_name = if parts.len() > 2 {
Some(parts[1..parts.len() - 1].join(" "))
} else {
None
};
Author {
first_name,
last_name,
middle_name,
suffix: None,
orcid: None,
affiliation: None,
}
} else {
Author {
first_name: String::new(),
last_name: author_part.to_string(),
middle_name: None,
suffix: None,
orcid: None,
affiliation: None,
}
}
}
})
.collect()
}
}
impl Default for CitationSettings {
fn default() -> Self {
Self {
auto_generate_keys: true,
key_pattern: "{author}{year}".to_string(),
auto_import_doi: true,
auto_import_url: false,
duplicate_detection: true,
backup_enabled: true,
export_formats: vec![ExportFormat::BibTeX, ExportFormat::RIS],
}
}
}
impl Default for BibTeXSettings {
fn default() -> Self {
Self {
preserve_case: true,
utf8_conversion: true,
cleanup_formatting: true,
validate_entries: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_citation_manager_creation() {
let manager = CitationManager::new();
assert!(manager.styles.contains_key("APA"));
assert!(manager.styles.contains_key("IEEE"));
assert!(manager.styles.contains_key("ACM"));
assert_eq!(manager.default_style, "APA");
}
#[test]
fn test_add_citation() {
let mut manager = CitationManager::new();
let citation = Citation {
key: "test2023".to_string(),
publication_type: PublicationType::Article,
title: "Test Article".to_string(),
authors: vec![Author {
first_name: "John".to_string(),
last_name: "Doe".to_string(),
middle_name: None,
suffix: None,
orcid: None,
affiliation: None,
}],
year: Some(2023),
venue: Some("Test Journal".to_string()),
volume: None,
issue: None,
pages: None,
doi: None,
url: None,
abstracttext: None,
keywords: Vec::new(),
notes: None,
custom_fields: HashMap::new(),
attachments: Vec::new(),
groups: Vec::new(),
import_source: None,
created_at: Utc::now(),
modified_at: Utc::now(),
};
assert!(manager.add_citation(citation).is_ok());
assert!(manager.citations.contains_key("test2023"));
}
#[test]
fn test_search_citations() {
let mut manager = CitationManager::new();
let citation = Citation {
key: "test2023".to_string(),
publication_type: PublicationType::Article,
title: "Machine Learning Optimization".to_string(),
authors: vec![Author {
first_name: "Jane".to_string(),
last_name: "Smith".to_string(),
middle_name: None,
suffix: None,
orcid: None,
affiliation: None,
}],
year: Some(2023),
venue: None,
volume: None,
issue: None,
pages: None,
doi: None,
url: None,
abstracttext: None,
keywords: vec!["optimization".to_string(), "machine learning".to_string()],
notes: None,
custom_fields: HashMap::new(),
attachments: Vec::new(),
groups: Vec::new(),
import_source: None,
created_at: Utc::now(),
modified_at: Utc::now(),
};
manager.add_citation(citation).expect("unwrap failed");
let results = manager.search_citations("optimization");
assert_eq!(results.len(), 1);
let results = manager.search_citations("Smith");
assert_eq!(results.len(), 1);
}
}