Skip to main content

mos_bib/
record.rs

1//! The parsed bibliography data model: [`Bibliography`], [`BibEntry`], and
2//! the document-body [`Citation`] reference.
3
4use std::collections::BTreeMap;
5use std::ops::Range;
6
7/// A parsed bibliography: every [`BibEntry`] keyed by its citation key.
8///
9/// Entries live in a [`BTreeMap`], so iterating them yields a deterministic,
10/// sorted-by-citation-key order that is easy to assert on in tests. Build
11/// one from BibTeX source with [`parse_bibtex`](crate::parse_bibtex), which
12/// rejects a duplicate citation key with
13/// [`BibParseErrorKind::DuplicateKey`](crate::BibParseErrorKind::DuplicateKey)
14/// at the duplicate key's offset. Last-value-wins applies only to a repeated
15/// field name inside one entry.
16///
17/// # Examples
18///
19/// ```
20/// use mos_bib::Bibliography;
21///
22/// let empty = Bibliography::default();
23/// assert!(empty.entries.is_empty());
24/// ```
25#[derive(Clone, Debug, Default, PartialEq, Eq)]
26pub struct Bibliography {
27    /// Parsed entries keyed by citation key, in sorted key order.
28    pub entries: BTreeMap<String, BibEntry>,
29}
30
31/// A single parsed BibTeX entry: one `@type{...}` record.
32///
33/// The entry type and field names are normalized to lowercase, because
34/// BibTeX treats them case-insensitively; the citation [`key`](Self::key) is
35/// preserved verbatim, because keys *are* case-sensitive. Fields live in a
36/// [`BTreeMap`], so [`fields`](Self::fields) iterates in sorted, stable
37/// order. Values are stored as raw text exactly as written, outer `{}` or
38/// `""` delimiters included; bare values have none. A field name repeated
39/// within one entry keeps its last value. No `TeX` decoding or name parsing.
40///
41/// # Examples
42///
43/// ```
44/// use mos_bib::parse_bibtex;
45///
46/// # fn main() -> Result<(), mos_bib::BibParseError> {
47/// let bib = parse_bibtex("@article{knuth1984, title = {Literate Programming}}")?;
48/// let entry = &bib.entries["knuth1984"];
49/// assert_eq!(entry.entry_type, "article");
50/// assert_eq!(entry.key, "knuth1984");
51/// assert_eq!(entry.fields["title"], "{Literate Programming}");
52/// # Ok(())
53/// # }
54/// ```
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct BibEntry {
57    /// The entry type without the leading `@`, lowercased (e.g. `article`).
58    pub entry_type: String,
59    /// The citation key, preserved verbatim (e.g. `knuth1984`).
60    pub key: String,
61    /// Byte range of [`key`](Self::key) inside the parsed BibTeX source.
62    pub key_span: Range<usize>,
63    /// Field name (lowercased) to raw value text including its outer
64    /// delimiters, in sorted name order.
65    pub fields: BTreeMap<String, String>,
66}
67
68impl BibEntry {
69    /// A field's value with its outer `{}` / `""` delimiters removed; see
70    /// [`unwrap_value`]. Inner text is still raw (no `TeX` decoding).
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use mos_bib::parse_bibtex;
76    ///
77    /// # fn main() -> Result<(), mos_bib::BibParseError> {
78    /// let bib = parse_bibtex(r#"@book{k, title = {The {TeX}book}, year = "1984"}"#)?;
79    /// let entry = &bib.entries["k"];
80    /// assert_eq!(entry.field_text("title"), Some("The {TeX}book"));
81    /// assert_eq!(entry.field_text("year"), Some("1984"));
82    /// assert_eq!(entry.field_text("pages"), None);
83    /// # Ok(())
84    /// # }
85    /// ```
86    #[must_use]
87    pub fn field_text(&self, name: &str) -> Option<&str> {
88        self.fields.get(name).map(|raw| unwrap_value(raw))
89    }
90}
91
92/// Strip one matching outer `{...}` or `"..."` pair from a raw field value.
93///
94/// The pair must enclose the whole value: a `{` balances against the final
95/// `}` only, and a `"` is not closed by an interior unescaped quote outside a
96/// braced group, mirroring [`parse_bibtex`](crate::parse_bibtex). Bare values
97/// and anything else are returned unchanged.
98///
99/// # Examples
100///
101/// ```
102/// use mos_bib::unwrap_value;
103///
104/// assert_eq!(unwrap_value("{Literate Programming}"), "Literate Programming");
105/// assert_eq!(unwrap_value(r#""Quoted""#), "Quoted");
106/// assert_eq!(unwrap_value("1984"), "1984");
107/// assert_eq!(unwrap_value("{a} {b}"), "{a} {b}");
108/// ```
109#[must_use]
110pub fn unwrap_value(raw: &str) -> &str {
111    let bytes = raw.as_bytes();
112    let enclosed = match (bytes.first(), bytes.last()) {
113        (Some(b'{'), Some(b'}')) => brace_pair_encloses(bytes),
114        (Some(b'"'), Some(b'"')) => quote_pair_encloses(bytes),
115        _ => false,
116    };
117    if enclosed {
118        &raw[1..raw.len() - 1]
119    } else {
120        raw
121    }
122}
123
124fn brace_pair_encloses(bytes: &[u8]) -> bool {
125    let mut depth = 0_usize;
126    for (i, &b) in bytes.iter().enumerate() {
127        if depth == 0 && i > 0 {
128            return false;
129        }
130        match b {
131            b'{' => depth += 1,
132            b'}' => match depth.checked_sub(1) {
133                Some(next) => depth = next,
134                None => return false,
135            },
136            _ => {}
137        }
138    }
139    depth == 0 && bytes.len() >= 2
140}
141
142fn quote_pair_encloses(bytes: &[u8]) -> bool {
143    let mut depth = 0_usize;
144    let mut i = 1;
145    while i < bytes.len() {
146        match bytes[i] {
147            b'\\' => {
148                i += 2;
149                continue;
150            }
151            b'{' => depth += 1,
152            b'}' if depth > 0 => depth -= 1,
153            b'"' if depth == 0 => return i == bytes.len() - 1,
154            _ => {}
155        }
156        i += 1;
157    }
158    false
159}
160
161/// A citation reference within the document body: a single key that
162/// resolves into a [`Bibliography`] entry at render time.
163///
164/// # Examples
165///
166/// ```
167/// use mos_bib::Citation;
168///
169/// let citation = Citation {
170///     key: "knuth1984".to_owned(),
171/// };
172///
173/// assert_eq!(citation.key, "knuth1984");
174/// ```
175#[derive(Clone, Debug, PartialEq, Eq)]
176pub struct Citation {
177    /// The citation key as written in `[@key]`, matched verbatim against
178    /// [`BibEntry::key`].
179    pub key: String,
180}