diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f571afa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings + +jobs: + test: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Install hunspell (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libhunspell-dev \ + hunspell-en-us \ + libclang-dev + + - name: Test + run: cargo test --all-targets + + fmt: + name: fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: Install hunspell + run: | + sudo apt-get update + sudo apt-get install -y libhunspell-dev hunspell-en-us libclang-dev + - run: cargo clippy --all-targets -- -D warnings \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f87333e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -os: - - osx -language: rust -rust: - - stable - - beta - - nightly -matrix: - allow_failures: - - rust: nightly - fast_finish: true - diff --git a/README.md b/README.md index 83372cd..b5a78f9 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,45 @@ # spellbound -[![travis-ci Build Status](https://travis-ci.com/euclio/spellbound.svg?branch=master)](https://travis-ci.com/euclio/spellbound) -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/euclio/spellbound?svg=true)](https://ci.appveyor.com/project/euclio/spellbound) -`spellbound` is a small crate that binds to the native platform's spell checking -APIs and wraps them in a friendlier, rustic interface. +[![CI](https://github.com/rtmongold/spellbound/actions/workflows/ci.yml/badge.svg)](https://github.com/rtmongold/spellbound/actions/workflows/ci.yml) -Supported platforms and corresponding APIs: +Native spell checking with a small Rust API. + +This is a **maintained fork** of [euclio/spellbound](https://github.com/euclio/spellbound) +(last upstream commit 2020). API 0.2 returns `Result` from `Checker::new` and exposes +UTF-8 byte ranges on spelling errors. | Platform | API | | -------- | ------------------ | -| MacOS | [`NSSpellChecker`] | +| macOS | [`NSSpellChecker`] | | Windows | [`ISpellChecker`] | -| *nix | [`hunspell`] +| *nix | [`hunspell`] | [`ISpellChecker`]: https://docs.microsoft.com/en-us/windows/desktop/api/spellcheck/nn-spellcheck-ispellchecker [`NSSpellChecker`]: https://developer.apple.com/documentation/appkit/nsspellchecker [`hunspell`]: https://hunspell.github.io/ + +## Example + +```rust +use spellbound::Checker; + +fn main() -> Result<(), spellbound::Error> { + let mut checker = Checker::new()?; + for err in checker.check("I beleeve I can fly") { + println!("{} @ {}..{}", err.text(), err.start(), err.end()); + } + Ok(()) +} +``` + +## Linux + +Needs a hunspell dictionary on disk (default search includes `/usr/share/hunspell`). Example: + +- Arch: `pacman -S hunspell hunspell-en_us` +- Debian/Ubuntu: `apt install libhunspell-dev hunspell-en-us` + +Without a dictionary, `Checker::new()` returns `Error::Unavailable`. + +## License +MIT OR Apache-2.0 \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 472ed7b..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,21 +0,0 @@ -environment: - matrix: - - channel: stable - - channel: beta - - channel: nightly - -matrix: - allow_failures: - - channel: nightly - -install: - - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - - rustup-init -y --default-toolchain %channel% - - set PATH=%PATH%;%USERPROFILE%\.cargo\bin - - rustc -vV - - cargo -vV - -build: false - -test_script: -- cargo test diff --git a/src/lib.rs b/src/lib.rs index 9ef13de..62ba529 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,9 +85,15 @@ pub struct SpellingError(imp::SpellingError); impl SpellingError { /// Returns the text of the misspelled word. - pub fn text(&self) -> &str { self.0.text() } - pub fn start(&self) -> usize { self.0.start() } - pub fn end(&self) -> usize { self.0.end() } + 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)] @@ -98,14 +104,14 @@ mod tests { fn no_errors() { let text = "I'm happy that this sentence has no errors."; let mut checker = Checker::new().unwrap(); - assert_eq!(checker.check(&text).count(), 0); + assert_eq!(checker.check(text).count(), 0); } #[test] fn single_error() { let text = "asdf"; let mut checker = Checker::new().unwrap(); - let errors = checker.check(&text).collect::>(); + let errors = checker.check(text).collect::>(); assert_eq!(errors.len(), 1); assert_eq!(errors[0].text(), "asdf"); assert_eq!(&text[errors[0].start()..errors[0].end()], "asdf"); @@ -115,7 +121,7 @@ mod tests { fn multiple_errors() { let text = "asdf hjkl qwer uiop"; let mut checker = Checker::new().unwrap(); - let errors = checker.check(&text).collect::>(); + let errors = checker.check(text).collect::>(); assert_eq!(errors.len(), 4); assert_eq!(errors[0].text(), "asdf"); assert_eq!(errors[1].text(), "hjkl"); diff --git a/src/mac.rs b/src/mac.rs index ad0c176..45eafba 100644 --- a/src/mac.rs +++ b/src/mac.rs @@ -69,7 +69,7 @@ impl Checker { pub fn check(&mut self, text: &str) -> impl Iterator { SpellcheckIter { document_tag: self.document_tag, - ns_text: unsafe { NSString::alloc(nil).init_str(text)}, + ns_text: unsafe { NSString::alloc(nil).init_str(text) }, ns_offset: 0, original: text.to_owned(), byte_cursor: 0, @@ -85,9 +85,15 @@ pub struct SpellingError { } impl SpellingError { - pub fn text(&self) -> &str { &self.text } - pub fn start(&self) -> usize { self.start } - pub fn end(&self) -> usize { self.end } + pub fn text(&self) -> &str { + &self.text + } + pub fn start(&self) -> usize { + self.start + } + pub fn end(&self) -> usize { + self.end + } } struct SpellcheckIter { @@ -129,7 +135,7 @@ impl Iterator for SpellcheckIter { let end = start + misspelling.len(); self.byte_cursor = end; self.ns_offset = range.location + range.length; - + Some(SpellingError { text: misspelling.to_owned(), start, diff --git a/src/unix.rs b/src/unix.rs index aba2be4..1849a1e 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -55,7 +55,9 @@ impl Checker { words(text).filter_map(move |(start, end, word)| { let cstr = CString::new(word).ok()?; - let ok = unsafe { Hunspell_spell(hunspell, cstr.as_bytes_with_nul().as_ptr() as *const i8) } != 0; + let ok = + unsafe { Hunspell_spell(hunspell, cstr.as_bytes_with_nul().as_ptr() as *const i8) } + != 0; if ok { None } else { @@ -71,7 +73,12 @@ impl Checker { pub fn ignore(&mut self, word: &str) { let cstr = CString::new(word).unwrap(); - unsafe { Hunspell_add(self.hunspell, cstr.as_bytes_with_nul().as_ptr() as *const i8) }; + unsafe { + Hunspell_add( + self.hunspell, + cstr.as_bytes_with_nul().as_ptr() as *const i8, + ) + }; } } @@ -90,9 +97,15 @@ pub struct SpellingError { } impl SpellingError { - pub fn text(&self) -> &str { &self.text } - pub fn start(&self) -> usize { self.start } - pub fn end(&self) -> usize { self.end } + pub fn text(&self) -> &str { + &self.text + } + pub fn start(&self) -> usize { + self.start + } + pub fn end(&self) -> usize { + self.end + } } fn is_word_char(c: char) -> bool { @@ -102,12 +115,12 @@ fn is_word_char(c: char) -> bool { fn words(text: &str) -> impl Iterator { let mut words = Vec::new(); let mut chars = text.char_indices().peekable(); - + while let Some((start, c)) = chars.next() { if !is_word_char(c) { continue; } - + let mut end = start + c.len_utf8(); while let Some(&(i, next)) = chars.peek() { if !is_word_char(next) { diff --git a/src/win.rs b/src/win.rs index aedbf39..caa9ab3 100644 --- a/src/win.rs +++ b/src/win.rs @@ -8,24 +8,28 @@ use std::os::windows::ffi::OsStrExt; use std::ptr::{self, NonNull}; use winapi::{ - Class, - Interface, shared::{ winerror::{SUCCEEDED, S_FALSE, S_OK}, wtypesbase::CLSCTX_INPROC_SERVER, }, um::{ - combaseapi::{CoInitializeEx, CoCreateInstance}, + combaseapi::{CoCreateInstance, CoInitializeEx}, objbase::COINIT_MULTITHREADED, - spellcheck::{IEnumSpellingError, SpellCheckerFactory, ISpellChecker, ISpellCheckerFactory}, + spellcheck::{ + IEnumSpellingError, ISpellChecker, ISpellCheckerFactory, SpellCheckerFactory, + }, unknwnbase::IUnknown, }, + Class, Interface, }; struct ComPtr(NonNull); impl ComPtr { - fn new(p: *mut T) -> ComPtr where T: Interface { + fn new(p: *mut T) -> ComPtr + where + T: Interface, + { ComPtr(NonNull::new(p).unwrap()) } } @@ -178,7 +182,8 @@ impl Iterator for ErrorIter { let utf16_start = start as usize; let utf16_len = length as usize; - let err_text = String::from_utf16(&self.text[utf16_start..utf16_start + utf16_len]).ok()?; + let err_text = + String::from_utf16(&self.text[utf16_start..utf16_start + utf16_len]).ok()?; let byte_start = utf16_offset_to_utf8(&self.original, utf16_start); let byte_end = utf16_offset_to_utf8(&self.original, utf16_start + utf16_len); @@ -201,7 +206,13 @@ pub struct SpellingError { } impl SpellingError { - pub fn text(&self) -> &str { &self.text } - pub fn start(&self) -> usize { self.start } - pub fn end(&self) -> usize { self.end } + pub fn text(&self) -> &str { + &self.text + } + pub fn start(&self) -> usize { + self.start + } + pub fn end(&self) -> usize { + self.end + } }