hayagriva/
lib.rs

1/*!
2Hayagriva provides a YAML-backed format and data model for various
3bibliography items as well as a CSL processor formatting both in-text citations and
4reference lists based on these literature databases.
5
6The crate is intended to assist scholarly writing and reference management
7and can be used both through a CLI and an API.
8
9Below, there is an example of how to parse a YAML database and get a Modern
10Language Association-style citation.
11
12# Supported styles
13
14Hayagriva supports all styles provided in the
15[official Citation Style Language repository](https://github.com/citation-style-language/styles),
16currently over 2,600. You can provide your own style files or use the ones
17bundled with this library in the [`archive`] module.
18
19# Usage
20
21```rust
22use hayagriva::io::from_yaml_str;
23
24let yaml = r#"
25crazy-rich:
26    type: Book
27    title: Crazy Rich Asians
28    author: Kwan, Kevin
29    date: 2014
30    publisher: Anchor Books
31    location: New York, NY, US
32"#;
33
34// Parse a bibliography
35let bib = from_yaml_str(yaml).unwrap();
36assert_eq!(bib.get("crazy-rich").unwrap().date().unwrap().year, 2014);
37
38// Format the reference
39use std::fs;
40use hayagriva::{
41    BibliographyDriver, BibliographyRequest, BufWriteFormat,
42    CitationItem, CitationRequest,
43};
44use hayagriva::citationberg::{LocaleFile, IndependentStyle};
45
46let en_locale = fs::read_to_string("tests/data/locales-en-US.xml").unwrap();
47let locales = [LocaleFile::from_xml(&en_locale).unwrap().into()];
48
49let style = fs::read_to_string("tests/data/art-history.csl").unwrap();
50let style = IndependentStyle::from_xml(&style).unwrap();
51
52let mut driver = BibliographyDriver::new();
53
54for entry in bib.iter() {
55    let items = vec![CitationItem::with_entry(entry)];
56    driver.citation(CitationRequest::from_items(items, &style, &locales));
57}
58
59let result = driver.finish(BibliographyRequest {
60    style: &style,
61    locale: None,
62    locale_files: &locales,
63});
64
65for cite in result.citations {
66    println!("{}", cite.citation)
67}
68```
69
70To format entries, you need to wrap them in a [`CitationRequest`]. Each of these
71can reference multiple entries in their respective [`CitationItem`]s.
72Use these with a [`BibliographyDriver`] to obtain formatted citations and bibliographies.
73
74If the default features are enabled, Hayagriva supports BibTeX and BibLaTeX
75bibliographies. You can use [`io::from_biblatex_str`] to parse such
76bibliographies.
77
78Should you need more manual control, the library's native `Entry` struct
79also offers an implementation of the `From<&biblatex::Entry>`-Trait. You will
80need to depend on the [biblatex](https://docs.rs/biblatex/latest/biblatex/)
81crate to obtain its `Entry`. Therefore, you could also use your BibLaTeX
82content like this:
83
84```ignore
85use hayagriva::Entry;
86let converted: Entry = your_biblatex_entry.into();
87```
88
89If you do not need BibLaTeX compatibility, you can use Hayagriva without the
90default features by writing this in your `Cargo.toml`:
91
92```toml
93[dependencies]
94hayagriva = { version = "0.9", default-features = false }
95```
96
97# Selectors
98
99Hayagriva uses a custom selector language that enables you to filter
100bibliographies by type of media. For more information about selectors, refer
101to the [selectors.md
102file](https://github.com/typst/hayagriva/blob/main/docs/selectors.md). While
103you can parse user-defined selectors using the function `Selector::parse`,
104you may instead want to use the selector macro to avoid the run time cost of
105parsing a selector when working with constant selectors.
106
107```rust
108use hayagriva::select;
109use hayagriva::io::from_yaml_str;
110
111let yaml = r#"
112quantized-vortex:
113    type: Article
114    author: Gross, E. P.
115    title: Structure of a Quantized Vortex in Boson Systems
116    date: 1961-05
117    page-range: 454-477
118    serial-number:
119        doi: 10.1007/BF02731494
120    parent:
121        issue: 3
122        volume: 20
123        title: Il Nuovo Cimento
124"#;
125
126let entries = from_yaml_str(yaml).unwrap();
127let journal = select!((Article["date"]) > ("journal":Periodical));
128assert!(journal.matches(entries.nth(0).unwrap()));
129```
130
131There are two ways to check if a selector matches an entry.
132You should use [`Selector::matches`] if you just want to know if an item
133matches a selector and [`Selector::apply`] to continue to work with the data from
134parents of a matching entry. Keep in mind that the latter function will
135return `Some` even if no sub-entry was bound / if the hash map is empty.
136*/
137
138#![warn(missing_docs)]
139#![allow(clippy::comparison_chain)]
140
141#[macro_use]
142mod selectors;
143#[cfg(feature = "biblatex")]
144mod interop;
145
146mod csl;
147pub mod io;
148pub mod lang;
149pub mod types;
150mod util;
151
152use std::collections::BTreeMap;
153
154#[cfg(feature = "archive")]
155pub use crate::csl::archive;
156pub use citationberg;
157pub use csl::{
158    BibliographyDriver, BibliographyItem, BibliographyRequest, Brackets, BufWriteFormat,
159    CitationItem, CitationRequest, CitePurpose, Elem, ElemChild, ElemChildren, ElemMeta,
160    Formatted, Formatting, LocatorPayload, Rendered, RenderedBibliography,
161    RenderedCitation, SpecificLocator, TransparentLocator, standalone_citation,
162};
163pub use selectors::{Selector, SelectorError};
164
165use indexmap::IndexMap;
166use paste::paste;
167use serde::{Deserialize, Serialize, de::Visitor};
168use types::*;
169use unic_langid::LanguageIdentifier;
170use util::{
171    OneOrMany, deserialize_one_or_many_opt, serialize_one_or_many,
172    serialize_one_or_many_opt,
173};
174
175/// A collection of bibliographic entries.
176#[derive(Debug, Clone, Default, PartialEq, Serialize)]
177pub struct Library(IndexMap<String, Entry>);
178
179impl Library {
180    /// Construct a new, empty bibliography library.
181    pub fn new() -> Self {
182        Self(IndexMap::new())
183    }
184
185    /// Add an entry to the library.
186    pub fn push(&mut self, entry: &Entry) {
187        self.0.insert(entry.key.clone(), entry.clone());
188    }
189
190    /// Retrieve an entry from the library.
191    pub fn get(&self, key: &str) -> Option<&Entry> {
192        self.0.get(key)
193    }
194
195    /// Get an iterator over the entries in the library.
196    pub fn iter(&self) -> impl Iterator<Item = &Entry> {
197        self.0.values()
198    }
199
200    /// Get an iterator over the keys in the library.
201    pub fn keys(&self) -> impl Iterator<Item = &str> {
202        self.0.keys().map(|k| k.as_str())
203    }
204
205    /// Remove an entry from the library.
206    pub fn remove(&mut self, key: &str) -> Option<Entry> {
207        self.0.shift_remove(key)
208    }
209
210    /// Get the length of the library.
211    pub fn len(&self) -> usize {
212        self.0.len()
213    }
214
215    /// Check whether the library is empty.
216    pub fn is_empty(&self) -> bool {
217        self.0.is_empty()
218    }
219
220    /// Get the nth entry in the library.
221    pub fn nth(&self, n: usize) -> Option<&Entry> {
222        self.0.get_index(n).map(|(_, v)| v)
223    }
224}
225
226impl<'a> IntoIterator for &'a Library {
227    type Item = &'a Entry;
228    type IntoIter = indexmap::map::Values<'a, String, Entry>;
229
230    fn into_iter(self) -> Self::IntoIter {
231        self.0.values()
232    }
233}
234
235impl IntoIterator for Library {
236    type Item = Entry;
237    type IntoIter = std::iter::Map<
238        indexmap::map::IntoIter<String, Entry>,
239        fn((String, Entry)) -> Entry,
240    >;
241
242    fn into_iter(self) -> Self::IntoIter {
243        self.0.into_iter().map(|(_, v)| v)
244    }
245}
246
247impl FromIterator<Entry> for Library {
248    fn from_iter<T: IntoIterator<Item = Entry>>(iter: T) -> Self {
249        Self(iter.into_iter().map(|e| (e.key().to_string(), e)).collect())
250    }
251}
252
253macro_rules! entry {
254    ($(
255        $(#[doc = $doc:literal])*
256        $(#[serde $serde:tt])*
257        $s:literal => $i:ident : $t:ty
258        $(| $d:ty)? $(,)?
259    ),*) => {
260        // Build the struct and make it serializable.
261
262        /// A citable item in a bibliography.
263        #[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)]
264        pub struct Entry {
265            /// The key of the entry.
266            #[serde(skip)]
267            key: String,
268            /// The type of the item.
269            #[serde(rename = "type")]
270            entry_type: EntryType,
271            $(
272                $(#[doc = $doc])*
273                $(#[serde $serde])*
274                #[serde(skip_serializing_if = "Option::is_none")]
275                #[serde(rename = $s)]
276                $i: Option<$t>,
277            )*
278            /// Item in which the item was published / to which it is strongly
279            /// associated to.
280            #[serde(serialize_with = "serialize_one_or_many")]
281            #[serde(skip_serializing_if = "Vec::is_empty")]
282            #[serde(rename = "parent")]
283            parents: Vec<Entry>,
284        }
285
286        impl Entry {
287            /// Get the key of the entry.
288            pub fn key(&self) -> &str {
289                &self.key
290            }
291
292            /// Construct a new, empty entry.
293            pub fn new(key: &str, entry_type: EntryType) -> Self {
294                Self {
295                    key: key.to_owned(),
296                    entry_type,
297                    $(
298                        $i: None,
299                    )*
300                    parents: Vec::new(),
301                }
302            }
303
304            /// Check whether the entry has some key.
305            pub fn has(&self, key: &str) -> bool {
306                match key {
307                    $(
308                        $s => self.$i.is_some(),
309                    )*
310                    _ => false,
311                }
312            }
313        }
314
315        /// Getters.
316        impl Entry {
317            /// Get the type of the entry.
318            pub fn entry_type(&self) -> &EntryType {
319                &self.entry_type
320            }
321
322            /// Get the parents of the entry.
323            pub fn parents(&self) -> &[Entry] {
324                &self.parents
325            }
326
327            $(
328                entry!(@get $(#[doc = $doc])* $s => $i : $t $(| $d)?);
329            )*
330        }
331
332        /// Setters.
333        impl Entry {
334            /// Set the parents of the entry.
335            pub fn set_parents(&mut self, parents: Vec<Entry>) {
336                self.parents = parents;
337            }
338
339
340            $(
341                entry!(@set $s => $i : $t);
342            )*
343        }
344
345        /// The library deserialization also handles entries.
346        ///
347        /// Entries do not implement [`Deserialize`] because they have a data
348        /// dependency on their key (stored in the parent map) and their
349        /// children for default types.
350        impl<'de> Deserialize<'de> for Library {
351            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
352            where
353                D: serde::Deserializer<'de>,
354            {
355                struct MyVisitor;
356
357                #[derive(Deserialize)]
358                struct NakedEntry {
359                    #[serde(rename = "type")]
360                    entry_type: Option<EntryType>,
361                    #[serde(default)]
362                    #[serde(rename = "parent")]
363                    parents: OneOrMany<NakedEntry>,
364                    $(
365                        $(#[serde $serde])*
366                        #[serde(rename = $s)]
367                        #[serde(default)]
368                        $i: Option<$t>,
369                    )*
370                }
371
372                impl NakedEntry {
373                    /// Convert into a full entry using the child entry type
374                    /// (if any) and the key.
375                    fn into_entry<E>(
376                        self,
377                        key: &str,
378                        child_entry_type: Option<EntryType>,
379                    ) -> Result<Entry, E>
380                        where E: serde::de::Error
381                    {
382                        let entry_type = self.entry_type
383                            .or_else(|| child_entry_type.map(|e| e.default_parent()))
384                            .ok_or_else(|| E::custom("no entry type"))?;
385
386                        let parents: Result<Vec<_>, _> = self.parents
387                            .into_iter()
388                            .map(|p| p.into_entry(key, Some(entry_type)))
389                            .collect();
390
391                        Ok(Entry {
392                            key: key.to_owned(),
393                            entry_type,
394                            parents: parents?,
395                            $(
396                                $i: self.$i,
397                            )*
398                        })
399                    }
400                }
401
402                impl<'de> Visitor<'de> for MyVisitor {
403                    type Value = Library;
404
405                    fn expecting(&self, formatter: &mut std::fmt::Formatter)
406                        -> std::fmt::Result
407                    {
408                        formatter.write_str(
409                            "a map between cite keys and entries"
410                        )
411                    }
412
413                    fn visit_map<A>(self, mut map: A)
414                        -> Result<Self::Value, A::Error>
415                    where
416                        A: serde::de::MapAccess<'de>,
417                    {
418                        let mut entries = Vec::with_capacity(
419                            map.size_hint().unwrap_or(0).min(128)
420                        );
421                        while let Some(key) = map.next_key::<String>()? {
422                            if entries.iter().any(|(k, _)| k == &key) {
423                                return Err(serde::de::Error::custom(format!(
424                                    "duplicate key {}",
425                                    key
426                                )));
427                            }
428
429                            let entry: NakedEntry = map.next_value()?;
430                            entries.push((key, entry));
431                        }
432
433                        let entries: Result<IndexMap<_, _>, A::Error> =
434                            entries.into_iter().map(|(k, v)| {
435                                v.into_entry(&k, None).map(|e| (k, e))
436                            }).collect();
437
438                        Ok(Library(entries?))
439                    }
440                }
441
442                deserializer.deserialize_map(MyVisitor)
443            }
444        }
445    };
446
447    (@match
448        $s:literal => $i:ident,
449        $naked:ident, $map:ident $(,)?
450    ) => {
451        $naked.$i = Some($map.next_value()?)
452    };
453
454    // All items with a serde attribute are expected to be collections.
455    (@match
456        $(#[serde $serde:tt])+
457        $s:literal => $i:ident,
458        $naked:ident, $map:ident $(,)?
459    ) => {
460        let one_or_many: OneOrMany = $map.next_value()?;
461        $naked.$i = Some(one_or_many.into());
462    };
463
464    // Getter macro for deref types
465    (@get $(#[$docs:meta])+ $s:literal => $i:ident : $t:ty | $d:ty $(,)?) => {
466            $(#[$docs])+
467            pub fn $i(&self) -> Option<&$d> {
468                self.$i.as_deref()
469            }
470    };
471
472    // Getter macro for regular types.
473    (@get $(#[$docs:meta])+ $s:literal => $i:ident : $t:ty $(,)?) => {
474        $(#[$docs])+
475        pub fn $i(&self) -> Option<&$t> {
476            self.$i.as_ref()
477        }
478    };
479
480    // Setter for all types.
481    (@set $s:literal => $i:ident : $t:ty $(,)?) => {
482        paste! {
483            #[doc = "Set the `" $s "` field."]
484            pub fn [<set_ $i>](&mut self, $i: $t) {
485                self.$i = Some($i);
486            }
487        }
488    };
489}
490
491entry! {
492    /// Title of the item.
493    "title" => title: FormatString,
494    /// Persons primarily responsible for creating the item.
495    #[serde(serialize_with = "serialize_one_or_many_opt")]
496    #[serde(deserialize_with = "deserialize_one_or_many_opt")]
497    "author" => authors: Vec<Person> | [Person],
498    /// Date at which the item was published.
499    "date" => date: Date,
500    /// Persons responsible for selecting and revising the content of the item.
501    #[serde(serialize_with = "serialize_one_or_many_opt")]
502    #[serde(deserialize_with = "deserialize_one_or_many_opt")]
503    "editor" => editors: Vec<Person> | [Person],
504    /// Persons involved in the production of the item that are not authors or editors.
505    #[serde(serialize_with = "serialize_one_or_many_opt")]
506    #[serde(deserialize_with = "deserialize_one_or_many_opt")]
507    "affiliated" => affiliated: Vec<PersonsWithRoles> | [PersonsWithRoles],
508    /// Publisher of the item, which may have a name and a location.
509    "publisher" => publisher: Publisher,
510    /// Physical location at which an entry is physically located or took place.
511    "location" => location: FormatString,
512    /// Organization at/for which the item was created.
513    "organization" => organization: FormatString,
514    /// For an item whose parent has multiple issues, indicates the position in
515    /// the issue sequence. Also used to indicate the episode number for TV.
516    "issue" => issue: MaybeTyped<Numeric>,
517    /// The number of the chapter in the referenced work where this item can be found.
518    ///
519    /// When the chapter itself is the item being cited, which is common if it
520    /// has its own non-numeric title, prefer using `type: chapter` for the
521    /// entry while specifying the containing work's data as its parent.
522    "chapter" => chapter: MaybeTyped<Numeric>,
523    /// For an item whose parent has multiple volumes/parts/seasons ... of which
524    /// this item is one.
525    "volume" => volume: MaybeTyped<Numeric>,
526    /// Total number of volumes/parts/seasons ... this item consists of.
527    "volume-total" => volume_total: Numeric,
528    /// Published version of an item.
529    "edition" => edition: MaybeTyped<Numeric>,
530    /// The range of pages within the parent this item occupies
531    "page-range" => page_range: MaybeTyped<PageRanges>,
532    /// The total number of pages the item has.
533    "page-total" => page_total: Numeric,
534    /// The time range within the parent this item starts and ends at.
535    "time-range" => time_range: MaybeTyped<DurationRange>,
536    /// The total runtime of the item.
537    "runtime" => runtime: MaybeTyped<Duration>,
538    /// Canonical public URL of the item, can have access date.
539    "url" => url: QualifiedUrl,
540    /// Any serial number or version describing the item that is not appropriate
541    /// for the fields doi, edition, isbn or issn (may be assigned by the author
542    /// of the item; especially useful for preprint archives).
543    #[serde(alias = "serial")]
544    "serial-number" => serial_number: SerialNumber,
545    /// The language of the item.
546    "language" => language: LanguageIdentifier,
547    /// Name of the institution/collection where the item is kept.
548    "archive" => archive: FormatString,
549    /// Physical location of the institution/collection where the item is kept.
550    "archive-location" => archive_location: FormatString,
551    /// The call number of the item in the institution/collection.
552    "call-number" => call_number: FormatString,
553    /// Additional description to be appended in the bibliographic entry.
554    "note" => note: FormatString,
555    /// Abstract of the item (e.g. the abstract of a journal article).
556    "abstract" => abstract_: FormatString,
557    /// Type, class, or subtype of the item (e.g. “Doctoral dissertation” for
558    /// a PhD thesis; “NIH Publication” for an NIH technical report);
559    /// Do not use for topical descriptions or categories (e.g. “adventure” for an adventure movie).
560    "genre" => genre: FormatString,
561}
562
563impl Entry {
564    /// Get and parse the `affiliated` field and only return persons of a given
565    /// [role](PersonRole).
566    pub(crate) fn affiliated_with_role(&self, role: PersonRole) -> Vec<&Person> {
567        self.affiliated
568            .iter()
569            .flatten()
570            .filter_map(
571                |PersonsWithRoles { names, role: r }| {
572                    if r == &role { Some(names) } else { None }
573                },
574            )
575            .flatten()
576            .collect()
577    }
578
579    /// Get the unconverted value of a certain field from this entry or any of
580    /// its parents.
581    pub fn map<'a, F, T>(&'a self, mut f: F) -> Option<T>
582    where
583        F: FnMut(&'a Self) -> Option<T>,
584    {
585        if let Some(value) = f(self) { Some(value) } else { self.map_parents(f) }
586    }
587
588    /// Get the unconverted value of a certain field from the parents only by BFS.
589    pub fn map_parents<'a, F, T>(&'a self, mut f: F) -> Option<T>
590    where
591        F: FnMut(&'a Self) -> Option<T>,
592    {
593        let mut path: Vec<usize> = vec![0];
594        let up = |path: &mut Vec<usize>| {
595            path.pop();
596            if let Some(last) = path.last_mut() {
597                *last += 1;
598            }
599        };
600
601        'outer: loop {
602            // Index parents with the items in path. If, at any level, the index
603            // exceeds the number of parents, increment the index at the
604            // previous level. If no other level remains, return.
605            let first_path = path.first()?;
606
607            if self.parents.len() <= *first_path {
608                return None;
609            }
610
611            let mut item = &self.parents[*first_path];
612
613            for i in 1..path.len() {
614                if path[i] >= item.parents.len() {
615                    up(&mut path);
616                    continue 'outer;
617                }
618                item = &item.parents[path[i]];
619            }
620
621            if let Some(first_path) = path.first_mut() {
622                *first_path += 1;
623            }
624
625            if let Some(value) = f(item) {
626                return Some(value);
627            }
628        }
629    }
630
631    /// Apply a selector and return a bound parent entry or self.
632    pub fn bound_select(&self, selector: &Selector, binding: &str) -> Option<&Entry> {
633        selector.apply(self).and_then(|map| map.get(binding).copied())
634    }
635
636    /// Will recursively get a date off either the entry or any of its ancestors.
637    pub fn date_any(&self) -> Option<&Date> {
638        self.map(|e| e.date.as_ref())
639    }
640
641    /// Will recursively get an URL off either the entry or any of its ancestors.
642    pub fn url_any(&self) -> Option<&QualifiedUrl> {
643        self.map(|e| e.url.as_ref())
644    }
645
646    /// Retrieve a keyed serial number.
647    pub fn keyed_serial_number(&self, key: &str) -> Option<&str> {
648        self.serial_number
649            .as_ref()
650            .and_then(|s| s.0.get(key).map(|s| s.as_str()))
651    }
652
653    /// Set a keyed serial number.
654    pub fn set_keyed_serial_number(&mut self, key: &str, value: String) {
655        if let Some(serials) = &mut self.serial_number {
656            serials.0.insert(key.to_owned(), value);
657        } else {
658            let mut map = BTreeMap::new();
659            map.insert(key.to_owned(), value);
660            self.serial_number = Some(SerialNumber(map));
661        }
662    }
663
664    /// The Digital Object Identifier of the item.
665    pub fn doi(&self) -> Option<&str> {
666        self.keyed_serial_number("doi")
667    }
668
669    /// Set the `doi` field.
670    pub fn set_doi(&mut self, doi: String) {
671        self.set_keyed_serial_number("doi", doi);
672    }
673
674    /// International Standard Book Number (ISBN), prefer ISBN-13.
675    pub fn isbn(&self) -> Option<&str> {
676        self.keyed_serial_number("isbn")
677    }
678
679    /// Set the `isbn` field.
680    pub fn set_isbn(&mut self, isbn: String) {
681        self.set_keyed_serial_number("isbn", isbn);
682    }
683
684    /// International Standard Serial Number (ISSN).
685    pub fn issn(&self) -> Option<&str> {
686        self.keyed_serial_number("issn")
687    }
688
689    /// Set the `issn` field.
690    pub fn set_issn(&mut self, issn: String) {
691        self.set_keyed_serial_number("issn", issn);
692    }
693
694    /// PubMed Identifier (PMID).
695    pub fn pmid(&self) -> Option<&str> {
696        self.keyed_serial_number("pmid")
697    }
698
699    /// Set the `pmid` field.
700    pub fn set_pmid(&mut self, pmid: String) {
701        self.set_keyed_serial_number("pmid", pmid);
702    }
703
704    /// PubMed Central Identifier (PMCID).
705    pub fn pmcid(&self) -> Option<&str> {
706        self.keyed_serial_number("pmcid")
707    }
708
709    /// Set the `pmcid` field.
710    pub fn set_pmcid(&mut self, pmcid: String) {
711        self.set_keyed_serial_number("pmcid", pmcid);
712    }
713
714    /// ArXiv identifier.
715    pub fn arxiv(&self) -> Option<&str> {
716        self.keyed_serial_number("arxiv")
717    }
718
719    /// Set the `arxiv` field.
720    pub fn set_arxiv(&mut self, arxiv: String) {
721        self.set_keyed_serial_number("arxiv", arxiv);
722    }
723
724    /// Get the container of an entry like CSL defines it.
725    pub(crate) fn get_container(&self) -> Option<&Self> {
726        let retrieve_container = |possible: &[EntryType]| {
727            for possibility in possible {
728                if let Some(container) =
729                    self.parents.iter().find(|e| e.entry_type == *possibility)
730                {
731                    return Some(container);
732                }
733            }
734
735            None
736        };
737
738        match &self.entry_type {
739            EntryType::Article => retrieve_container(&[
740                EntryType::Book,
741                // Proceedings must come before Conference.
742                // Because @inproceedings will result in a Article entry with a Proceedings and a
743                // Conference parent. But only the Proceedings has the correct title.
744                EntryType::Proceedings,
745                EntryType::Conference,
746                EntryType::Periodical,
747                EntryType::Newspaper,
748                EntryType::Blog,
749                EntryType::Reference,
750                EntryType::Web,
751            ]),
752            EntryType::Anthos => retrieve_container(&[
753                EntryType::Book,
754                EntryType::Anthology,
755                EntryType::Reference,
756                EntryType::Report,
757            ]),
758            EntryType::Chapter => retrieve_container(&[
759                EntryType::Book,
760                EntryType::Anthology,
761                EntryType::Reference,
762                EntryType::Report,
763            ]),
764            EntryType::Report => {
765                retrieve_container(&[EntryType::Book, EntryType::Anthology])
766            }
767            EntryType::Web => retrieve_container(&[EntryType::Web]),
768            EntryType::Scene => retrieve_container(&[
769                EntryType::Audio,
770                EntryType::Video,
771                EntryType::Performance,
772                EntryType::Artwork,
773            ]),
774            EntryType::Case => retrieve_container(&[
775                EntryType::Book,
776                EntryType::Anthology,
777                EntryType::Reference,
778                EntryType::Report,
779            ]),
780            EntryType::Post => {
781                retrieve_container(&[EntryType::Thread, EntryType::Blog, EntryType::Web])
782            }
783            EntryType::Thread => {
784                retrieve_container(&[EntryType::Thread, EntryType::Web, EntryType::Blog])
785            }
786            _ => None,
787        }
788    }
789
790    /// Get the non-partial parent of the entry.
791    pub(crate) fn get_full(&self) -> &Self {
792        let mut parent = self.parents().first();
793        let mut entry = self;
794        while select!(Chapter | Scene).matches(entry) && entry.title().is_none() {
795            if let Some(p) = parent {
796                entry = p;
797                parent = entry.parents().first();
798            } else {
799                break;
800            }
801        }
802
803        entry
804    }
805
806    /// Get the collection of an entry like CSL defines it.
807    pub(crate) fn get_collection(&self) -> Option<&Self> {
808        match &self.entry_type {
809            EntryType::Anthology
810            | EntryType::Newspaper
811            | EntryType::Performance
812            | EntryType::Periodical
813            | EntryType::Proceedings
814            | EntryType::Book
815            | EntryType::Reference
816            | EntryType::Exhibition => self.parents.iter().find(|e| {
817                e.entry_type == self.entry_type || e.entry_type == EntryType::Anthology
818            }),
819            _ => self.parents.iter().find_map(|e| e.get_collection()),
820        }
821    }
822
823    /// Search a parent by DFS.
824    pub(crate) fn dfs_parent(&self, kind: EntryType) -> Option<&Self> {
825        if self.entry_type == kind {
826            return Some(self);
827        }
828
829        for parent in &self.parents {
830            if let Some(entry) = parent.dfs_parent(kind) {
831                return Some(entry);
832            }
833        }
834
835        None
836    }
837
838    /// Get the original entry.
839    pub(crate) fn get_original(&self) -> Option<&Self> {
840        self.dfs_parent(EntryType::Original)
841    }
842}
843
844#[cfg(feature = "biblatex")]
845impl Entry {
846    /// Adds a parent to the current entry. The parent
847    /// list will be created if there is none.
848    pub(crate) fn add_parent(&mut self, entry: Self) {
849        self.parents.push(entry);
850    }
851
852    /// Adds affiliated persons. The list will be created if there is none.
853    pub(crate) fn add_affiliated_persons(
854        &mut self,
855        new_persons: (Vec<Person>, PersonRole),
856    ) {
857        let obj = PersonsWithRoles { names: new_persons.0, role: new_persons.1 };
858        if let Some(affiliated) = &mut self.affiliated {
859            affiliated.push(obj);
860        } else {
861            self.affiliated = Some(vec![obj]);
862        }
863    }
864
865    pub(crate) fn parents_mut(&mut self) -> &mut [Self] {
866        &mut self.parents
867    }
868}
869
870#[cfg(test)]
871mod tests {
872    use std::fs;
873
874    use super::*;
875    use crate::io::from_yaml_str;
876
877    macro_rules! select_all {
878        ($select:expr, $entries:tt, [$($key:expr),* $(,)*] $(,)*) => {
879            let keys = [$($key,)*];
880            let selector = Selector::parse($select).unwrap();
881            for entry in $entries.iter() {
882                let res = selector.apply(entry);
883                if keys.contains(&entry.key.as_str()) {
884                    if res.is_none() {
885                        panic!("Key {} not found in results", entry.key);
886                    }
887                } else {
888                    if res.is_some() {
889                        panic!("Key {} found in results", entry.key);
890                    }
891                }
892            }
893        }
894    }
895
896    macro_rules! select {
897        ($select:expr, $entries:tt >> $entry_key:expr, [$($key:expr),* $(,)*] $(,)*) => {
898            let keys = vec![ $( $key , )* ];
899            let entry = $entries.iter().filter_map(|i| if i.key == $entry_key {Some(i)} else {None}).next().unwrap();
900            let selector = Selector::parse($select).unwrap();
901            let res = selector.apply(entry).unwrap();
902            if !keys.into_iter().all(|k| res.get(k).is_some()) {
903                panic!("Results do not contain binding");
904            }
905        }
906    }
907
908    #[test]
909    fn selectors() {
910        let contents = fs::read_to_string("tests/data/basic.yml").unwrap();
911        let entries = from_yaml_str(&contents).unwrap();
912
913        select_all!("article > proceedings", entries, ["zygos"]);
914        select_all!(
915            "article > (periodical | newspaper)",
916            entries,
917            ["omarova-libra", "kinetics", "house", "swedish",]
918        );
919        select_all!(
920            "(chapter | anthos) > (anthology | book)",
921            entries,
922            ["harry", "gedanken", "lamb-chapter", "snail-chapter"]
923        );
924        select_all!(
925            "*[url]",
926            entries,
927            [
928                "omarova-libra",
929                "science-e-issue",
930                "oiseau",
931                "georgia",
932                "really-habitable",
933                "electronic-music",
934                "mattermost",
935                "worth",
936                "wrong",
937                "un-hdr",
938                "audio-descriptions",
939                "camb",
940                "logician",
941                "dns-encryption",
942                "overleaf",
943                "editors",
944            ]
945        );
946        select_all!(
947            "!(*[url] | (* > *[url]))",
948            entries,
949            [
950                "zygos",
951                "harry",
952                "terminator-2",
953                "interior",
954                "wire",
955                "kinetics",
956                "house",
957                "plaque",
958                "renaissance",
959                "gedanken",
960                "donne",
961                "roe-wade",
962                "foia",
963                "drill",
964                "swedish",
965                "latex-users",
966                "barb",
967                "lamb",
968                "snail",
969                "lamb-chapter",
970                "snail-chapter",
971            ]
972        );
973        select_all!("*[abstract, note, genre]", entries, ["wire"]);
974    }
975
976    #[test]
977    fn selector_bindings() {
978        let contents = fs::read_to_string("tests/data/basic.yml").unwrap();
979        let entries = from_yaml_str(&contents).unwrap();
980
981        select!(
982            "a:article > (b:conference & c:(video|blog|web))",
983            entries >> "wwdc-network",
984            ["a", "b", "c"]
985        );
986    }
987
988    #[test]
989    #[cfg(feature = "biblatex")]
990    fn test_troublesome_page_ranges() {
991        use io::from_biblatex_str;
992
993        let bibtex = r#"
994            @article{b,
995                title={My page ranges},
996                pages={150--es}
997            }
998        "#;
999
1000        let library = from_biblatex_str(bibtex).unwrap();
1001
1002        for entry in library.iter() {
1003            assert!(entry.page_range.is_some())
1004        }
1005    }
1006}