Skip to main content

cargo_spellcheck/config/
hunspell.rs

1//! Hunspell checker configuration.
2
3use super::{Lang5, SearchDirs, WrappedRegex};
4use std::path::{Path, PathBuf};
5
6use crate::errors::*;
7
8use serde::{Deserialize, Serialize};
9
10const fn yes() -> bool {
11    true
12}
13
14#[derive(Deserialize, Serialize, Debug, Clone)]
15pub struct Quirks {
16    /// A regular expression, whose capture groups will be checked, instead of
17    /// the initial token. Only the first one that matches will be used to split
18    /// the word.
19    #[serde(default)]
20    pub transform_regex: Vec<WrappedRegex>,
21    /// Allow concatenated words instead of dashed connection. Note that this
22    /// only applies, if one of the suggested replacements has an item that is
23    /// equivalent except for addition dashes (`-`).
24    #[serde(default)]
25    pub allow_concatenation: bool,
26    /// The counterpart of `allow_concatenation`. Accepts words which have
27    /// replacement suggestions that contain additional dashes.
28    #[serde(default)]
29    pub allow_dashes: bool,
30    /// Treats sequences of emojis as OK.
31    #[serde(default = "yes")]
32    pub allow_emojis: bool,
33    /// Check the expressions in the footnote references. By default this is
34    /// turned on to remain backwards compatible but disabling it could be
35    /// particularly useful when one uses abbreviations instead of numbers as
36    /// footnote references.  For instance by default the fragment `hello[^xyz]`
37    /// would be spellchecked as `helloxyz` which is obviously a misspelled
38    /// word, but by turning this check off, it will skip validating the
39    /// reference altogether and will only check the word `hello`.
40    #[serde(default = "yes")]
41    pub check_footnote_references: bool,
42}
43
44impl Default for Quirks {
45    fn default() -> Self {
46        Self {
47            transform_regex: Vec::new(),
48            allow_concatenation: false,
49            allow_dashes: false,
50            allow_emojis: true,
51            check_footnote_references: true,
52        }
53    }
54}
55
56impl Quirks {
57    pub(crate) const fn allow_concatenated(&self) -> bool {
58        self.allow_concatenation
59    }
60
61    pub(crate) const fn allow_dashed(&self) -> bool {
62        self.allow_dashes
63    }
64
65    pub(crate) const fn allow_emojis(&self) -> bool {
66        self.allow_emojis
67    }
68
69    pub(crate) fn transform_regex(&self) -> &[WrappedRegex] {
70        &self.transform_regex
71    }
72
73    pub(crate) fn check_footnote_references(&self) -> bool {
74        self.check_footnote_references
75    }
76}
77
78fn default_tokenization_splitchars() -> String {
79    "\",;:.!?#(){}[]|/_-‒'`&@§¶…".to_owned()
80}
81
82pub type ZetConfig = HunspellConfig;
83pub type SpellbookConfig = HunspellConfig;
84
85#[derive(Deserialize, Serialize, Debug, Clone)]
86#[serde(deny_unknown_fields)]
87pub struct HunspellConfig {
88    /// The language we want to check against, used as the dictionary and
89    /// affixes file name.
90    #[serde(default)]
91    pub lang: Lang5,
92    /// Additional search directories for `.dic` and `.aff` files.
93    // must be option so it can be omitted in the config
94    #[serde(default)]
95    pub search_dirs: SearchDirs,
96
97    /// Avoid the OS provided dictionaries and only use the builtin ones,
98    /// besides those defined in `extra_dictionaries`.
99    #[serde(default)]
100    pub skip_os_lookups: bool,
101
102    /// Use the builtin dictionaries as last resort. Usually combined with
103    /// `skip_os_lookups=true` to enforce the `builtin` usage. Does not prevent
104    /// the usage of `extra_dictionaries`.
105    #[serde(default)]
106    pub use_builtin: bool,
107
108    #[serde(default = "default_tokenization_splitchars")]
109    pub tokenization_splitchars: String,
110
111    /// Additional dictionaries for topic specific lingo.
112    #[serde(default)]
113    pub extra_dictionaries: Vec<PathBuf>,
114    /// Additional quirks besides dictionary lookups.
115    #[serde(default)]
116    pub quirks: Quirks,
117}
118
119impl Default for HunspellConfig {
120    fn default() -> Self {
121        Self {
122            lang: Lang5::en_US,
123            search_dirs: SearchDirs::default(),
124            extra_dictionaries: Vec::default(),
125            quirks: Quirks::default(),
126            tokenization_splitchars: default_tokenization_splitchars(),
127            skip_os_lookups: false,
128            use_builtin: true,
129        }
130    }
131}
132
133impl HunspellConfig {
134    pub fn lang(&self) -> Lang5 {
135        self.lang
136    }
137
138    pub fn search_dirs(&self) -> impl Iterator<Item = &PathBuf> {
139        self.search_dirs.iter(!self.skip_os_lookups)
140    }
141
142    pub fn extra_dictionaries(&self) -> impl Iterator<Item = &PathBuf> {
143        self.extra_dictionaries.iter()
144    }
145
146    pub fn sanitize_paths(&mut self, base: &Path) -> Result<()> {
147        self.search_dirs = self
148            .search_dirs
149            .iter(!self.skip_os_lookups)
150            .filter_map(|search_dir| {
151                let abspath = if !search_dir.is_absolute() {
152                    base.join(search_dir)
153                } else {
154                    search_dir.to_owned()
155                };
156
157                abspath.canonicalize().ok().inspect(|abspath| {
158                    log::trace!(
159                        "Sanitized ({} + {}) -> {}",
160                        base.display(),
161                        search_dir.display(),
162                        abspath.display()
163                    );
164                })
165            })
166            .collect::<Vec<PathBuf>>()
167            .into();
168
169        // convert all extra dictionaries to absolute paths
170
171        'o: for extra_dic in self.extra_dictionaries.iter_mut() {
172            for search_dir in
173                self.search_dirs
174                    .iter(!self.skip_os_lookups)
175                    .filter_map(|search_dir| {
176                        if !extra_dic.is_absolute() {
177                            base.join(search_dir).canonicalize().ok()
178                        } else {
179                            Some(search_dir.to_owned())
180                        }
181                    })
182            {
183                let abspath = if !extra_dic.is_absolute() {
184                    search_dir.join(&extra_dic)
185                } else {
186                    continue 'o;
187                };
188                if let Ok(abspath) = abspath.canonicalize() {
189                    if abspath.is_file() {
190                        *extra_dic = abspath;
191                        continue 'o;
192                    }
193                } else {
194                    log::debug!("Failed to canonicalize {}", abspath.display());
195                }
196            }
197            bail!(
198                "Could not find extra dictionary {} in any of the search paths",
199                extra_dic.display()
200            );
201        }
202
203        Ok(())
204    }
205}