Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 0 additions & 12 deletions .travis.yml

This file was deleted.

41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
21 changes: 0 additions & 21 deletions appveyor.yml

This file was deleted.

18 changes: 12 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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::<Vec<_>>();
let errors = checker.check(text).collect::<Vec<_>>();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].text(), "asdf");
assert_eq!(&text[errors[0].start()..errors[0].end()], "asdf");
Expand All @@ -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::<Vec<_>>();
let errors = checker.check(text).collect::<Vec<_>>();
assert_eq!(errors.len(), 4);
assert_eq!(errors[0].text(), "asdf");
assert_eq!(errors[1].text(), "hjkl");
Expand Down
16 changes: 11 additions & 5 deletions src/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl Checker {
pub fn check(&mut self, text: &str) -> impl Iterator<Item = SpellingError> {
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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 20 additions & 7 deletions src/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
)
};
}
}

Expand All @@ -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 {
Expand All @@ -102,12 +115,12 @@ fn is_word_char(c: char) -> bool {
fn words(text: &str) -> impl Iterator<Item = (usize, usize, &str)> {
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) {
Expand Down
29 changes: 20 additions & 9 deletions src/win.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(NonNull<T>);

impl<T> ComPtr<T> {
fn new(p: *mut T) -> ComPtr<T> where T: Interface {
fn new(p: *mut T) -> ComPtr<T>
where
T: Interface,
{
ComPtr(NonNull::new(p).unwrap())
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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
}
}
Loading