Skip to main content

oxihuman_export/
endnote_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! EndNote XML export stub.
6
7/// Reference type for EndNote.
8#[derive(Debug, Clone, PartialEq)]
9pub enum EndnoteRefType {
10    JournalArticle,
11    Book,
12    BookSection,
13    ConferencePaper,
14    Report,
15    Thesis,
16    WebPage,
17}
18
19impl EndnoteRefType {
20    /// Human-readable label.
21    pub fn label(&self) -> &'static str {
22        match self {
23            Self::JournalArticle => "Journal Article",
24            Self::Book => "Book",
25            Self::BookSection => "Book Section",
26            Self::ConferencePaper => "Conference Paper",
27            Self::Report => "Report",
28            Self::Thesis => "Thesis",
29            Self::WebPage => "Web Page",
30        }
31    }
32}
33
34/// A single EndNote reference.
35#[derive(Debug, Clone)]
36pub struct EndnoteRef {
37    pub ref_type: EndnoteRefType,
38    pub title: String,
39    pub authors: Vec<String>,
40    pub year: Option<u32>,
41    pub journal: Option<String>,
42    pub volume: Option<String>,
43    pub pages: Option<String>,
44    pub doi: Option<String>,
45}
46
47impl EndnoteRef {
48    /// Create a minimal reference.
49    pub fn new(ref_type: EndnoteRefType, title: impl Into<String>) -> Self {
50        Self {
51            ref_type,
52            title: title.into(),
53            authors: Vec::new(),
54            year: None,
55            journal: None,
56            volume: None,
57            pages: None,
58            doi: None,
59        }
60    }
61
62    /// Add an author.
63    pub fn add_author(&mut self, author: impl Into<String>) {
64        self.authors.push(author.into());
65    }
66}
67
68/// A collection of EndNote references.
69#[derive(Debug, Clone, Default)]
70pub struct EndnoteLibrary {
71    pub refs: Vec<EndnoteRef>,
72}
73
74impl EndnoteLibrary {
75    /// Add a reference.
76    pub fn add_ref(&mut self, r: EndnoteRef) {
77        self.refs.push(r);
78    }
79
80    /// Number of references.
81    pub fn ref_count(&self) -> usize {
82        self.refs.len()
83    }
84}
85
86/// Render a reference as EndNote XML.
87pub fn render_ref_xml(r: &EndnoteRef) -> String {
88    let mut out = format!(
89        "  <record>\n    <ref-type name=\"{}\"/>\n",
90        r.ref_type.label()
91    );
92    out.push_str(&format!("    <title>{}</title>\n", xml_escape(&r.title)));
93    for author in &r.authors {
94        out.push_str(&format!("    <author>{}</author>\n", xml_escape(author)));
95    }
96    if let Some(y) = r.year {
97        out.push_str(&format!("    <year>{y}</year>\n"));
98    }
99    if let Some(doi) = &r.doi {
100        out.push_str(&format!(
101            "    <electronic-resource-num>{doi}</electronic-resource-num>\n"
102        ));
103    }
104    out.push_str("  </record>\n");
105    out
106}
107
108/// Render the full EndNote XML document.
109pub fn render_endnote_xml(lib: &EndnoteLibrary) -> String {
110    let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xml>\n  <records>\n");
111    for r in &lib.refs {
112        out.push_str(&render_ref_xml(r));
113    }
114    out.push_str("  </records>\n</xml>\n");
115    out
116}
117
118/// Validate that a reference has a non-empty title.
119pub fn validate_ref(r: &EndnoteRef) -> bool {
120    !r.title.is_empty()
121}
122
123/// Count references by type.
124pub fn count_by_type(lib: &EndnoteLibrary, ref_type: &EndnoteRefType) -> usize {
125    lib.refs.iter().filter(|r| &r.ref_type == ref_type).count()
126}
127
128fn xml_escape(s: &str) -> String {
129    s.replace('&', "&amp;")
130        .replace('<', "&lt;")
131        .replace('>', "&gt;")
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    fn sample_ref() -> EndnoteRef {
139        let mut r = EndnoteRef::new(EndnoteRefType::JournalArticle, "Test Paper");
140        r.add_author("Smith, J.");
141        r.year = Some(2026);
142        r.doi = Some("10.1234/test".into());
143        r
144    }
145
146    #[test]
147    fn ref_type_label() {
148        assert_eq!(EndnoteRefType::JournalArticle.label(), "Journal Article");
149    }
150
151    #[test]
152    fn ref_count() {
153        let mut lib = EndnoteLibrary::default();
154        lib.add_ref(sample_ref());
155        assert_eq!(lib.ref_count(), 1);
156    }
157
158    #[test]
159    fn render_xml_starts_correctly() {
160        let mut lib = EndnoteLibrary::default();
161        lib.add_ref(sample_ref());
162        let s = render_endnote_xml(&lib);
163        assert!(s.starts_with("<?xml"));
164    }
165
166    #[test]
167    fn render_contains_title() {
168        let s = render_ref_xml(&sample_ref());
169        assert!(s.contains("Test Paper"));
170    }
171
172    #[test]
173    fn render_contains_author() {
174        let s = render_ref_xml(&sample_ref());
175        assert!(s.contains("Smith"));
176    }
177
178    #[test]
179    fn render_contains_doi() {
180        let s = render_ref_xml(&sample_ref());
181        assert!(s.contains("10.1234/test"));
182    }
183
184    #[test]
185    fn validate_ok() {
186        assert!(validate_ref(&sample_ref()));
187    }
188
189    #[test]
190    fn validate_empty_title() {
191        let r = EndnoteRef::new(EndnoteRefType::Book, "");
192        assert!(!validate_ref(&r));
193    }
194
195    #[test]
196    fn count_by_type_correct() {
197        let mut lib = EndnoteLibrary::default();
198        lib.add_ref(sample_ref());
199        lib.add_ref(EndnoteRef::new(EndnoteRefType::Book, "A Book"));
200        assert_eq!(count_by_type(&lib, &EndnoteRefType::JournalArticle), 1);
201    }
202
203    #[test]
204    fn xml_escape_works() {
205        /* & should be escaped */
206        let s = render_ref_xml(&EndnoteRef::new(EndnoteRefType::WebPage, "A & B"));
207        assert!(s.contains("&amp;"));
208    }
209}