Skip to main content

README.md

# 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.