gospell

package module
v0.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 16 Imported by: 18

README

gospell

Go Reference Build Status license

gospell is a pure Go spell checker for Hunspell-style dictionaries.

It is designed for:

  • loading Hunspell .aff and .dic files
  • exact word checking
  • word splitting and compound handling
  • per-document or per-session word list overlays
  • lazy mutation-based spelling suggestions

The package keeps the core checker small and uses lazy lookup for both spelling and suggestions.

Features

  • Pure Go implementation
  • Hunspell dictionary format support
  • Exact spell checking with affix and compound handling
  • WordList overlays — add allowed or forbidden words without touching the base dictionary
  • Checker — combines a base dictionary with any number of WordLists; supports per-document reset at zero cost
  • Built-in English/QWERTY mutation suggester with n-gram fallback

Install

go get github.com/client9/gospell

Basic Use

package main

import (
	"fmt"
	"log"

	"github.com/client9/gospell"
)

func main() {
	gs, err := gospell.NewGoSpell("hunspell-en_US/en_US.aff", "hunspell-en_US/en_US.dic")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(gs.Spell("silly"))
	fmt.Println(gs.Spell("sillly"))
}

Suggestions

Suggestions use the built-in mutation suggester by default. The suggester generates common typo mutations first, then falls back to an n-gram root scan that expands only the best matching roots.

gs, err := gospell.NewGoSpell("hunspell-en_US/en_US.aff", "hunspell-en_US/en_US.dic")
if err != nil {
	log.Fatal(err)
}

sugs, err := gs.Suggest("sillly", 5)
if err != nil {
	log.Fatal(err)
}
for _, sug := range sugs {
	fmt.Println(sug.Word, sug.Score)
}

You can still replace the default suggester if you provide your own engine:

if err := gs.SetSuggester(gospell.NewMutationSuggester(
	gospell.MutationOptions{CandidateCap: 256},
)); err != nil {
	log.Fatal(err)
}
Suggestion Engines

gospell ships with a small interface so you can swap suggestion strategies without changing the checker:

type Suggestions interface {
	Init(src SuggestionSource) error
	Suggest(word string, limit int) ([]Suggestion, error)
}

This makes it easy to experiment with:

  • query-time mutation generation
  • n-gram fallback scoring
  • chained or composite engines
Built-In Suggesters

gospell currently ships with one built-in suggestion engine:

  • NewMutationSuggester - generates English/QWERTY candidates on demand and falls back to n-gram root matching

Defaults:

  • MutationOptions{CandidateCap: 256, NGramRootCap: 64}
Mutation-Based Suggestions

The mutation suggester takes the misspelled word, generates a bounded set of single-edit candidates, and checks each candidate with the dictionary's lazy Spell method. If no mutation survives, it scans root dictionary entries by n-gram similarity, expands only the best roots, and reranks those candidates. It does not materialize all dictionary surfaces.

The current first pass assumes English and a QWERTY keyboard:

  • deletions and adjacent transpositions are always generated
  • substitutions prefer nearby keyboard keys first, then fall back to the English alphabet
  • insertions use nearby keyboard keys first, then the English alphabet

That keeps startup cost near zero while still finding common typos such as missing letters, doubled letters, transpositions, and near-key substitutions.

See docs/mutation-suggestions.md for a longer explanation of the approach and tradeoffs.

Word Lists

WordList is a lightweight overlay of allowed and forbidden words. It does not require rebuilding the base dictionary and can be attached or detached at any time.

Format: one entry per line. Lines starting with # are comments. Lines starting with * forbid the word. All other non-blank lines allow the word.

# project-specific terms
Kubernetes
gRPC
*irregardless
gs, err := gospell.NewGoSpell("en_US.aff", "en_US.dic")
if err != nil {
    log.Fatal(err)
}

checker := gospell.NewChecker(gs)

// Load a global personal word list once.
global, err := gospell.NewWordListFile("personal.txt")
if err != nil {
    log.Fatal(err)
}
checker.AddWordList(global)

// Per-document: add, use, then remove.
doc, _ := gospell.NewWordList(strings.NewReader("ProjectName\n*badterm\n"))
checker.AddWordList(doc)

