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