use cfg_if::cfg_if;
use std::fmt;
#[derive(Debug)]
pub enum Error {
Unavailable,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Unavailable => write!(f, "spell checker unavailable"),
}
}
}
impl std::error::Error for Error {}
fn normalize_locale(locale: &str) -> (String, String) {
let mut parts = locale
.trim()
.split(['_', '-'])
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect::<Vec<_>>();
if let Some(lang) = parts.first_mut() {
*lang = lang.to_lowercase();
}
if let Some(region) = parts.get_mut(1) {
*region = region.to_uppercase();
}
let hunspell = parts.join("_");
let bcp47 = parts.join("-");
(hunspell, bcp47)
}
cfg_if! {
if #[cfg(target_os = "macos")] {
mod mac;
use crate::mac as imp;
} else if #[cfg(windows)] {
mod win;
use crate::win as imp;
} else if #[cfg(unix)] {
mod unix;
use crate::unix as imp;
} else {
compile_error!("target platform is not supported");
}
}
#[derive(Debug)]
pub struct Checker(imp::Checker);
impl Checker {
pub fn new() -> Result<Self, Error> {
Ok(Checker(imp::Checker::new()?))
}
pub fn with_locale(locale: &str) -> Result<Self, Error> {
let (hunspell, bcp47) = normalize_locale(locale);
Ok(Checker(imp::Checker::with_locale(&hunspell, &bcp47)?))
}
pub fn suggest(&self, word: &str) -> Vec<String> {
self.0.suggest(word)
}
pub fn check<'a>(&self, text: &'a str) -> impl Iterator<Item = SpellingError> + 'a + use<'a> {
self.0.check(text).map(SpellingError)
}
pub fn is_correct(&self, word: &str) -> bool {
self.check(word).next().is_none()
}
pub fn ignore(&mut self, word: &str) {
self.0.ignore(word)
}
}
pub struct SpellingError(imp::SpellingError);
impl SpellingError {
pub fn text(&self) -> &str {
self.0.text()
}
pub fn start(&self) -> usize {
self.0.start()
}
pub fn end(&self) -> usize {
self.0.end()
}
}
#[cfg(test)]
mod tests {
use super::Checker;
#[test]
fn no_errors() {
let text = "I'm happy that this sentence has no errors.";
let checker = Checker::new().unwrap();
assert_eq!(checker.check(text).count(), 0);
}
#[test]
fn single_error() {
let text = "beleeve";
let checker = Checker::new().unwrap();
let errors = checker.check(text).collect::<Vec<_>>();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].text(), "beleeve");
assert_eq!(&text[errors[0].start()..errors[0].end()], "beleeve");
}
#[test]
fn multiple_errors() {
let text = "asdf hjkl qwer uiop";
let checker = Checker::new().unwrap();
let errors = checker.check(text).collect::<Vec<_>>();
assert_eq!(errors.len(), 4);
assert_eq!(errors[0].text(), "asdf");
assert_eq!(errors[1].text(), "hjkl");
assert_eq!(errors[2].text(), "qwer");
assert_eq!(errors[3].text(), "uiop");
}
#[test]
fn error_ranges() {
let text = "one asdf two";
let checker = Checker::new().unwrap();
let errors: Vec<_> = checker.check(text).collect();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].text(), "asdf");
assert_eq!(errors[0].start(), 4);
assert_eq!(errors[0].end(), 8);
assert_eq!(&text[errors[0].start()..errors[0].end()], "asdf");
}
#[test]
fn empty() {
let checker = Checker::new().unwrap();
assert_eq!(checker.check("").count(), 0);
}
#[test]
fn ignore() {
let mut checker = Checker::new().unwrap();
assert_eq!(checker.check("foobarbaz").count(), 1);
checker.ignore("foobarbaz");
assert_eq!(checker.check("foobarbaz").count(), 0);
}
#[test]
fn ignore_not_permanent() {
let mut checker = Checker::new().unwrap();
checker.ignore("foobarbaz");
drop(checker);
let checker = Checker::new().unwrap();
assert_eq!(checker.check("foobarbaz").count(), 1);
}
#[test]
fn with_locale_en_us() {
assert!(Checker::with_locale("en_US").is_ok());
assert!(Checker::with_locale("en-US").is_ok());
}
#[test]
#[cfg(all(unix, not(target_os = "macos")))]
fn with_locale_unknown() {
assert!(Checker::with_locale("zz_ZZ").is_err());
}
#[test]
fn suggest_misspelling() {
let checker = Checker::new().unwrap();
let suggestions = checker.suggest("beleeve");
assert!(!suggestions.is_empty());
}
#[test]
fn is_correct() {
let checker = Checker::new().unwrap();
assert!(checker.is_correct("believe"));
assert!(!checker.is_correct("beleeve"));
}
}