mdwright_lint/stdlib/
unicodeable_subscript.rs1use std::sync::OnceLock;
9
10use regex::Regex;
11
12use crate::diagnostic::{Diagnostic, Fix};
13use crate::regex_util::compile_static;
14use crate::rule::LintRule;
15use mdwright_document::Document;
16use mdwright_latex::{unicode_sub, unicode_super};
17
18pub struct UnicodeableSubscript;
19
20fn pattern() -> &'static Regex {
21 static RE: OnceLock<Regex> = OnceLock::new();
22 RE.get_or_init(|| {
23 compile_static(
24 r"\^\{-1\}|\^\{-(?P<sneg>[0-9])\}|\^\{(?P<sd>[0-9])\}|_\{(?P<bd>[0-9])\}|\^\{n\}|_\{n\}|\^\{i\}|_\{i\}",
25 )
26 })
27}
28
29impl LintRule for UnicodeableSubscript {
30 fn name(&self) -> &str {
31 "unicodeable-subscript"
32 }
33
34 fn description(&self) -> &str {
35 "Braced super/subscript that has a single-codepoint Unicode form."
36 }
37
38 fn explain(&self) -> &str {
39 include_str!("explain/unicodeable_subscript.md")
40 }
41
42 fn produces_fix(&self) -> bool {
43 true
44 }
45
46 fn is_advisory(&self) -> bool {
47 true
48 }
49
50 fn check(&self, doc: &Document, out: &mut Vec<Diagnostic>) {
51 for chunk in doc.prose_chunks() {
52 for cap in pattern().captures_iter(&chunk.text) {
53 let Some(m) = cap.get(0) else { continue };
54 let matched = m.as_str();
55 let replacement = match matched {
56 "^{-1}" => "⁻¹".to_owned(),
57 "^{n}" => "ⁿ".to_owned(),
58 "_{n}" => "ₙ".to_owned(),
59 "^{i}" => "ⁱ".to_owned(),
60 "_{i}" => "ᵢ".to_owned(),
61 _ => {
62 if let Some(d) = cap.name("sneg") {
63 let Some(c) = d.as_str().chars().next() else {
64 continue;
65 };
66 match unicode_super(c) {
67 Some(u) => format!("⁻{u}"),
68 None => continue,
69 }
70 } else if let Some(d) = cap.name("sd") {
71 let Some(c) = d.as_str().chars().next() else {
72 continue;
73 };
74 match unicode_super(c) {
75 Some(u) => u.to_string(),
76 None => continue,
77 }
78 } else if let Some(d) = cap.name("bd") {
79 let Some(c) = d.as_str().chars().next() else {
80 continue;
81 };
82 match unicode_sub(c) {
83 Some(u) => u.to_string(),
84 None => continue,
85 }
86 } else {
87 continue;
88 }
89 }
90 };
91 let message = format!("`{matched}` has a Unicode equivalent `{replacement}` — clearer to read");
92 if let Some(d) = Diagnostic::at(
93 doc,
94 chunk.byte_offset,
95 m.range(),
96 message,
97 Some(Fix {
98 replacement,
99 safe: true,
100 }),
101 ) {
102 out.push(d);
103 }
104 }
105 }
106 }
107}