use std::collections::HashMap; use std::io::Read; pub trait ConfigSource { fn walk(&self) -> Option<&dyn WalkSource> { None } fn default(&self) -> Option<&dyn FileSource> { None } } pub trait WalkSource { /// Search binary files. fn binary(&self) -> Option { None } /// The root for `ignore_patterns` fn ignore_root(&self) -> Option<&std::path::Path> { None } /// Ignore the specified patterns (gitignore syntax) fn ignore_patterns(&self) -> Option<&[String]> { None } /// Skip hidden files and directories. fn ignore_hidden(&self) -> Option { None } /// Respect ignore files. fn ignore_files(&self) -> Option { None } /// Respect .ignore files. fn ignore_dot(&self) -> Option { None } /// Respect ignore files in vcs directories. fn ignore_vcs(&self) -> Option { None } /// Respect global ignore files. fn ignore_global(&self) -> Option { None } /// Respect ignore files in parent directories. fn ignore_parent(&self) -> Option { None } } pub trait FileSource { /// Verifying spelling in file names. fn check_filename(&self) -> Option { None } /// Verifying spelling in files. fn check_file(&self) -> Option { None } /// Do not check identifiers that appear to be hexadecimal values. fn ignore_hex(&self) -> Option { None } /// Allow identifiers to start with digits, in addition to letters. fn identifier_leading_digits(&self) -> Option { None } /// Allow identifiers to start with one of these characters. fn identifier_leading_chars(&self) -> Option<&str> { None } /// Allow identifiers to include digits, in addition to letters. fn identifier_include_digits(&self) -> Option { None } /// Allow identifiers to include these characters. fn identifier_include_chars(&self) -> Option<&str> { None } fn locale(&self) -> Option { None } fn extend_identifiers(&self) -> Box + '_> { Box::new(None.into_iter()) } fn extend_words(&self) -> Box + '_> { Box::new(None.into_iter()) } } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, default)] #[serde(rename_all = "kebab-case")] pub struct Config { pub files: Walk, pub default: FileConfig, } impl Config { pub fn from_file(path: &std::path::Path) -> Result { let mut file = std::fs::File::open(path)?; let mut s = String::new(); file.read_to_string(&mut s)?; let mut c = Self::from_toml(&s)?; c.files.ignore_root = path.parent().map(|p| p.to_owned()); Ok(c) } pub fn from_toml(data: &str) -> Result { let content = toml::from_str(data)?; Ok(content) } pub fn derive(cwd: &std::path::Path) -> Result { if let Some(path) = find_project_file(cwd, &["typos.toml", "_typos.toml", ".typos.toml"]) { Self::from_file(&path) } else { Ok(Default::default()) } } pub fn update(&mut self, source: &dyn ConfigSource) { if let Some(walk) = source.walk() { self.files.update(walk); } if let Some(default) = source.default() { self.default.update(default); } } } impl ConfigSource for Config { fn walk(&self) -> Option<&dyn WalkSource> { Some(&self.files) } fn default(&self) -> Option<&dyn FileSource> { Some(&self.default) } } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, default)] #[serde(rename_all = "kebab-case")] pub struct Walk { pub binary: Option, #[serde(skip)] pub ignore_root: Option, pub ignore_patterns: Option>, pub ignore_hidden: Option, pub ignore_files: Option, pub ignore_dot: Option, pub ignore_vcs: Option, pub ignore_global: Option, pub ignore_parent: Option, } impl Walk { pub fn update(&mut self, source: &dyn WalkSource) { if let Some(source) = source.binary() { self.binary = Some(source); } if let (Some(root), Some(source)) = (source.ignore_root(), source.ignore_patterns()) { self.ignore_root = Some(root.to_owned()); self.ignore_patterns = Some(source.to_owned()); } if let Some(source) = source.ignore_hidden() { self.ignore_hidden = Some(source); } if let Some(source) = source.ignore_files() { self.ignore_files = Some(source); self.ignore_dot = None; self.ignore_vcs = None; self.ignore_global = None; self.ignore_parent = None; } if let Some(source) = source.ignore_dot() { self.ignore_dot = Some(source); } if let Some(source) = source.ignore_vcs() { self.ignore_vcs = Some(source); self.ignore_global = None; } if let Some(source) = source.ignore_global() { self.ignore_global = Some(source); } if let Some(source) = source.ignore_parent() { self.ignore_parent = Some(source); } } pub fn binary(&self) -> bool { self.binary.unwrap_or(false) } pub fn ignore_root(&self) -> Option<&std::path::Path> { self.ignore_root.as_deref() } pub fn ignore_patterns(&self) -> Option<&[String]> { self.ignore_patterns.as_deref() } pub fn ignore_hidden(&self) -> bool { self.ignore_hidden.unwrap_or(true) } pub fn ignore_dot(&self) -> bool { self.ignore_dot.or(self.ignore_files).unwrap_or(true) } pub fn ignore_vcs(&self) -> bool { self.ignore_vcs.or(self.ignore_files).unwrap_or(true) } pub fn ignore_global(&self) -> bool { self.ignore_global .or(self.ignore_vcs) .or(self.ignore_files) .unwrap_or(true) } pub fn ignore_parent(&self) -> bool { self.ignore_parent.or(self.ignore_files).unwrap_or(true) } } impl WalkSource for Walk { fn binary(&self) -> Option { self.binary } fn ignore_root(&self) -> Option<&std::path::Path> { self.ignore_root.as_deref() } fn ignore_patterns(&self) -> Option<&[String]> { self.ignore_patterns.as_deref() } fn ignore_hidden(&self) -> Option { self.ignore_hidden } fn ignore_files(&self) -> Option { self.ignore_files } fn ignore_dot(&self) -> Option { self.ignore_dot } fn ignore_vcs(&self) -> Option { self.ignore_vcs } fn ignore_global(&self) -> Option { self.ignore_global } fn ignore_parent(&self) -> Option { self.ignore_parent } } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, default)] #[serde(rename_all = "kebab-case")] pub struct FileConfig { pub check_filename: Option, pub check_file: Option, pub ignore_hex: Option, pub identifier_leading_digits: Option, pub identifier_leading_chars: Option, pub identifier_include_digits: Option, pub identifier_include_chars: Option, pub locale: Option, pub extend_identifiers: HashMap, pub extend_words: HashMap, } impl FileConfig { pub fn update(&mut self, source: &dyn FileSource) { if let Some(source) = source.check_filename() { self.check_filename = Some(source); } if let Some(source) = source.check_file() { self.check_file = Some(source); } if let Some(source) = source.ignore_hex() { self.ignore_hex = Some(source); } if let Some(source) = source.identifier_leading_digits() { self.identifier_leading_digits = Some(source); } if let Some(source) = source.identifier_leading_chars() { self.identifier_leading_chars = Some(source.to_owned()); } if let Some(source) = source.identifier_include_digits() { self.identifier_include_digits = Some(source); } if let Some(source) = source.identifier_include_chars() { self.identifier_include_chars = Some(source.to_owned()); } if let Some(source) = source.locale() { self.locale = Some(source); } self.extend_identifiers.extend( source .extend_identifiers() .map(|(k, v)| (k.to_owned(), v.to_owned())), ); self.extend_words.extend( source .extend_words() .map(|(k, v)| (k.to_owned(), v.to_owned())), ); } pub fn check_filename(&self) -> bool { self.check_filename.unwrap_or(true) } pub fn check_file(&self) -> bool { self.check_file.unwrap_or(true) } pub fn ignore_hex(&self) -> bool { self.ignore_hex.unwrap_or(true) } pub fn identifier_leading_digits(&self) -> bool { self.identifier_leading_digits.unwrap_or(false) } pub fn identifier_leading_chars(&self) -> &str { self.identifier_leading_chars.as_deref().unwrap_or("_") } pub fn identifier_include_digits(&self) -> bool { self.identifier_include_digits.unwrap_or(true) } pub fn identifier_include_chars(&self) -> &str { self.identifier_include_chars.as_deref().unwrap_or("_'") } pub fn locale(&self) -> Locale { self.locale.unwrap_or_default() } pub fn extend_identifiers(&self) -> Box + '_> { Box::new( self.extend_identifiers .iter() .map(|(k, v)| (k.as_str(), v.as_str())), ) } pub fn extend_words(&self) -> Box + '_> { Box::new( self.extend_words .iter() .map(|(k, v)| (k.as_str(), v.as_str())), ) } } impl FileSource for FileConfig { fn check_filename(&self) -> Option { self.check_filename } fn check_file(&self) -> Option { self.check_file } fn ignore_hex(&self) -> Option { self.ignore_hex } fn identifier_leading_digits(&self) -> Option { self.identifier_leading_digits } fn identifier_leading_chars(&self) -> Option<&str> { self.identifier_leading_chars.as_deref() } fn identifier_include_digits(&self) -> Option { self.identifier_include_digits } fn identifier_include_chars(&self) -> Option<&str> { self.identifier_include_chars.as_deref() } fn locale(&self) -> Option { self.locale } fn extend_identifiers(&self) -> Box + '_> { Box::new( self.extend_identifiers .iter() .map(|(k, v)| (k.as_str(), v.as_str())), ) } fn extend_words(&self) -> Box + '_> { Box::new( self.extend_words .iter() .map(|(k, v)| (k.as_str(), v.as_str())), ) } } fn find_project_file(dir: &std::path::Path, names: &[&str]) -> Option { for ancestor in dir.ancestors() { let mut file_path = ancestor.join("placeholder"); for name in names { file_path.set_file_name(name); if file_path.exists() { return Some(file_path); } } } None } #[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Locale { En, EnUs, EnGb, EnCa, EnAu, } impl Locale { pub fn category(self) -> Option { match self { Locale::En => None, Locale::EnUs => Some(typos_vars::Category::American), Locale::EnGb => Some(typos_vars::Category::BritishIse), Locale::EnCa => Some(typos_vars::Category::Canadian), Locale::EnAu => Some(typos_vars::Category::Australian), } } pub fn variants() -> [&'static str; 5] { ["en", "en-us", "en-gb", "en-ca", "en-au"] } } impl Default for Locale { fn default() -> Self { Locale::En } } impl std::str::FromStr for Locale { type Err = String; fn from_str(s: &str) -> std::result::Result { match s { "en" => Ok(Locale::En), "en-us" => Ok(Locale::EnUs), "en-gb" => Ok(Locale::EnGb), "en-ca" => Ok(Locale::EnCa), "en-au" => Ok(Locale::EnAu), _ => Err("valid values: en, en-us, en-gb, en-ca, en-au".to_owned()), } } } impl std::fmt::Display for Locale { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { Locale::En => write!(f, "en"), Locale::EnUs => write!(f, "en-us"), Locale::EnGb => write!(f, "en-gb"), Locale::EnCa => write!(f, "en-ca"), Locale::EnAu => write!(f, "en-au"), } } }