|
5 | 5 | //! sets so runtime checks stay cheap and platform-neutral. |
6 | 6 |
|
7 | 7 | use std::collections::HashSet; |
| 8 | +use std::path::Path; |
8 | 9 | use std::sync::OnceLock; |
9 | 10 |
|
10 | 11 | pub const RU_HUNSPELL: &str = "/usr/share/hunspell/ru_RU.dic"; |
@@ -37,6 +38,7 @@ pub fn warm_up() { |
37 | 38 | let _ = ru_hyphen_particles().len(); |
38 | 39 | let _ = visual_b_default_replacement(); |
39 | 40 | let _ = visual_b_after_ascii_replacement(); |
| 41 | + let _ = user_protected_ascii_words().len(); |
40 | 42 | } |
41 | 43 |
|
42 | 44 | pub fn is_common_ru_word(word: &str) -> bool { |
@@ -79,10 +81,39 @@ pub fn visual_b_after_ascii_replacement() -> &'static str { |
79 | 81 | first_data_word(VISUAL_B_AFTER_ASCII_DATA) |
80 | 82 | } |
81 | 83 |
|
| 84 | +pub fn is_user_protected_ascii_word(word: &str) -> bool { |
| 85 | + if !word.is_ascii() { |
| 86 | + return false; |
| 87 | + } |
| 88 | + user_protected_ascii_words().contains(&word.to_ascii_lowercase()) |
| 89 | +} |
| 90 | + |
| 91 | +pub fn extend_user_protected_ascii_words(words: &mut HashSet<String>, min_chars: usize) { |
| 92 | + words.extend( |
| 93 | + user_protected_ascii_words() |
| 94 | + .iter() |
| 95 | + .filter(|word| word.chars().count() >= min_chars) |
| 96 | + .cloned(), |
| 97 | + ); |
| 98 | +} |
| 99 | + |
82 | 100 | pub fn extend_common_ru_words(words: &mut HashSet<String>) { |
83 | 101 | words.extend(common_ru_words().iter().cloned()); |
84 | 102 | } |
85 | 103 |
|
| 104 | +fn user_protected_ascii_words() -> &'static HashSet<String> { |
| 105 | + static WORDS: OnceLock<HashSet<String>> = OnceLock::new(); |
| 106 | + WORDS.get_or_init(|| { |
| 107 | + let Some(home) = std::env::var_os("HOME") else { |
| 108 | + return HashSet::new(); |
| 109 | + }; |
| 110 | + let path = std::path::PathBuf::from(home).join(PROTECTED_WORDS_PATH); |
| 111 | + load_plain_words(&path) |
| 112 | + .map(|words| ascii_words_from_iter(words, 1)) |
| 113 | + .unwrap_or_default() |
| 114 | + }) |
| 115 | +} |
| 116 | + |
86 | 117 | fn common_ru_words() -> &'static HashSet<String> { |
87 | 118 | static WORDS: OnceLock<HashSet<String>> = OnceLock::new(); |
88 | 119 | WORDS.get_or_init(|| parse_word_data(COMMON_RU_DATA)) |
@@ -131,6 +162,39 @@ fn parse_word_data(data: &str) -> HashSet<String> { |
131 | 162 | .collect() |
132 | 163 | } |
133 | 164 |
|
| 165 | +#[cfg(test)] |
| 166 | +pub(crate) fn parse_ascii_word_data(data: &str, min_chars: usize) -> HashSet<String> { |
| 167 | + ascii_words_from_iter( |
| 168 | + data.lines() |
| 169 | + .map(str::trim) |
| 170 | + .filter(|line| !line.is_empty() && !line.starts_with('#')) |
| 171 | + .map(str::to_string), |
| 172 | + min_chars, |
| 173 | + ) |
| 174 | +} |
| 175 | + |
| 176 | +fn ascii_words_from_iter<I>(words: I, min_chars: usize) -> HashSet<String> |
| 177 | +where |
| 178 | + I: IntoIterator<Item = String>, |
| 179 | +{ |
| 180 | + words |
| 181 | + .into_iter() |
| 182 | + .map(|word| word.trim().to_ascii_lowercase()) |
| 183 | + .filter(|word| word.chars().count() >= min_chars) |
| 184 | + .filter(|word| { |
| 185 | + word.is_ascii() |
| 186 | + && word |
| 187 | + .chars() |
| 188 | + .all(|ch| ch.is_ascii_alphabetic() || ch == '-' || ch == '_') |
| 189 | + }) |
| 190 | + .collect() |
| 191 | +} |
| 192 | + |
| 193 | +fn load_plain_words(path: &Path) -> std::io::Result<HashSet<String>> { |
| 194 | + let text = std::fs::read_to_string(path)?; |
| 195 | + Ok(parse_word_data(&text)) |
| 196 | +} |
| 197 | + |
134 | 198 | fn first_data_word(data: &'static str) -> &'static str { |
135 | 199 | data.lines() |
136 | 200 | .map(str::trim) |
|
0 commit comments