2be0e6586e
CommonMark fence scanner (a closing fence is at least as long as the opening one, tilde fences, quote prefixes), used by every fence scan in the note view; math: Bmatrix, Vmatrix, smallmatrix, array, align, hline, lenient parse where an unknown command becomes one red atom; plain/text/txt code blocks are no unknown language; corpus test against pulldown-cmark over the help vault (0 disagreements on 179 notes). Adds the render audit of the help vault (20 findings). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
143 lines
5.8 KiB
Rust
143 lines
5.8 KiB
Rust
//! 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<f32> {
|
|
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:?}");
|
|
}
|
|
}
|