Skip to main content

badness_parser/bib/
ast.rs

1//! Typed, read-only wrappers over the BibTeX CST.
2//!
3//! Accessors expose syntax without assigning meaning to fields or entries.
4//!
5//! Free functions provide the same operations for untyped [`SyntaxNode`] callers.
6
7pub mod nodes;
8
9pub use nodes::{Entry, EntryType, Field, FieldName, Key, StringEntry, Value};
10
11use rowan::TextRange;
12
13use crate::bib::syntax::{SyntaxKind, SyntaxNode};
14
15/// A typed wrapper over BibTeX CST nodes of a particular [`SyntaxKind`].
16pub trait AstNode {
17    fn can_cast(kind: SyntaxKind) -> bool
18    where
19        Self: Sized;
20    fn cast(syntax: SyntaxNode) -> Option<Self>
21    where
22        Self: Sized;
23    fn syntax(&self) -> &SyntaxNode;
24}
25
26/// The first child node castable to `N`.
27pub fn child<N: AstNode>(parent: &SyntaxNode) -> Option<N> {
28    parent.children().find_map(N::cast)
29}
30
31/// All child nodes castable to `N`, in source order.
32pub fn children<N: AstNode>(parent: &SyntaxNode) -> impl Iterator<Item = N> {
33    parent.children().filter_map(N::cast)
34}
35
36// --- Free-function shims (kind-agnostic; see module docs) ---------------------
37
38/// The entry type of an `ENTRY` / `STRING_ENTRY` / … node — the word following `@`
39/// (e.g. `"article"`, `"string"`). `None` for a malformed entry with no `ENTRY_TYPE`
40/// child. Case is preserved; callers normalize.
41pub fn entry_type(entry: &SyntaxNode) -> Option<String> {
42    child::<EntryType>(entry).and_then(|t| t.text())
43}
44
45/// The cite key of a regular `ENTRY` and the byte range of its `KEY` node. `None` when
46/// the entry has no key (a recovery case) or the key is empty.
47pub fn cite_key(entry: &SyntaxNode) -> Option<(String, TextRange)> {
48    let key = child::<Key>(entry)?;
49    key.text().map(|text| (text, key.syntax().text_range()))
50}
51
52/// The macro name defined by a `STRING_ENTRY` (`@string{ name = value }`) and the
53/// byte range of its `FIELD_NAME` node. `None` for a malformed `@string` with no
54/// `name = …` field.
55pub fn string_def_name(string_entry: &SyntaxNode) -> Option<(String, TextRange)> {
56    let name = child::<Field>(string_entry)?.name_node()?;
57    name.text().map(|text| (text, name.syntax().text_range()))
58}
59
60/// The `FIELD` children of an entry, in source order.
61pub fn fields(entry: &SyntaxNode) -> impl Iterator<Item = SyntaxNode> {
62    children::<Field>(entry).map(|f| f.syntax().clone())
63}
64
65/// The name of a `FIELD` (the text of its `FIELD_NAME`), or `None` if absent.
66pub fn field_name(field: &SyntaxNode) -> Option<String> {
67    child::<FieldName>(field).and_then(|n| n.text())
68}
69
70/// The `VALUE` node of a `FIELD` (the right-hand side of `=`), or `None` if absent.
71pub fn field_value(field: &SyntaxNode) -> Option<SyntaxNode> {
72    child::<Value>(field).map(|v| v.syntax().clone())
73}
74
75/// A field `VALUE` as plain display text: the node text, trimmed, with one layer of
76/// surrounding `{…}`/`"…"` removed and interior whitespace collapsed. Shared by the
77/// LSP hover/completion cards and the semantic model's cached title/author facts.
78pub fn value_text_cleaned(value: &SyntaxNode) -> String {
79    let raw = value.text().to_string();
80    let trimmed = raw.trim();
81    let inner = trimmed
82        .strip_prefix('{')
83        .and_then(|s| s.strip_suffix('}'))
84        .or_else(|| trimmed.strip_prefix('"').and_then(|s| s.strip_suffix('"')))
85        .unwrap_or(trimmed);
86    inner.split_whitespace().collect::<Vec<_>>().join(" ")
87}
88
89/// The bare-macro *uses* inside a `VALUE`: each `LITERAL` piece whose single token is
90/// a `WORD` (an unquoted, unbraced name) is an `@string` reference. A `LITERAL`
91/// wrapping a `NUMBER` is a literal number, not a macro use, and is skipped. Yields
92/// `(name, range)` with the range of the `LITERAL` piece.
93pub fn value_macro_uses(value: &SyntaxNode) -> impl Iterator<Item = (String, TextRange)> {
94    nodes::macro_uses_of(value)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::bib::parse;
101
102    fn node(src: &str, kind: SyntaxKind) -> SyntaxNode {
103        parse(src)
104            .syntax()
105            .descendants()
106            .find(|n| n.kind() == kind)
107            .unwrap_or_else(|| panic!("a {kind:?} node"))
108    }
109
110    #[test]
111    fn entry_type_reads_word() {
112        let entry = node("@article{k, title = {Hi}}\n", SyntaxKind::ENTRY);
113        assert_eq!(entry_type(&entry).as_deref(), Some("article"));
114    }
115
116    #[test]
117    fn cite_key_reassembles_colon_key() {
118        let entry = node("@book{westfahl:space, title = {X}}\n", SyntaxKind::ENTRY);
119        let (key, _range) = cite_key(&entry).expect("a key");
120        assert_eq!(key, "westfahl:space");
121    }
122
123    #[test]
124    fn cite_key_none_without_key() {
125        let entry = node("@misc{", SyntaxKind::ENTRY);
126        assert_eq!(cite_key(&entry), None);
127    }
128
129    #[test]
130    fn string_def_name_reads_field_name() {
131        let s = node("@string{jan = \"January\"}\n", SyntaxKind::STRING_ENTRY);
132        let (name, _range) = string_def_name(&s).expect("a name");
133        assert_eq!(name, "jan");
134    }
135
136    #[test]
137    fn fields_and_names() {
138        let entry = node("@misc{k, a = {x}, b = 3}\n", SyntaxKind::ENTRY);
139        let names: Vec<_> = fields(&entry).filter_map(|f| field_name(&f)).collect();
140        assert_eq!(names, vec!["a", "b"]);
141    }
142
143    #[test]
144    fn value_macro_uses_finds_word_not_number() {
145        let field = node("@misc{k, t = pub # {x} # 2020}\n", SyntaxKind::FIELD);
146        let value = field_value(&field).expect("a value");
147        let uses: Vec<_> = value_macro_uses(&value).map(|(n, _)| n).collect();
148        assert_eq!(uses, vec!["pub"]);
149    }
150
151    #[test]
152    fn cast_is_kind_exact() {
153        let entry = node("@article{k, title = {Hi}}\n", SyntaxKind::ENTRY);
154        assert!(Entry::cast(entry.clone()).is_some());
155        assert!(Field::cast(entry).is_none());
156    }
157
158    #[test]
159    fn entry_wrapper_reads_type_key_and_fields() {
160        let entry = Entry::cast(node("@article{k, a = {x}, b = 3}\n", SyntaxKind::ENTRY)).unwrap();
161        assert_eq!(entry.entry_type().as_deref(), Some("article"));
162        assert_eq!(entry.cite_key().map(|(k, _)| k).as_deref(), Some("k"));
163        let names: Vec<_> = entry.fields().filter_map(|f| f.name()).collect();
164        assert_eq!(names, vec!["a", "b"]);
165    }
166}