mdwright_lint/stdlib/
latex_command.rs1use std::sync::OnceLock;
10
11use regex::Regex;
12
13use crate::diagnostic::{Diagnostic, Fix};
14use crate::regex_util::compile_static;
15use crate::rule::LintRule;
16use mdwright_document::Document;
17use mdwright_latex::latex_symbol;
18
19pub struct LatexCommand;
20
21fn pattern() -> &'static Regex {
22 static RE: OnceLock<Regex> = OnceLock::new();
23 RE.get_or_init(|| compile_static(r"\\\\?([A-Za-z]+)(?:\{[^}]*\})?"))
24}
25
26impl LintRule for LatexCommand {
27 fn name(&self) -> &str {
28 "latex-command"
29 }
30
31 fn description(&self) -> &str {
32 "LaTeX control sequence in prose (opt-in for Unicode-math projects)."
33 }
34
35 fn explain(&self) -> &str {
36 include_str!("explain/latex_command.md")
37 }
38
39 fn produces_fix(&self) -> bool {
40 true
41 }
42
43 fn is_default(&self) -> bool {
44 false
45 }
46
47 fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
48 let math = doc.math_regions();
49 for chunk in doc.prose_chunks() {
50 for cap in pattern().captures_iter(&chunk.text) {
51 let Some(m) = cap.get(0) else { continue };
52 let Some(name_match) = cap.get(1) else {
53 continue;
54 };
55 let abs_start = chunk.byte_offset.saturating_add(m.start());
56 let abs_end = chunk.byte_offset.saturating_add(m.end());
57 if math
62 .iter()
63 .any(|r| r.range.start <= abs_start && abs_end <= r.range.end)
64 {
65 continue;
66 }
67 let name = name_match.as_str();
68 let fix = latex_symbol(name).map(|u| Fix {
69 replacement: u.to_owned(),
70 safe: true,
71 });
72 let message = format!(
73 "LaTeX command `\\{name}` in prose — replace with Unicode math; \
74 this project does not render LaTeX"
75 );
76 if let Some(d) = Diagnostic::at(doc, chunk.byte_offset, m.range(), message, fix) {
77 out.push(d);
78 }
79 }
80 }
81 }
82}