feat: first release !

This commit is contained in:
Kharec committed 2026-09-28 14:21:06 +02:00
1 parent 77a333e8d9
commit 664527182d
66 files changed
+13812 -16

No files matched your search

+360
View File
@@ -0,0 +1,360 @@
package spell
import (
"os"
"path/filepath"
"reflect"
"slices"
"testing"
"plotzilla/internal/screenplay"
)
const enAff = `SET UTF-8
TRY esianrtolcdugmphbyfvkwz'
ICONV 1
ICONV ’ '
NOSUGGEST !
KEEPCASE K
FORBIDDENWORD F
NEEDAFFIX N
REP 1
REP alot a_lot
PFX U Y 1
PFX U 0 un .
SFX S Y 4
SFX S y ies [^aeiou]y
SFX S 0 s [aeiou]y
SFX S 0 s [^sxzy]
SFX S 0 es [sxz]
SFX D Y 2
SFX D 0 ed [^e]
SFX D 0 d e
SFX M Y 1
SFX M 0 's .
# "ness" allows a plural on top of it: kindnesses.
SFX Q Y 1
SFX Q 0 ness/S .
`
const enDic = `16
kind/QU
open/SD
for
door/S
city/SM
play/SD
love/DS
the
a
lot
Paris/M
iPhone
OpenGL/K
walkd/F
realis/N
rude/!
`
func testDict(t *testing.T) *Dictionary {
t.Helper()
d, err := Parse([]byte(enAff), []byte(enDic))
if err != nil {
t.Fatal(err)
}
return d
}
func TestCheck(t *testing.T) {
d := testDict(t)
for word, want := range map[string]bool{
"door": true,
"doors": true,
"cities": true,
"citys": false, // condition [^y] rejects it
"plays": true,
"played": true,
"loved": true,
"loveed": false,
"unkind": true,
"kindness": true,
"kindnesses": true, // two suffixes
"unkindness": true, // cross product
"undoor": false,
"city's": true,
"city’s": true, // typographic apostrophe
"Door": true,
"DOORS": true,
"Paris": true,
"PARIS": true,
"Paris's": true,
"paris": false, // proper nouns keep their capital
"iPhone": true,
"OpenGL": true,
"OPENGL": false, // KEEPCASE
"walkd": false, // FORBIDDENWORD
"realis": false, // NEEDAFFIX
"cty": false,
"": false,
"rude": true,
"UnKind": false,
"the": true,
"supercalifrag": false,
} {
if got := d.Check(word); got != want {
t.Errorf("Check(%q) = %v, want %v", word, got, want)
}
}
}
func TestSuggest(t *testing.T) {
d := testDict(t)
for word, want := range map[string][]string{
"teh": {"the"},
"dor": {"for", "door"},
"doorz": {"door", "doors"},
"DOOR'S": {"DOORS"},
"Citys": {"City", "City's"},
"paris": {"Paris"},
"alot": {"a lot", "lot"},
"thedoor": {"the door"},
"rudee": nil, // NOSUGGEST
"luve": {"love"},
} {
if got := d.Suggest(word); !reflect.DeepEqual(got, want) {
t.Errorf("Suggest(%q) = %q, want %q", word, got, want)
}
}
}
// Long flags, flag aliases and prefixes allowed by a suffix's
// continuation class, as in the French dictionary ("l'homme", "d'hommes").
func TestAliasesAndLongFlags(t *testing.T) {
aff := `SET UTF-8
FLAG long
AF 2
AF L'S.
AF S.
PFX L' Y 1
PFX L' 0 l' [aeiouh]
SFX S. Y 1
SFX S. 0 s/L' [^sxz]
`
dic := "2\nhomme/1\nami/2\n"
d, err := Parse([]byte(aff), []byte(dic))
if err != nil {
t.Fatal(err)
}
for word, want := range map[string]bool{
"homme": true, "hommes": true, "l'homme": true, "l'hommes": true,
"L'homme": true, "amis": true, "l'ami": false, "l'amis": true,
} {
if got := d.Check(word); got != want {
t.Errorf("Check(%q) = %v, want %v", word, got, want)
}
}
}
func TestLatin1(t *testing.T) {
aff := []byte("SET ISO8859-1\nTRY ae\xe4\n")
dic := []byte("1\nH\xe4user\n")
d, err := Parse(aff, dic)
if err != nil {
t.Fatal(err)
}
if !d.Check("Häuser") || d.Check("Hauser") {
t.Error("Latin-1 dictionary not decoded")
}
if got := d.Suggest("Hauser"); !slices.Contains(got, "Häuser") {
t.Errorf("Suggest(Hauser) = %q", got)
}
if _, err := Parse([]byte("SET KOI8-R\n"), nil); err == nil {
t.Error("unsupported encoding accepted")
}
}
func TestWords(t *testing.T) {
got := Words("Élise's well-known R2D2, l’homme… 'quoted' x")
want := []Word{
{"Élise's", 0, 7}, {"well", 8, 4}, {"known", 13, 5}, {"R2D2", 19, 4},
{"l’homme", 25, 7}, {"quoted", 35, 6}, {"x", 43, 1},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Words = %+v\nwant %+v", got, want)
}
}
func TestMisspellings(t *testing.T) {
c := NewChecker(testDict(t), []string{"Zorblax"})
doc := &screenplay.Document{Elements: []screenplay.Element{
screenplay.NewElement(screenplay.Scene, "INT. THE CTY - DAY"),
screenplay.NewElement(screenplay.Action, "GRUNDLE opens the dor for Zorblax."),
screenplay.NewElement(screenplay.Character, "Grundle (CONT'D)"),
screenplay.NewElement(screenplay.Dialogue, "Élan loves 3rd doors"),
screenplay.NewElement(screenplay.Action, "GRUNDLE's door."),
}}
got := c.Misspellings(doc)
want := []Misspelling{
{Element: 0, Offset: 9, Length: 3, Word: "CTY"},
{Element: 0, Offset: 15, Length: 3, Word: "DAY"},
{Element: 1, Offset: 18, Length: 3, Word: "dor"},
{Element: 3, Offset: 0, Length: 4, Word: "Élan"},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Misspellings = %+v\nwant %+v", got, want)
}
c.Add("élan")
c.Add("day")
if got := c.Misspellings(doc); len(got) != 2 {
t.Errorf("after Add, Misspellings = %+v", got)
}
}
func TestAvailable(t *testing.T) {
a, b := t.TempDir(), t.TempDir()
for _, f := range []string{
filepath.Join(a, "fr_FR.aff"), filepath.Join(a, "fr_FR.dic"),
filepath.Join(a, "de.dic"), // no .aff: skipped
filepath.Join(a, "hyph_fr.dic"), filepath.Join(a, "hyph_fr.aff"),
filepath.Join(b, "fr_FR.aff"), filepath.Join(b, "fr_FR.dic"),
filepath.Join(b, "en_US.aff"), filepath.Join(b, "en_US.dic"),
} {
if err := os.WriteFile(f, nil, 0o644); err != nil {
t.Fatal(err)
}
}
got := Available([]string{a, b, filepath.Join(a, "missing")})
want := []Language{
{"fr_FR", filepath.Join(a, "fr_FR.aff"), filepath.Join(a, "fr_FR.dic")},
{"en_US", filepath.Join(b, "en_US.aff"), filepath.Join(b, "en_US.dic")},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Available = %+v", got)
}
}
func TestPreferred(t *testing.T) {
codes := []string{"de_DE_frami", "en_GB", "en_US", "fr"}
for locale, want := range map[string]string{
"fr_FR.UTF-8": "fr",
"en_GB.UTF-8": "en_GB",
"de_AT.UTF-8": "de_DE_frami",
"C": "en_US",
"": "en_US",
} {
if got := Preferred(codes, locale); got != want {
t.Errorf("Preferred(%q) = %q, want %q", locale, got, want)
}
}
if got := Preferred(nil, "fr_FR"); got != "" {
t.Errorf("Preferred(nil) = %q", got)
}
}
func TestCompounds(t *testing.T) {
// German-style compounds: parts marked for the start, middle or end,
// a linking "s" that only exists inside compounds, and an affix that
// lower-cases a noun inside a compound (COMPOUNDPERMITFLAG).
aff := `SET UTF-8
COMPOUNDBEGIN B
COMPOUNDMIDDLE M
COMPOUNDLAST E
ONLYINCOMPOUND O
COMPOUNDPERMITFLAG P
COMPOUNDMIN 1
COMPOUNDWORDMAX 3
CHECKCOMPOUNDDUP
PFX L Y 3
PFX L T t/P T
PFX L S s/P S
PFX L Z z/P Z
SFX N Y 1
SFX N 0 en .
`
dic := `6
Haus/BM
Tür/LMEN
Schlüssel/BMLE
s/MO
Arbeit/BM
Zeit/LE
`
d, err := Parse([]byte(aff), []byte(dic))
if err != nil {
t.Fatal(err)
}
for _, w := range []string{"Haus", "Haustür", "Haustüren", "Haustürschlüssel", "Arbeitszeit", "HAUSTÜR"} {
if !d.Check(w) {
t.Errorf("%q should be correct", w)
}
}
for _, w := range []string{"s", "Türhaus", "Hausshaus", "Haushaus", "Arbeitsarbeitszeit", "Haustürx"} {
if d.Check(w) {
t.Errorf("%q should be wrong", w)
}
}
// COMPOUNDFLAG: any marked word can go anywhere.
d, err = Parse([]byte("SET UTF-8\nCOMPOUNDFLAG X\n"), []byte("2\nfoot/X\nball/X\n"))
if err != nil {
t.Fatal(err)
}
if !d.Check("football") || !d.Check("ballfoot") || d.Check("footbal") {
t.Errorf("COMPOUNDFLAG compounds")
}
}
func TestCompoundRule(t *testing.T) {
// en_US style: numbers built from digits, ordinal endings that only
// exist in compounds, and a word pattern with long flags.
aff := `SET UTF-8
ONLYINCOMPOUND c
COMPOUNDMIN 1
COMPOUNDRULE 2
COMPOUNDRULE n*1t
COMPOUNDRULE n*mp
`
dic := `8
0/nm
1/n1
2/nm
3/nm
st/tc
nd/pc
th/pc
rd/pc
`
d, err := Parse([]byte(aff), []byte(dic))
if err != nil {
t.Fatal(err)
}
for _, w := range []string{"1st", "21st", "2nd", "33rd", "20th"} {
if !d.Check(w) {
t.Errorf("%q should be correct", w)
}
}
for _, w := range []string{"st", "1nd", "2st", "st1"} {
if d.Check(w) {
t.Errorf("%q should be wrong", w)
}
}
d, err = Parse([]byte("SET UTF-8\nFLAG long\nCOMPOUNDRULE 1\nCOMPOUNDRULE (aa)(bb)?(cc)\n"),
[]byte("3\nsun/aa\nflower/cc\nlit/bb\n"))
if err != nil {
t.Fatal(err)
}
if !d.Check("sunflower") || !d.Check("sunlitflower") || d.Check("flowersun") || d.Check("sunlit") {
t.Errorf("long-flag COMPOUNDRULE")
}
}