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 // The `{0}` default must not be read as another argument.
230 assert_eq!(
231 kinds("O{0} m"),
232 vec![(false, ArgKind::Bracket), (true, ArgKind::Brace)]
233 );
234 }
235
236 #[test]
237 fn star_and_token_yield_no_slot() {
238 assert_eq!(kinds("s m"), vec![(true, ArgKind::Brace)]);
239 // `t` consumes its test token (`*`), leaving just the `m`.
240 assert_eq!(kinds("t* m"), vec![(true, ArgKind::Brace)]);
241 }
242
243 #[test]
244 fn verbatim_yields_no_slot() {
245 assert_eq!(kinds("v"), vec![]);
246 }
247
248 #[test]
249 fn bracket_delimited_maps_to_bracket() {
250 // `d[]` and `r[]` are `[…]`-delimited, so they yield a bracket slot.
251 assert_eq!(kinds("d[]"), vec![(false, ArgKind::Bracket)]);
252 assert_eq!(kinds("r[]"), vec![(true, ArgKind::Bracket)]);
253 }
254
255 #[test]
256 fn paren_delimited_yields_no_slot() {
257 // `()`-delimited args produce no CST node, so no slot — but the two
258 // delimiter tokens are still consumed, so a trailing `m` is found.
259 assert_eq!(kinds("d() m"), vec![(true, ArgKind::Brace)]);
260 assert_eq!(kinds("r<> m"), vec![(true, ArgKind::Brace)]);
261 }
262
263 #[test]
264 fn required_delimited_with_default_consumes_group() {
265 // `R(){default}`: two delimiter tokens then a default group, then `m`.
266 assert_eq!(kinds("R(){x} m"), vec![(true, ArgKind::Brace)]);
267 // `D[]{default}`: bracket-delimited optional with a default → one bracket
268 // slot, then `m`.
269 assert_eq!(
270 kinds("D[]{x} m"),
271 vec![(false, ArgKind::Bracket), (true, ArgKind::Brace)]
272 );
273 }
274
275 #[test]
276 fn embellishments_consume_their_groups() {
277 // `e{^_}` consumes one group; `E{^_}{\d\d}` consumes two. Neither yields a
278 // slot, so only the `m` remains.
279 assert_eq!(kinds("e{^_} m"), vec![(true, ArgKind::Brace)]);
280 assert_eq!(kinds("E{^_}{00} m"), vec![(true, ArgKind::Brace)]);
281 }
282
283 #[test]
284 fn modifiers_and_processors_skipped() {
285 assert_eq!(kinds("+m"), vec![(true, ArgKind::Brace)]);
286 assert_eq!(kinds("!o"), vec![(false, ArgKind::Bracket)]);
287 assert_eq!(kinds(">{\\TrimSpaces} m"), vec![(true, ArgKind::Brace)]);
288 }
289
290 #[test]
291 fn empty_and_whitespace_specs() {
292 assert_eq!(kinds(""), vec![]);
293 assert_eq!(kinds(" "), vec![]);
294 assert_eq!(
295 kinds(" m o "),
296 vec![(true, ArgKind::Brace), (false, ArgKind::Bracket)]
297 );
298 }
299
300 #[test]
301 fn unknown_letter_stops_scan() {
302 // A garbage letter halts parsing; the `m` before it is kept, the rest dropped.
303 assert_eq!(kinds("m z m"), vec![(true, ArgKind::Brace)]);
304 }
305
306 #[test]
307 fn control_sequence_delimiters_consumed() {
308 // `d\langle\rangle`: control-word delimiter tokens, non-bracket → no slot,
309 // but both are consumed so the `m` is reached.
310 assert_eq!(kinds("d\\langle\\rangle m"), vec![(true, ArgKind::Brace)]);
311 }
312}