rumdl_lib/rules/md040_fenced_code_language/
md040_config.rs1use crate::linguist_data::{get_aliases, is_valid_alias, resolve_canonical};
2use crate::rule_config_serde::RuleConfig;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
8#[serde(rename_all = "kebab-case")]
9pub enum LanguageStyle {
10 #[default]
12 Disabled,
13 Consistent,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
19#[serde(rename_all = "kebab-case")]
20pub enum UnknownLanguageAction {
21 #[default]
23 Ignore,
24 Warn,
26 Error,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
32#[serde(rename_all = "kebab-case")]
33pub struct MD040Config {
34 #[serde(default)]
36 pub style: LanguageStyle,
37
38 #[serde(default, alias = "preferred_aliases")]
41 pub preferred_aliases: HashMap<String, String>,
42
43 #[serde(default, alias = "allowed_languages")]
46 pub allowed_languages: Vec<String>,
47
48 #[serde(default, alias = "disallowed_languages")]
51 pub disallowed_languages: Vec<String>,
52
53 #[serde(default, alias = "unknown_language_action")]
55 pub unknown_language_action: UnknownLanguageAction,
56
57 #[serde(default)]
60 pub custom_languages: Vec<String>,
61}
62
63impl MD040Config {
64 pub fn custom_language(&self, label: &str) -> Option<&str> {
66 self.custom_languages
67 .iter()
68 .find(|declared| declared.eq_ignore_ascii_case(label))
69 .map(String::as_str)
70 }
71
72 pub fn preferred_alias_problem(&self, language: &str, alias: &str) -> Option<String> {
78 if let Some(canonical) = resolve_canonical(language) {
79 if is_valid_alias(canonical, alias) {
80 return None;
81 }
82 let examples = get_aliases(canonical).map_or_else(String::new, |valid_aliases| {
83 let valid_str = valid_aliases
84 .iter()
85 .take(5)
86 .map(|s| format!("'{s}'"))
87 .collect::<Vec<_>>()
88 .join(", ");
89 let suffix = if valid_aliases.len() > 5 { ", ..." } else { "" };
90 format!(" Valid aliases include: {valid_str}{suffix}")
91 });
92 return Some(format!("Invalid alias '{alias}' for language '{canonical}'.{examples}"));
93 }
94
95 let Some(declared) = self.custom_language(language) else {
98 return Some(format!(
99 "Unknown language '{language}' in preferred-aliases. Use GitHub Linguist canonical names, or declare it in custom-languages."
100 ));
101 };
102 if alias.eq_ignore_ascii_case(declared) {
103 return None;
104 }
105 Some(format!(
106 "Invalid alias '{alias}' for custom language '{declared}'. A custom language has no aliases, so only a spelling of '{declared}' can be preferred."
107 ))
108 }
109
110 pub fn preferred_label(&self, language: &str) -> Option<&str> {
112 self.preferred_aliases
113 .iter()
114 .find(|(configured, _)| configured.eq_ignore_ascii_case(language))
115 .map(|(_, alias)| alias.as_str())
116 .filter(|alias| self.preferred_alias_problem(language, alias).is_none())
117 }
118}
119
120impl RuleConfig for MD040Config {
121 const RULE_NAME: &'static str = "MD040";
122}