Skip to main content

badness_parser/ast/
nodes.rs

1//! Typed [`AstNode`] wrappers over CST *nodes*, with positional/structural
2//! accessors. These are a read-only typed view over the generic, greedy CST
3//! (AGENTS.md decision #8): a `\section` and a `\newcommand` share the `COMMAND`
4//! shape, so accessors are *positional* ([`Command::nth_group`]) and tolerate
5//! greedily over-attached groups by construction. They expose structure only, never
6//! command *meaning* (decision #2) — no signature-DB lookup lives here.
7
8use rowan::{NodeOrToken, TextRange, TextSize};
9
10use super::{AstNode, AstToken, child, children};
11use crate::ast::tokens::ControlWord;
12use crate::syntax::{SyntaxKind, SyntaxNode};
13
14/// Declares a newtype wrapper over a `SyntaxNode` of exactly one `SyntaxKind`,
15/// implementing [`AstNode`]. Only the *identity* (`can_cast`/`cast`/`syntax`) is
16/// generated; every accessor is hand-written in a separate `impl` block. This is
17/// ordinary in-tree Rust, not codegen — no build step, no generated artifacts.
18macro_rules! ast_node {
19    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
20        $(#[$meta])*
21        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
22        pub struct $name {
23            syntax: SyntaxNode,
24        }
25
26        impl AstNode for $name {
27            fn can_cast(kind: SyntaxKind) -> bool {
28                kind == SyntaxKind::$kind
29            }
30
31            fn cast(syntax: SyntaxNode) -> Option<Self> {
32                Self::can_cast(syntax.kind()).then_some(Self { syntax })
33            }
34
35            fn syntax(&self) -> &SyntaxNode {
36                &self.syntax
37            }
38        }
39    };
40}
41
42ast_node!(
43    /// A control sequence with its greedily-attached argument groups.
44    Command, COMMAND
45);
46ast_node!(
47    /// A `{ … }` group — an argument, or a nested brace group.
48    Group, GROUP
49);
50ast_node!(
51    /// A `[ … ]` optional argument.
52    Optional, OPTIONAL
53);
54ast_node!(
55    /// The `{name}` group following `\begin` / `\end`.
56    NameGroup, NAME_GROUP
57);
58ast_node!(
59    /// A `\begin{name}` node.
60    Begin, BEGIN
61);
62ast_node!(
63    /// An `\end{name}` node.
64    End, END
65);
66ast_node!(
67    /// A `\begin{…} … \end{…}` environment.
68    Environment, ENVIRONMENT
69);
70
71impl Command {
72    /// The leading `CONTROL_WORD` token, or `None` for a control symbol. The
73    /// grammar bumps the control word as the command's first token.
74    pub fn control_word(&self) -> Option<ControlWord> {
75        self.syntax
76            .children_with_tokens()
77            .filter_map(NodeOrToken::into_token)
78            .find_map(ControlWord::cast)
79    }
80
81    /// The control-word name (leading `\` stripped), or `None` for a control
82    /// symbol.
83    pub fn name(&self) -> Option<String> {
84        self.control_word().map(|cw| cw.name())
85    }
86
87    /// The range of the leading `CONTROL_WORD` token (the `\foo` itself, backslash
88    /// included), or `None` for a control symbol. Callers use this to underline just
89    /// the control word rather than the whole node, which may carry greedily-attached
90    /// argument groups.
91    pub fn control_word_range(&self) -> Option<TextRange> {
92        self.control_word().map(|cw| cw.range())
93    }
94
95    /// The `n`-th `GROUP` argument, if present. Filters `GROUP` only, so `OPTIONAL`
96    /// arguments do *not* shift brace indexing (`\cmd[o]{a}` → `nth_group(0)` is
97    /// `{a}`).
98    pub fn nth_group(&self, n: usize) -> Option<Group> {
99        self.groups().nth(n)
100    }
101
102    /// The `GROUP` argument nodes, in source order.
103    pub fn groups(&self) -> impl Iterator<Item = Group> {
104        children::<Group>(&self.syntax)
105    }
106
107    /// The `OPTIONAL` argument nodes, in source order.
108    pub fn optionals(&self) -> impl Iterator<Item = Optional> {
109        children::<Optional>(&self.syntax)
110    }
111
112    /// The literal text inside the `n`-th `GROUP` argument, braces dropped. Returns
113    /// `None` when there is no `n`-th group or it holds non-token content (a nested
114    /// command — not a flat literal). See [`Group::inner_text`].
115    pub fn nth_group_text(&self, n: usize) -> Option<String> {
116        self.nth_group(n)?.inner_text()
117    }
118
119    /// The byte range of the content *inside* the `n`-th `GROUP` argument together
120    /// with that inner text — the location-aware counterpart to
121    /// [`Command::nth_group_text`]. See [`Group::inner`].
122    pub fn nth_group_inner(&self, n: usize) -> Option<(TextRange, String)> {
123        self.nth_group(n)?.inner()
124    }
125
126    /// The byte range of this command spanning its control word through the end of
127    /// its *first* `{…}` group — e.g. `\label{key}` up to the closing brace of
128    /// `{key}`. Deliberately not [`SyntaxNode::text_range`], which the greedy parser
129    /// may stretch over a *second* group it attached without knowing arity
130    /// (`\label{a}\n{…}`; decision #8). Falls back to the full command range when the
131    /// first group is absent.
132    pub fn first_group_range(&self) -> TextRange {
133        match self.nth_group(0) {
134            Some(group) => TextRange::new(
135                self.syntax.text_range().start(),
136                group.syntax.text_range().end(),
137            ),
138            None => self.syntax.text_range(),
139        }
140    }
141}
142
143impl Group {
144    /// The literal text inside this group, with the enclosing braces dropped.
145    /// Concatenates the inner token text so content split across `WORD`/`.`/`/`/…
146    /// tokens (e.g. `chapters/my_file`, `sec:intro`) reassembles. Returns `None` when
147    /// the group holds non-token content (a nested command — not a flat literal).
148    pub fn inner_text(&self) -> Option<String> {
149        let mut text = String::new();
150        for element in self.syntax.children_with_tokens() {
151            match element {
152                NodeOrToken::Token(token) => match token.kind() {
153                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
154                    _ => text.push_str(token.text()),
155                },
156                // A nested node (e.g. a COMMAND) means the argument isn't a flat
157                // literal; treat the whole thing as unresolvable.
158                NodeOrToken::Node(_) => return None,
159            }
160        }
161        Some(text)
162    }
163
164    /// The byte range of the content *inside* this group (the span between the
165    /// braces) together with that inner text — the location-aware counterpart to
166    /// [`Group::inner_text`]. The inner range runs from the first inner token's start
167    /// to the last inner token's end; an empty group (`{}`) yields a zero-width range
168    /// just after the `{`. Returns `None` under the same conditions as
169    /// [`Group::inner_text`].
170    ///
171    /// The text/range correspondence is exact: in the success path the group holds
172    /// only flat tokens, so its inner bytes are contiguous and per-key sub-ranges can
173    /// be sliced off the range by byte offset (used by the semantic builder to give
174    /// each key in a `\cref{a,b}` its own precise span).
175    pub fn inner(&self) -> Option<(TextRange, String)> {
176        let mut text = String::new();
177        let mut start: Option<TextSize> = None;
178        let mut end: Option<TextSize> = None;
179        // Fallback anchor for an empty group: the byte just after the opening brace.
180        let mut after_l_brace = self.syntax.text_range().start();
181        for element in self.syntax.children_with_tokens() {
182            match element {
183                NodeOrToken::Token(token) => match token.kind() {
184                    SyntaxKind::L_BRACE => after_l_brace = token.text_range().end(),
185                    SyntaxKind::R_BRACE => {}
186                    _ => {
187                        let range = token.text_range();
188                        start.get_or_insert(range.start());
189                        end = Some(range.end());
190                        text.push_str(token.text());
191                    }
192                },
193                // A nested node means the argument isn't a flat literal; treat the
194                // whole thing as unresolvable, like `inner_text`.
195                NodeOrToken::Node(_) => return None,
196            }
197        }
198        let range = match (start, end) {
199            (Some(start), Some(end)) => TextRange::new(start, end),
200            _ => TextRange::empty(after_l_brace),
201        };
202        Some((range, text))
203    }
204
205    /// The raw inner source of this group with its outer braces dropped, but *all*
206    /// interior text preserved — nested `{…}` braces included. Unlike
207    /// [`Group::inner_text`], which bails on nested nodes, this reconstructs the
208    /// verbatim content needed for an xparse argument spec like `{m O{0} m}` (whose
209    /// `{0}` default parses as a nested `GROUP`). Trivia is kept verbatim; the caller
210    /// tokenizes the result.
211    pub fn inner_source(&self) -> String {
212        inner_source_of(&self.syntax)
213    }
214
215    /// The single `COMMAND` child wrapped in this group, if any.
216    pub fn command(&self) -> Option<Command> {
217        child::<Command>(&self.syntax)
218    }
219
220    /// The control-word name (leading `\` stripped) of a single `COMMAND` wrapped in
221    /// this group, as in a `\newcommand{\foo}` name group. Returns `None` unless the
222    /// group's only relevant child is exactly one control word.
223    pub fn command_name(&self) -> Option<String> {
224        self.command()?.name()
225    }
226}
227
228/// The shared body of [`Group::inner_source`], kept kind-agnostic so the
229/// free-function shim can call it on any node — an xparse default like `O{0}` parses
230/// its `{0}` as a nested group but a top-level default body may be an `OPTIONAL`
231/// rather than a `GROUP`. Concatenates all descendant token text, then drops a single
232/// leading `{` and trailing `}` if present (a bracket-delimited `OPTIONAL` keeps its
233/// brackets, matching the pre-wrapper behavior).
234pub(crate) fn inner_source_of(node: &SyntaxNode) -> String {
235    let mut text = String::new();
236    for element in node.descendants_with_tokens() {
237        if let NodeOrToken::Token(token) = element {
238            text.push_str(token.text());
239        }
240    }
241    let inner = text.strip_prefix('{').unwrap_or(&text);
242    inner.strip_suffix('}').unwrap_or(inner).to_string()
243}
244
245impl NameGroup {
246    /// The environment name — the literal text of this `NAME_GROUP`, braces dropped.
247    /// Returns `None` when it holds non-token content.
248    pub fn text(&self) -> Option<String> {
249        let mut text = String::new();
250        for element in self.syntax.children_with_tokens() {
251            match element {
252                NodeOrToken::Token(token) => match token.kind() {
253                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
254                    _ => text.push_str(token.text()),
255                },
256                NodeOrToken::Node(_) => return None,
257            }
258        }
259        Some(text)
260    }
261
262    /// The byte range of the name *inside* this `NAME_GROUP` (the span between the
263    /// braces) — the location-aware counterpart to [`NameGroup::text`]. Returns
264    /// `None` when it holds a nested node or the name is empty (`\begin{}`, nothing to
265    /// highlight).
266    pub fn range(&self) -> Option<TextRange> {
267        let mut start: Option<TextSize> = None;
268        let mut end: Option<TextSize> = None;
269        for element in self.syntax.children_with_tokens() {
270            match element {
271                NodeOrToken::Token(token) => match token.kind() {
272                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
273                    _ => {
274                        let range = token.text_range();
275                        start.get_or_insert(range.start());
276                        end = Some(range.end());
277                    }
278                },
279                NodeOrToken::Node(_) => return None,
280            }
281        }
282        Some(TextRange::new(start?, end?))
283    }
284}
285
286impl Begin {
287    /// The `{name}` group following `\begin`.
288    pub fn name_group(&self) -> Option<NameGroup> {
289        child::<NameGroup>(&self.syntax)
290    }
291
292    /// The environment name (braces dropped), or `None` for a malformed `\begin`.
293    pub fn name(&self) -> Option<String> {
294        self.name_group()?.text()
295    }
296
297    /// The byte range of the environment name inside the `NAME_GROUP`.
298    pub fn name_range(&self) -> Option<TextRange> {
299        self.name_group()?.range()
300    }
301}
302
303impl End {
304    /// The `{name}` group following `\end`.
305    pub fn name_group(&self) -> Option<NameGroup> {
306        child::<NameGroup>(&self.syntax)
307    }
308
309    /// The environment name (braces dropped), or `None` for a malformed `\end`.
310    pub fn name(&self) -> Option<String> {
311        self.name_group()?.text()
312    }
313
314    /// The byte range of the environment name inside the `NAME_GROUP`.
315    pub fn name_range(&self) -> Option<TextRange> {
316        self.name_group()?.range()
317    }
318}
319
320impl Environment {
321    /// The `\begin{…}` node, replacing the raw `children().find(==BEGIN)` idiom.
322    pub fn begin(&self) -> Option<Begin> {
323        child::<Begin>(&self.syntax)
324    }
325
326    /// The `\end{…}` node.
327    pub fn end(&self) -> Option<End> {
328        child::<End>(&self.syntax)
329    }
330
331    /// The environment name, read from the `\begin` node.
332    pub fn name(&self) -> Option<String> {
333        self.begin()?.name()
334    }
335}