Editor: document tone, right-click word lookup, expanded stats
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
This commit is contained in:
76
internal/lexicon/lexicon_test.go
Normal file
76
internal/lexicon/lexicon_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user