1use std::sync::LazyLock;
2
3use crate::rule::Rule;
4
5use super::flavor::normalize_key;
6
7static DEFAULT_REGISTRY: LazyLock<RuleRegistry> = LazyLock::new(|| {
13 let default_config = super::types::Config::default();
14 let rules = crate::rules::all_rules(&default_config);
15 RuleRegistry::from_rules(&rules)
16});
17
18pub fn default_registry() -> &'static RuleRegistry {
24 &DEFAULT_REGISTRY
25}
26
27pub struct RuleRegistry {
29 pub rule_schemas: std::collections::BTreeMap<String, toml::map::Map<String, toml::Value>>,
31 pub rule_aliases: std::collections::BTreeMap<String, std::collections::HashMap<String, String>>,
33}
34
35impl RuleRegistry {
36 pub fn from_rules(rules: &[Box<dyn Rule>]) -> Self {
38 let mut rule_schemas = std::collections::BTreeMap::new();
39 let mut rule_aliases = std::collections::BTreeMap::new();
40
41 for rule in rules {
42 let norm_name = if let Some((name, toml::Value::Table(table))) = rule.default_config_section() {
43 let norm_name = normalize_key(&name); rule_schemas.insert(norm_name.clone(), table);
45 norm_name
46 } else {
47 let norm_name = normalize_key(rule.name()); rule_schemas.insert(norm_name.clone(), toml::map::Map::new());
49 norm_name
50 };
51
52 if let Some(aliases) = rule.config_aliases() {
54 rule_aliases.insert(norm_name, aliases);
55 }
56 }
57
58 RuleRegistry {
59 rule_schemas,
60 rule_aliases,
61 }
62 }
63
64 pub fn rule_names(&self) -> std::collections::BTreeSet<String> {
66 self.rule_schemas.keys().cloned().collect()
67 }
68
69 pub fn config_keys_for(&self, rule: &str) -> Option<std::collections::BTreeSet<String>> {
71 self.rule_schemas.get(rule).map(|schema| {
72 let mut all_keys = std::collections::BTreeSet::new();
73
74 all_keys.insert("severity".to_string());
76
77 for key in schema.keys() {
79 all_keys.insert(key.clone());
80 }
81
82 for key in schema.keys() {
84 all_keys.insert(key.replace('_', "-"));
86 all_keys.insert(key.replace('-', "_"));
88 all_keys.insert(normalize_key(key));
90 }
91
92 if let Some(aliases) = self.rule_aliases.get(rule) {
94 for alias_key in aliases.keys() {
95 all_keys.insert(alias_key.clone());
96 all_keys.insert(alias_key.replace('_', "-"));
98 all_keys.insert(alias_key.replace('-', "_"));
99 all_keys.insert(normalize_key(alias_key));
100 }
101 }
102
103 all_keys
104 })
105 }
106
107 pub fn expected_value_for(&self, rule: &str, key: &str) -> Option<&toml::Value> {
109 if let Some(schema) = self.rule_schemas.get(rule) {
110 if let Some(aliases) = self.rule_aliases.get(rule)
112 && let Some(canonical_key) = aliases.get(key)
113 {
114 if let Some(value) = schema.get(canonical_key) {
116 return Some(value);
117 }
118 }
119
120 if let Some(value) = schema.get(key) {
122 return Some(value);
123 }
124
125 let key_variants = [
127 key.replace('-', "_"), key.replace('_', "-"), normalize_key(key), ];
131
132 for variant in &key_variants {
133 if let Some(value) = schema.get(variant) {
134 return Some(value);
135 }
136 }
137 }
138 None
139 }
140
141 pub fn resolve_rule_name(&self, name: &str) -> Option<String> {
148 let normalized = normalize_key(name);
150 if self.rule_schemas.contains_key(&normalized) {
151 return Some(normalized);
152 }
153
154 resolve_rule_name_alias(name).map(|s| s.to_string())
156 }
157}
158
159pub static RULE_ALIAS_MAP: phf::Map<&'static str, &'static str> = phf::phf_map! {
162 "MD001" => "MD001",
164 "MD003" => "MD003",
165 "MD004" => "MD004",
166 "MD005" => "MD005",
167 "MD007" => "MD007",
168 "MD009" => "MD009",
169 "MD010" => "MD010",
170 "MD011" => "MD011",
171 "MD012" => "MD012",
172 "MD013" => "MD013",
173 "MD014" => "MD014",
174 "MD018" => "MD018",
175 "MD019" => "MD019",
176 "MD020" => "MD020",
177 "MD021" => "MD021",
178 "MD022" => "MD022",
179 "MD023" => "MD023",
180 "MD024" => "MD024",
181 "MD025" => "MD025",
182 "MD026" => "MD026",
183 "MD027" => "MD027",
184 "MD028" => "MD028",
185 "MD029" => "MD029",
186 "MD030" => "MD030",
187 "MD031" => "MD031",
188 "MD032" => "MD032",
189 "MD033" => "MD033",
190 "MD034" => "MD034",
191 "MD035" => "MD035",
192 "MD036" => "MD036",
193 "MD037" => "MD037",
194 "MD038" => "MD038",
195 "MD039" => "MD039",
196 "MD040" => "MD040",
197 "MD041" => "MD041",
198 "MD042" => "MD042",
199 "MD043" => "MD043",
200 "MD044" => "MD044",
201 "MD045" => "MD045",
202 "MD046" => "MD046",
203 "MD047" => "MD047",
204 "MD048" => "MD048",
205 "MD049" => "MD049",
206 "MD050" => "MD050",
207 "MD051" => "MD051",
208 "MD052" => "MD052",
209 "MD053" => "MD053",
210 "MD054" => "MD054",
211 "MD055" => "MD055",
212 "MD056" => "MD056",
213 "MD057" => "MD057",
214 "MD058" => "MD058",
215 "MD059" => "MD059",
216 "MD060" => "MD060",
217 "MD061" => "MD061",
218 "MD062" => "MD062",
219 "MD063" => "MD063",
220 "MD064" => "MD064",
221 "MD065" => "MD065",
222 "MD066" => "MD066",
223 "MD067" => "MD067",
224 "MD068" => "MD068",
225 "MD069" => "MD069",
226 "MD070" => "MD070",
227 "MD071" => "MD071",
228 "MD072" => "MD072",
229 "MD073" => "MD073",
230 "MD074" => "MD074",
231
232 "HEADING-INCREMENT" => "MD001",
234 "HEADING-STYLE" => "MD003",
235 "UL-STYLE" => "MD004",
236 "LIST-INDENT" => "MD005",
237 "UL-INDENT" => "MD007",
238 "NO-TRAILING-SPACES" => "MD009",
239 "NO-HARD-TABS" => "MD010",
240 "NO-REVERSED-LINKS" => "MD011",
241 "NO-MULTIPLE-BLANKS" => "MD012",
242 "LINE-LENGTH" => "MD013",
243 "COMMANDS-SHOW-OUTPUT" => "MD014",
244 "NO-MISSING-SPACE-ATX" => "MD018",
245 "NO-MULTIPLE-SPACE-ATX" => "MD019",
246 "NO-MISSING-SPACE-CLOSED-ATX" => "MD020",
247 "NO-MULTIPLE-SPACE-CLOSED-ATX" => "MD021",
248 "BLANKS-AROUND-HEADINGS" => "MD022",
249 "HEADING-START-LEFT" => "MD023",
250 "NO-DUPLICATE-HEADING" => "MD024",
251 "SINGLE-TITLE" => "MD025",
252 "SINGLE-H1" => "MD025",
253 "NO-TRAILING-PUNCTUATION" => "MD026",
254 "NO-MULTIPLE-SPACE-BLOCKQUOTE" => "MD027",
255 "NO-BLANKS-BLOCKQUOTE" => "MD028",
256 "OL-PREFIX" => "MD029",
257 "LIST-MARKER-SPACE" => "MD030",
258 "BLANKS-AROUND-FENCES" => "MD031",
259 "BLANKS-AROUND-LISTS" => "MD032",
260 "NO-INLINE-HTML" => "MD033",
261 "NO-BARE-URLS" => "MD034",
262 "HR-STYLE" => "MD035",
263 "NO-EMPHASIS-AS-HEADING" => "MD036",
264 "NO-SPACE-IN-EMPHASIS" => "MD037",
265 "NO-SPACE-IN-CODE" => "MD038",
266 "NO-SPACE-IN-LINKS" => "MD039",
267 "FENCED-CODE-LANGUAGE" => "MD040",
268 "FIRST-LINE-HEADING" => "MD041",
269 "FIRST-LINE-H1" => "MD041",
270 "NO-EMPTY-LINKS" => "MD042",
271 "REQUIRED-HEADINGS" => "MD043",
272 "PROPER-NAMES" => "MD044",
273 "NO-ALT-TEXT" => "MD045",
274 "CODE-BLOCK-STYLE" => "MD046",
275 "SINGLE-TRAILING-NEWLINE" => "MD047",
276 "CODE-FENCE-STYLE" => "MD048",
277 "EMPHASIS-STYLE" => "MD049",
278 "STRONG-STYLE" => "MD050",
279 "LINK-FRAGMENTS" => "MD051",
280 "REFERENCE-LINKS-IMAGES" => "MD052",
281 "LINK-IMAGE-REFERENCE-DEFINITIONS" => "MD053",
282 "LINK-IMAGE-STYLE" => "MD054",
283 "TABLE-PIPE-STYLE" => "MD055",
284 "TABLE-COLUMN-COUNT" => "MD056",
285 "EXISTING-RELATIVE-LINKS" => "MD057",
286 "BLANKS-AROUND-TABLES" => "MD058",
287 "DESCRIPTIVE-LINK-TEXT" => "MD059",
288 "TABLE-CELL-ALIGNMENT" => "MD060",
289 "TABLE-FORMAT" => "MD060",
290 "FORBIDDEN-TERMS" => "MD061",
291 "LINK-DESTINATION-WHITESPACE" => "MD062",
292 "HEADING-CAPITALIZATION" => "MD063",
293 "NO-MULTIPLE-CONSECUTIVE-SPACES" => "MD064",
294 "BLANKS-AROUND-HORIZONTAL-RULES" => "MD065",
295 "FOOTNOTE-VALIDATION" => "MD066",
296 "FOOTNOTE-DEFINITION-ORDER" => "MD067",
297 "EMPTY-FOOTNOTE-DEFINITION" => "MD068",
298 "NO-DUPLICATE-LIST-MARKERS" => "MD069",
299 "NESTED-CODE-FENCE" => "MD070",
300 "BLANK-LINE-AFTER-FRONTMATTER" => "MD071",
301 "FRONTMATTER-KEY-SORT" => "MD072",
302 "TOC-VALIDATION" => "MD073",
303 "MKDOCS-NAV" => "MD074",
304};
305
306pub fn resolve_rule_name_alias(key: &str) -> Option<&'static str> {
310 let normalized_key = key.to_ascii_uppercase().replace('_', "-");
312
313 RULE_ALIAS_MAP.get(normalized_key.as_str()).copied()
315}
316
317pub fn resolve_rule_name(name: &str) -> String {
325 resolve_rule_name_alias(name)
326 .map(|s| s.to_string())
327 .unwrap_or_else(|| normalize_key(name))
328}
329
330pub fn resolve_rule_names(input: &str) -> std::collections::HashSet<String> {
334 input
335 .split(',')
336 .map(|s| s.trim())
337 .filter(|s| !s.is_empty())
338 .map(resolve_rule_name)
339 .collect()
340}
341
342pub fn is_valid_rule_name(name: &str) -> bool {
346 if name.eq_ignore_ascii_case("all") {
348 return true;
349 }
350 resolve_rule_name_alias(name).is_some()
351}