Skip to main content

badness_parser/semantic/
xparse.rs

1//! Parser for the **xparse argument specification** mini-language — the string in
2//! the second group of `\NewDocumentCommand{\foo}{<spec>}{…}` (and the environment
3//! variants). It describes argument shapes; processing behavior is ignored.
4//!
5//! The full grammar is tokenized so the cursor never desyncs on a type's trailing
6//! material (delimiter tokens, `{default}` groups, embellishment sets). But our
7//! [`ArgSpec`] model only distinguishes a `{…}` [`ArgKind::Brace`] from a `[…]`
8//! [`ArgKind::Bracket`] slot, because that is all the CST produces and all a
9//! consumer (the formatter's arity glue) can act on. So an [`ArgSpec`] is emitted
10//! **only** for argument types that yield an actual `{…}`/`[…]` node:
11//!
12//! - `m` → required brace; `o`, `O{default}` → optional bracket.
13//! - `r⟨t1⟩⟨t2⟩` / `R⟨…⟩{default}` (required delimited) and `d⟨t1⟩⟨t2⟩` /
14//!   `D⟨…⟩{default}` (optional delimited) → an [`ArgSpec`] **only** when the
15//!   delimiters are literally `[`/`]` (bracket) or `{`/`}` (brace); other delimiters
16//!   (`(`/`)`, `<`/`>`, …) produce no CST node, so no slot.
17//! - `s` (star), `t⟨token⟩` (optional token), `v` (verbatim), and `e`/`E`
18//!   (embellishments) produce no `{…}`/`[…]` node, so no slot — but their trailing
19//!   material is still consumed.
20//!
21//! This keeps the emitted slot count equal to the `GROUP`/`OPTIONAL` nodes the
22//! greedy parser actually attaches, which is what the formatter counts. Modifiers
23//! (`+`, `!`) and argument processors (`>{…}`) are skipped. Unknown type letters
24//! stop the scan (conservative: never panic, never invent slots).
25
26use super::signature::{ArgKind, ArgSpec, ContentKind};
27
28/// Parse an xparse argument-spec string into the `{…}`/`[…]` argument slots it
29/// declares, in order. See the module docs for the type-by-type mapping.
30pub fn parse_spec(spec: &str) -> Vec<ArgSpec> {
31    let chars: Vec<char> = spec.chars().collect();
32    let mut cursor = Cursor {
33        chars: &chars,
34        i: 0,
35    };
36    let mut args = Vec::new();
37
38    loop {
39        cursor.skip_modifiers();
40        cursor.skip_ws();
41        let Some(c) = cursor.bump() else { break };
42        match c {
43            'm' => args.push(brace(true)),
44            'o' => args.push(bracket(false)),
45            'O' => {
46                cursor.skip_group();
47                args.push(bracket(false));
48            }
49            // Required (`r`/`R`) and optional (`d`/`D`) delimited args: a slot only
50            // when the delimiters are the bracket/brace pair the CST models.
51            'r' | 'R' | 'd' | 'D' => {
52                let required = matches!(c, 'r' | 'R');
53                let open = cursor.read_token();
54                let close = cursor.read_token();
55                if matches!(c, 'R' | 'D') {
56                    cursor.skip_group(); // the {default}
57                }
58                if let Some(kind) = delimiter_kind(open.as_deref(), close.as_deref()) {
59                    args.push(ArgSpec {
60                        required,
61                        kind,
62                        content: ContentKind::Opaque,
63                        domain: crate::semantic::ArgumentDomain::Unknown,
64                        verbatim: false,
65                    });
66                }
67            }
68            't' => {
69                cursor.read_token(); // the test token; yields no node
70            }
71            'e' => {
72                cursor.skip_group(); // {<tokens>}
73            }
74            'E' => {
75                cursor.skip_group(); // {<tokens>}
76                cursor.skip_group(); // {<defaults>}
77            }
78            // `s` (star), `v` (verbatim): consumed, no `{…}`/`[…]` node.
79            's' | 'v' => {}
80            // Unknown letter: stop rather than guess and miscount.
81            _ => break,
82        }
83    }
84
85    args
86}
87
88fn brace(required: bool) -> ArgSpec {
89    ArgSpec {
90        required,
91        kind: ArgKind::Brace,
92        content: ContentKind::Opaque,
93        domain: crate::semantic::ArgumentDomain::Unknown,
94        verbatim: false,
95    }
96}
97
98fn bracket(required: bool) -> ArgSpec {
99    ArgSpec {
100        required,
101        kind: ArgKind::Bracket,
102        content: ContentKind::Opaque,
103        domain: crate::semantic::ArgumentDomain::Unknown,
104        verbatim: false,
105    }
106}
107
108/// The `ArgKind` for a delimited arg whose delimiters are `open`/`close`, or `None`
109/// when the pair is not one the CST produces a node for.
110fn delimiter_kind(open: Option<&str>, close: Option<&str>) -> Option<ArgKind> {
111    match (open, close) {
112        (Some("["), Some("]")) => Some(ArgKind::Bracket),
113        (Some("{"), Some("}")) => Some(ArgKind::Brace),
114        _ => None,
115    }
116}
117
118/// A char cursor over the spec string with the small consumption primitives the
119/// xparse types need.
120struct Cursor<'a> {
121    chars: &'a [char],
122    i: usize,
123}
124
125impl Cursor<'_> {
126    fn peek(&self) -> Option<char> {
127        self.chars.get(self.i).copied()
128    }
129
130    fn bump(&mut self) -> Option<char> {
131        let c = self.peek()?;
132        self.i += 1;
133        Some(c)
134    }
135
136    fn skip_ws(&mut self) {
137        while self.peek().is_some_and(char::is_whitespace) {
138            self.i += 1;
139        }
140    }
141
142    /// Skip the type-prefix modifiers that may precede any argument type: `+`
143    /// (long), `!` (no-leading-space), and `>{processor}` argument processors.
144    fn skip_modifiers(&mut self) {
145        loop {
146            self.skip_ws();
147            match self.peek() {
148                Some('+') | Some('!') => self.i += 1,
149                Some('>') => {
150                    self.i += 1;
151                    self.skip_group();
152                }
153                _ => break,
154            }
155        }
156    }
157
158    /// Read a single spec token after optional whitespace: a control sequence
159    /// (`\` + a letter run, or `\` + one symbol) or a single character. Used for the
160    /// delimiter tokens of `r`/`R`/`d`/`D` and the test token of `t`.
161    fn read_token(&mut self) -> Option<String> {
162        self.skip_ws();
163        let first = self.bump()?;
164        if first != '\\' {
165            return Some(first.to_string());
166        }
167        let mut token = String::from('\\');
168        match self.peek() {
169            Some(c) if c.is_ascii_alphabetic() => {
170                while self.peek().is_some_and(|c| c.is_ascii_alphabetic()) {
171                    token.push(self.bump().expect("peeked"));
172                }
173            }
174            Some(_) => token.push(self.bump().expect("peeked")),
175            None => {}
176        }
177        Some(token)
178    }
179
180    /// If the next non-whitespace char opens a `{…}` group, skip the whole balanced
181    /// group (nested braces included). A no-op otherwise — tolerant of a malformed
182    /// spec missing the group a type would normally carry.
183    fn skip_group(&mut self) {
184        self.skip_ws();
185        if self.peek() != Some('{') {
186            return;
187        }
188        let mut depth = 0;
189        while let Some(c) = self.bump() {
190            match c {
191                '{' => depth += 1,
192                '}' => {
193                    depth -= 1;
194                    if depth == 0 {
195                        return;
196                    }
197                }
198                _ => {}
199            }
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    fn kinds(spec: &str) -> Vec<(bool, ArgKind)> {
209        parse_spec(spec)
210            .into_iter()
211            .map(|a| (a.required, a.kind))
212            .collect()
213    }
214
215    #[test]
216    fn mandatory_and_optional_basics() {
217        assert_eq!(
218            kinds("m o m"),
219            vec![
220                (true, ArgKind::Brace),
221                (false, ArgKind::Bracket),
222                (true, ArgKind::Brace),
223            ]
224        );
225    }
226
227    #[test]
228    fn optional_with_default_consumes_group() {
229        assert_eq!(
230            kinds("O{0} m"),
231            vec![(false, ArgKind::Bracket), (true, ArgKind::Brace)]
232        );
233    }
234
235    #[test]
236    fn star_and_token_yield_no_slot() {
237        assert_eq!(kinds("s m"), vec![(true, ArgKind::Brace)]);
238        assert_eq!(kinds("t* m"), vec![(true, ArgKind::Brace)]);
239    }
240
241    #[test]
242    fn verbatim_yields_no_slot() {
243        assert_eq!(kinds("v"), vec![]);
244    }
245
246    #[test]
247    fn bracket_delimited_maps_to_bracket() {
248        assert_eq!(kinds("d[]"), vec![(false, ArgKind::Bracket)]);
249        assert_eq!(kinds("r[]"), vec![(true, ArgKind::Bracket)]);
250    }
251
252    #[test]
253    fn paren_delimited_yields_no_slot() {
254        assert_eq!(kinds("d() m"), vec![(true, ArgKind::Brace)]);
255        assert_eq!(kinds("r<> m"), vec![(true, ArgKind::Brace)]);
256    }
257
258    #[test]
259    fn required_delimited_with_default_consumes_group() {
260        assert_eq!(kinds("R(){x} m"), vec![(true, ArgKind::Brace)]);
261        assert_eq!(
262            kinds("D[]{x} m"),
263            vec![(false, ArgKind::Bracket), (true, ArgKind::Brace)]
264        );
265    }
266
267    #[test]
268    fn embellishments_consume_their_groups() {
269        assert_eq!(kinds("e{^_} m"), vec![(true, ArgKind::Brace)]);
270        assert_eq!(kinds("E{^_}{00} m"), vec![(true, ArgKind::Brace)]);
271    }
272
273    #[test]
274    fn modifiers_and_processors_skipped() {
275        assert_eq!(kinds("+m"), vec![(true, ArgKind::Brace)]);
276        assert_eq!(kinds("!o"), vec![(false, ArgKind::Bracket)]);
277        assert_eq!(kinds(">{\\TrimSpaces} m"), vec![(true, ArgKind::Brace)]);
278    }
279
280    #[test]
281    fn empty_and_whitespace_specs() {
282        assert_eq!(kinds(""), vec![]);
283        assert_eq!(kinds("   "), vec![]);
284        assert_eq!(
285            kinds("  m   o  "),
286            vec![(true, ArgKind::Brace), (false, ArgKind::Bracket)]
287        );
288    }
289
290    #[test]
291    fn unknown_letter_stops_scan() {
292        assert_eq!(kinds("m z m"), vec![(true, ArgKind::Brace)]);
293    }
294
295    #[test]
296    fn control_sequence_delimiters_consumed() {
297        assert_eq!(kinds("d\\langle\\rangle m"), vec![(true, ArgKind::Brace)]);
298    }
299}