#![allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum EndnoteRefType {
JournalArticle,
Book,
BookSection,
ConferencePaper,
Report,
Thesis,
WebPage,
}
impl EndnoteRefType {
pub fn label(&self) -> &'static str {
match self {
Self::JournalArticle => "Journal Article",
Self::Book => "Book",
Self::BookSection => "Book Section",
Self::ConferencePaper => "Conference Paper",
Self::Report => "Report",
Self::Thesis => "Thesis",
Self::WebPage => "Web Page",
}
}
}
#[derive(Debug, Clone)]
pub struct EndnoteRef {
pub ref_type: EndnoteRefType,
pub title: String,
pub authors: Vec<String>,
pub year: Option<u32>,
pub journal: Option<String>,
pub volume: Option<String>,
pub pages: Option<String>,
pub doi: Option<String>,
}
impl EndnoteRef {
pub fn new(ref_type: EndnoteRefType, title: impl Into<String>) -> Self {
Self {
ref_type,
title: title.into(),
authors: Vec::new(),
year: None,
journal: None,
volume: None,
pages: None,
doi: None,
}
}
pub fn add_author(&mut self, author: impl Into<String>) {
self.authors.push(author.into());
}
}
#[derive(Debug, Clone, Default)]
pub struct EndnoteLibrary {
pub refs: Vec<EndnoteRef>,
}
impl EndnoteLibrary {
pub fn add_ref(&mut self, r: EndnoteRef) {
self.refs.push(r);
}
pub fn ref_count(&self) -> usize {
self.refs.len()
}
}
pub fn render_ref_xml(r: &EndnoteRef) -> String {
let mut out = format!(
" <record>\n <ref-type name=\"{}\"/>\n",
r.ref_type.label()
);
out.push_str(&format!(" <title>{}</title>\n", xml_escape(&r.title)));
for author in &r.authors {
out.push_str(&format!(" <author>{}</author>\n", xml_escape(author)));
}
if let Some(y) = r.year {
out.push_str(&format!(" <year>{y}</year>\n"));
}
if let Some(doi) = &r.doi {
out.push_str(&format!(
" <electronic-resource-num>{doi}</electronic-resource-num>\n"
));
}
out.push_str(" </record>\n");
out
}
pub fn render_endnote_xml(lib: &EndnoteLibrary) -> String {
let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<xml>\n <records>\n");
for r in &lib.refs {
out.push_str(&render_ref_xml(r));
}
out.push_str(" </records>\n</xml>\n");
out
}
pub fn validate_ref(r: &EndnoteRef) -> bool {
!r.title.is_empty()
}
pub fn count_by_type(lib: &EndnoteLibrary, ref_type: &EndnoteRefType) -> usize {
lib.refs.iter().filter(|r| &r.ref_type == ref_type).count()
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_ref() -> EndnoteRef {
let mut r = EndnoteRef::new(EndnoteRefType::JournalArticle, "Test Paper");
r.add_author("Smith, J.");
r.year = Some(2026);
r.doi = Some("10.1234/test".into());
r
}
#[test]
fn ref_type_label() {
assert_eq!(EndnoteRefType::JournalArticle.label(), "Journal Article");
}
#[test]
fn ref_count() {
let mut lib = EndnoteLibrary::default();
lib.add_ref(sample_ref());
assert_eq!(lib.ref_count(), 1);
}
#[test]
fn render_xml_starts_correctly() {
let mut lib = EndnoteLibrary::default();
lib.add_ref(sample_ref());
let s = render_endnote_xml(&lib);
assert!(s.starts_with("<?xml"));
}
#[test]
fn render_contains_title() {
let s = render_ref_xml(&sample_ref());
assert!(s.contains("Test Paper"));
}
#[test]
fn render_contains_author() {
let s = render_ref_xml(&sample_ref());
assert!(s.contains("Smith"));
}
#[test]
fn render_contains_doi() {
let s = render_ref_xml(&sample_ref());
assert!(s.contains("10.1234/test"));
}
#[test]
fn validate_ok() {
assert!(validate_ref(&sample_ref()));
}
#[test]
fn validate_empty_title() {
let r = EndnoteRef::new(EndnoteRefType::Book, "");
assert!(!validate_ref(&r));
}
#[test]
fn count_by_type_correct() {
let mut lib = EndnoteLibrary::default();
lib.add_ref(sample_ref());
lib.add_ref(EndnoteRef::new(EndnoteRefType::Book, "A Book"));
assert_eq!(count_by_type(&lib, &EndnoteRefType::JournalArticle), 1);
}
#[test]
fn xml_escape_works() {
let s = render_ref_xml(&EndnoteRef::new(EndnoteRefType::WebPage, "A & B"));
assert!(s.contains("&"));
}
}