Skip to main content

ratex_parser/
environments.rs

1use std::collections::HashMap;
2
3use ratex_lexer::token::Token;
4
5use crate::error::{ParseError, ParseResult};
6use crate::macro_expander::MacroDefinition;
7use crate::parse_node::{
8    AlignSpec, AlignType, ArrayTag, Measurement, Mode, ParseNode, ProofBranch, ProofLineStyle,
9    StyleStr,
10};
11use crate::parser::Parser;
12use crate::stack_safety::parse_nodes_logical_depth;
13
14// ── Environment registry ─────────────────────────────────────────────────
15
16pub struct EnvContext<'a, 'b> {
17    pub mode: Mode,
18    pub env_name: String,
19    pub parser: &'a mut Parser<'b>,
20}
21
22pub type EnvHandler = fn(
23    ctx: &mut EnvContext,
24    args: Vec<ParseNode>,
25    opt_args: Vec<Option<ParseNode>>,
26) -> ParseResult<ParseNode>;
27
28pub struct EnvSpec {
29    pub num_args: usize,
30    pub num_optional_args: usize,
31    pub handler: EnvHandler,
32}
33
34pub static ENVIRONMENTS: std::sync::LazyLock<HashMap<&'static str, EnvSpec>> =
35    std::sync::LazyLock::new(|| {
36        let mut map = HashMap::new();
37        register_array(&mut map);
38        register_matrix(&mut map);
39        register_cases(&mut map);
40        register_align(&mut map);
41        register_gathered(&mut map);
42        register_equation(&mut map);
43        register_smallmatrix(&mut map);
44        register_alignat(&mut map);
45        register_subarray(&mut map);
46        register_cd(&mut map);
47        register_prooftree(&mut map);
48        map
49    });
50
51// ── ArrayConfig ──────────────────────────────────────────────────────────
52
53#[derive(Default)]
54pub struct ArrayConfig {
55    pub hskip_before_and_after: Option<bool>,
56    pub add_jot: Option<bool>,
57    pub cols: Option<Vec<AlignSpec>>,
58    pub arraystretch: Option<f64>,
59    pub col_separation_type: Option<String>,
60    pub single_row: bool,
61    pub empty_single_row: bool,
62    pub max_num_cols: Option<usize>,
63    pub leqno: Option<bool>,
64    pub auto_number: bool,
65}
66
67// ── parseArray ───────────────────────────────────────────────────────────
68
69/// Pull a trailing `\\tag{…}` or `\\nonumber`/`\\notag` off the last cell of a row.
70/// Returns `Auto(true)` when the row is eligible for auto-numbering.
71/// The `auto_number` parameter controls the default when no marker is found.
72fn extract_trailing_tag_from_last_cell(
73    row: &mut [ParseNode],
74    auto_number: bool,
75) -> ParseResult<ArrayTag> {
76    let default_tag = if auto_number {
77        ArrayTag::Auto(true)
78    } else {
79        ArrayTag::Auto(false)
80    };
81    let Some(last) = row.last_mut() else {
82        return Ok(default_tag);
83    };
84
85    let inner: &mut ParseNode = match last {
86        ParseNode::Styling { body, .. } => {
87            if body.len() != 1 {
88                return Ok(default_tag);
89            }
90            &mut body[0]
91        }
92        _ => last,
93    };
94
95    let obody = match inner {
96        ParseNode::OrdGroup { body, .. } => body,
97        _ => return Ok(default_tag),
98    };
99
100    // Look for \\tag
101    let tag_indices: Vec<usize> = obody
102        .iter()
103        .enumerate()
104        .filter(|(_, n)| matches!(n, ParseNode::Tag { .. }))
105        .map(|(i, _)| i)
106        .collect();
107
108    // Look for \\nonumber / \\notag
109    let nonumber_indices: Vec<usize> = obody
110        .iter()
111        .enumerate()
112        .filter(|(_, n)| matches!(n, ParseNode::NoNumber { .. }))
113        .map(|(i, _)| i)
114        .collect();
115
116    // Can't have both \\tag and \\nonumber in the same row
117    if !tag_indices.is_empty() && !nonumber_indices.is_empty() {
118        return Err(ParseError::msg(
119            "Cannot use both \\tag and \\nonumber in the same row",
120        ));
121    }
122
123    // Handle \\tag
124    if !tag_indices.is_empty() {
125        if tag_indices.len() > 1 {
126            return Err(ParseError::msg("Multiple \\tag in a row"));
127        }
128        let idx = tag_indices[0];
129        if idx != obody.len() - 1 {
130            return Err(ParseError::msg(
131                "\\tag must appear at the end of the row after the equation body",
132            ));
133        }
134        match obody.pop() {
135            Some(ParseNode::Tag { tag, .. }) => {
136                if tag.is_empty() {
137                    Ok(ArrayTag::Auto(false))
138                } else {
139                    Ok(ArrayTag::Explicit(tag))
140                }
141            }
142            _ => Ok(default_tag),
143        }
144    } else if !nonumber_indices.is_empty() {
145        // Handle \\nonumber / \\notag
146        if nonumber_indices.len() > 1 {
147            return Err(ParseError::msg("Multiple \\nonumber in a row"));
148        }
149        let idx = nonumber_indices[0];
150        if idx != obody.len() - 1 {
151            return Err(ParseError::msg(
152                "\\nonumber must appear at the end of the row",
153            ));
154        }
155        obody.pop(); // discard the NoNumber node
156        Ok(ArrayTag::Auto(false))
157    } else {
158        // Neither \\tag nor \\nonumber
159        Ok(default_tag)
160    }
161}
162
163fn get_hlines(parser: &mut Parser) -> ParseResult<Vec<bool>> {
164    let mut hline_info = Vec::new();
165    parser.consume_spaces()?;
166
167    let mut nxt = parser.fetch()?.text.clone();
168    if nxt == "\\relax" {
169        parser.consume();
170        parser.consume_spaces()?;
171        nxt = parser.fetch()?.text.clone();
172    }
173    while nxt == "\\hline" || nxt == "\\hdashline" {
174        parser.consume();
175        hline_info.push(nxt == "\\hdashline");
176        parser.consume_spaces()?;
177        nxt = parser.fetch()?.text.clone();
178    }
179    Ok(hline_info)
180}
181
182fn d_cell_style(env_name: &str) -> Option<StyleStr> {
183    if env_name.starts_with('d') {
184        Some(StyleStr::Display)
185    } else {
186        Some(StyleStr::Text)
187    }
188}
189
190pub fn parse_array(
191    parser: &mut Parser,
192    config: ArrayConfig,
193    style: Option<StyleStr>,
194) -> ParseResult<ParseNode> {
195    parser.gullet.begin_group();
196
197    if !config.single_row {
198        parser.gullet.set_text_macro("\\cr", "\\\\\\relax");
199    }
200
201    let arraystretch = config.arraystretch.unwrap_or_else(|| {
202        // Check if \arraystretch is defined as a macro (e.g., via \def\arraystretch{1.5})
203        if let Some(def) = parser.gullet.get_macro("\\arraystretch") {
204            let s = match def {
205                MacroDefinition::Text(s) => s.clone(),
206                MacroDefinition::Tokens { tokens, .. } => {
207                    // Tokens are stored in reverse order (stack convention for expansion)
208                    tokens
209                        .iter()
210                        .rev()
211                        .map(|t| t.text.as_str())
212                        .collect::<String>()
213                }
214                MacroDefinition::Function(_) => String::new(),
215            };
216            s.parse::<f64>().unwrap_or(1.0)
217        } else {
218            1.0
219        }
220    });
221
222    parser.gullet.begin_group();
223
224    let mut row: Vec<ParseNode> = Vec::new();
225    let mut body: Vec<Vec<ParseNode>> = Vec::new();
226    let mut row_tags: Vec<ArrayTag> = Vec::new();
227    let mut row_gaps: Vec<Option<Measurement>> = Vec::new();
228    let mut hlines_before_row: Vec<Vec<bool>> = Vec::new();
229
230    hlines_before_row.push(get_hlines(parser)?);
231
232    loop {
233        let break_token = if config.single_row { "\\end" } else { "\\\\" };
234        let cell_body = parser.parse_expression(false, Some(break_token))?;
235        parser.gullet.end_group();
236        parser.gullet.begin_group();
237
238        let mut cell = ParseNode::OrdGroup {
239            mode: parser.mode,
240            body: cell_body,
241            semisimple: None,
242            loc: None,
243        };
244
245        if let Some(s) = style {
246            cell = ParseNode::Styling {
247                mode: parser.mode,
248                style: s,
249                body: vec![cell],
250                loc: None,
251            };
252        }
253
254        row.push(cell.clone());
255        let next = parser.fetch()?.text.clone();
256
257        if next == "&" {
258            if let Some(max) = config.max_num_cols {
259                if row.len() >= max {
260                    return Err(ParseError::msg("Too many tab characters: &"));
261                }
262            }
263            parser.consume();
264        } else if next == "\\end" {
265            // Check for trailing empty row and remove it
266            let is_empty_trailing = if let Some(s) = style {
267                if s == StyleStr::Text || s == StyleStr::Display {
268                    if let ParseNode::Styling { body: ref sb, .. } = cell {
269                        if let Some(ParseNode::OrdGroup { body: ref ob, .. }) = sb.first() {
270                            ob.is_empty()
271                        } else {
272                            false
273                        }
274                    } else {
275                        false
276                    }
277                } else {
278                    false
279                }
280            } else if let ParseNode::OrdGroup { body: ref ob, .. } = cell {
281                ob.is_empty()
282            } else {
283                false
284            };
285
286            let row_tag = extract_trailing_tag_from_last_cell(&mut row, config.auto_number)?;
287            row_tags.push(row_tag);
288            body.push(row);
289
290            if is_empty_trailing && (body.len() > 1 || !config.empty_single_row) {
291                body.pop();
292                row_tags.pop();
293            }
294
295            if hlines_before_row.len() < body.len() + 1 {
296                hlines_before_row.push(vec![]);
297            }
298            break;
299        } else if next == "\\\\" {
300            parser.consume();
301            let size = if parser.gullet.future().text != " " {
302                parser.parse_size_group(true)?
303            } else {
304                None
305            };
306            let gap = size.and_then(|s| {
307                if let ParseNode::Size { value, .. } = s {
308                    Some(value)
309                } else {
310                    None
311                }
312            });
313            row_gaps.push(gap);
314
315            let row_tag = extract_trailing_tag_from_last_cell(&mut row, config.auto_number)?;
316            row_tags.push(row_tag);
317            body.push(row);
318            hlines_before_row.push(get_hlines(parser)?);
319            row = Vec::new();
320        } else {
321            return Err(ParseError::msg(format!(
322                "Expected & or \\\\ or \\cr or \\end, got '{}'",
323                next
324            )));
325        }
326    }
327
328    parser.gullet.end_group();
329    parser.gullet.end_group();
330
331    // Post-process row tags for auto-numbering
332    let tags = if config.auto_number {
333        let mut processed: Vec<ArrayTag> = Vec::with_capacity(row_tags.len());
334        let mut any_visible = false;
335        for raw_tag in &row_tags {
336            match raw_tag {
337                ArrayTag::Explicit(nodes) if !nodes.is_empty() => {
338                    // Explicit \\tag{...}: step counter, keep tag content as-is
339                    parser.equation_counter += 1;
340                    processed.push(ArrayTag::Explicit(nodes.clone()));
341                    any_visible = true;
342                }
343                ArrayTag::Explicit(_) => {
344                    // Empty \\tag{}: treat as suppressed
345                    processed.push(ArrayTag::Auto(false));
346                }
347                ArrayTag::Auto(true) => {
348                    // Auto-number this row: step counter, generate "(N)"
349                    parser.equation_counter += 1;
350                    let num_str = parser.equation_counter.to_string();
351                    let tag_nodes = vec![
352                        ParseNode::MathOrd {
353                            mode: Mode::Math,
354                            text: "(".to_string(),
355                            loc: None,
356                        },
357                        ParseNode::MathOrd {
358                            mode: Mode::Math,
359                            text: num_str,
360                            loc: None,
361                        },
362                        ParseNode::MathOrd {
363                            mode: Mode::Math,
364                            text: ")".to_string(),
365                            loc: None,
366                        },
367                    ];
368                    processed.push(ArrayTag::Explicit(tag_nodes));
369                    any_visible = true;
370                }
371                ArrayTag::Auto(false) => {
372                    // Suppressed by \\nonumber or empty \\tag{}: no counter step, no tag
373                    processed.push(ArrayTag::Auto(false));
374                }
375            }
376        }
377        if any_visible {
378            Some(processed)
379        } else {
380            None
381        }
382    } else {
383        // Not an auto-numbering environment: keep original behavior
384        if row_tags
385            .iter()
386            .any(|t| matches!(t, ArrayTag::Explicit(nodes) if !nodes.is_empty()))
387        {
388            Some(row_tags)
389        } else {
390            None
391        }
392    };
393
394    Ok(ParseNode::Array {
395        mode: parser.mode,
396        body,
397        row_gaps,
398        hlines_before_row,
399        cols: config.cols,
400        col_separation_type: config.col_separation_type,
401        hskip_before_and_after: config.hskip_before_and_after,
402        add_jot: config.add_jot,
403        arraystretch,
404        tags,
405        leqno: config.leqno,
406        is_cd: None,
407        loc: None,
408    })
409}
410
411// ── array / darray ───────────────────────────────────────────────────────
412
413fn register_array(map: &mut HashMap<&'static str, EnvSpec>) {
414    fn handle_array(
415        ctx: &mut EnvContext,
416        args: Vec<ParseNode>,
417        _opt_args: Vec<Option<ParseNode>>,
418    ) -> ParseResult<ParseNode> {
419        let colalign = match &args[0] {
420            ParseNode::OrdGroup { body, .. } => body.clone(),
421            other if other.is_symbol_node() => vec![other.clone()],
422            _ => return Err(ParseError::msg("Invalid column alignment for array")),
423        };
424
425        let mut cols = Vec::new();
426        for nde in &colalign {
427            let ca = nde
428                .symbol_text()
429                .ok_or_else(|| ParseError::msg("Expected column alignment character"))?;
430            match ca {
431                "l" | "c" | "r" => cols.push(AlignSpec {
432                    align_type: AlignType::Align,
433                    align: Some(ca.to_string()),
434                    pregap: None,
435                    postgap: None,
436                }),
437                "|" => cols.push(AlignSpec {
438                    align_type: AlignType::Separator,
439                    align: Some("|".to_string()),
440                    pregap: None,
441                    postgap: None,
442                }),
443                ":" => cols.push(AlignSpec {
444                    align_type: AlignType::Separator,
445                    align: Some(":".to_string()),
446                    pregap: None,
447                    postgap: None,
448                }),
449                _ => return Err(ParseError::msg(format!("Unknown column alignment: {}", ca))),
450            }
451        }
452
453        let max_num_cols = cols.len();
454        let config = ArrayConfig {
455            cols: Some(cols),
456            hskip_before_and_after: Some(true),
457            max_num_cols: Some(max_num_cols),
458            ..Default::default()
459        };
460        parse_array(ctx.parser, config, d_cell_style(&ctx.env_name))
461    }
462
463    for name in &["array", "darray"] {
464        map.insert(
465            name,
466            EnvSpec {
467                num_args: 1,
468                num_optional_args: 0,
469                handler: handle_array,
470            },
471        );
472    }
473}
474
475// ── matrix variants ──────────────────────────────────────────────────────
476
477fn register_matrix(map: &mut HashMap<&'static str, EnvSpec>) {
478    fn handle_matrix(
479        ctx: &mut EnvContext,
480        _args: Vec<ParseNode>,
481        _opt_args: Vec<Option<ParseNode>>,
482    ) -> ParseResult<ParseNode> {
483        let base_name = ctx.env_name.replace('*', "");
484        let delimiters: Option<(&str, &str)> = match base_name.as_str() {
485            "matrix" => None,
486            "pmatrix" => Some(("(", ")")),
487            "bmatrix" => Some(("[", "]")),
488            "Bmatrix" => Some(("\\{", "\\}")),
489            "vmatrix" => Some(("|", "|")),
490            "Vmatrix" => Some(("\\Vert", "\\Vert")),
491            _ => None,
492        };
493
494        let mut col_align = "c".to_string();
495
496        // mathtools starred matrix: parse optional [l|c|r] alignment
497        if ctx.env_name.ends_with('*') {
498            ctx.parser.gullet.consume_spaces();
499            if ctx.parser.gullet.future().text == "[" {
500                ctx.parser.gullet.pop_token();
501                ctx.parser.gullet.consume_spaces();
502                let align_tok = ctx.parser.gullet.pop_token();
503                if !"lcr".contains(align_tok.text.as_str()) {
504                    return Err(ParseError::new(
505                        "Expected l or c or r".to_string(),
506                        Some(&align_tok),
507                    ));
508                }
509                col_align = align_tok.text.clone();
510                ctx.parser.gullet.consume_spaces();
511                let close = ctx.parser.gullet.pop_token();
512                if close.text != "]" {
513                    return Err(ParseError::new("Expected ]".to_string(), Some(&close)));
514                }
515            }
516        }
517
518        let config = ArrayConfig {
519            hskip_before_and_after: Some(false),
520            cols: Some(vec![AlignSpec {
521                align_type: AlignType::Align,
522                align: Some(col_align.clone()),
523                pregap: None,
524                postgap: None,
525            }]),
526            ..Default::default()
527        };
528
529        let mut res = parse_array(ctx.parser, config, d_cell_style(&ctx.env_name))?;
530
531        // Fix cols to match actual number of columns
532        if let ParseNode::Array {
533            ref body,
534            ref mut cols,
535            ..
536        } = res
537        {
538            let num_cols = body.iter().map(|r| r.len()).max().unwrap_or(0);
539            *cols = Some(
540                (0..num_cols)
541                    .map(|_| AlignSpec {
542                        align_type: AlignType::Align,
543                        align: Some(col_align.to_string()),
544                        pregap: None,
545                        postgap: None,
546                    })
547                    .collect(),
548            );
549        }
550
551        match delimiters {
552            Some((left, right)) => Ok(ParseNode::LeftRight {
553                mode: ctx.mode,
554                body: vec![res],
555                left: left.to_string(),
556                right: right.to_string(),
557                right_color: None,
558                loc: None,
559            }),
560            None => Ok(res),
561        }
562    }
563
564    for name in &[
565        "matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix", "matrix*", "pmatrix*",
566        "bmatrix*", "Bmatrix*", "vmatrix*", "Vmatrix*",
567    ] {
568        map.insert(
569            name,
570            EnvSpec {
571                num_args: 0,
572                num_optional_args: 0,
573                handler: handle_matrix,
574            },
575        );
576    }
577}
578
579// ── cases / dcases / rcases / drcases ────────────────────────────────────
580
581fn register_cases(map: &mut HashMap<&'static str, EnvSpec>) {
582    fn handle_cases(
583        ctx: &mut EnvContext,
584        _args: Vec<ParseNode>,
585        _opt_args: Vec<Option<ParseNode>>,
586    ) -> ParseResult<ParseNode> {
587        let config = ArrayConfig {
588            arraystretch: Some(1.2),
589            cols: Some(vec![
590                AlignSpec {
591                    align_type: AlignType::Align,
592                    align: Some("l".to_string()),
593                    pregap: Some(0.0),
594                    postgap: Some(1.0),
595                },
596                AlignSpec {
597                    align_type: AlignType::Align,
598                    align: Some("l".to_string()),
599                    pregap: Some(0.0),
600                    postgap: Some(0.0),
601                },
602            ]),
603            ..Default::default()
604        };
605
606        let res = parse_array(ctx.parser, config, d_cell_style(&ctx.env_name))?;
607
608        let (left, right) = if ctx.env_name.contains('r') {
609            (".", "\\}")
610        } else {
611            ("\\{", ".")
612        };
613
614        Ok(ParseNode::LeftRight {
615            mode: ctx.mode,
616            body: vec![res],
617            left: left.to_string(),
618            right: right.to_string(),
619            right_color: None,
620            loc: None,
621        })
622    }
623
624    for name in &["cases", "dcases", "rcases", "drcases"] {
625        map.insert(
626            name,
627            EnvSpec {
628                num_args: 0,
629                num_optional_args: 0,
630                handler: handle_cases,
631            },
632        );
633    }
634}
635
636// ── align / align* / aligned / split / alignat / alignat* / alignedat ────
637
638fn handle_aligned(
639    ctx: &mut EnvContext,
640    args: Vec<ParseNode>,
641    _opt_args: Vec<Option<ParseNode>>,
642) -> ParseResult<ParseNode> {
643    let is_split = ctx.env_name == "split";
644    let is_alignat = ctx.env_name.contains("at");
645    let sep_type = if is_alignat { "alignat" } else { "align" };
646    let auto_number = !ctx.env_name.ends_with('*')
647        && !is_split
648        && ctx.env_name != "aligned"
649        && ctx.env_name != "alignedat";
650
651    let config = ArrayConfig {
652        add_jot: Some(true),
653        empty_single_row: true,
654        col_separation_type: Some(sep_type.to_string()),
655        max_num_cols: if is_split { Some(2) } else { None },
656        auto_number,
657        ..Default::default()
658    };
659
660    let mut res = parse_array(ctx.parser, config, Some(StyleStr::Display))?;
661
662    // Extract explicit column count from first arg (alignat only)
663    let mut num_maths = 0usize;
664    let mut explicit_cols = 0usize;
665    if let Some(ParseNode::OrdGroup { body, .. }) = args.first() {
666        let mut arg_str = String::new();
667        for node in body {
668            if let Some(t) = node.symbol_text() {
669                arg_str.push_str(t);
670            }
671        }
672        if let Ok(n) = arg_str.parse::<usize>() {
673            num_maths = n;
674            explicit_cols = n * 2;
675        }
676    }
677    let is_aligned = explicit_cols == 0;
678
679    // Determine actual number of columns
680    let mut num_cols = if let ParseNode::Array { ref body, .. } = res {
681        body.iter().map(|r| r.len()).max().unwrap_or(0)
682    } else {
683        0
684    };
685
686    if let ParseNode::Array {
687        body: ref mut array_body,
688        ..
689    } = res
690    {
691        for row in array_body.iter_mut() {
692            // Prepend empty group at every even-indexed cell (2nd, 4th, ...)
693            let mut i = 1;
694            while i < row.len() {
695                if let ParseNode::Styling {
696                    body: ref mut styling_body,
697                    ..
698                } = row[i]
699                {
700                    if let Some(ParseNode::OrdGroup {
701                        body: ref mut og_body,
702                        ..
703                    }) = styling_body.first_mut()
704                    {
705                        og_body.insert(
706                            0,
707                            ParseNode::OrdGroup {
708                                mode: ctx.mode,
709                                body: vec![],
710                                semisimple: None,
711                                loc: None,
712                            },
713                        );
714                    }
715                }
716                i += 2;
717            }
718
719            if !is_aligned {
720                let cur_maths = row.len() / 2;
721                if num_maths < cur_maths {
722                    return Err(ParseError::msg(format!(
723                        "Too many math in a row: expected {}, but got {}",
724                        num_maths, cur_maths
725                    )));
726                }
727            } else if num_cols < row.len() {
728                num_cols = row.len();
729            }
730        }
731    }
732
733    if !is_aligned {
734        num_cols = explicit_cols;
735    }
736
737    let mut cols = Vec::new();
738    for i in 0..num_cols {
739        let (align, pregap) = if i % 2 == 1 {
740            ("l", 0.0)
741        } else if i > 0 && is_aligned {
742            ("r", 1.0)
743        } else {
744            ("r", 0.0)
745        };
746        cols.push(AlignSpec {
747            align_type: AlignType::Align,
748            align: Some(align.to_string()),
749            pregap: Some(pregap),
750            postgap: Some(0.0),
751        });
752    }
753
754    if let ParseNode::Array {
755        cols: ref mut array_cols,
756        col_separation_type: ref mut array_sep_type,
757        ..
758    } = res
759    {
760        *array_cols = Some(cols);
761        *array_sep_type = Some(if is_aligned { "align" } else { "alignat" }.to_string());
762    }
763
764    Ok(res)
765}
766
767fn register_align(map: &mut HashMap<&'static str, EnvSpec>) {
768    for name in &["align", "align*", "aligned", "split"] {
769        map.insert(
770            name,
771            EnvSpec {
772                num_args: 0,
773                num_optional_args: 0,
774                handler: handle_aligned,
775            },
776        );
777    }
778}
779
780// ── gathered / gather / gather* ──────────────────────────────────────────
781
782fn register_gathered(map: &mut HashMap<&'static str, EnvSpec>) {
783    fn handle_gathered(
784        ctx: &mut EnvContext,
785        _args: Vec<ParseNode>,
786        _opt_args: Vec<Option<ParseNode>>,
787    ) -> ParseResult<ParseNode> {
788        let auto_number = !ctx.env_name.ends_with('*') && ctx.env_name != "gathered";
789        let config = ArrayConfig {
790            cols: Some(vec![AlignSpec {
791                align_type: AlignType::Align,
792                align: Some("c".to_string()),
793                pregap: None,
794                postgap: None,
795            }]),
796            add_jot: Some(true),
797            col_separation_type: Some("gather".to_string()),
798            empty_single_row: true,
799            auto_number,
800            ..Default::default()
801        };
802        parse_array(ctx.parser, config, Some(StyleStr::Display))
803    }
804
805    for name in &["gathered", "gather", "gather*"] {
806        map.insert(
807            name,
808            EnvSpec {
809                num_args: 0,
810                num_optional_args: 0,
811                handler: handle_gathered,
812            },
813        );
814    }
815}
816
817// ── equation / equation* ─────────────────────────────────────────────────
818
819fn register_equation(map: &mut HashMap<&'static str, EnvSpec>) {
820    fn handle_equation(
821        ctx: &mut EnvContext,
822        _args: Vec<ParseNode>,
823        _opt_args: Vec<Option<ParseNode>>,
824    ) -> ParseResult<ParseNode> {
825        let auto_number = !ctx.env_name.ends_with('*');
826        let config = ArrayConfig {
827            empty_single_row: true,
828            single_row: true,
829            max_num_cols: Some(1),
830            auto_number,
831            ..Default::default()
832        };
833        parse_array(ctx.parser, config, Some(StyleStr::Display))
834    }
835
836    for name in &["equation", "equation*"] {
837        map.insert(
838            name,
839            EnvSpec {
840                num_args: 0,
841                num_optional_args: 0,
842                handler: handle_equation,
843            },
844        );
845    }
846}
847
848// ── smallmatrix ──────────────────────────────────────────────────────────
849
850fn register_smallmatrix(map: &mut HashMap<&'static str, EnvSpec>) {
851    fn handle_smallmatrix(
852        ctx: &mut EnvContext,
853        _args: Vec<ParseNode>,
854        _opt_args: Vec<Option<ParseNode>>,
855    ) -> ParseResult<ParseNode> {
856        let config = ArrayConfig {
857            arraystretch: Some(0.5),
858            ..Default::default()
859        };
860        let mut res = parse_array(ctx.parser, config, Some(StyleStr::Script))?;
861        if let ParseNode::Array {
862            ref mut col_separation_type,
863            ..
864        } = res
865        {
866            *col_separation_type = Some("small".to_string());
867        }
868        Ok(res)
869    }
870
871    map.insert(
872        "smallmatrix",
873        EnvSpec {
874            num_args: 0,
875            num_optional_args: 0,
876            handler: handle_smallmatrix,
877        },
878    );
879}
880
881// ── alignat / alignat* / alignedat ──────────────────────────────────────
882
883fn register_alignat(map: &mut HashMap<&'static str, EnvSpec>) {
884    for name in &["alignat", "alignat*", "alignedat"] {
885        map.insert(
886            name,
887            EnvSpec {
888                num_args: 1,
889                num_optional_args: 0,
890                handler: handle_aligned,
891            },
892        );
893    }
894}
895
896// ── CD (amscd commutative diagrams) ──────────────────────────────────────
897
898fn register_cd(map: &mut HashMap<&'static str, EnvSpec>) {
899    fn handle_cd(
900        ctx: &mut EnvContext,
901        _args: Vec<ParseNode>,
902        _opt_args: Vec<Option<ParseNode>>,
903    ) -> ParseResult<ParseNode> {
904        // Collect all raw tokens until \end
905        let mut raw: Vec<Token> = Vec::new();
906        loop {
907            let tok = ctx.parser.gullet.future().clone();
908            if tok.text == "\\end" || tok.text == "EOF" {
909                break;
910            }
911            ctx.parser.gullet.pop_token();
912            raw.push(tok);
913        }
914
915        // Split into rows at \\ or \cr
916        let rows = cd_split_rows(raw);
917
918        let mut body: Vec<Vec<ParseNode>> = Vec::new();
919        let mut row_gaps: Vec<Option<Measurement>> = Vec::new();
920        let mut hlines_before_row: Vec<Vec<bool>> = Vec::new();
921        hlines_before_row.push(vec![]);
922
923        for row_toks in rows {
924            // Skip purely-whitespace rows
925            if row_toks.iter().all(|t| t.text == " ") {
926                continue;
927            }
928            let cells = cd_parse_row(ctx.parser, row_toks)?;
929            if !cells.is_empty() {
930                body.push(cells);
931                row_gaps.push(None);
932                hlines_before_row.push(vec![]);
933            }
934        }
935
936        if body.is_empty() {
937            body.push(vec![]);
938            hlines_before_row.push(vec![]);
939        }
940
941        Ok(ParseNode::Array {
942            mode: ctx.mode,
943            body,
944            row_gaps,
945            hlines_before_row,
946            cols: None,
947            col_separation_type: Some("CD".to_string()),
948            hskip_before_and_after: Some(false),
949            add_jot: None,
950            arraystretch: 1.0,
951            tags: None,
952            leqno: None,
953            is_cd: Some(true),
954            loc: None,
955        })
956    }
957
958    map.insert(
959        "CD",
960        EnvSpec {
961            num_args: 0,
962            num_optional_args: 0,
963            handler: handle_cd,
964        },
965    );
966}
967
968/// Split a flat token list into rows at `\\` or `\cr` boundaries.
969fn cd_split_rows(tokens: Vec<Token>) -> Vec<Vec<Token>> {
970    let mut rows: Vec<Vec<Token>> = Vec::new();
971    let mut current: Vec<Token> = Vec::new();
972    for tok in tokens {
973        if tok.text == "\\\\" || tok.text == "\\cr" {
974            rows.push(current);
975            current = Vec::new();
976        } else {
977            current.push(tok);
978        }
979    }
980    if !current.is_empty() {
981        rows.push(current);
982    }
983    rows
984}
985
986/// Collect tokens from `tokens[start..]` up to (but not including) the first
987/// token whose text equals `delimiter`.  Returns (collected_tokens, tokens_consumed).
988/// `tokens_consumed` includes the delimiter itself if found.
989fn cd_collect_until(tokens: &[Token], start: usize, delimiter: &str) -> (Vec<Token>, usize) {
990    let mut result = Vec::new();
991    let mut i = start;
992    while i < tokens.len() {
993        if tokens[i].text == delimiter {
994            i += 1; // consume the delimiter
995            break;
996        }
997        result.push(tokens[i].clone());
998        i += 1;
999    }
1000    (result, i - start)
1001}
1002
1003/// Collect tokens from `tokens[start..]` up to (but not including) the next `@`.
1004fn cd_collect_until_at(tokens: &[Token], start: usize) -> (Vec<Token>, usize) {
1005    let mut result = Vec::new();
1006    let mut i = start;
1007    while i < tokens.len() && tokens[i].text != "@" {
1008        result.push(tokens[i].clone());
1009        i += 1;
1010    }
1011    (result, i - start)
1012}
1013
1014/// Use the parser to parse a token slice as a math OrdGroup.
1015/// Tokens must be in forward order; this function reverses them internally for subparse().
1016fn cd_parse_tokens(parser: &mut Parser, tokens: Vec<Token>) -> ParseResult<ParseNode> {
1017    // Filter pure whitespace
1018    let has_content = tokens.iter().any(|t| t.text != " ");
1019    if !has_content {
1020        return Ok(ParseNode::OrdGroup {
1021            mode: parser.mode,
1022            body: vec![],
1023            semisimple: None,
1024            loc: None,
1025        });
1026    }
1027    // subparse() expects tokens in reverse order (stack convention)
1028    let mut rev = tokens;
1029    rev.reverse();
1030    let body = parser.subparse(rev)?;
1031    Ok(ParseNode::OrdGroup {
1032        mode: parser.mode,
1033        body,
1034        semisimple: None,
1035        loc: None,
1036    })
1037}
1038
1039/// Parse one row of a CD environment from its raw token list.
1040/// Returns the list of ParseNode cells for the grid row.
1041fn cd_parse_row(parser: &mut Parser, row_tokens: Vec<Token>) -> ParseResult<Vec<ParseNode>> {
1042    let toks = &row_tokens;
1043    let n = toks.len();
1044    let mut cells: Vec<ParseNode> = Vec::new();
1045    let mut i = 0usize;
1046
1047    while i < n {
1048        // Skip spaces at start of each cell
1049        while i < n && toks[i].text == " " {
1050            i += 1;
1051        }
1052        if i >= n {
1053            break;
1054        }
1055
1056        if toks[i].text == "@" {
1057            i += 1; // consume `@`
1058            if i >= n {
1059                return Err(ParseError::msg("Unexpected end of CD row after @"));
1060            }
1061            let dir = toks[i].text.clone();
1062            i += 1; // consume direction char
1063
1064            let mode = parser.mode;
1065            let arrow = match dir.as_str() {
1066                ">" | "<" => {
1067                    let (above_toks, c1) = cd_collect_until(toks, i, &dir);
1068                    i += c1;
1069                    let (below_toks, c2) = cd_collect_until(toks, i, &dir);
1070                    i += c2;
1071                    let label_above = cd_parse_tokens(parser, above_toks)?;
1072                    let label_below = cd_parse_tokens(parser, below_toks)?;
1073                    ParseNode::CdArrow {
1074                        mode,
1075                        direction: if dir == ">" { "right" } else { "left" }.to_string(),
1076                        label_above: Some(Box::new(label_above)),
1077                        label_below: Some(Box::new(label_below)),
1078                        loc: None,
1079                    }
1080                }
1081                "V" | "A" => {
1082                    let (left_toks, c1) = cd_collect_until(toks, i, &dir);
1083                    i += c1;
1084                    let (right_toks, c2) = cd_collect_until(toks, i, &dir);
1085                    i += c2;
1086                    let label_above = cd_parse_tokens(parser, left_toks)?;
1087                    let label_below = cd_parse_tokens(parser, right_toks)?;
1088                    ParseNode::CdArrow {
1089                        mode,
1090                        direction: if dir == "V" { "down" } else { "up" }.to_string(),
1091                        label_above: Some(Box::new(label_above)),
1092                        label_below: Some(Box::new(label_below)),
1093                        loc: None,
1094                    }
1095                }
1096                "=" => ParseNode::CdArrow {
1097                    mode,
1098                    direction: "horiz_eq".to_string(),
1099                    label_above: None,
1100                    label_below: None,
1101                    loc: None,
1102                },
1103                "|" => ParseNode::CdArrow {
1104                    mode,
1105                    direction: "vert_eq".to_string(),
1106                    label_above: None,
1107                    label_below: None,
1108                    loc: None,
1109                },
1110                "." => ParseNode::CdArrow {
1111                    mode,
1112                    direction: "none".to_string(),
1113                    label_above: None,
1114                    label_below: None,
1115                    loc: None,
1116                },
1117                _ => return Err(ParseError::msg(format!("Unknown CD directive: @{}", dir))),
1118            };
1119            cells.push(arrow);
1120        } else {
1121            // Object cell: collect until next `@`
1122            let (obj_toks, consumed) = cd_collect_until_at(toks, i);
1123            i += consumed;
1124            let obj = cd_parse_tokens(parser, obj_toks)?;
1125            cells.push(obj);
1126        }
1127    }
1128
1129    // Post-process: structure cells into the (2n-1) grid pattern.
1130    Ok(cd_structure_row(cells, parser.mode))
1131}
1132
1133/// Given the raw parsed cells of one CD row, produce the correctly-structured grid row.
1134///
1135/// Object rows already alternate: obj, h-arrow, obj, h-arrow, …, obj.
1136/// Arrow rows contain only CdArrow nodes (plus whitespace OrdGroups which we strip),
1137/// and need empty OrdGroup fillers inserted between consecutive arrows.
1138fn cd_structure_row(cells: Vec<ParseNode>, mode: Mode) -> Vec<ParseNode> {
1139    // Detect arrow row: all cells are either CdArrow or empty OrdGroup
1140    let is_arrow_row = cells.iter().all(|c| match c {
1141        ParseNode::CdArrow { .. } => true,
1142        ParseNode::OrdGroup { body, .. } => body.is_empty(),
1143        _ => false,
1144    }) && cells.iter().any(|c| matches!(c, ParseNode::CdArrow { .. }));
1145
1146    if is_arrow_row {
1147        let arrows: Vec<ParseNode> = cells
1148            .into_iter()
1149            .filter(|c| matches!(c, ParseNode::CdArrow { .. }))
1150            .collect();
1151
1152        if arrows.is_empty() {
1153            return vec![];
1154        }
1155
1156        let empty = || ParseNode::OrdGroup {
1157            mode,
1158            body: vec![],
1159            semisimple: None,
1160            loc: None,
1161        };
1162
1163        let mut result = Vec::with_capacity(arrows.len() * 2 - 1);
1164        for (idx, arrow) in arrows.into_iter().enumerate() {
1165            if idx > 0 {
1166                result.push(empty());
1167            }
1168            result.push(arrow);
1169        }
1170        result
1171    } else {
1172        // Object row: already in correct format
1173        cells
1174    }
1175}
1176
1177// ── subarray ────────────────────────────────────────────────────────────
1178
1179fn register_subarray(map: &mut HashMap<&'static str, EnvSpec>) {
1180    fn handle_subarray(
1181        ctx: &mut EnvContext,
1182        args: Vec<ParseNode>,
1183        _opt_args: Vec<Option<ParseNode>>,
1184    ) -> ParseResult<ParseNode> {
1185        let colalign = match &args[0] {
1186            ParseNode::OrdGroup { body, .. } => body.clone(),
1187            other if other.is_symbol_node() => vec![other.clone()],
1188            _ => return Err(ParseError::msg("Invalid column alignment for subarray")),
1189        };
1190
1191        let mut cols = Vec::new();
1192        for nde in &colalign {
1193            let ca = nde
1194                .symbol_text()
1195                .ok_or_else(|| ParseError::msg("Expected column alignment character"))?;
1196            match ca {
1197                "l" | "c" => cols.push(AlignSpec {
1198                    align_type: AlignType::Align,
1199                    align: Some(ca.to_string()),
1200                    pregap: None,
1201                    postgap: None,
1202                }),
1203                _ => return Err(ParseError::msg(format!("Unknown column alignment: {}", ca))),
1204            }
1205        }
1206
1207        if cols.len() > 1 {
1208            return Err(ParseError::msg("{subarray} can contain only one column"));
1209        }
1210
1211        let config = ArrayConfig {
1212            cols: Some(cols),
1213            hskip_before_and_after: Some(false),
1214            arraystretch: Some(0.5),
1215            ..Default::default()
1216        };
1217
1218        let res = parse_array(ctx.parser, config, Some(StyleStr::Script))?;
1219
1220        if let ParseNode::Array { ref body, .. } = res {
1221            if !body.is_empty() && body[0].len() > 1 {
1222                return Err(ParseError::msg("{subarray} can contain only one column"));
1223            }
1224        }
1225
1226        Ok(res)
1227    }
1228
1229    map.insert(
1230        "subarray",
1231        EnvSpec {
1232            num_args: 1,
1233            num_optional_args: 0,
1234            handler: handle_subarray,
1235        },
1236    );
1237}
1238
1239// ── prooftree (bussproofs subset) ───────────────────────────────────────
1240//
1241// Implemented: \AxiomC, \UnaryInfC, \BinaryInfC, \TrinaryInfC, \QuaternaryInfC,
1242//   \QuinaryInfC (and their abbreviations \AXC, \UIC, \BIC, \TIC),
1243//   \LeftLabel/\RightLabel (and \LL, \RL), \fCenter,
1244//   line styles: \solidLine, \dashedLine, \noLine (and \always* variants),
1245//   \rootAtTop, \rootAtBottom (and \always* variants).
1246//
1247// Not yet implemented: \InsertBetweenHyps, \ScoreTree, \Cell, \noCell.
1248
1249fn register_prooftree(map: &mut HashMap<&'static str, EnvSpec>) {
1250    fn handle_prooftree(
1251        ctx: &mut EnvContext,
1252        _args: Vec<ParseNode>,
1253        _opt_args: Vec<Option<ParseNode>>,
1254    ) -> ParseResult<ParseNode> {
1255        parse_prooftree(ctx.parser)
1256    }
1257
1258    map.insert(
1259        "prooftree",
1260        EnvSpec {
1261            num_args: 0,
1262            num_optional_args: 0,
1263            handler: handle_prooftree,
1264        },
1265    );
1266}
1267
1268fn proof_command_arity(name: &str) -> Option<usize> {
1269    match name {
1270        "\\UnaryInfC" | "\\UnaryInf" | "\\UIC" => Some(1),
1271        "\\BinaryInfC" | "\\BinaryInf" | "\\BIC" => Some(2),
1272        "\\TrinaryInfC" | "\\TrinaryInf" | "\\TIC" => Some(3),
1273        "\\QuaternaryInfC" | "\\QuaternaryInf" => Some(4),
1274        "\\QuinaryInfC" | "\\QuinaryInf" => Some(5),
1275        _ => None,
1276    }
1277}
1278
1279fn parse_prooftree_arg(parser: &mut Parser, command: &str) -> ParseResult<Vec<ParseNode>> {
1280    let arg = parser
1281        .parse_argument_group(false, None)?
1282        .ok_or_else(|| ParseError::msg(format!("Expected argument for {}", command)))?;
1283    Ok(ParseNode::ord_argument(arg))
1284}
1285
1286fn parse_prooftree(parser: &mut Parser) -> ParseResult<ParseNode> {
1287    // Track structural depth alongside each branch.  A prooftree is assembled
1288    // iteratively, so expression recursion alone cannot protect this tree shape.
1289    let mut stack: Vec<(ProofBranch, usize)> = Vec::new();
1290    let mut left_label: Option<Vec<ParseNode>> = None;
1291    let mut right_label: Option<Vec<ParseNode>> = None;
1292    let mut next_line_style = ProofLineStyle::Solid;
1293    let mut default_line_style = ProofLineStyle::Solid;
1294    let mut next_root_at_top = false;
1295    let mut default_root_at_top = false;
1296
1297    loop {
1298        parser.consume_spaces()?;
1299        let token = parser.fetch()?;
1300        let command = token.text.clone();
1301
1302        if command == "\\end" {
1303            break;
1304        }
1305        parser.consume();
1306
1307        match command.as_str() {
1308            "\\AxiomC" | "\\Axiom" | "\\AXC" => {
1309                let conclusion = parse_prooftree_arg(parser, &command)?;
1310                let depth = 1 + parse_nodes_logical_depth(&conclusion);
1311                parser
1312                    .depth_budget()
1313                    .ensure_additional(depth)
1314                    .map_err(|_| ParseError::recursion_limit_exceeded())?;
1315                stack.push((
1316                    ProofBranch {
1317                        conclusion,
1318                        premises: Vec::new(),
1319                        left_label: None,
1320                        right_label: None,
1321                        line_style: ProofLineStyle::None,
1322                        root_at_top: false,
1323                    },
1324                    depth,
1325                ));
1326            }
1327            "\\LeftLabel" | "\\LL" => {
1328                left_label = Some(parse_prooftree_arg(parser, &command)?);
1329            }
1330            "\\RightLabel" | "\\RL" => {
1331                right_label = Some(parse_prooftree_arg(parser, &command)?);
1332            }
1333            "\\singleLine" | "\\solidLine" => {
1334                next_line_style = ProofLineStyle::Solid;
1335            }
1336            "\\dashedLine" => {
1337                next_line_style = ProofLineStyle::Dashed;
1338            }
1339            "\\noLine" => {
1340                next_line_style = ProofLineStyle::None;
1341            }
1342            "\\alwaysSingleLine" | "\\alwaysSolidLine" => {
1343                default_line_style = ProofLineStyle::Solid;
1344                next_line_style = ProofLineStyle::Solid;
1345            }
1346            "\\alwaysDashedLine" => {
1347                default_line_style = ProofLineStyle::Dashed;
1348                next_line_style = ProofLineStyle::Dashed;
1349            }
1350            "\\alwaysNoLine" => {
1351                default_line_style = ProofLineStyle::None;
1352                next_line_style = ProofLineStyle::None;
1353            }
1354            "\\rootAtTop" => {
1355                next_root_at_top = true;
1356            }
1357            "\\rootAtBottom" => {
1358                next_root_at_top = false;
1359            }
1360            "\\alwaysRootAtTop" => {
1361                default_root_at_top = true;
1362                next_root_at_top = true;
1363            }
1364            "\\alwaysRootAtBottom" => {
1365                default_root_at_top = false;
1366                next_root_at_top = false;
1367            }
1368            name if proof_command_arity(name).is_some() => {
1369                let arity = proof_command_arity(name).unwrap();
1370                if stack.len() < arity {
1371                    return Err(ParseError::msg(format!(
1372                        "{} needs {} premise(s), but only {} available",
1373                        name,
1374                        arity,
1375                        stack.len()
1376                    )));
1377                }
1378                let conclusion = parse_prooftree_arg(parser, name)?;
1379                let start = stack.len() - arity;
1380                let premise_entries = stack.split_off(start);
1381                let payload_depth = parse_nodes_logical_depth(&conclusion)
1382                    .max(
1383                        left_label
1384                            .as_deref()
1385                            .map(parse_nodes_logical_depth)
1386                            .unwrap_or(0),
1387                    )
1388                    .max(
1389                        right_label
1390                            .as_deref()
1391                            .map(parse_nodes_logical_depth)
1392                            .unwrap_or(0),
1393                    );
1394                let premise_depth = premise_entries
1395                    .iter()
1396                    .map(|(_, depth)| *depth)
1397                    .max()
1398                    .unwrap_or(0);
1399                let depth = 1 + payload_depth.max(premise_depth);
1400                parser
1401                    .depth_budget()
1402                    .ensure_additional(depth)
1403                    .map_err(|_| ParseError::recursion_limit_exceeded())?;
1404                let premises = premise_entries
1405                    .into_iter()
1406                    .map(|(branch, _)| branch)
1407                    .collect();
1408                stack.push((
1409                    ProofBranch {
1410                        conclusion,
1411                        premises,
1412                        left_label: left_label.take(),
1413                        right_label: right_label.take(),
1414                        line_style: next_line_style.clone(),
1415                        root_at_top: next_root_at_top,
1416                    },
1417                    depth,
1418                ));
1419                next_line_style = default_line_style.clone();
1420                next_root_at_top = default_root_at_top;
1421            }
1422            _ => {
1423                return Err(ParseError::msg(format!(
1424                    "{} valid only as a supported bussproofs command within prooftree",
1425                    command
1426                )));
1427            }
1428        }
1429    }
1430
1431    if stack.len() != 1 {
1432        return Err(ParseError::msg(format!(
1433            "prooftree ended with {} proof stack item(s), expected 1",
1434            stack.len()
1435        )));
1436    }
1437
1438    if left_label.is_some() || right_label.is_some() {
1439        return Err(ParseError::msg(
1440            "prooftree has a \\LeftLabel or \\RightLabel without a following inference command",
1441        ));
1442    }
1443
1444    Ok(ParseNode::ProofTree {
1445        mode: parser.mode,
1446        tree: stack.pop().unwrap().0,
1447        loc: None,
1448    })
1449}