use biblatex as biblatex_crate;
use citum_schema::reference::{
InputReference, LangID, Numbering, NumberingType, Publisher, RefID, RichText, WorkRelation,
contributor::{Contributor, ContributorList, StructuredName},
date::EdtfString,
types::{
Collection, CollectionComponent, CollectionType, Monograph, MonographComponentType,
MonographType, NumOrStr, Serial, SerialComponent, SerialComponentType, SerialType, Title,
},
};
use std::collections::HashMap;
use url::Url;
struct BibRefContext<'a> {
id: Option<RefID>,
title: Option<Title>,
author: Option<Contributor>,
editor: Option<Contributor>,
issued: EdtfString,
publisher: Option<Publisher>,
language: Option<LangID>,
field_str: &'a dyn Fn(&str) -> Option<String>,
}
fn build_inbook_reference(ctx: BibRefContext<'_>) -> InputReference {
let field_str = ctx.field_str;
let parent_title = field_str("booktitle").map(Title::Single);
let mut parent_numbering = Vec::new();
if let Some(n) = field_str("number") {
parent_numbering.push(Numbering {
r#type: NumberingType::Volume,
value: n,
});
}
InputReference::CollectionComponent(Box::new(CollectionComponent {
id: ctx.id,
r#type: MonographComponentType::Chapter,
title: ctx.title,
author: ctx.author,
translator: None,
created: EdtfString(String::new()),
issued: ctx.issued,
container: Some(WorkRelation::Embedded(Box::new(
InputReference::Collection(Box::new(Collection {
id: None,
r#type: CollectionType::EditedBook,
title: parent_title,
short_title: None,
container: None,
editor: ctx.editor,
translator: None,
created: EdtfString(String::new()),
issued: EdtfString(String::new()),
publisher: ctx.publisher,
numbering: parent_numbering,
..Default::default()
})),
))),
numbering: Vec::new(),
pages: field_str("pages").map(NumOrStr::Str),
url: field_str("url").and_then(|u| Url::parse(&u).ok()),
accessed: field_str("urldate").map(EdtfString),
language: ctx.language,
field_languages: HashMap::new(),
note: field_str("note").map(RichText::Plain),
doi: field_str("doi"),
genre: field_str("type"),
..Default::default()
}))
}
fn build_article_reference(ctx: BibRefContext<'_>) -> InputReference {
let field_str = ctx.field_str;
let parent_title = field_str("journaltitle")
.or_else(|| field_str("journal"))
.map(Title::Single);
let mut component_numbering = Vec::new();
if let Some(v) = field_str("volume") {
component_numbering.push(Numbering {
r#type: NumberingType::Volume,
value: v,
});
}
if let Some(i) = field_str("number") {
component_numbering.push(Numbering {
r#type: NumberingType::Issue,
value: i,
});
}
InputReference::SerialComponent(Box::new(SerialComponent {
id: ctx.id,
r#type: SerialComponentType::Article,
title: ctx.title,
author: ctx.author,
translator: None,
created: EdtfString(String::new()),
issued: ctx.issued,
container: Some(WorkRelation::Embedded(Box::new(InputReference::Serial(
Box::new(Serial {
id: None,
r#type: SerialType::AcademicJournal,
title: parent_title,
short_title: None,
container: None,
editor: None,
contributors: Vec::new(),
publisher: None,
url: None,
accessed: None,
language: None,
field_languages: HashMap::new(),
note: None,
issn: field_str("issn"),
unknown_fields: Default::default(),
}),
)))),
numbering: component_numbering,
url: field_str("url").and_then(|u| Url::parse(&u).ok()),
accessed: field_str("urldate").map(EdtfString),
language: ctx.language,
field_languages: HashMap::new(),
note: field_str("note").map(RichText::Plain),
doi: field_str("doi"),
ads_bibcode: field_str("bibcode"),
pages: field_str("pages"),
genre: field_str("type"),
..Default::default()
}))
}
pub fn input_reference_from_biblatex(entry: &biblatex_crate::Entry) -> InputReference {
let id = Some(entry.key.clone().into());
let field_str = |key: &str| {
entry.fields.get(key).map(|f| {
f.iter()
.map(|c| match &c.v {
biblatex_crate::Chunk::Normal(s) | biblatex_crate::Chunk::Verbatim(s) => {
s.as_str()
}
_ => "",
})
.collect::<String>()
})
};
let title = field_str("title").map(Title::Single);
let issued = field_str("date").map_or(EdtfString(String::new()), EdtfString);
let publisher = field_str("publisher").map(|p| Publisher {
name: p.into(),
place: field_str("location").map(Into::into),
});
let author = entry
.author()
.ok()
.map(|p| contributors_from_biblatex_persons(&p));
let editor = entry.editors().ok().map(|e| {
let all_persons: Vec<biblatex_crate::Person> =
e.into_iter().flat_map(|(persons, _)| persons).collect();
contributors_from_biblatex_persons(&all_persons)
});
let language = field_str("langid")
.or_else(|| field_str("language"))
.map(Into::into);
let entry_type = entry.entry_type.to_string().to_lowercase();
let ctx = BibRefContext {
id,
title,
author,
editor,
issued,
publisher,
language,
field_str: &field_str,
};
match entry_type.as_str() {
"book" | "mvbook" | "collection" | "mvcollection" | "manual" | "report" => {
let mono_type = match entry_type.as_str() {
"manual" => MonographType::Manual,
"report" => MonographType::Report,
_ => MonographType::Book,
};
InputReference::Monograph(Box::new(biblatex_monograph(mono_type, &entry_type, ctx)))
}
"inbook" | "incollection" | "inproceedings" => build_inbook_reference(ctx),
"article" => build_article_reference(ctx),
_ => InputReference::Monograph(Box::new(biblatex_monograph(
MonographType::Document,
&entry_type,
ctx,
))),
}
}
fn biblatex_monograph(
r#type: MonographType,
entry_type: &str,
ctx: BibRefContext<'_>,
) -> Monograph {
let field_str = ctx.field_str;
let mut numbering = Vec::new();
if let Some(ed) = field_str("edition") {
numbering.push(Numbering {
r#type: NumberingType::Edition,
value: ed,
});
}
if let Some(n) = field_str("number") {
if entry_type == "report" {
numbering.push(Numbering {
r#type: NumberingType::Report,
value: n,
});
} else {
numbering.push(Numbering {
r#type: NumberingType::Number,
value: n,
});
}
}
Monograph {
id: ctx.id,
r#type,
title: ctx.title,
short_title: None,
container: None,
author: ctx.author,
editor: ctx.editor,
translator: None,
created: EdtfString(String::new()),
issued: ctx.issued,
publisher: ctx.publisher,
url: field_str("url").and_then(|u| Url::parse(&u).ok()),
accessed: field_str("urldate").map(EdtfString),
language: ctx.language,
field_languages: HashMap::new(),
note: field_str("note").map(RichText::Plain),
isbn: field_str("isbn"),
doi: field_str("doi"),
ads_bibcode: field_str("bibcode"),
numbering,
genre: field_str("type"),
..Default::default()
}
}
pub fn contributors_from_biblatex_persons(persons: &[biblatex_crate::Person]) -> Contributor {
let contributors: Vec<Contributor> = persons
.iter()
.map(|p| {
Contributor::StructuredName(StructuredName {
given: p.given_name.clone().into(),
family: p.name.clone().into(),
suffix: if p.suffix.is_empty() {
None
} else {
Some(p.suffix.clone())
},
dropping_particle: None,
non_dropping_particle: if p.prefix.is_empty() {
None
} else {
Some(p.prefix.clone())
},
})
})
.collect();
Contributor::ContributorList(ContributorList(contributors))
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
fn parse_single_entry(source: &str) -> biblatex_crate::Entry {
let bibliography =
biblatex_crate::Bibliography::parse(source).expect("biblatex should parse");
bibliography
.into_iter()
.next()
.expect("bibliography should contain one entry")
}
#[test]
fn biblatex_report_number_maps_to_report_numbering() {
let entry = parse_single_entry(
"@report{r1,\n title = {Report},\n date = {2024},\n number = {TR-7}\n}",
);
let converted = input_reference_from_biblatex(&entry);
assert_eq!(converted.ref_type(), "report");
assert_eq!(converted.number(), None);
assert_eq!(converted.report_number(), Some("TR-7".to_string()));
}
#[test]
fn biblatex_book_number_maps_to_generic_numbering() {
let entry =
parse_single_entry("@book{b1,\n title = {Book},\n date = {2024},\n number = {2}\n}");
let converted = input_reference_from_biblatex(&entry);
assert_eq!(converted.number(), Some("2".to_string()));
assert_eq!(converted.report_number(), None);
}
}