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