Skip to main content

mdwright_lint/stdlib/
math_unbalanced_delim.rs

1//! `math/unbalanced-delim` — TeX-style math opener with no matching
2//! close.
3//!
4//! Mdwright recognises `\[ … \]`, `\( … \)`, `$$ … $$`, and `$ … $`.
5//! An open delimiter with no matching close is almost always a typo
6//! or a copy-paste accident: the rest of the document collapses into
7//! "math content" in the author's mental model, but pulldown-cmark
8//! parses it as prose and the document renders badly.
9//!
10//! Companion rule [`super::math_unbalanced_env::MathUnbalancedEnv`]
11//! covers `\begin{env}` / `\end{env}` imbalance.
12
13use crate::diagnostic::Diagnostic;
14use crate::rule::LintRule;
15use mdwright_document::Document;
16use mdwright_document::MathError;
17
18pub struct MathUnbalancedDelim;
19
20impl LintRule for MathUnbalancedDelim {
21    fn name(&self) -> &str {
22        "math/unbalanced-delim"
23    }
24
25    fn description(&self) -> &str {
26        "TeX-style math open delimiter (`\\[`, `\\(`, `$$`, `$`) with no matching close."
27    }
28
29    fn explain(&self) -> &str {
30        include_str!("explain/math__unbalanced_delim.md")
31    }
32
33    fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
34        for err in doc.math_errors() {
35            let MathError::UnbalancedDelim { delim, range } = err else {
36                continue;
37            };
38            let open_lit = delim.open();
39            let close_lit = delim.close();
40            let flavour = if delim.is_display() {
41                "display-math"
42            } else {
43                "inline-math"
44            };
45            let message = format!(
46                "unbalanced {flavour} `{open_lit}` — no matching `{close_lit}` before end of document or next code/HTML block"
47            );
48            if let Some(d) = Diagnostic::at(doc, 0, range.clone(), message, None) {
49                out.push(d);
50            }
51        }
52    }
53}