fmt.Println(checker.Spell("ProjectName")) // true
fmt.Println(checker.Spell("badterm"))     // false

checker.RemoveWordList(doc) // reset for next document

Checker.Suggest merges base dictionary suggestions with a brute-force scan of all active WordLists, so per-document words appear in suggestions automatically.

API Overview

The main entry points are:

Hunspell Compatibility

This package understands the Hunspell dictionary format and supports:

  • affix expansion
  • compound rules
  • iconv conversions
  • case handling
  • custom word lists

For a feature checklist and compatibility matrix, see docs/hunspell-compatibility.md.

Dictionary Files

The repository includes an English Hunspell dictionary used by tests and benchmarks:

  • hunspell-en_US/en_US.aff
  • hunspell-en_US/en_US.dic

Contributing

This project is still evolving. If you find a bug, a mismatch with Hunspell behavior, or a better suggestion strategy, patches are welcome.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SearchPaths

func SearchPaths() []string

SearchPaths returns the ordered list of directories to search for named dictionaries. DICPATH (colon-separated, hunspell convention) is read first; standard system locations are appended as fallback. Callers may prepend their own tool-specific paths before passing the result to Open or OpenSupplement.

Types

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker combines a base GoSpell dictionary with zero or more WordLists. WordLists are consulted before the base dictionary: the first forbidden match rejects the word; the first allowed match accepts it; otherwise the base dictionary decides.

func NewChecker

func NewChecker(base *GoSpell) *Checker

NewChecker creates a Checker backed by base.

func (*Checker) AddWordList

func (c *Checker) AddWordList(wl *WordList)

AddWordList appends wl to the active word lists.

func (*Checker) InputConversion

func (c *Checker) InputConversion(raw []byte) string

InputConversion applies the base dictionary's ICONV rules to raw input.

func (*Checker) RemoveWordList

func (c *Checker) RemoveWordList(wl *WordList)

RemoveWordList removes wl from the active word lists by pointer identity.

func (*Checker) Spell

func (c *Checker) Spell(word string) bool

Spell reports whether word is correctly spelled.

func (*Checker) Suggest

func (c *Checker) Suggest(word string, limit int) ([]Suggestion, error)

Suggest returns spelling suggestions. The base dictionary suggester provides the primary results; active WordLists are scanned with brute-force edit distance and merged into the final set.

Known limitation: words in active WordLists are not indexed by the base suggester. Only the brute-force scan covers them, which is fast given typical WordList sizes.

type GoSpell

type GoSpell struct {
	// contains filtered or unexported fields
}

GoSpell is main struct

func NewGoSpell

func NewGoSpell(affFile, dicFile string) (*GoSpell, error)

NewGoSpell creates a GoSpell from Hunspell AFF and DIC file paths.

func NewGoSpellReader

func NewGoSpellReader(aff, dic io.Reader) (*GoSpell, error)

NewGoSpellReader creates a GoSpell from io.Readers for Hunspell AFF and DIC data.

func Open

func Open(name string, paths []string) (*GoSpell, error)

Open finds and loads a named base dictionary (.aff + .dic pair) by searching paths in order. name is a bare dictionary name ("en_US"), an absolute path, or a relative path containing a separator. The ".aff" or ".dic" extension is stripped if present. Returns a descriptive error if no matching pair is found in any of the provided paths.

func (*GoSpell) AddWordRaw

func (s *GoSpell) AddWordRaw(word string) bool

AddWordRaw adds a single word to the internal dictionary without modifications. Returns true if added, false if already present.

func (*GoSpell) ForEachWord

func (s *GoSpell) ForEachWord(fn func(word string) bool)

ForEachWord calls fn for every word in the dictionary. Iteration stops early if fn returns false.

func (*GoSpell) HasWord

func (s *GoSpell) HasWord(word string) bool

HasWord reports whether word exists in the dictionary as an exact entry.

func (*GoSpell) InputConversion

func (s *GoSpell) InputConversion(raw []byte) string

InputConversion does any character substitution before checking based on the ICONV stanza in the AFF file.

func (*GoSpell) MaxWordLen

func (s *GoSpell) MaxWordLen() int

MaxWordLen returns the longest loaded dictionary word length.

