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}