Skip to main content

badness_parser/ast/
nodes.rs

1//! Typed [`AstNode`] wrappers over the generic CST.
2//!
3//! Accessors are positional and tolerate greedily attached groups. They expose
4//! syntax without assigning command meaning or consulting the signature database.
5
6use rowan::{NodeOrToken, TextRange, TextSize};
7
8use super::{AstNode, AstToken, child, child_token, children};
9use crate::ast::tokens::ControlWord;
10use crate::syntax::{SyntaxKind, SyntaxNode};
11
12/// Declares a newtype wrapper over a `SyntaxNode` of exactly one `SyntaxKind`,
13/// implementing [`AstNode`]. Only the *identity* (`can_cast`/`cast`/`syntax`) is
14/// generated; every accessor is hand-written in a separate `impl` block. This is
15/// ordinary in-tree Rust, not codegen — no build step, no generated artifacts.
16macro_rules! ast_node {
17    ($(#[$meta:meta])* $name:ident, $kind:ident) => {
18        $(#[$meta])*
19        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
20        pub struct $name {
21            syntax: SyntaxNode,
22        }
23
24        impl AstNode for $name {
25            fn can_cast(kind: SyntaxKind) -> bool {
26                kind == SyntaxKind::$kind
27            }
28
29            fn cast(syntax: SyntaxNode) -> Option<Self> {
30                Self::can_cast(syntax.kind()).then_some(Self { syntax })
31            }
32
33            fn syntax(&self) -> &SyntaxNode {
34                &self.syntax
35            }
36        }
37    };
38}
39
40ast_node!(
41    /// A control sequence with its greedily-attached argument groups.
42    Command, COMMAND
43);
44ast_node!(
45    /// A `{ … }` group — an argument, or a nested brace group.
46    Group, GROUP
47);
48ast_node!(
49    /// A `[ … ]` optional argument.
50    Optional, OPTIONAL
51);
52ast_node!(
53    /// The `{name}` group following `\begin` / `\end`.
54    NameGroup, NAME_GROUP
55);
56ast_node!(
57    /// A `\begin{name}` node.
58    Begin, BEGIN
59);
60ast_node!(
61    /// An `\end{name}` node.
62    End, END
63);
64ast_node!(
65    /// A `\begin{…} … \end{…}` environment.
66    Environment, ENVIRONMENT
67);
68ast_node!(
69    /// An `\if… … \else … \or … \fi` conditional the shape gate paired.
70    Conditional, CONDITIONAL
71);
72ast_node!(
73    /// One branch of a [`Conditional`]. The first holds the opener, its test, and
74    /// the then-body; every later one opens with its `\else`/`\or` divider.
75    ConditionalBranch, CONDITIONAL_BRANCH
76);
77
78impl Command {
79    /// The leading `CONTROL_WORD` token, or `None` for a control symbol. The
80    /// grammar bumps the control word as the command's first token.
81    pub fn control_word(&self) -> Option<ControlWord> {
82        self.syntax
83            .children_with_tokens()
84            .filter_map(NodeOrToken::into_token)
85            .find_map(ControlWord::cast)
86    }
87
88    /// The control-word name (leading `\` stripped), or `None` for a control
89    /// symbol.
90    pub fn name(&self) -> Option<String> {
91        self.control_word().map(|cw| cw.name())
92    }
93
94    /// The range of the leading `CONTROL_WORD` token (the `\foo` itself, backslash
95    /// included), or `None` for a control symbol. Callers use this to underline just
96    /// the control word rather than the whole node, which may carry greedily-attached
97    /// argument groups.
98    pub fn control_word_range(&self) -> Option<TextRange> {
99        self.control_word().map(|cw| cw.range())
100    }
101
102    /// The `n`-th `GROUP` argument, if present. Filters `GROUP` only, so `OPTIONAL`
103    /// arguments do *not* shift brace indexing (`\cmd[o]{a}` → `nth_group(0)` is
104    /// `{a}`).
105    pub fn nth_group(&self, n: usize) -> Option<Group> {
106        self.groups().nth(n)
107    }
108
109    /// The `GROUP` argument nodes, in source order.
110    pub fn groups(&self) -> impl Iterator<Item = Group> {
111        children::<Group>(&self.syntax)
112    }
113
114    /// The `OPTIONAL` argument nodes, in source order.
115    pub fn optionals(&self) -> impl Iterator<Item = Optional> {
116        children::<Optional>(&self.syntax)
117    }
118
119    /// The literal text inside the `n`-th `GROUP` argument, braces dropped. Returns
120    /// `None` when there is no `n`-th group or it holds non-token content (a nested
121    /// command — not a flat literal). See [`Group::inner_text`].
122    pub fn nth_group_text(&self, n: usize) -> Option<String> {
123        self.nth_group(n)?.inner_text()
124    }
125
126    /// The byte range of the content *inside* the `n`-th `GROUP` argument together
127    /// with that inner text — the location-aware counterpart to
128    /// [`Command::nth_group_text`]. See [`Group::inner`].
129    pub fn nth_group_inner(&self, n: usize) -> Option<(TextRange, String)> {
130        self.nth_group(n)?.inner()
131    }
132
133    /// The byte range of this command spanning its control word through the end of
134    /// its *first* `{…}` group — e.g. `\label{key}` up to the closing brace of
135    /// `{key}`. Deliberately not [`SyntaxNode::text_range`], which the greedy parser
136    /// may stretch over a *second* group it attached without knowing arity
137    /// (`\label{a}\n{…}`; decision #8). Falls back to the full command range when the
138    /// first group is absent.
139    pub fn first_group_range(&self) -> TextRange {
140        match self.nth_group(0) {
141            Some(group) => TextRange::new(
142                self.syntax.text_range().start(),
143                group.syntax.text_range().end(),
144            ),
145            None => self.syntax.text_range(),
146        }
147    }
148}
149
150impl Group {
151    /// The literal text inside this group, with the enclosing braces dropped.
152    /// Concatenates the inner token text so content split across `WORD`/`.`/`/`/…
153    /// tokens (e.g. `chapters/my_file`, `sec:intro`) reassembles. Returns `None` when
154    /// the group holds non-token content (a nested command — not a flat literal) or a
155    /// parameter token (`\ref{#1}`, `\eqref{##1}` — a macro-parameter template whose
156    /// literal value exists only at expansion time).
157    pub fn inner_text(&self) -> Option<String> {
158        let mut text = String::new();
159        for element in self.syntax.children_with_tokens() {
160            match element {
161                NodeOrToken::Token(token) => match token.kind() {
162                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
163                    SyntaxKind::HASH => return None,
164                    _ => text.push_str(token.text()),
165                },
166                // A nested node (e.g. a COMMAND) means the argument isn't a flat
167                // literal; treat the whole thing as unresolvable.
168                NodeOrToken::Node(_) => return None,
169            }
170        }
171        Some(text)
172    }
173
174    /// The byte range of the content *inside* this group (the span between the
175    /// braces) together with that inner text — the location-aware counterpart to
176    /// [`Group::inner_text`]. The inner range runs from the first inner token's start
177    /// to the last inner token's end; an empty group (`{}`) yields a zero-width range
178    /// just after the `{`. Returns `None` under the same conditions as
179    /// [`Group::inner_text`].
180    ///
181    /// The text/range correspondence is exact: in the success path the group holds
182    /// only flat tokens, so its inner bytes are contiguous and per-key sub-ranges can
183    /// be sliced off the range by byte offset (used by the semantic builder to give
184    /// each key in a `\cref{a,b}` its own precise span).
185    pub fn inner(&self) -> Option<(TextRange, String)> {
186        let mut text = String::new();
187        let mut start: Option<TextSize> = None;
188        let mut end: Option<TextSize> = None;
189        // Fallback anchor for an empty group: the byte just after the opening brace.
190        let mut after_l_brace = self.syntax.text_range().start();
191        for element in self.syntax.children_with_tokens() {
192            match element {
193                NodeOrToken::Token(token) => match token.kind() {
194                    SyntaxKind::L_BRACE => after_l_brace = token.text_range().end(),
195                    SyntaxKind::R_BRACE => {}
196                    SyntaxKind::HASH => return None,
197                    _ => {
198                        let range = token.text_range();
199                        start.get_or_insert(range.start());
200                        end = Some(range.end());
201                        text.push_str(token.text());
202                    }
203                },
204                // A nested node means the argument isn't a flat literal; treat the
205                // whole thing as unresolvable, like `inner_text`.
206                NodeOrToken::Node(_) => return None,
207            }
208        }
209        let range = match (start, end) {
210            (Some(start), Some(end)) => TextRange::new(start, end),
211            _ => TextRange::empty(after_l_brace),
212        };
213        Some((range, text))
214    }
215
216    /// The raw inner source of this group with its outer braces dropped, but *all*
217    /// interior text preserved — nested `{…}` braces included. Unlike
218    /// [`Group::inner_text`], which bails on nested nodes, this reconstructs the
219    /// verbatim content needed for an xparse argument spec like `{m O{0} m}` (whose
220    /// `{0}` default parses as a nested `GROUP`). Trivia is kept verbatim; the caller
221    /// tokenizes the result.
222    pub fn inner_source(&self) -> String {
223        inner_source_of(&self.syntax)
224    }
225
226    /// The single `COMMAND` child wrapped in this group, if any.
227    pub fn command(&self) -> Option<Command> {
228        child::<Command>(&self.syntax)
229    }
230
231    /// The control-word name (leading `\` stripped) of a single `COMMAND` wrapped in
232    /// this group, as in a `\newcommand{\foo}` name group. Returns `None` unless the
233    /// group's only relevant child is exactly one control word.
234    pub fn command_name(&self) -> Option<String> {
235        self.command()?.name()
236    }
237}
238
239/// The shared body of [`Group::inner_source`], kept kind-agnostic so the
240/// free-function shim can call it on any node — an xparse default like `O{0}` parses
241/// its `{0}` as a nested group but a top-level default body may be an `OPTIONAL`
242/// rather than a `GROUP`. Concatenates all descendant token text, then drops a single
243/// leading `{` and trailing `}` if present (a bracket-delimited `OPTIONAL` keeps its
244/// brackets, matching the pre-wrapper behavior).
245pub(crate) fn inner_source_of(node: &SyntaxNode) -> String {
246    let mut text = String::new();
247    for element in node.descendants_with_tokens() {
248        if let NodeOrToken::Token(token) = element {
249            text.push_str(token.text());
250        }
251    }
252    let inner = text.strip_prefix('{').unwrap_or(&text);
253    inner.strip_suffix('}').unwrap_or(inner).to_string()
254}
255
256impl NameGroup {
257    /// The environment name — the literal text of this `NAME_GROUP`, braces dropped.
258    /// Returns `None` when it holds non-token content.
259    pub fn text(&self) -> Option<String> {
260        let mut text = String::new();
261        for element in self.syntax.children_with_tokens() {
262            match element {
263                NodeOrToken::Token(token) => match token.kind() {
264                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
265                    _ => text.push_str(token.text()),
266                },
267                NodeOrToken::Node(_) => return None,
268            }
269        }
270        Some(text)
271    }
272
273    /// The byte range of the name *inside* this `NAME_GROUP` (the span between the
274    /// braces) — the location-aware counterpart to [`NameGroup::text`]. Returns
275    /// `None` when it holds a nested node or the name is empty (`\begin{}`, nothing to
276    /// highlight).
277    pub fn range(&self) -> Option<TextRange> {
278        let mut start: Option<TextSize> = None;
279        let mut end: Option<TextSize> = None;
280        for element in self.syntax.children_with_tokens() {
281            match element {
282                NodeOrToken::Token(token) => match token.kind() {
283                    SyntaxKind::L_BRACE | SyntaxKind::R_BRACE => {}
284                    _ => {
285                        let range = token.text_range();
286                        start.get_or_insert(range.start());
287                        end = Some(range.end());
288                    }
289                },
290                NodeOrToken::Node(_) => return None,
291            }
292        }
293        Some(TextRange::new(start?, end?))
294    }
295}
296
297/// The environment an alias-delimiter node names: its bare `CONTROL_WORD` with the
298/// leading `\` stripped, when that word is not `keyword` (the spelled-out
299/// `\begin`/`\end`, whose name lives in a `NAME_GROUP` instead).
300fn alias_delimiter_name(node: &SyntaxNode, keyword: &str) -> Option<String> {
301    let head = child_token::<ControlWord>(node)?;
302    let text = head.syntax().text();
303    (text != keyword)
304        .then(|| text.strip_prefix('\\'))
305        .flatten()
306        .filter(|name| !name.is_empty())
307        .map(str::to_owned)
308}
309
310impl Begin {
311    /// The `{name}` group following `\begin`.
312    pub fn name_group(&self) -> Option<NameGroup> {
313        child::<NameGroup>(&self.syntax)
314    }
315
316    /// The environment name (braces dropped), or `None` for a malformed `\begin`.
317    ///
318    /// A `BEGIN` opened by an *environment alias* (`\bea`, issue #109) carries no
319    /// `NAME_GROUP` at all — the whole node is the bare control word — so the name
320    /// falls back to that word with its `\` stripped. Positional and meaning-free,
321    /// per decision #10: it reads the name from wherever the tree puts it and looks
322    /// nothing up. `Signatures::environment` is what maps `bea` on to the target's
323    /// curated behavior.
324    ///
325    /// The fallback is guarded on the head *not* being `\begin`, so the malformed
326    /// `\begin`-without-a-name path (which builds a `BEGIN` with no `NAME_GROUP`)
327    /// keeps reporting `None` rather than suddenly claiming to be named `begin`.
328    pub fn name(&self) -> Option<String> {
329        match self.name_group() {
330            Some(group) => group.text(),
331            None => alias_delimiter_name(&self.syntax, "\\begin"),
332        }
333    }
334
335    /// Whether this `BEGIN` is an *environment-alias* delimiter — a bare control
336    /// word standing in for `\begin{X}` — rather than a spelled-out `\begin{X}`.
337    ///
338    /// Purely structural (no `NAME_GROUP`, head is not `\begin`), like every other
339    /// accessor here. It exists because [`name`](Self::name) makes the two shapes
340    /// indistinguishable by name, and the alias table describes the *command*, not
341    /// the name: a literal `\begin{bea}` written in a file that also defines `\bea`
342    /// as an alias is a different, unrelated environment and must not inherit the
343    /// target's behavior. `Signatures::environment_at` is the consumer.
344    pub fn is_alias(&self) -> bool {
345        self.name_group().is_none() && alias_delimiter_name(&self.syntax, "\\begin").is_some()
346    }
347
348    /// The byte range of the environment name inside the `NAME_GROUP`.
349    pub fn name_range(&self) -> Option<TextRange> {
350        self.name_group()?.range()
351    }
352}
353
354impl End {
355    /// The `{name}` group following `\end`.
356    pub fn name_group(&self) -> Option<NameGroup> {
357        child::<NameGroup>(&self.syntax)
358    }
359
360    /// The environment name (braces dropped), or `None` for a malformed `\end`.
361    pub fn name(&self) -> Option<String> {
362        self.name_group()?.text()
363    }
364
365    /// The byte range of the environment name inside the `NAME_GROUP`.
366    pub fn name_range(&self) -> Option<TextRange> {
367        self.name_group()?.range()
368    }
369}
370
371impl Environment {
372    /// The `\begin{…}` node, replacing the raw `children().find(==BEGIN)` idiom.
373    pub fn begin(&self) -> Option<Begin> {
374        child::<Begin>(&self.syntax)
375    }
376
377    /// The `\end{…}` node.
378    pub fn end(&self) -> Option<End> {
379        child::<End>(&self.syntax)
380    }
381
382    /// The environment name, read from the `\begin` node.
383    pub fn name(&self) -> Option<String> {
384        self.begin()?.name()
385    }
386}
387
388impl Conditional {
389    /// The branches, in source order — at least one, since the grammar opens a
390    /// branch before the opener.
391    pub fn branches(&self) -> impl Iterator<Item = ConditionalBranch> {
392        children::<ConditionalBranch>(&self.syntax)
393    }
394
395    /// The closing `\fi`, read *positionally* as the last child node rather than
396    /// by matching the name: which control word closes a conditional is the
397    /// grammar's call, and re-deciding it here would be the same meaning check
398    /// twice (decision #10). `None` only if the gate's guarantee is ever broken,
399    /// which callers must tolerate rather than assume away.
400    pub fn closer(&self) -> Option<Command> {
401        self.syntax.last_child().and_then(Command::cast)
402    }
403}
404
405impl ConditionalBranch {
406    /// The leading `\if…`/`\else`/`\or` control word of this branch, if it opens
407    /// with one. Positional: the first child node, cast to a `COMMAND`.
408    pub fn head(&self) -> Option<Command> {
409        self.syntax.first_child().and_then(Command::cast)
410    }
411}