Files
Marcos Paulo c40225838c
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
fix(ui,math): mouse wheel scrolls cards again, assistant change preview as full-window side-by-side diff, more LaTeX commands
- card areas now cover their card, so egui sees the pointer over them (wheel scroll in note, diff, sidebar, assistant)
- Preview button on pending changes opens a full-window review; Side by side / Unified toggle
- math: log/ln/det/trig operators, \operatorname, \bmod, \pmod, \iint, \xrightarrow, negated relations

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 23:27:08 -03:00

201 lines
5.9 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Error cases with byte positions (positions computed with `str::find`, never by hand).
use notescraft_math::{Error, parse};
fn err(src: &str) -> Error {
match parse(src) {
Ok(_) => panic!("expected error for {src:?}"),
Err(e) => e,
}
}
fn pos_of(src: &str, needle: &str) -> usize {
src.find(needle).unwrap_or(0)
}
fn unknown(src: &str) {
let e = err(src);
let Error::Unknown { pos, .. } = e else { panic!("{src:?}: {e:?}") };
assert_eq!(pos, pos_of(src, "\\"), "{src:?}");
}
#[test]
fn unknown_commands_carry_backslash_position() {
unknown("\\frax");
unknown("a\\foo{b}");
unknown("\\underbrace");
}
#[test]
fn unbalanced_braces() {
let e = err("{x");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 0);
let e = err("\\sqrt{x");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 5);
let e = err("ab{c{d}");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, pos_of("ab{c{d}", "{"));
}
#[test]
fn unexpected_tokens() {
let e = err("x}");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "}");
assert_eq!(pos, 1);
let e = err("a & b");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "&");
assert_eq!(pos, pos_of("a & b", "&"));
}
#[test]
fn left_right_errors() {
let e = err("\\left(x");
let Error::MissingRight { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 0);
let e = err("\\right)");
let Error::Unexpected { what, .. } = e else { panic!("{e:?}") };
assert_eq!(what, "\\right outside \\left..\\right");
let e = err("\\left(x\\right");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing delimiter");
assert_eq!(pos, pos_of("\\left(x\\right", "\\right"));
}
#[test]
fn script_errors() {
let e = err("x^^y");
let Error::DoubleScript { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 2);
let e = err("x^");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing script argument");
assert_eq!(pos, 1);
let e = err("x__y");
let Error::DoubleScript { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 2);
}
#[test]
fn command_argument_errors() {
let e = err("\\frac{a}");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing command argument");
assert_eq!(pos, 0);
let e = err("\\hat");
let Error::Unexpected { what, pos } = e else { panic!("{e:?}") };
assert_eq!(what, "missing command argument");
assert_eq!(pos, 0);
}
#[test]
fn environment_errors() {
let e = err("\\begin{matrix} x \\end{pmatrix}");
let Error::EnvMismatch { begin, end, pos } = e else { panic!("{e:?}") };
assert_eq!(begin, "matrix");
assert_eq!(end, "pmatrix");
assert_eq!(pos, pos_of("\\begin{matrix} x \\end{pmatrix}", "\\end"));
let e = err("\\begin{matrix} x");
let Error::Unbalanced { pos } = e else { panic!("{e:?}") };
// The unclosed `{` opened at byte 6.
assert_eq!(pos, 6);
let e = err("\\begin{qmatrix} x \\end{qmatrix}");
let Error::Unknown { cmd, pos } = e else { panic!("{e:?}") };
assert_eq!(cmd, "qmatrix");
assert_eq!(pos, 0);
let e = err("\\begin x\\end x");
match e {
Error::Unexpected { what, .. } => assert_eq!(what, "\\begin environment name"),
other => panic!("{other:?}"),
}
}
#[test]
fn limits_are_bounded() {
// Depth: 65 nested groups exceed the limit.
let deep = "{".repeat(65);
let e = err(&deep);
let Error::DepthLimit { pos } = e else { panic!("{e:?}") };
assert_eq!(pos, 64);
// 63 deep with matching closes parses fine.
let mut ok = "{".repeat(63);
ok.push_str(&"}".repeat(63));
assert!(parse(&ok).is_ok(), "63 deep should parse");
}
#[test]
fn input_cap() {
let src = "x".repeat(20_001);
let e = err(&src);
let Error::TooLong { len } = e else { panic!("{e:?}") };
assert_eq!(len, 20_001);
let src = "x".repeat(20_000);
assert!(parse(&src).is_ok(), "exactly 20000 should parse");
}
#[test]
fn lone_and_escaped_tokens() {
// `\\` outside an environment: unknown command `\\`.
let e = err("\\\\");
let Error::Unknown { cmd, pos } = e else { panic!("{e:?}") };
assert_eq!(cmd, "\\");
assert_eq!(pos, 0);
// Empty input parses to an empty row.
match parse("") {
Ok(notescraft_math::Node::Row(list)) => assert_eq!(list, []),
other => panic!("{other:?}"),
}
}
#[test]
fn errors_do_not_consume_past_positions() {
// `αé`: the first non-ASCII source char is rejected (`Unexpected`), and the position
// is the BYTE offset: `α` spans 2 bytes, so `é`... wait, `α` itself errors first at
// byte 0. To prove byte offsets, consume multibytes through `\text{}` (legal), then
// error: the `\\` after `\text{a é}` is at byte 10 (char index 9 would be wrong).
match parse("αé") {
Err(Error::Unexpected { what, pos }) => {
assert_eq!(what, "α");
assert_eq!(pos, 0);
}
other => panic!("{other:?}"),
}
let src = "\\text{a é}\\\\foo";
match parse(src) {
Err(Error::Unknown { pos, cmd: _ }) => {
// Byte 11 (the `}` is byte 10, then the first `\\`); a char-index
// implementation would report 10.
assert_eq!(pos, pos_of(src, "\\\\foo"));
assert_eq!(pos, 11);
}
other => panic!("{other:?}"),
}
}
#[test]
fn named_operators_parse() {
for src in [
r"\log(xy)=\log x+\log y,\qquad \log\frac{x}{y}=\log x-\log y",
r"\log_b x=\frac{\ln x}{\ln b}",
r"\operatorname{Var}(X)=a\bmod b,\ a\equiv 1\pmod{n},\ \iint_D f\,dA\xrightarrow{d}N(0,1)",
r"\det\begin{pmatrix} a & b \\ c & d \end{pmatrix}=ad-bc",
] {
assert!(parse(src).is_ok(), "{src}");
}
}