Four enhancements to make the editor fit real school usage:
- Per-document tone (academic/professional/casual/humorous/creative/
persuasive/general): new documents.tone column (migration 0002), threaded
through the docs API, a bilingual ToneSelect dropdown on the title row, and
injected into the grammar-checkpoint LLM prompt so advice fits the register.
The voice pass stays tone-agnostic.
- Right-click word lookup: a new offline `lexicon` package serves definitions
(Wordset, modern ESL-friendly glosses) and synonyms (WordNet synsets first,
then frequency+stopword-ranked Moby for breadth) from gzipped embedded data,
behind /api/word/{word} with light morphology. The WordCard popover shows the
definition and tappable synonym pills that swap the word in place.
- Expanded writing stats: clicking the word count opens a StatsPanel with page
count, sentences, paragraphs, reading time, average word length, word variety,
and Flesch-Kincaid reading level — all computed client-side.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
77 lines
1.9 KiB
Go
77 lines
1.9 KiB
Go
package lexicon
|
|
|
|
import "testing"
|
|
|
|
func TestLookupKnownWord(t *testing.T) {
|
|
l := New()
|
|
res, err := l.Lookup("happy")
|
|
if err != nil {
|
|
t.Fatalf("Lookup: %v", err)
|
|
}
|
|
if len(res.Definitions) == 0 {
|
|
t.Errorf("expected definitions for %q, got none", "happy")
|
|
}
|
|
if len(res.Synonyms) == 0 {
|
|
t.Errorf("expected synonyms for %q, got none", "happy")
|
|
}
|
|
if len(res.Synonyms) > maxSynonyms {
|
|
t.Errorf("synonyms not capped: got %d, want <= %d", len(res.Synonyms), maxSynonyms)
|
|
}
|
|
if len(res.Definitions) > maxDefinitions {
|
|
t.Errorf("definitions not capped: got %d, want <= %d", len(res.Definitions), maxDefinitions)
|
|
}
|
|
}
|
|
|
|
func TestLookupMorphology(t *testing.T) {
|
|
l := New()
|
|
// Inflected forms should resolve to their base entry via candidates().
|
|
for _, w := range []string{"running", "studies", "boxes", "quickly", "stopped"} {
|
|
res, err := l.Lookup(w)
|
|
if err != nil {
|
|
t.Fatalf("Lookup(%q): %v", w, err)
|
|
}
|
|
if len(res.Definitions) == 0 && len(res.Synonyms) == 0 {
|
|
t.Errorf("expected some result for inflected %q, got nothing", w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLookupUnknownWord(t *testing.T) {
|
|
l := New()
|
|
res, err := l.Lookup("zzzxqqq")
|
|
if err != nil {
|
|
t.Fatalf("Lookup: %v", err)
|
|
}
|
|
if len(res.Definitions) != 0 || len(res.Synonyms) != 0 {
|
|
t.Errorf("expected empty result for nonsense word, got %+v", res)
|
|
}
|
|
// Empty result must still serialize as [] not null for the frontend.
|
|
if res.Definitions == nil || res.Synonyms == nil {
|
|
t.Errorf("empty slices must be non-nil for JSON []: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestCandidates(t *testing.T) {
|
|
cases := map[string]string{
|
|
"cats": "cat",
|
|
"studies": "study",
|
|
"running": "run",
|
|
"hoped": "hope",
|
|
"quickly": "quick",
|
|
"happily": "happy",
|
|
}
|
|
for word, want := range cases {
|
|
got := candidates(word)
|
|
found := false
|
|
for _, c := range got {
|
|
if c == want {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("candidates(%q) = %v, missing expected base %q", word, got, want)
|
|
}
|
|
}
|
|
}
|