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
14pub 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#[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
67fn 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 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 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 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 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 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(); Ok(ArrayTag::Auto(false))
157 } else {
158 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 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
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 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 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 parser.equation_counter += 1;
340 processed.push(ArrayTag::Explicit(nodes.clone()));
341 any_visible = true;
342 }
343 ArrayTag::Explicit(_) => {
344 processed.push(ArrayTag::Auto(false));
346 }
347 ArrayTag::Auto(true) => {
348 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 processed.push(ArrayTag::Auto(false));
374 }
375 }
376 }
377 if any_visible {
378 Some(processed)
379 } else {
380 None
381 }
382 } else {
383 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
411fn 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
475fn 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 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 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
579fn 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
636fn 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 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 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 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
780fn 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
817fn 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
848fn 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
881fn 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
896fn 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 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 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 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
968fn 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
986fn 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; break;
996 }
997 result.push(tokens[i].clone());
998 i += 1;
999 }
1000 (result, i - start)
1001}
1002
1003fn 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
1014fn cd_parse_tokens(parser: &mut Parser, tokens: Vec<Token>) -> ParseResult<ParseNode> {
1017 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 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
1039fn 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 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; 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; 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 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 Ok(cd_structure_row(cells, parser.mode))
1131}
1132
1133fn cd_structure_row(cells: Vec<ParseNode>, mode: Mode) -> Vec<ParseNode> {
1139 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 cells
1174 }
1175}
1176
1177fn 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
1239fn 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 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}