# Harper
Elixir bindings to [Harper](https://github.com/Automattic/harper) — a fast,
offline, privacy-first English grammar and spell checker written in Rust by
Automattic. Harper runs entirely in-process: no server, no network, no LLM, no
model download. It's the engine behind [writewithharper.com](https://writewithharper.com).
This package wraps `harper-core` as a [Rustler](https://github.com/rusterlium/rustler)
NIF and ships **precompiled binaries** for common targets, so most consumers
don't need a Rust toolchain.
## Installation
```elixir
def deps do
[{:harper, "~> 0.1"}]
end
```
Precompiled artifacts are provided for macOS (aarch64/x86_64) and Linux
(aarch64/x86_64, gnu). Other targets — including FreeBSD — build from source
via `rustler`, which needs a Rust toolchain (≥ 1.85, for edition 2024). To force
a source build on a supported target:
```sh
RUSTLER_PRECOMPILATION_FORCE_BUILD=1 mix deps.compile harper
```
## Usage
`Harper.lint/1,2,3` returns a list of `Harper.Lint` structs. It's a pure,
stateless function and runs on a dirty CPU scheduler, so linting a large
document won't block the BEAM's normal schedulers.
```elixir
iex> Harper.lint("Ths is a testt.")
[
%Harper.Lint{
start: 0, end: 3, kind: "Spelling", priority: 63,
message: "Did you mean to spell `Ths` this way?",
suggestions: [%Harper.Suggestion{kind: "replace", text: "This"}, ...]
},
...
]
iex> Harper.lint("This is a clean sentence.")
[]
```
### Dialects
English spelling is dialect-sensitive. `lint/2` takes one of `Harper.dialects/0`
(`:american` — the default — `:british`, `:canadian`, `:australian`, `:indian`):
```elixir
iex> Harper.lint("I love the colour blue.", :american) |> Enum.any?(& &1.kind == "Spelling")
true
iex> Harper.lint("I love the colour blue.", :british) |> Enum.any?(& &1.kind == "Spelling")
false
```
### Custom dictionary
Pass words (proper nouns, brand terms, handles) that should never be flagged as
misspellings — the server-side equivalent of "add to dictionary":
```elixir
iex> Harper.lint("I write for acme daily.", :american, ["acme"]) |> Enum.any?(& &1.kind == "Spelling")
false
```
### Offsets
`Harper.Lint` `:start`/`:end` are **UTF-16 code-unit offsets** into the linted
string, end-exclusive — chosen so they drop straight into JavaScript string
indexing and editor APIs (Quill, contenteditable ranges) without conversion.
## Notes
- **English only.** Gate callers to English-authored content.
- `:kind` is Harper's `LintKind` (`"Spelling"`, `"Agreement"`, `"WordChoice"`,
`"Style"`, `"Typo"`, `"Miscellaneous"`, …) — useful for grouping/colouring.
- `Harper.Suggestion` `:kind` is `"replace"`, `"insert_after"`, or `"remove"`;
apply it over the lint's span.
## Releasing (maintainers)
Precompiled tarballs are built locally on macOS and hosted on the
[sr.ht release ref](https://git.sr.ht/~sbr/harper). See `scripts/`:
- `build_artifacts.sh` — mac (native) + linux (via `cargo-zigbuild`).
- `build_freebsd_via_ssh.sh` — FreeBSD, built on a remote box (opt-in).
- `deploy.sh` — bump, build, hash into the checksum manifest, tag, upload, `mix hex.publish`.
## License
Apache-2.0. Bundles the Apache-2.0 `harper-core` (statically linked into the
NIF); each precompiled tarball ships `LICENSE`, `LICENSE-APACHE`, and a
`THIRD-PARTY-NOTICES` file generated from the full dependency tree.