//! Matrix-like environments: every name parses, delimiters stretch, cells align, and a command the //! parser does not know degrades to one error atom in lenient mode. use notescraft_math::testutil::FakeMetrics; use notescraft_math::{EnvKind, Error, Face, Item, MathBox, Node, Style, layout, parse, parse_lenient}; const STYLE: Style = Style { size_pt: 10.0, display: false }; fn boxed(src: &str) -> MathBox { layout(&parse(src).unwrap_or_else(|e| panic!("{src}: {e}")), &FakeMetrics, STYLE) } fn kind(src: &str) -> EnvKind { match parse(src) { Ok(Node::Row(rows)) => match rows.first() { Some(Node::Env { kind, .. }) => *kind, other => panic!("{src}: {other:?}"), }, other => panic!("{src}: {other:?}"), } } fn paths(b: &MathBox) -> usize { b.items.iter().filter(|i| matches!(i, Item::Path { .. })).count() } fn rules(b: &MathBox) -> usize { b.items.iter().filter(|i| matches!(i, Item::Rule { .. })).count() } fn glyph_x(b: &MathBox, text: &str) -> Option { b.items.iter().find_map(|i| match i { Item::Glyphs { text: t, x, .. } if t == text => Some(*x), _ => None, }) } #[test] fn every_environment_name_parses_to_its_kind() { let cases = [ ("matrix", EnvKind::Matrix), ("pmatrix", EnvKind::PMatrix), ("bmatrix", EnvKind::BMatrix), ("Bmatrix", EnvKind::BraceMatrix), ("vmatrix", EnvKind::VMatrix), ("Vmatrix", EnvKind::DoubleBarMatrix), ("smallmatrix", EnvKind::SmallMatrix), ("cases", EnvKind::Cases), ("aligned", EnvKind::Aligned), ("align", EnvKind::Aligned), ("align*", EnvKind::Aligned), ]; for (name, want) in cases { assert_eq!(kind(&format!("\\begin{{{name}}}a&b\\\\c&d\\end{{{name}}}")), want, "{name}"); } } #[test] fn the_note_formula_with_newlines_after_the_row_break_parses() { let src = "\\begin{vmatrix}a & b\\\\\nc & d\n\\end{vmatrix}=ad-bc"; assert!(parse(src).is_ok()); let b = boxed(src); assert!(b.width > 0.0); assert_eq!(rules(&b), 2, "two vertical bars"); } #[test] fn delimiters_stretch_with_the_body() { let one = boxed("\\begin{pmatrix}a\\end{pmatrix}"); let two = boxed("\\begin{pmatrix}a\\\\b\\end{pmatrix}"); assert!(two.height + two.depth > one.height + one.depth); assert_eq!(paths(&boxed("\\begin{Bmatrix}a&b\\\\c&d\\end{Bmatrix}")), 2, "two braces"); assert_eq!(paths(&boxed("\\begin{bmatrix}a&b\\\\c&d\\end{bmatrix}")), 2, "two brackets"); assert_eq!(rules(&boxed("\\begin{Vmatrix}a&b\\\\c&d\\end{Vmatrix}")), 4, "two double bars are four rules"); assert_eq!(paths(&boxed("\\begin{matrix}a&b\\end{matrix}")) + rules(&boxed("\\begin{matrix}a&b\\end{matrix}")), 0); } #[test] fn matrix_cells_are_centred_in_their_column() { let b = boxed("\\begin{matrix}abc&d\\\\e&f\\end{matrix}"); let narrow = glyph_x(&b, "e").unwrap_or(f32::NAN); assert!((narrow - 5.0).abs() < 0.01, "e is centred in a 15 wide column, got {narrow}"); } #[test] fn smallmatrix_is_smaller_than_matrix() { let big = boxed("\\begin{matrix}a&b\\\\c&d\\end{matrix}"); let small = boxed("\\begin{smallmatrix}a&b\\\\c&d\\end{smallmatrix}"); assert!(small.width < big.width && small.height + small.depth < big.height + big.depth); } #[test] fn array_spec_aligns_columns_and_draws_rules() { let src = "\\begin{array}{l|r}abc&d\\\\e&fgh\\end{array}"; let EnvKind::Array(spec) = kind(src) else { panic!("array expected") }; assert_eq!((spec.cols, spec.align(0), spec.align(1), spec.bars), (2, 1, 2, 0b010)); let b = boxed(src); assert_eq!(rules(&b), 1); assert_eq!(glyph_x(&b, "e"), Some(0.0), "left column starts at the edge"); let abc_end = 15.0; let right = glyph_x(&b, "d").unwrap_or(f32::NAN); assert!(right > abc_end, "right column starts after the left one, got {right}"); assert_eq!(kind("\\begin{array}{ccc}a&b&c\\end{array}"), EnvKind::Array(notescraft_math::ArraySpec { aligns: 0, cols: 3, bars: 0 })); assert!(matches!(parse("\\begin{array}a\\end{array}"), Err(Error::Unexpected { .. })), "the column spec is required"); } #[test] fn aligned_and_cases_keep_their_alignment() { let b = boxed("\\begin{align*}ab&=c\\\\abc&=d\\end{align*}"); assert!(glyph_x(&b, "ab").is_some_and(|x| x > 0.0), "first column is right aligned"); assert_eq!(paths(&boxed("\\begin{cases}a\\\\b\\end{cases}")), 1); } #[test] fn spacing_commands_inside_cells_are_not_row_breaks() { assert!(parse("\\begin{matrix}a\\,b&c\\end{matrix}").is_ok()); match parse("\\begin{matrix}a\\,b&c\\end{matrix}") { Ok(Node::Row(items)) => assert!(matches!(items.first(), Some(Node::Env { rows, .. }) if rows.len() == 1)), other => panic!("{other:?}"), } } #[test] fn an_unknown_command_is_one_error_atom_in_lenient_mode() { assert!(matches!(parse("x+\\foo y"), Err(Error::Unknown { .. })), "strict mode still fails"); let node = parse_lenient("x+\\foo y").unwrap_or_else(|e| panic!("{e}")); let b = layout(&node, &FakeMetrics, STYLE); let bad: Vec<&str> = b.items.iter().filter_map(|i| if let Item::Glyphs { text, face: Face::Invalid, .. } = i { Some(text.as_str()) } else { None }).collect(); assert_eq!(bad, vec!["\\foo"]); assert!(b.items.iter().any(|i| matches!(i, Item::Glyphs { face, .. } if *face != Face::Invalid)), "the rest still lays out"); assert!(parse_lenient("é").is_ok(), "a stray character becomes a plain glyph"); assert!(matches!(parse_lenient("\\frac{a"), Err(Error::Unbalanced { .. })), "structural errors stay errors"); } #[test] fn multi_line_display_blocks_and_common_commands_parse() { for src in ["\nx^2 + y^2 = z^2\n", "\n\\int_0^1 x\\,dx\n", "\\frac12", "\r\n a \r\n + b\r\n"] { let node = parse(src).unwrap_or_else(|e| panic!("{src:?}: {e}")); assert!(layout(&node, &FakeMetrics, STYLE).width > 0.0, "{src:?}"); } }