use badness::parser::parse;
use badness::syntax::SyntaxNode;
use rowan::NodeOrToken;
fn tree(input: &str) -> String {
let parsed = parse(input);
assert_eq!(
parsed.syntax().to_string(),
input,
"losslessness violated for {input:?}"
);
let mut out = String::new();
render(&parsed.syntax(), 0, &mut out);
for err in &parsed.errors {
out.push_str(&format!(
"error @{}..{}: {}\n",
err.start, err.end, err.message
));
}
out
}
fn render(node: &SyntaxNode, depth: usize, out: &mut String) {
out.push_str(&format!(
"{:indent$}{:?}@{:?}\n",
"",
node.kind(),
node.text_range(),
indent = depth * 2
));
for child in node.children_with_tokens() {
match child {
NodeOrToken::Node(n) => render(&n, depth + 1, out),
NodeOrToken::Token(t) => out.push_str(&format!(
"{:indent$}{:?}@{:?} {:?}\n",
"",
t.kind(),
t.text_range(),
t.text(),
indent = (depth + 1) * 2
)),
}
}
}
#[test]
fn command_with_required_and_optional_args() {
insta::assert_snapshot!(tree(r"\cmd[opt]{req}"));
}
#[test]
fn nested_groups() {
insta::assert_snapshot!(tree(r"{a {b} c}"));
}
#[test]
fn environment_with_body() {
insta::assert_snapshot!(tree("\\begin{itemize}\n\\item x\n\\end{itemize}"));
}
#[test]
fn inline_and_display_math() {
insta::assert_snapshot!(tree(r"$x^2$ and \[ y_i \]"));
}
#[test]
fn display_math_dollars() {
insta::assert_snapshot!(tree(r"$$a + b$$"));
}
#[test]
fn paragraphs_split_on_blank_lines() {
insta::assert_snapshot!(tree("First line,\nsame paragraph.\n\nSecond paragraph."));
}
#[test]
fn verbatim_environment_is_opaque() {
insta::assert_snapshot!(tree(
"\\begin{verbatim}\n\\notacommand $x$ %literal\n\\end{verbatim}"
));
}
#[test]
fn inline_verb_is_a_single_token() {
insta::assert_snapshot!(tree(r"text \verb|$x$| more"));
}
#[test]
fn makeatletter_control_word_with_at() {
insta::assert_snapshot!(tree(r"\makeatletter\foo@bar\makeatother"));
}
#[test]
fn line_break_groups_star_and_optional_length() {
insta::assert_snapshot!(tree(r"a \\ b \\* c \\[2ex] d \\*[2ex] e \\"));
}
#[test]
fn line_break_does_not_cross_trivia_for_its_optional() {
insta::assert_snapshot!(tree("row \\\\\n[x] next"));
}
#[test]
fn environment_mismatch_recovers() {
insta::assert_snapshot!(tree(r"\begin{a}\begin{b}\end{a}"));
}
#[test]
fn unmatched_closing_brace() {
insta::assert_snapshot!(tree("a } b"));
}
#[test]
fn unclosed_environment_at_eof() {
insta::assert_snapshot!(tree(r"\begin{proof} text"));
}
#[test]
fn stray_end_at_top_level() {
let parsed = parse(r"\end{itemize}");
assert_eq!(parsed.errors.len(), 1);
assert!(parsed.errors[0].message.contains("without matching"));
assert_eq!(parsed.syntax().to_string(), r"\end{itemize}");
}
#[test]
fn unclosed_dollar_math_in_group_does_not_escape() {
let parsed = parse("\\begin{a}\\code{$ x}\\end{a}");
assert_eq!(parsed.syntax().to_string(), "\\begin{a}\\code{$ x}\\end{a}");
let messages: Vec<&str> = parsed.errors.iter().map(|e| e.message.as_str()).collect();
assert_eq!(messages, ["unclosed `$`"], "only the open math is reported");
}
#[test]
fn nested_mismatch_unwinds_to_two_errors() {
let parsed = parse(r"\begin{a}\begin{b}\end{a}");
let unclosed = parsed
.errors
.iter()
.filter(|e| e.message.contains("unclosed environment"))
.count();
assert_eq!(unclosed, 1, "only `b` is unclosed; `a` matches");
}