use crate::linter::diagnostics::{Diagnostic, Location};
use crate::linter::rules::{DiagnosticCode, LintContext, Requirement, Rule, RuleMeta};
use crate::syntax::{MathDiagnosticKind, SyntaxKind, math_diagnostics};
pub struct MathContentRule;
impl Rule for MathContentRule {
fn name(&self) -> &str {
"math-syntax"
}
fn metadata(&self) -> RuleMeta {
RuleMeta {
name: "math-syntax",
default_on: true,
requires: Requirement::TexMath,
auto_fix: false,
codes: const {
&[
DiagnosticCode::error("math-unclosed-group"),
DiagnosticCode::error("math-unexpected-close-brace"),
DiagnosticCode::error("math-unclosed-environment"),
DiagnosticCode::error("math-mismatched-environment"),
DiagnosticCode::error("math-unexpected-end"),
DiagnosticCode::error("math-unclosed-delimiter"),
DiagnosticCode::error("math-unexpected-right"),
]
},
}
}
fn node_interests(&self) -> &'static [SyntaxKind] {
&[SyntaxKind::MATH_CONTENT]
}
fn check(&self, cx: &LintContext) -> Vec<Diagnostic> {
let input = cx.input;
let mut out = Vec::new();
for content in cx.nodes(SyntaxKind::MATH_CONTENT) {
for d in math_diagnostics(content) {
let (code, message) = describe(d.kind);
out.push(Diagnostic::error(
Location::from_range(d.range, input),
code,
message,
));
}
}
out
}
}
fn describe(kind: MathDiagnosticKind) -> (&'static str, &'static str) {
match kind {
MathDiagnosticKind::UnclosedGroup => ("math-unclosed-group", "unclosed `{` group"),
MathDiagnosticKind::UnexpectedCloseBrace => {
("math-unexpected-close-brace", "unmatched closing brace `}`")
}
MathDiagnosticKind::UnclosedEnvironment => (
"math-unclosed-environment",
r"`\begin` without a matching `\end`",
),
MathDiagnosticKind::MismatchedEnvironment => (
"math-mismatched-environment",
r"`\end` name does not match the open `\begin`",
),
MathDiagnosticKind::UnexpectedEnd => {
("math-unexpected-end", r"`\end` without a matching `\begin`")
}
MathDiagnosticKind::UnclosedDelimiter => (
"math-unclosed-delimiter",
r"`\left` without a matching `\right`",
),
MathDiagnosticKind::UnexpectedRight => (
"math-unexpected-right",
r"`\right` without a matching `\left`",
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
fn parse_and_lint(input: &str) -> Vec<Diagnostic> {
let mut config = Config::default();
config.extensions.tex_math_dollars = true;
let tree = crate::parser::parse(input, Some(config.clone()));
MathContentRule.check_tree(&tree, input, &config, None)
}
fn codes(diags: &[Diagnostic]) -> Vec<&str> {
diags.iter().map(|d| d.code.as_str()).collect()
}
#[test]
fn flags_unclosed_group() {
let input = "$\\frac{1}{2$\n";
let diags = parse_and_lint(input);
assert_eq!(codes(&diags), vec!["math-unclosed-group"]);
let start: usize = diags[0].location.range.start().into();
assert_eq!(&input[start..start + 1], "{");
}
#[test]
fn flags_stray_close_brace() {
let diags = parse_and_lint("$a}b$\n");
assert_eq!(codes(&diags), vec!["math-unexpected-close-brace"]);
}
#[test]
fn flags_unclosed_environment() {
let diags = parse_and_lint("$$\n\\begin{aligned} x &= 1\n$$\n");
assert_eq!(codes(&diags), vec!["math-unclosed-environment"]);
}
#[test]
fn flags_mismatched_environment() {
let diags = parse_and_lint("$$\\begin{aligned}x\\end{matrix}$$\n");
assert_eq!(codes(&diags), vec!["math-mismatched-environment"]);
}
#[test]
fn flags_stray_end() {
let diags = parse_and_lint("$x \\end{aligned}$\n");
assert_eq!(codes(&diags), vec!["math-unexpected-end"]);
}
#[test]
fn well_formed_math_is_clean() {
let diags = parse_and_lint("$\\frac{1}{2} + x^{2}$\n");
assert!(diags.is_empty());
}
#[test]
fn nested_environments_are_clean() {
let diags = parse_and_lint("$$\\begin{a}\\begin{b}x\\end{b}\\end{a}$$\n");
assert!(diags.is_empty(), "well-formed nesting: {:?}", codes(&diags));
}
#[test]
fn non_math_document_is_clean() {
let config = Config::default();
let input = "# Heading\n\nText with `\\end{x}` in a code span.\n";
let tree = crate::parser::parse(input, Some(config.clone()));
let diags = MathContentRule.check_tree(&tree, input, &config, None);
assert!(diags.is_empty());
}
#[test]
fn blockquote_display_math_maps_host_range() {
let input = "> $$\n> \\frac{1\n> $$\n";
let diags = parse_and_lint(input);
assert_eq!(codes(&diags), vec!["math-unclosed-group"]);
let start: usize = diags[0].location.range.start().into();
assert_eq!(&input[start..start + 1], "{");
}
}