func (*GoSpell) SetSuggester

func (s *GoSpell) SetSuggester(suggester Suggestions) error

SetSuggester configures the suggestion engine and initializes it from the current dictionary contents.

func (*GoSpell) Spell

func (s *GoSpell) Spell(word string) bool

Spell reports whether word is correctly spelled.

func (*GoSpell) Suggest

func (s *GoSpell) Suggest(word string, limit int) ([]Suggestion, error)

Suggest returns spelling suggestions using the configured suggester.

func (*GoSpell) WordCount

func (s *GoSpell) WordCount() int

WordCount returns the number of dictionary entries currently loaded.

type MutationOptions

type MutationOptions struct {
	CandidateCap int
	NGramRootCap int
}

MutationOptions controls the query-time mutation suggester.

type MutationSuggester

type MutationSuggester struct {
	// contains filtered or unexported fields
}

MutationSuggester generates one-edit candidates from the query word and checks each candidate directly against the dictionary. It does not build an inverted index up front.

func NewMutationSuggester

func NewMutationSuggester(opts MutationOptions) *MutationSuggester

NewMutationSuggester creates a mutation-based suggester.

func (*MutationSuggester) Init

Init stores the suggestion source for later membership checks.

func (*MutationSuggester) Suggest

func (s *MutationSuggester) Suggest(word string, limit int) ([]Suggestion, error)

Suggest returns the best matches ordered by edit distance, then keyboard penalty, then lexicographic order.

type Suggestion

type Suggestion struct {
	Word  string
	Score int
}

Suggestion is a ranked candidate returned by a suggestion engine.

type SuggestionSource

type SuggestionSource interface {
	ForEachWord(func(word string) bool)
	HasWord(word string) bool
	WordCount() int
	MaxWordLen() int
}

SuggestionSource provides read-only access to a loaded dictionary so a suggester can build indexes or other internal structures.

type Suggestions

type Suggestions interface {
	Init(src SuggestionSource) error
	Suggest(word string, limit int) ([]Suggestion, error)
}

Suggestions is the pluggable suggestion engine interface used by GoSpell.

type WordList

type WordList struct {
	// contains filtered or unexported fields
}

WordList is a small overlay of allowed and forbidden words used as a per-context supplement to a base GoSpell dictionary. Multiple WordLists can be active simultaneously; see Checker. Loading policy — global or per-document — is the caller's responsibility.

Format (one entry per line):

# comment
word          → allowed
*word         → forbidden

func NewWordList

func NewWordList(r io.Reader) (*WordList, error)

NewWordList parses a WordList from r.

func NewWordListFile

func NewWordListFile(name string) (*WordList, error)

NewWordListFile parses a WordList from a file.

func NewWordListFromDic

func NewWordListFromDic(r io.Reader) (*WordList, error)

NewWordListFromDic parses a Hunspell .dic file from r and returns the word stems as an allowed WordList. The word-count header line is skipped; affix flags (the part after '/') are stripped from each entry.

This is distinct from NewWordList, which parses the personal word list format (plain words or *word for forbidden entries). Use NewWordListFromDic when loading a raw Hunspell .dic file as a vocabulary supplement.

func OpenSupplement

func OpenSupplement(name string, paths []string) (*WordList, error)

OpenSupplement finds and loads a .dic-only domain supplement (no .aff file) by searching paths in order. The result is a WordList ready to attach to a Checker. Supplement dictionaries extend the vocabulary of a base dictionary with domain-specific terms (medical, legal, technical) without affix processing. Encoding is assumed UTF-8; for non-UTF-8 supplements use a full base dictionary (.aff + .dic) as the first entry instead.

func (*WordList) Add

func (wl *WordList) Add(word string)

Add adds word to the allowed set.

func (*WordList) Forbid

func (wl *WordList) Forbid(word string)

Forbid adds word to the forbidden set.

func (*WordList) HasWord

func (wl *WordList) HasWord(word string) bool

HasWord reports whether word is in the allowed set.

func (*WordList) IsForbidden

func (wl *WordList) IsForbidden(word string) bool

IsForbidden reports whether word is in the forbidden set.

Directories

Path Synopsis
cmd
gospell command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL