1use crate::error::{OptimError, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CitationManager {
14 pub citations: HashMap<String, Citation>,
16 pub styles: HashMap<String, CitationStyle>,
18 pub default_style: String,
20 pub groups: HashMap<String, CitationGroup>,
22 pub settings: CitationSettings,
24 pub modified_at: DateTime<Utc>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Citation {
31 pub key: String,
33 pub publication_type: PublicationType,
35 pub title: String,
37 pub authors: Vec<Author>,
39 pub year: Option<u32>,
41 pub venue: Option<String>,
43 pub volume: Option<String>,
45 pub issue: Option<String>,
47 pub pages: Option<String>,
49 pub doi: Option<String>,
51 pub url: Option<String>,
53 pub abstracttext: Option<String>,
55 pub keywords: Vec<String>,
57 pub notes: Option<String>,
59 pub custom_fields: HashMap<String, String>,
61 pub attachments: Vec<String>,
63 pub groups: Vec<String>,
65 pub import_source: Option<String>,
67 pub created_at: DateTime<Utc>,
69 pub modified_at: DateTime<Utc>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub enum PublicationType {
76 Article,
78 InProceedings,
80 Book,
82 InCollection,
84 PhDThesis,
86 MastersThesis,
88 TechReport,
90 Manual,
92 Misc,
94 Unpublished,
96 Preprint,
98 Patent,
100 Software,
102 Dataset,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Author {
109 pub first_name: String,
111 pub last_name: String,
113 pub middle_name: Option<String>,
115 pub suffix: Option<String>,
117 pub orcid: Option<String>,
119 pub affiliation: Option<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct CitationStyle {
126 pub name: String,
128 pub description: String,
130 pub intext_format: InTextFormat,
132 pub bibliography_format: BibliographyFormat,
134 pub formatting_rules: FormattingRules,
136 pub sorting_rules: SortingRules,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
142pub enum InTextFormat {
143 AuthorYear,
145 Numbered,
147 Superscript,
149 AuthorNumber,
151 Footnote,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct BibliographyFormat {
158 pub entry_separator: String,
160 pub field_separators: HashMap<String, String>,
162 pub name_format: NameFormat,
164 pub title_format: TitleFormat,
166 pub date_format: DateFormat,
168 pub punctuation: PunctuationRules,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174pub enum NameFormat {
175 LastFirstMiddle,
177 FirstMiddleLast,
179 LastFirstInitial,
181 FirstInitialLast,
183 LastFirstInitialNoSpace,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
189pub enum TitleFormat {
190 TitleCase,
192 SentenceCase,
194 Uppercase,
196 Lowercase,
198 AsEntered,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204pub enum DateFormat {
205 Year,
207 MonthYear,
209 MonthAbbrevYear,
211 FullDate,
213 ISODate,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PunctuationRules {
220 pub periods_after_abbreviations: bool,
222 pub commas_between_fields: bool,
224 pub parentheses_around_year: bool,
226 pub quote_titles: bool,
228 pub italicize_journals: bool,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct FormattingRules {
235 pub max_authors: Option<usize>,
237 pub et_altext: String,
239 pub et_al_threshold: usize,
241 pub title_case: bool,
243 pub abbreviate_journals: bool,
245 pub include_doi: bool,
247 pub include_url: bool,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct SortingRules {
254 pub primary_sort: SortField,
256 pub secondary_sort: Option<SortField>,
258 pub sort_direction: SortDirection,
260 pub group_by_type: bool,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
266pub enum SortField {
267 Author,
269 Year,
271 Title,
273 Venue,
275 Key,
277 DateAdded,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
283pub enum SortDirection {
284 Ascending,
286 Descending,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct CitationGroup {
293 pub name: String,
295 pub description: String,
297 pub color: Option<String>,
299 pub citation_keys: Vec<String>,
301 pub created_at: DateTime<Utc>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct CitationSettings {
308 pub auto_generate_keys: bool,
310 pub key_pattern: String,
312 pub auto_import_doi: bool,
314 pub auto_import_url: bool,
316 pub duplicate_detection: bool,
318 pub backup_enabled: bool,
320 pub export_formats: Vec<ExportFormat>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
326pub enum ExportFormat {
327 BibTeX,
329 RIS,
331 EndNote,
333 JSON,
335 CSV,
337 Word,
339}
340
341#[derive(Debug)]
343pub struct BibTeXProcessor {
344 settings: BibTeXSettings,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct BibTeXSettings {
351 pub preserve_case: bool,
353 pub utf8_conversion: bool,
355 pub cleanup_formatting: bool,
357 pub validate_entries: bool,
359}
360
361#[derive(Debug)]
363pub struct CitationDiscovery {
364 search_engines: Vec<SearchEngine>,
366 api_keys: HashMap<String, String>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct SearchEngine {
373 pub name: String,
375 pub endpoint: String,
377 pub rate_limit: f64,
379 pub query_types: Vec<QueryType>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
385pub enum QueryType {
386 DOI,
388 Title,
390 Author,
392 ArXiv,
394 PubMed,
396 ISBN,
398 FreeText,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct CitationNetwork {
405 pub citations: Vec<String>,
407 pub relationships: Vec<CitationRelationship>,
409 pub metrics: NetworkMetrics,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct CitationRelationship {
416 pub citing: String,
418 pub cited: String,
420 pub relationship_type: RelationshipType,
422 pub strength: f64,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
428pub enum RelationshipType {
429 DirectCitation,
431 CoCitation,
433 BibliographicCoupling,
435 SameAuthor,
437 SameVenue,
439 SimilarTopic,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct NetworkMetrics {
446 pub total_nodes: usize,
448 pub total_edges: usize,
450 pub density: f64,
452 pub clustering_coefficient: f64,
454 pub most_cited: Vec<(String, usize)>,
456 pub most_influential_authors: Vec<(String, f64)>,
458}
459
460impl Default for CitationManager {
461 fn default() -> Self {
462 Self::new()
463 }
464}
465
466impl CitationManager {
467 pub fn new() -> Self {
469 let mut styles = HashMap::new();
470 styles.insert("APA".to_string(), Self::create_apa_style());
471 styles.insert("IEEE".to_string(), Self::create_ieee_style());
472 styles.insert("ACM".to_string(), Self::create_acm_style());
473
474 Self {
475 citations: HashMap::new(),
476 styles,
477 default_style: "APA".to_string(),
478 groups: HashMap::new(),
479 settings: CitationSettings::default(),
480 modified_at: Utc::now(),
481 }
482 }
483
484 pub fn add_citation(&mut self, citation: Citation) -> Result<()> {
486 if self.citations.contains_key(&citation.key) {
487 return Err(OptimError::InvalidConfig(format!(
488 "Citation with key '{}' already exists",
489 citation.key
490 )));
491 }
492
493 self.citations.insert(citation.key.clone(), citation);
494 self.modified_at = Utc::now();
495 Ok(())
496 }
497
498 pub fn get_citation(&self, key: &str) -> Option<&Citation> {
500 self.citations.get(key)
501 }
502
503 pub fn update_citation(&mut self, key: &str, citation: Citation) -> Result<()> {
505 if !self.citations.contains_key(key) {
506 return Err(OptimError::InvalidConfig(format!(
507 "Citation with key '{}' not found",
508 key
509 )));
510 }
511
512 self.citations.insert(key.to_string(), citation);
513 self.modified_at = Utc::now();
514 Ok(())
515 }
516
517 pub fn remove_citation(&mut self, key: &str) -> Result<()> {
519 if self.citations.remove(key).is_none() {
520 return Err(OptimError::InvalidConfig(format!(
521 "Citation with key '{}' not found",
522 key
523 )));
524 }
525
526 self.modified_at = Utc::now();
527 Ok(())
528 }
529
530 pub fn search_citations(&self, query: &str) -> Vec<&Citation> {
532 let query_lower = query.to_lowercase();
533
534 self.citations
535 .values()
536 .filter(|citation| {
537 citation.title.to_lowercase().contains(&query_lower)
538 || citation.authors.iter().any(|author| {
539 author.last_name.to_lowercase().contains(&query_lower)
540 || author.first_name.to_lowercase().contains(&query_lower)
541 })
542 || citation
543 .keywords
544 .iter()
545 .any(|keyword| keyword.to_lowercase().contains(&query_lower))
546 || citation
547 .venue
548 .as_ref()
549 .is_some_and(|venue| venue.to_lowercase().contains(&query_lower))
550 })
551 .collect()
552 }
553
554 pub fn format_citation(&self, key: &str, style: Option<&str>) -> Result<String> {
556 let citation = self
557 .get_citation(key)
558 .ok_or_else(|| OptimError::InvalidConfig(format!("Citation '{}' not found", key)))?;
559
560 let style_name = style.unwrap_or(&self.default_style);
561 let citation_style = self.styles.get(style_name).ok_or_else(|| {
562 OptimError::InvalidConfig(format!("Style '{}' not found", style_name))
563 })?;
564
565 self.format_citation_with_style(citation, citation_style)
566 }
567
568 pub fn generate_bibliography(
570 &self,
571 citation_keys: &[String],
572 style: Option<&str>,
573 ) -> Result<String> {
574 let style_name = style.unwrap_or(&self.default_style);
575 let citation_style = self.styles.get(style_name).ok_or_else(|| {
576 OptimError::InvalidConfig(format!("Style '{}' not found", style_name))
577 })?;
578
579 let mut citations: Vec<&Citation> = citation_keys
580 .iter()
581 .filter_map(|key| self.citations.get(key))
582 .collect();
583
584 self.sort_citations(&mut citations, &citation_style.sorting_rules);
586
587 let mut bibliography = String::new();
588 for citation in citations {
589 let formatted = self.format_citation_with_style(citation, citation_style)?;
590 bibliography.push_str(&formatted);
591 bibliography.push('\n');
592 }
593
594 Ok(bibliography)
595 }
596
597 pub fn export_bibtex(&self, citation_keys: Option<&[String]>) -> String {
599 let citations: Vec<&Citation> = if let Some(_keys) = citation_keys {
600 _keys
601 .iter()
602 .filter_map(|key| self.citations.get(key))
603 .collect()
604 } else {
605 self.citations.values().collect()
606 };
607
608 let mut bibtex = String::new();
609 for citation in citations {
610 bibtex.push_str(&self.citation_to_bibtex(citation));
611 bibtex.push('\n');
612 }
613
614 bibtex
615 }
616
617 pub fn import_bibtex(&mut self, bibtex_content: &str) -> Result<usize> {
619 let processor = BibTeXProcessor::new(BibTeXSettings::default());
620 let citations = processor.parse_bibtex(bibtex_content)?;
621
622 let mut imported_count = 0;
623 for citation in citations {
624 if !self.citations.contains_key(&citation.key) {
625 self.citations.insert(citation.key.clone(), citation);
626 imported_count += 1;
627 }
628 }
629
630 self.modified_at = Utc::now();
631 Ok(imported_count)
632 }
633
634 pub fn create_group(&mut self, name: &str, description: &str) -> String {
636 let group_id = uuid::Uuid::new_v4().to_string();
637 let group = CitationGroup {
638 name: name.to_string(),
639 description: description.to_string(),
640 color: None,
641 citation_keys: Vec::new(),
642 created_at: Utc::now(),
643 };
644
645 self.groups.insert(group_id.clone(), group);
646 group_id
647 }
648
649 pub fn add_to_group(&mut self, group_id: &str, citation_key: &str) -> Result<()> {
651 let group = self
652 .groups
653 .get_mut(group_id)
654 .ok_or_else(|| OptimError::InvalidConfig(format!("Group '{}' not found", group_id)))?;
655
656 if !group.citation_keys.contains(&citation_key.to_string()) {
657 group.citation_keys.push(citation_key.to_string());
658 }
659
660 Ok(())
661 }
662
663 fn format_citation_with_style(
664 &self,
665 citation: &Citation,
666 style: &CitationStyle,
667 ) -> Result<String> {
668 match style.intext_format {
669 InTextFormat::AuthorYear => self.format_author_year(citation, style),
670 InTextFormat::Numbered => self.format_numbered(citation, style),
671 InTextFormat::Superscript => self.format_superscript(citation, style),
672 InTextFormat::AuthorNumber => self.format_author_number(citation, style),
673 InTextFormat::Footnote => self.format_footnote(citation, style),
674 }
675 }
676
677 fn format_author_year(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
678 let authors = self.format_authors(&citation.authors, &style.formatting_rules);
679 let year = citation
680 .year
681 .map(|y| y.to_string())
682 .unwrap_or_else(|| "n.d.".to_string());
683
684 Ok(format!("({}, {})", authors, year))
685 }
686
687 fn format_numbered(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
688 Ok(format!("[{}]", 1)) }
691
692 fn format_superscript(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
693 Ok("ยน".to_string()) }
695
696 fn format_author_number(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
697 let authors = self.format_authors(&citation.authors, &style.formatting_rules);
698 Ok(format!("{} [1]", authors)) }
700
701 fn format_footnote(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
702 self.format_full_citation(citation, style)
703 }
704
705 fn format_full_citation(&self, citation: &Citation, style: &CitationStyle) -> Result<String> {
706 let mut formatted = String::new();
707
708 let authors = self.format_authors(&citation.authors, &style.formatting_rules);
710 formatted.push_str(&authors);
711
712 let title = self.format_title(&citation.title, &style.bibliography_format.title_format);
714 formatted.push_str(&format!(". {}.", title));
715
716 if let Some(venue) = &citation.venue {
718 let venue_formatted = if style.bibliography_format.punctuation.italicize_journals {
719 format!(" *{}*", venue)
720 } else {
721 format!(" {venue}")
722 };
723 formatted.push_str(&venue_formatted);
724 }
725
726 if let Some(year) = citation.year {
728 if style
729 .bibliography_format
730 .punctuation
731 .parentheses_around_year
732 {
733 formatted.push_str(&format!(" ({})", year));
734 } else {
735 formatted.push_str(&format!(" {year}"));
736 }
737 }
738
739 if style.formatting_rules.include_doi {
741 if let Some(doi) = &citation.doi {
742 formatted.push_str(&format!(". DOI: {doi}"));
743 }
744 }
745
746 Ok(formatted)
747 }
748
749 fn format_authors(&self, authors: &[Author], rules: &FormattingRules) -> String {
750 if authors.is_empty() {
751 return "Anonymous".to_string();
752 }
753
754 let max_authors = rules.max_authors.unwrap_or(authors.len());
755 let display_authors = if authors.len() > max_authors && max_authors > 0 {
756 &authors[..max_authors]
757 } else {
758 authors
759 };
760
761 let mut formatted_authors = Vec::new();
762 for author in display_authors {
763 let formatted = format!("{}, {}", author.last_name, author.first_name);
764 formatted_authors.push(formatted);
765 }
766
767 let mut result = formatted_authors.join(", ");
768
769 if authors.len() > max_authors {
770 result.push_str(&format!(", {}", rules.et_altext));
771 }
772
773 result
774 }
775
776 fn format_title(&self, title: &str, format: &TitleFormat) -> String {
777 match format {
778 TitleFormat::TitleCase => self.to_title_case(title),
779 TitleFormat::SentenceCase => self.to_sentence_case(title),
780 TitleFormat::Uppercase => title.to_uppercase(),
781 TitleFormat::Lowercase => title.to_lowercase(),
782 TitleFormat::AsEntered => title.to_string(),
783 }
784 }
785
786 fn to_title_case(&self, s: &str) -> String {
787 s.split_whitespace()
788 .map(|word| {
789 let mut chars = word.chars();
790 match chars.next() {
791 None => String::new(),
792 Some(first) => {
793 first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase()
794 }
795 }
796 })
797 .collect::<Vec<_>>()
798 .join(" ")
799 }
800
801 fn to_sentence_case(&self, s: &str) -> String {
802 if s.is_empty() {
803 return String::new();
804 }
805
806 let mut chars = s.chars();
807 let first = chars.next().unwrap().to_uppercase().collect::<String>();
808 first + &chars.as_str().to_lowercase()
809 }
810
811 fn sort_citations(&self, citations: &mut Vec<&Citation>, rules: &SortingRules) {
812 citations.sort_by(|a, b| {
813 let primary_cmp = self.compare_by_field(a, b, &rules.primary_sort);
814 if primary_cmp == std::cmp::Ordering::Equal {
815 if let Some(secondary) = &rules.secondary_sort {
816 self.compare_by_field(a, b, secondary)
817 } else {
818 std::cmp::Ordering::Equal
819 }
820 } else {
821 primary_cmp
822 }
823 });
824
825 if rules.sort_direction == SortDirection::Descending {
826 citations.reverse();
827 }
828 }
829
830 fn compare_by_field(
831 &self,
832 a: &Citation,
833 b: &Citation,
834 field: &SortField,
835 ) -> std::cmp::Ordering {
836 match field {
837 SortField::Author => {
838 let a_author = a
839 .authors
840 .first()
841 .map(|au| au.last_name.as_str())
842 .unwrap_or("");
843 let b_author = b
844 .authors
845 .first()
846 .map(|au| au.last_name.as_str())
847 .unwrap_or("");
848 a_author.cmp(b_author)
849 }
850 SortField::Year => a.year.cmp(&b.year),
851 SortField::Title => a.title.cmp(&b.title),
852 SortField::Venue => a.venue.cmp(&b.venue),
853 SortField::Key => a.key.cmp(&b.key),
854 SortField::DateAdded => a.created_at.cmp(&b.created_at),
855 }
856 }
857
858 fn citation_to_bibtex(&self, citation: &Citation) -> String {
859 let mut bibtex = format!(
860 "@{}{{{},\n",
861 self.publication_type_to_bibtex(&citation.publication_type),
862 citation.key
863 );
864
865 bibtex.push_str(&format!(" title = {{{}}},\n", citation.title));
866
867 if !citation.authors.is_empty() {
868 let authors = citation
869 .authors
870 .iter()
871 .map(|a| format!("{} {}", a.first_name, a.last_name))
872 .collect::<Vec<_>>()
873 .join(" and ");
874 bibtex.push_str(&format!(" author = {{{}}},\n", authors));
875 }
876
877 if let Some(year) = citation.year {
878 bibtex.push_str(&format!(" year = {{{}}},\n", year));
879 }
880
881 if let Some(venue) = &citation.venue {
882 let field_name = match citation.publication_type {
883 PublicationType::Article => "journal",
884 PublicationType::InProceedings => "booktitle",
885 PublicationType::Book => "publisher",
886 PublicationType::InCollection => "booktitle",
887 PublicationType::PhDThesis => "school",
888 PublicationType::MastersThesis => "school",
889 PublicationType::TechReport => "institution",
890 PublicationType::Manual => "organization",
891 PublicationType::Misc => "howpublished",
892 PublicationType::Unpublished => "note",
893 PublicationType::Preprint => "archivePrefix",
894 PublicationType::Patent => "assignee",
895 PublicationType::Software => "url",
896 PublicationType::Dataset => "url",
897 };
898 bibtex.push_str(&format!(" {} = {{{}}},\n", field_name, venue));
899 }
900
901 if let Some(volume) = &citation.volume {
902 bibtex.push_str(&format!(" volume = {{{}}},\n", volume));
903 }
904
905 if let Some(pages) = &citation.pages {
906 bibtex.push_str(&format!(" pages = {{{}}},\n", pages));
907 }
908
909 if let Some(doi) = &citation.doi {
910 bibtex.push_str(&format!(" doi = {{{}}},\n", doi));
911 }
912
913 bibtex.push_str("}\n");
914 bibtex
915 }
916
917 fn publication_type_to_bibtex(&self, pub_type: &PublicationType) -> &'static str {
918 match pub_type {
919 PublicationType::Article => "article",
920 PublicationType::InProceedings => "inproceedings",
921 PublicationType::Book => "book",
922 PublicationType::InCollection => "incollection",
923 PublicationType::PhDThesis => "phdthesis",
924 PublicationType::MastersThesis => "mastersthesis",
925 PublicationType::TechReport => "techreport",
926 PublicationType::Manual => "manual",
927 PublicationType::Misc => "misc",
928 PublicationType::Unpublished => "unpublished",
929 PublicationType::Preprint => "misc",
930 PublicationType::Patent => "misc",
931 PublicationType::Software => "misc",
932 PublicationType::Dataset => "misc",
933 }
934 }
935
936 fn create_apa_style() -> CitationStyle {
937 CitationStyle {
938 name: "APA".to_string(),
939 description: "American Psychological Association style".to_string(),
940 intext_format: InTextFormat::AuthorYear,
941 bibliography_format: BibliographyFormat {
942 entry_separator: "\n".to_string(),
943 field_separators: {
944 let mut separators = HashMap::new();
945 separators.insert("author_title".to_string(), ". ".to_string());
946 separators.insert("title_venue".to_string(), ". ".to_string());
947 separators
948 },
949 name_format: NameFormat::LastFirstInitial,
950 title_format: TitleFormat::SentenceCase,
951 date_format: DateFormat::Year,
952 punctuation: PunctuationRules {
953 periods_after_abbreviations: true,
954 commas_between_fields: true,
955 parentheses_around_year: true,
956 quote_titles: false,
957 italicize_journals: true,
958 },
959 },
960 formatting_rules: FormattingRules {
961 max_authors: Some(7),
962 et_altext: "et al.".to_string(),
963 et_al_threshold: 8,
964 title_case: false,
965 abbreviate_journals: false,
966 include_doi: true,
967 include_url: false,
968 },
969 sorting_rules: SortingRules {
970 primary_sort: SortField::Author,
971 secondary_sort: Some(SortField::Year),
972 sort_direction: SortDirection::Ascending,
973 group_by_type: false,
974 },
975 }
976 }
977
978 fn create_ieee_style() -> CitationStyle {
979 CitationStyle {
980 name: "IEEE".to_string(),
981 description: "Institute of Electrical and Electronics Engineers style".to_string(),
982 intext_format: InTextFormat::Numbered,
983 bibliography_format: BibliographyFormat {
984 entry_separator: "\n".to_string(),
985 field_separators: HashMap::new(),
986 name_format: NameFormat::FirstInitialLast,
987 title_format: TitleFormat::AsEntered,
988 date_format: DateFormat::Year,
989 punctuation: PunctuationRules {
990 periods_after_abbreviations: true,
991 commas_between_fields: true,
992 parentheses_around_year: false,
993 quote_titles: true,
994 italicize_journals: true,
995 },
996 },
997 formatting_rules: FormattingRules {
998 max_authors: None,
999 et_altext: "et al.".to_string(),
1000 et_al_threshold: 7,
1001 title_case: false,
1002 abbreviate_journals: true,
1003 include_doi: true,
1004 include_url: false,
1005 },
1006 sorting_rules: SortingRules {
1007 primary_sort: SortField::Year,
1008 secondary_sort: Some(SortField::Author),
1009 sort_direction: SortDirection::Ascending,
1010 group_by_type: false,
1011 },
1012 }
1013 }
1014
1015 fn create_acm_style() -> CitationStyle {
1016 CitationStyle {
1017 name: "ACM".to_string(),
1018 description: "Association for Computing Machinery style".to_string(),
1019 intext_format: InTextFormat::Numbered,
1020 bibliography_format: BibliographyFormat {
1021 entry_separator: "\n".to_string(),
1022 field_separators: HashMap::new(),
1023 name_format: NameFormat::FirstMiddleLast,
1024 title_format: TitleFormat::TitleCase,
1025 date_format: DateFormat::Year,
1026 punctuation: PunctuationRules {
1027 periods_after_abbreviations: true,
1028 commas_between_fields: true,
1029 parentheses_around_year: false,
1030 quote_titles: false,
1031 italicize_journals: true,
1032 },
1033 },
1034 formatting_rules: FormattingRules {
1035 max_authors: None,
1036 et_altext: "et al.".to_string(),
1037 et_al_threshold: 3,
1038 title_case: true,
1039 abbreviate_journals: false,
1040 include_doi: true,
1041 include_url: true,
1042 },
1043 sorting_rules: SortingRules {
1044 primary_sort: SortField::Author,
1045 secondary_sort: Some(SortField::Year),
1046 sort_direction: SortDirection::Ascending,
1047 group_by_type: false,
1048 },
1049 }
1050 }
1051}
1052
1053impl BibTeXProcessor {
1054 pub fn new(settings: BibTeXSettings) -> Self {
1056 Self { settings }
1057 }
1058
1059 pub fn parse_bibtex(&self, content: &str) -> Result<Vec<Citation>> {
1061 let mut citations = Vec::new();
1064 let lines: Vec<&str> = content.lines().collect();
1065 let mut current_entry: Option<(String, PublicationType, HashMap<String, String>)> = None;
1066
1067 for line in lines {
1068 let line = line.trim();
1069
1070 if line.starts_with('@') {
1071 if let Some((key, pub_type, fields)) = current_entry.take() {
1073 if let Ok(citation) = self.fields_to_citation(key, pub_type, fields) {
1074 citations.push(citation);
1075 }
1076 }
1077
1078 if let Some(pos) = line.find('{') {
1080 let entry_type = line[1..pos].to_lowercase();
1081 let pub_type = self.bibtex_type_to_publication_type(&entry_type);
1082
1083 let key_part = &line[pos + 1..];
1084 if let Some(comma_pos) = key_part.find(',') {
1085 let key = key_part[..comma_pos].trim().to_string();
1086 current_entry = Some((key, pub_type, HashMap::new()));
1087 }
1088 }
1089 } else if line.contains('=') && current_entry.is_some() {
1090 if let Some(eq_pos) = line.find('=') {
1092 let field_name = line[..eq_pos].trim().to_lowercase();
1093 let field_value = line[eq_pos + 1..]
1094 .trim()
1095 .trim_start_matches('{')
1096 .trim_end_matches("},")
1097 .trim_start_matches('"')
1098 .trim_end_matches("\",")
1099 .to_string();
1100
1101 if let Some((_, _, ref mut fields)) = current_entry {
1102 fields.insert(field_name, field_value);
1103 }
1104 }
1105 }
1106 }
1107
1108 if let Some((key, pub_type, fields)) = current_entry {
1110 if let Ok(citation) = self.fields_to_citation(key, pub_type, fields) {
1111 citations.push(citation);
1112 }
1113 }
1114
1115 Ok(citations)
1116 }
1117
1118 fn bibtex_type_to_publication_type(&self, bibtex_type: &str) -> PublicationType {
1119 match bibtex_type {
1120 "article" => PublicationType::Article,
1121 "inproceedings" | "conference" => PublicationType::InProceedings,
1122 "book" => PublicationType::Book,
1123 "incollection" | "inbook" => PublicationType::InCollection,
1124 "phdthesis" => PublicationType::PhDThesis,
1125 "mastersthesis" => PublicationType::MastersThesis,
1126 "techreport" => PublicationType::TechReport,
1127 "manual" => PublicationType::Manual,
1128 "unpublished" => PublicationType::Unpublished,
1129 _ => PublicationType::Misc,
1130 }
1131 }
1132
1133 fn fields_to_citation(
1134 &self,
1135 key: String,
1136 pub_type: PublicationType,
1137 fields: HashMap<String, String>,
1138 ) -> Result<Citation> {
1139 let title = fields.get("title").cloned().unwrap_or_default();
1140
1141 let authors = if let Some(author_str) = fields.get("author") {
1143 self.parse_authors(author_str)
1144 } else {
1145 Vec::new()
1146 };
1147
1148 let year = fields.get("year").and_then(|y| y.parse().ok());
1150
1151 let venue = match pub_type {
1153 PublicationType::Article => fields.get("journal").cloned(),
1154 PublicationType::InProceedings => fields.get("booktitle").cloned(),
1155 PublicationType::Book => fields.get("publisher").cloned(),
1156 PublicationType::InCollection => fields.get("booktitle").cloned(),
1157 PublicationType::PhDThesis => fields.get("school").cloned(),
1158 PublicationType::MastersThesis => fields.get("school").cloned(),
1159 PublicationType::TechReport => fields.get("institution").cloned(),
1160 PublicationType::Manual => fields.get("organization").cloned(),
1161 PublicationType::Misc => fields.get("howpublished").cloned(),
1162 PublicationType::Unpublished => fields.get("note").cloned(),
1163 PublicationType::Preprint => fields.get("archivePrefix").cloned(),
1164 PublicationType::Patent => fields.get("assignee").cloned(),
1165 PublicationType::Software => fields.get("url").cloned(),
1166 PublicationType::Dataset => fields.get("url").cloned(),
1167 };
1168
1169 let now = Utc::now();
1170
1171 Ok(Citation {
1172 key,
1173 publication_type: pub_type,
1174 title,
1175 authors,
1176 year,
1177 venue,
1178 volume: fields.get("volume").cloned(),
1179 issue: fields.get("number").cloned(),
1180 pages: fields.get("pages").cloned(),
1181 doi: fields.get("doi").cloned(),
1182 url: fields.get("url").cloned(),
1183 abstracttext: fields.get("abstract").cloned(),
1184 keywords: Vec::new(),
1185 notes: fields.get("note").cloned(),
1186 custom_fields: HashMap::new(),
1187 attachments: Vec::new(),
1188 groups: Vec::new(),
1189 import_source: Some("BibTeX".to_string()),
1190 created_at: now,
1191 modified_at: now,
1192 })
1193 }
1194
1195 fn parse_authors(&self, author_str: &str) -> Vec<Author> {
1196 author_str
1197 .split(" and ")
1198 .map(|author_part| {
1199 let author_part = author_part.trim();
1200 if let Some(comma_pos) = author_part.find(',') {
1201 let last_name = author_part[..comma_pos].trim().to_string();
1203 let first_name = author_part[comma_pos + 1..].trim().to_string();
1204 Author {
1205 first_name,
1206 last_name,
1207 middle_name: None,
1208 suffix: None,
1209 orcid: None,
1210 affiliation: None,
1211 }
1212 } else {
1213 let parts: Vec<&str> = author_part.split_whitespace().collect();
1215 if parts.len() >= 2 {
1216 let first_name = parts[0].to_string();
1217 let last_name = parts[parts.len() - 1].to_string();
1218 let middle_name = if parts.len() > 2 {
1219 Some(parts[1..parts.len() - 1].join(" "))
1220 } else {
1221 None
1222 };
1223 Author {
1224 first_name,
1225 last_name,
1226 middle_name,
1227 suffix: None,
1228 orcid: None,
1229 affiliation: None,
1230 }
1231 } else {
1232 Author {
1234 first_name: String::new(),
1235 last_name: author_part.to_string(),
1236 middle_name: None,
1237 suffix: None,
1238 orcid: None,
1239 affiliation: None,
1240 }
1241 }
1242 }
1243 })
1244 .collect()
1245 }
1246}
1247
1248impl Default for CitationSettings {
1249 fn default() -> Self {
1250 Self {
1251 auto_generate_keys: true,
1252 key_pattern: "{author}{year}".to_string(),
1253 auto_import_doi: true,
1254 auto_import_url: false,
1255 duplicate_detection: true,
1256 backup_enabled: true,
1257 export_formats: vec![ExportFormat::BibTeX, ExportFormat::RIS],
1258 }
1259 }
1260}
1261
1262impl Default for BibTeXSettings {
1263 fn default() -> Self {
1264 Self {
1265 preserve_case: true,
1266 utf8_conversion: true,
1267 cleanup_formatting: true,
1268 validate_entries: true,
1269 }
1270 }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276
1277 #[test]
1278 fn test_citation_manager_creation() {
1279 let manager = CitationManager::new();
1280
1281 assert!(manager.styles.contains_key("APA"));
1282 assert!(manager.styles.contains_key("IEEE"));
1283 assert!(manager.styles.contains_key("ACM"));
1284 assert_eq!(manager.default_style, "APA");
1285 }
1286
1287 #[test]
1288 fn test_add_citation() {
1289 let mut manager = CitationManager::new();
1290
1291 let citation = Citation {
1292 key: "test2023".to_string(),
1293 publication_type: PublicationType::Article,
1294 title: "Test Article".to_string(),
1295 authors: vec![Author {
1296 first_name: "John".to_string(),
1297 last_name: "Doe".to_string(),
1298 middle_name: None,
1299 suffix: None,
1300 orcid: None,
1301 affiliation: None,
1302 }],
1303 year: Some(2023),
1304 venue: Some("Test Journal".to_string()),
1305 volume: None,
1306 issue: None,
1307 pages: None,
1308 doi: None,
1309 url: None,
1310 abstracttext: None,
1311 keywords: Vec::new(),
1312 notes: None,
1313 custom_fields: HashMap::new(),
1314 attachments: Vec::new(),
1315 groups: Vec::new(),
1316 import_source: None,
1317 created_at: Utc::now(),
1318 modified_at: Utc::now(),
1319 };
1320
1321 assert!(manager.add_citation(citation).is_ok());
1322 assert!(manager.citations.contains_key("test2023"));
1323 }
1324
1325 #[test]
1326 fn test_search_citations() {
1327 let mut manager = CitationManager::new();
1328
1329 let citation = Citation {
1330 key: "test2023".to_string(),
1331 publication_type: PublicationType::Article,
1332 title: "Machine Learning Optimization".to_string(),
1333 authors: vec![Author {
1334 first_name: "Jane".to_string(),
1335 last_name: "Smith".to_string(),
1336 middle_name: None,
1337 suffix: None,
1338 orcid: None,
1339 affiliation: None,
1340 }],
1341 year: Some(2023),
1342 venue: None,
1343 volume: None,
1344 issue: None,
1345 pages: None,
1346 doi: None,
1347 url: None,
1348 abstracttext: None,
1349 keywords: vec!["optimization".to_string(), "machine learning".to_string()],
1350 notes: None,
1351 custom_fields: HashMap::new(),
1352 attachments: Vec::new(),
1353 groups: Vec::new(),
1354 import_source: None,
1355 created_at: Utc::now(),
1356 modified_at: Utc::now(),
1357 };
1358
1359 manager.add_citation(citation).unwrap();
1360
1361 let results = manager.search_citations("optimization");
1362 assert_eq!(results.len(), 1);
1363
1364 let results = manager.search_citations("Smith");
1365 assert_eq!(results.len(), 1);
1366 }
1367}