mdwright_lint/stdlib/
escaped_emphasis.rs1use std::sync::OnceLock;
14
15use regex::Regex;
16
17use crate::diagnostic::{Diagnostic, Fix};
18use crate::regex_util::compile_static;
19use crate::rule::LintRule;
20use mdwright_document::Document;
21
22pub struct EscapedEmphasis;
23
24fn pattern() -> &'static Regex {
25 static RE: OnceLock<Regex> = OnceLock::new();
26 RE.get_or_init(|| compile_static(r"\\[_*`]"))
27}
28
29impl LintRule for EscapedEmphasis {
30 fn name(&self) -> &str {
31 "escaped-emphasis"
32 }
33
34 fn description(&self) -> &str {
35 "Literal `\\_`, `\\*`, or `` \\` `` escape in prose (mdformat damage)."
36 }
37
38 fn explain(&self) -> &str {
39 include_str!("explain/escaped_emphasis.md")
40 }
41
42 fn produces_fix(&self) -> bool {
43 true
44 }
45
46 fn is_default(&self) -> bool {
47 false
48 }
49
50 fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
51 for chunk in doc.prose_chunks() {
52 for m in pattern().find_iter(&chunk.text) {
53 let target = m.as_str().as_bytes().get(1).copied();
54 let (fix_char, label) = match target {
55 Some(b'_') => ("*", r"\_"),
56 Some(b'*') => ("*", r"\*"),
57 Some(b'`') => ("`", r"\`"),
58 _ => continue,
59 };
60 let message = format!(
61 "`{label}` escape — likely italic-delimiter damage from a previous \
62 formatter pass; prefer `*…*` for italics, never `_…_`"
63 );
64 if let Some(d) = Diagnostic::at(
65 doc,
66 chunk.byte_offset,
67 m.range(),
68 message,
69 Some(Fix {
70 replacement: fix_char.to_owned(),
71 safe: true,
72 }),
73 ) {
74 out.push(d);
75 }
76 }
77 }
78 }
79}