Skip to main content

badness_parser/bib/ast/
nodes.rs

1//! Typed [`AstNode`](super::AstNode) wrappers over the BibTeX CST, with syntactic
2//! accessors. The bib analog of [`crate::ast::nodes`]. Purely syntactic: they know
3//! nothing about what a field or entry *means* (AGENTS.md decisions #2, #10).
4
5use rowan::{NodeOrToken, TextRange};
6
7use super::{AstNode, child, children};
8use crate::bib::syntax::{SyntaxKind, SyntaxNode};
9
10/// Declares a newtype wrapper over a `SyntaxNode` of exactly one bib `SyntaxKind`,
11/// implementing [`AstNode`]. Only the identity is generated; accessors are
12/// hand-written. The bib analog of `crate::ast::nodes::ast_node!`.
13macro_rules! ast_node {
14    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
15        $(#[$meta])*
16        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
17        pub struct $name {
18            syntax: SyntaxNode,
19        }
20
21        impl AstNode for $name {
22            fn can_cast(kind: SyntaxKind) -> bool {
23                kind == SyntaxKind::$kind
24            }
25
26            fn cast(syntax: SyntaxNode) -> Option<Self> {
27                Self::can_cast(syntax.kind()).then_some(Self { syntax })
28            }
29
30            fn syntax(&self) -> &SyntaxNode {
31                &self.syntax
32            }
33        }
34    };
35}
36
37ast_node!(
38    /// A regular bibliographic entry: `@type{ key, fields }`.
39    Entry, ENTRY
40);
41ast_node!(
42    /// An `@string{ name = value }` macro definition.
43    StringEntry, STRING_ENTRY
44);
45ast_node!(
46    /// A `name = value` field.
47    Field, FIELD
48);
49ast_node!(
50    /// The right-hand side of a field's `=`; pieces separated by `#`.
51    Value, VALUE
52);
53ast_node!(
54    /// The type word following `@` (`article`, `string`, …).
55    EntryType, ENTRY_TYPE
56);
57ast_node!(
58    /// A cite key.
59    Key, KEY
60);
61ast_node!(
62    /// A field / macro name left of `=`.
63    FieldName, FIELD_NAME
64);
65
66/// Concatenated text of a node's direct `WORD`/`NUMBER` token children. Reassembles
67/// a name or key the lexer kept as one word run anyway (`westfahl:space` lexes as a
68/// single `WORD`, since `:`/`-`/`.` are word characters), and tolerates the rare run
69/// split across `WORD`+`NUMBER`. Structural punctuation and trivia are skipped. Kept
70/// free so both the typed accessors and the free-function shims share it.
71pub(crate) fn joined_words(node: &SyntaxNode) -> String {
72    let mut text = String::new();
73    for element in node.children_with_tokens() {
74        if let NodeOrToken::Token(token) = element
75            && matches!(token.kind(), SyntaxKind::WORD | SyntaxKind::NUMBER)
76        {
77            text.push_str(token.text());
78        }
79    }
80    text
81}
82
83/// [`joined_words`], but `None` for an empty run — the shape every name/key accessor
84/// wants.
85fn nonempty_words(node: &SyntaxNode) -> Option<String> {
86    let text = joined_words(node);
87    (!text.is_empty()).then_some(text)
88}
89
90impl EntryType {
91    /// The type word, or `None` when empty. Case is preserved; callers normalize.
92    pub fn text(&self) -> Option<String> {
93        nonempty_words(&self.syntax)
94    }
95}
96
97impl Key {
98    /// The cite-key text, or `None` when empty.
99    pub fn text(&self) -> Option<String> {
100        nonempty_words(&self.syntax)
101    }
102}
103
104impl FieldName {
105    /// The field / macro name, or `None` when empty.
106    pub fn text(&self) -> Option<String> {
107        nonempty_words(&self.syntax)
108    }
109}
110
111impl Entry {
112    /// The entry type — the word following `@` (`article`, …), or `None` for a
113    /// malformed entry with no `ENTRY_TYPE` child.
114    pub fn entry_type(&self) -> Option<String> {
115        child::<EntryType>(&self.syntax)?.text()
116    }
117
118    /// The cite key and the byte range of its `KEY` node, or `None` when the entry has
119    /// no key (a recovery case) or the key is empty.
120    pub fn cite_key(&self) -> Option<(String, TextRange)> {
121        let key = child::<Key>(&self.syntax)?;
122        key.text().map(|text| (text, key.syntax.text_range()))
123    }
124
125    /// The `FIELD` children, in source order.
126    pub fn fields(&self) -> impl Iterator<Item = Field> {
127        children::<Field>(&self.syntax)
128    }
129}
130
131impl StringEntry {
132    /// The macro name defined by `@string{ name = value }` and the byte range of its
133    /// `FIELD_NAME` node, or `None` for a malformed `@string` with no `name = …` field.
134    pub fn def_name(&self) -> Option<(String, TextRange)> {
135        let name = child::<Field>(&self.syntax)?.name_node()?;
136        name.text().map(|text| (text, name.syntax.text_range()))
137    }
138}
139
140impl Field {
141    /// The field's `FIELD_NAME` node, if present.
142    pub fn name_node(&self) -> Option<FieldName> {
143        child::<FieldName>(&self.syntax)
144    }
145
146    /// The field name (the text of its `FIELD_NAME`), or `None` if absent.
147    pub fn name(&self) -> Option<String> {
148        self.name_node()?.text()
149    }
150
151    /// The `VALUE` node (the right-hand side of `=`), or `None` if absent.
152    pub fn value(&self) -> Option<Value> {
153        child::<Value>(&self.syntax)
154    }
155}
156
157impl Value {
158    /// The bare-macro *uses* inside this value: each `LITERAL` piece whose single
159    /// token is a `WORD` (an unquoted, unbraced name) is an `@string` reference. A
160    /// `LITERAL` wrapping a `NUMBER` is a literal number, not a macro use, and is
161    /// skipped. Yields `(name, range)` with the range of the `LITERAL` piece.
162    pub fn macro_uses(&self) -> impl Iterator<Item = (String, TextRange)> {
163        macro_uses_of(&self.syntax)
164    }
165}
166
167/// The shared body of [`Value::macro_uses`], kept kind-agnostic so the free-function
168/// shim can call it on any node.
169pub(crate) fn macro_uses_of(node: &SyntaxNode) -> impl Iterator<Item = (String, TextRange)> {
170    node.children()
171        .filter(|n| n.kind() == SyntaxKind::LITERAL)
172        .filter_map(|literal| {
173            let token = literal
174                .children_with_tokens()
175                .filter_map(|e| e.into_token())
176                .find(|t| matches!(t.kind(), SyntaxKind::WORD | SyntaxKind::NUMBER))?;
177            (token.kind() == SyntaxKind::WORD)
178                .then(|| (token.text().to_string(), literal.text_range()))
179        })
180}