#![warn(missing_docs)]
#![allow(clippy::comparison_chain)]
#[macro_use]
mod selectors;
#[cfg(feature = "biblatex")]
mod interop;
mod csl;
pub mod io;
pub mod lang;
pub mod types;
mod util;
use std::collections::BTreeMap;
#[cfg(feature = "archive")]
pub use crate::csl::archive;
pub use citationberg;
pub use csl::{
BibliographyDriver, BibliographyItem, BibliographyRequest, Brackets, BufWriteFormat,
CitationItem, CitationRequest, CitePurpose, Elem, ElemChild, ElemChildren, ElemMeta,
Formatted, Formatting, LocatorPayload, Rendered, RenderedBibliography,
RenderedCitation, SpecificLocator, standalone_citation,
};
pub use selectors::{Selector, SelectorError};
use indexmap::IndexMap;
use paste::paste;
use serde::{Deserialize, Serialize, de::Visitor};
use types::*;
use unic_langid::LanguageIdentifier;
use util::{
OneOrMany, deserialize_one_or_many_opt, serialize_one_or_many,
serialize_one_or_many_opt,
};
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct Library(IndexMap<String, Entry>);
impl Library {
pub fn new() -> Self {
Self(IndexMap::new())
}
pub fn push(&mut self, entry: &Entry) {
self.0.insert(entry.key.clone(), entry.clone());
}
pub fn get(&self, key: &str) -> Option<&Entry> {
self.0.get(key)
}
pub fn iter(&self) -> impl Iterator<Item = &Entry> {
self.0.values()
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.0.keys().map(|k| k.as_str())
}
pub fn remove(&mut self, key: &str) -> Option<Entry> {
self.0.shift_remove(key)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn nth(&self, n: usize) -> Option<&Entry> {
self.0.get_index(n).map(|(_, v)| v)
}
}
impl<'a> IntoIterator for &'a Library {
type Item = &'a Entry;
type IntoIter = indexmap::map::Values<'a, String, Entry>;
fn into_iter(self) -> Self::IntoIter {
self.0.values()
}
}
impl IntoIterator for Library {
type Item = Entry;
type IntoIter = std::iter::Map<
indexmap::map::IntoIter<String, Entry>,
fn((String, Entry)) -> Entry,
>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter().map(|(_, v)| v)
}
}
impl FromIterator<Entry> for Library {
fn from_iter<T: IntoIterator<Item = Entry>>(iter: T) -> Self {
Self(iter.into_iter().map(|e| (e.key().to_string(), e)).collect())
}
}
macro_rules! entry {
($(
$(#[doc = $doc:literal])*
$(#[serde $serde:tt])*
$s:literal => $i:ident : $t:ty
$(| $d:ty)? $(,)?
),*) => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)]
pub struct Entry {
#[serde(skip)]
key: String,
#[serde(rename = "type")]
entry_type: EntryType,
$(
$(#[doc = $doc])*
$(#[serde $serde])*
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = $s)]
$i: Option<$t>,
)*
#[serde(serialize_with = "serialize_one_or_many")]
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(rename = "parent")]
parents: Vec<Entry>,
}
impl Entry {
pub fn key(&self) -> &str {
&self.key
}
pub fn new(key: &str, entry_type: EntryType) -> Self {
Self {
key: key.to_owned(),
entry_type,
$(
$i: None,
)*
parents: Vec::new(),
}
}
pub fn has(&self, key: &str) -> bool {
match key {
$(
$s => self.$i.is_some(),
)*
_ => false,
}
}
}
impl Entry {
pub fn entry_type(&self) -> &EntryType {
&self.entry_type
}
pub fn parents(&self) -> &[Entry] {
&self.parents
}
$(
entry!(@get $(#[doc = $doc])* $s => $i : $t $(| $d)?);
)*
}
impl Entry {
pub fn set_parents(&mut self, parents: Vec<Entry>) {
self.parents = parents;
}
$(
entry!(@set $s => $i : $t);
)*
}
impl<'de> Deserialize<'de> for Library {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct MyVisitor;
#[derive(Deserialize)]
struct NakedEntry {
#[serde(rename = "type")]
entry_type: Option<EntryType>,
#[serde(default)]
#[serde(rename = "parent")]
parents: OneOrMany<NakedEntry>,
$(
$(#[serde $serde])*
#[serde(rename = $s)]
#[serde(default)]
$i: Option<$t>,
)*
}
impl NakedEntry {
fn into_entry<E>(
self,
key: &str,
child_entry_type: Option<EntryType>,
) -> Result<Entry, E>
where E: serde::de::Error
{
let entry_type = self.entry_type
.or_else(|| child_entry_type.map(|e| e.default_parent()))
.ok_or_else(|| E::custom("no entry type"))?;
let parents: Result<Vec<_>, _> = self.parents
.into_iter()
.map(|p| p.into_entry(key, Some(entry_type)))
.collect();
Ok(Entry {
key: key.to_owned(),
entry_type,
parents: parents?,
$(
$i: self.$i,
)*
})
}
}
impl<'de> Visitor<'de> for MyVisitor {
type Value = Library;
fn expecting(&self, formatter: &mut std::fmt::Formatter)
-> std::fmt::Result
{
formatter.write_str(
"a map between cite keys and entries"
)
}
fn visit_map<A>(self, mut map: A)
-> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut entries = Vec::with_capacity(
map.size_hint().unwrap_or(0).min(128)
);
while let Some(key) = map.next_key::<String>()? {
if entries.iter().any(|(k, _)| k == &key) {
return Err(serde::de::Error::custom(format!(
"duplicate key {}",
key
)));
}
let entry: NakedEntry = map.next_value()?;
entries.push((key, entry));
}
let entries: Result<IndexMap<_, _>, A::Error> =
entries.into_iter().map(|(k, v)| {
v.into_entry(&k, None).map(|e| (k, e))
}).collect();
Ok(Library(entries?))
}
}
deserializer.deserialize_map(MyVisitor)
}
}
};
(@match
$s:literal => $i:ident,
$naked:ident, $map:ident $(,)?
) => {
$naked.$i = Some($map.next_value()?)
};
(@match
$(#[serde $serde:tt])+
$s:literal => $i:ident,
$naked:ident, $map:ident $(,)?
) => {
let one_or_many: OneOrMany = $map.next_value()?;
$naked.$i = Some(one_or_many.into());
};
(@get $(#[$docs:meta])+ $s:literal => $i:ident : $t:ty | $d:ty $(,)?) => {
$(#[$docs])+
pub fn $i(&self) -> Option<&$d> {
self.$i.as_deref()
}
};
(@get $(#[$docs:meta])+ $s:literal => $i:ident : $t:ty $(,)?) => {
$(#[$docs])+
pub fn $i(&self) -> Option<&$t> {
self.$i.as_ref()
}
};
(@set $s:literal => $i:ident : $t:ty $(,)?) => {
paste! {
#[doc = "Set the `" $s "` field."]
pub fn [<set_ $i>](&mut self, $i: $t) {
self.$i = Some($i);
}
}
};
}
entry! {
"title" => title: FormatString,
#[serde(serialize_with = "serialize_one_or_many_opt")]
#[serde(deserialize_with = "deserialize_one_or_many_opt")]
"author" => authors: Vec<Person> | [Person],
"date" => date: Date,
#[serde(serialize_with = "serialize_one_or_many_opt")]
#[serde(deserialize_with = "deserialize_one_or_many_opt")]
"editor" => editors: Vec<Person> | [Person],
#[serde(serialize_with = "serialize_one_or_many_opt")]
#[serde(deserialize_with = "deserialize_one_or_many_opt")]
"affiliated" => affiliated: Vec<PersonsWithRoles> | [PersonsWithRoles],
"publisher" => publisher: Publisher,
"location" => location: FormatString,
"organization" => organization: FormatString,
"issue" => issue: MaybeTyped<Numeric>,
"chapter" => chapter: MaybeTyped<Numeric>,
"volume" => volume: MaybeTyped<Numeric>,
"volume-total" => volume_total: Numeric,
"edition" => edition: MaybeTyped<Numeric>,
"page-range" => page_range: MaybeTyped<PageRanges>,
"page-total" => page_total: Numeric,
"time-range" => time_range: MaybeTyped<DurationRange>,
"runtime" => runtime: MaybeTyped<Duration>,
"url" => url: QualifiedUrl,
#[serde(alias = "serial")]
"serial-number" => serial_number: SerialNumber,
"language" => language: LanguageIdentifier,
"archive" => archive: FormatString,
"archive-location" => archive_location: FormatString,
"call-number" => call_number: FormatString,
"note" => note: FormatString,
"abstract" => abstract_: FormatString,
"genre" => genre: FormatString,
}
impl Entry {
pub(crate) fn affiliated_with_role(&self, role: PersonRole) -> Vec<&Person> {
self.affiliated
.iter()
.flatten()
.filter_map(
|PersonsWithRoles { names, role: r }| {
if r == &role { Some(names) } else { None }
},
)
.flatten()
.collect()
}
pub fn map<'a, F, T>(&'a self, mut f: F) -> Option<T>
where
F: FnMut(&'a Self) -> Option<T>,
{
if let Some(value) = f(self) { Some(value) } else { self.map_parents(f) }
}
pub fn map_parents<'a, F, T>(&'a self, mut f: F) -> Option<T>
where
F: FnMut(&'a Self) -> Option<T>,
{
let mut path: Vec<usize> = vec![0];
let up = |path: &mut Vec<usize>| {
path.pop();
if let Some(last) = path.last_mut() {
*last += 1;
}
};
'outer: loop {
let first_path = path.first()?;
if self.parents.len() <= *first_path {
return None;
}
let mut item = &self.parents[*first_path];
for i in 1..path.len() {
if path[i] >= item.parents.len() {
up(&mut path);
continue 'outer;
}
item = &item.parents[path[i]];
}
if let Some(first_path) = path.first_mut() {
*first_path += 1;
}
if let Some(value) = f(item) {
return Some(value);
}
}
}
pub fn bound_select(&self, selector: &Selector, binding: &str) -> Option<&Entry> {
selector.apply(self).and_then(|map| map.get(binding).copied())
}
pub fn date_any(&self) -> Option<&Date> {
self.map(|e| e.date.as_ref())
}
pub fn url_any(&self) -> Option<&QualifiedUrl> {
self.map(|e| e.url.as_ref())
}
pub fn keyed_serial_number(&self, key: &str) -> Option<&str> {
self.serial_number
.as_ref()
.and_then(|s| s.0.get(key).map(|s| s.as_str()))
}
pub fn set_keyed_serial_number(&mut self, key: &str, value: String) {
if let Some(serials) = &mut self.serial_number {
serials.0.insert(key.to_owned(), value);
} else {
let mut map = BTreeMap::new();
map.insert(key.to_owned(), value);
self.serial_number = Some(SerialNumber(map));
}
}
pub fn doi(&self) -> Option<&str> {
self.keyed_serial_number("doi")
}
pub fn set_doi(&mut self, doi: String) {
self.set_keyed_serial_number("doi", doi);
}
pub fn isbn(&self) -> Option<&str> {
self.keyed_serial_number("isbn")
}
pub fn set_isbn(&mut self, isbn: String) {
self.set_keyed_serial_number("isbn", isbn);
}
pub fn issn(&self) -> Option<&str> {
self.keyed_serial_number("issn")
}
pub fn set_issn(&mut self, issn: String) {
self.set_keyed_serial_number("issn", issn);
}
pub fn pmid(&self) -> Option<&str> {
self.keyed_serial_number("pmid")
}
pub fn set_pmid(&mut self, pmid: String) {
self.set_keyed_serial_number("pmid", pmid);
}
pub fn pmcid(&self) -> Option<&str> {
self.keyed_serial_number("pmcid")
}
pub fn set_pmcid(&mut self, pmcid: String) {
self.set_keyed_serial_number("pmcid", pmcid);
}
pub fn arxiv(&self) -> Option<&str> {
self.keyed_serial_number("arxiv")
}
pub fn set_arxiv(&mut self, arxiv: String) {
self.set_keyed_serial_number("arxiv", arxiv);
}
pub(crate) fn get_container(&self) -> Option<&Self> {
let retrieve_container = |possible: &[EntryType]| {
for possibility in possible {
if let Some(container) =
self.parents.iter().find(|e| e.entry_type == *possibility)
{
return Some(container);
}
}
None
};
match &self.entry_type {
EntryType::Article => retrieve_container(&[
EntryType::Book,
EntryType::Proceedings,
EntryType::Conference,
EntryType::Periodical,
EntryType::Newspaper,
EntryType::Blog,
EntryType::Reference,
EntryType::Web,
]),
EntryType::Anthos => retrieve_container(&[
EntryType::Book,
EntryType::Anthology,
EntryType::Reference,
EntryType::Report,
]),
EntryType::Chapter => retrieve_container(&[
EntryType::Book,
EntryType::Anthology,
EntryType::Reference,
EntryType::Report,
]),
EntryType::Report => {
retrieve_container(&[EntryType::Book, EntryType::Anthology])
}
EntryType::Web => retrieve_container(&[EntryType::Web]),
EntryType::Scene => retrieve_container(&[
EntryType::Audio,
EntryType::Video,
EntryType::Performance,
EntryType::Artwork,
]),
EntryType::Case => retrieve_container(&[
EntryType::Book,
EntryType::Anthology,
EntryType::Reference,
EntryType::Report,
]),
EntryType::Post => {
retrieve_container(&[EntryType::Thread, EntryType::Blog, EntryType::Web])
}
EntryType::Thread => {
retrieve_container(&[EntryType::Thread, EntryType::Web, EntryType::Blog])
}
_ => None,
}
}
pub(crate) fn get_full(&self) -> &Self {
let mut parent = self.parents().first();
let mut entry = self;
while select!(Chapter | Scene).matches(entry) && entry.title().is_none() {
if let Some(p) = parent {
entry = p;
parent = entry.parents().first();
} else {
break;
}
}
entry
}
pub(crate) fn get_collection(&self) -> Option<&Self> {
match &self.entry_type {
EntryType::Anthology
| EntryType::Newspaper
| EntryType::Performance
| EntryType::Periodical
| EntryType::Proceedings
| EntryType::Book
| EntryType::Reference
| EntryType::Exhibition => self.parents.iter().find(|e| {
e.entry_type == self.entry_type || e.entry_type == EntryType::Anthology
}),
_ => self.parents.iter().find_map(|e| e.get_collection()),
}
}
pub(crate) fn dfs_parent(&self, kind: EntryType) -> Option<&Self> {
if self.entry_type == kind {
return Some(self);
}
for parent in &self.parents {
if let Some(entry) = parent.dfs_parent(kind) {
return Some(entry);
}
}
None
}
pub(crate) fn get_original(&self) -> Option<&Self> {
self.dfs_parent(EntryType::Original)
}
}
#[cfg(feature = "biblatex")]
impl Entry {
pub(crate) fn add_parent(&mut self, entry: Self) {
self.parents.push(entry);
}
pub(crate) fn add_affiliated_persons(
&mut self,
new_persons: (Vec<Person>, PersonRole),
) {
let obj = PersonsWithRoles { names: new_persons.0, role: new_persons.1 };
if let Some(affiliated) = &mut self.affiliated {
affiliated.push(obj);
} else {
self.affiliated = Some(vec![obj]);
}
}
pub(crate) fn parents_mut(&mut self) -> &mut [Self] {
&mut self.parents
}
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
use crate::io::from_yaml_str;
macro_rules! select_all {
($select:expr, $entries:tt, [$($key:expr),* $(,)*] $(,)*) => {
let keys = [$($key,)*];
let selector = Selector::parse($select).unwrap();
for entry in $entries.iter() {
let res = selector.apply(entry);
if keys.contains(&entry.key.as_str()) {
if res.is_none() {
panic!("Key {} not found in results", entry.key);
}
} else {
if res.is_some() {
panic!("Key {} found in results", entry.key);
}
}
}
}
}
macro_rules! select {
($select:expr, $entries:tt >> $entry_key:expr, [$($key:expr),* $(,)*] $(,)*) => {
let keys = vec![ $( $key , )* ];
let entry = $entries.iter().filter_map(|i| if i.key == $entry_key {Some(i)} else {None}).next().unwrap();
let selector = Selector::parse($select).unwrap();
let res = selector.apply(entry).unwrap();
if !keys.into_iter().all(|k| res.get(k).is_some()) {
panic!("Results do not contain binding");
}
}
}
#[test]
fn selectors() {
let contents = fs::read_to_string("tests/data/basic.yml").unwrap();
let entries = from_yaml_str(&contents).unwrap();
select_all!("article > proceedings", entries, ["zygos"]);
select_all!(
"article > (periodical | newspaper)",
entries,
["omarova-libra", "kinetics", "house", "swedish",]
);
select_all!(
"(chapter | anthos) > (anthology | book)",
entries,
["harry", "gedanken", "lamb-chapter", "snail-chapter"]
);
select_all!(
"*[url]",
entries,
[
"omarova-libra",
"science-e-issue",
"oiseau",
"georgia",
"really-habitable",
"electronic-music",
"mattermost",
"worth",
"wrong",
"un-hdr",
"audio-descriptions",
"camb",
"logician",
"dns-encryption",
"overleaf",
"editors",
]
);
select_all!(
"!(*[url] | (* > *[url]))",
entries,
[
"zygos",
"harry",
"terminator-2",
"interior",
"wire",
"kinetics",
"house",
"plaque",
"renaissance",
"gedanken",
"donne",
"roe-wade",
"foia",
"drill",
"swedish",
"latex-users",
"barb",
"lamb",
"snail",
"lamb-chapter",
"snail-chapter",
]
);
select_all!("*[abstract, note, genre]", entries, ["wire"]);
}
#[test]
fn selector_bindings() {
let contents = fs::read_to_string("tests/data/basic.yml").unwrap();
let entries = from_yaml_str(&contents).unwrap();
select!(
"a:article > (b:conference & c:(video|blog|web))",
entries >> "wwdc-network",
["a", "b", "c"]
);
}
#[test]
#[cfg(feature = "biblatex")]
fn test_troublesome_page_ranges() {
use io::from_biblatex_str;
let bibtex = r#"
@article{b,
title={My page ranges},
pages={150--es}
}
"#;
let library = from_biblatex_str(bibtex).unwrap();
for entry in library.iter() {
assert!(entry.page_range.is_some())
}
}
}