@@ -25,21 +25,30 @@ type affix struct {
2525 CrossProduct bool // -
2626}
2727
28- // expand provides all variations of a given word based on this affix rule
29- func (a affix ) expand (word string , out []string ) []string {
28+ // form is one generated word and the continuation flags of the rule that
29+ // produced it, which may be empty.
30+ type form struct {
31+ Word string
32+ Cont string
33+ }
34+
35+ // forms provides all variations of a given word based on this affix rule,
36+ // each paired with the continuation flags that still apply to it.
37+ func (a affix ) forms (word string ) []form {
38+ var out []form
3039 for _ , r := range a .Rules {
3140 if r .matcher != nil && ! r .matcher .MatchString (word ) {
3241 continue
3342 }
3443 if a .Type == Prefix {
35- out = append (out , r .AffixText + word )
44+ out = append (out , form { Word : r .AffixText + word , Cont : r . Cont } )
3645 // TODO is does Strip apply to prefixes too?
3746 } else {
3847 stripWord := word
3948 if r .Strip != "" && strings .HasSuffix (word , r .Strip ) {
4049 stripWord = word [:len (word )- len (r .Strip )]
4150 }
42- out = append (out , stripWord + r .AffixText )
51+ out = append (out , form { Word : stripWord + r .AffixText , Cont : r . Cont } )
4352 }
4453 }
4554 return out
@@ -48,9 +57,16 @@ func (a affix) expand(word string, out []string) []string {
4857// rule is a Affix rule
4958type rule struct {
5059 Strip string
51- AffixText string // suffix or prefix text to add
52- Pattern string // original matching pattern from AFF file
53- matcher * regexp.Regexp // matcher to see if this rule applies or not
60+ AffixText string // suffix or prefix text to add
61+
62+ // Cont holds the continuation flags the rule carries, if any -- the
63+ // "34,22" of `SFX 1 0 t/34,22 e`. They name the affix classes that apply
64+ // again to the form this rule produces, which is how a dictionary spells
65+ // out an inflection built in more than one step.
66+ Cont string
67+
68+ Pattern string // original matching pattern from AFF file
69+ matcher * regexp.Regexp // matcher to see if this rule applies or not
5470}
5571
5672// dictConfig is a partial representation of a Hunspell AFF (Affix) file.
@@ -111,6 +127,11 @@ func (a dictConfig) parseFlags(flagStr string) []string {
111127//
112128// This also supports CompoundRule flags
113129func (a dictConfig ) expand (wordAffix string , out []string ) ([]string , error ) {
130+ return a .expandDepth (wordAffix , out , 0 )
131+ }
132+
133+ // expandDepth is expand, tracking how many continuation classes deep it is.
134+ func (a dictConfig ) expandDepth (wordAffix string , out []string , depth int ) ([]string , error ) {
114135 out = out [:0 ]
115136 idx := strings .Index (wordAffix , "/" )
116137
@@ -156,7 +177,7 @@ func (a dictConfig) expand(wordAffix string, out []string) ([]string, error) {
156177 continue
157178 }
158179 if ! af .CrossProduct {
159- out = af .expand (word , out )
180+ out = a . appendForms ( af .forms (word ) , out , depth )
160181 continue
161182 }
162183 if af .Type == Prefix {
@@ -168,22 +189,60 @@ func (a dictConfig) expand(wordAffix string, out []string) ([]string, error) {
168189
169190 // expand all suffixes with out any prefixes
170191 for _ , suf := range suffixes {
171- out = suf .expand (word , out )
192+ out = a . appendForms ( suf .forms (word ) , out , depth )
172193 }
173194 for _ , pre := range prefixes {
174- prewords := pre .expand (word , nil )
175- out = append ( out , prewords ... )
195+ prewords := pre .forms (word )
196+ out = a . appendForms ( prewords , out , depth )
176197
177198 // now do cross product
178199 for _ , suf := range suffixes {
179200 for _ , w := range prewords {
180- out = suf .expand ( w , out )
201+ out = a . appendForms ( suf .forms ( w . Word ) , out , depth )
181202 }
182203 }
183204 }
184205 return out , nil
185206}
186207
208+ // maxAffixDepth bounds how many times a continuation class may be followed.
209+ //
210+ // Hunspell's default is twofold affixation -- one continuation -- and this
211+ // allows one more for dictionaries that lean on longer chains. A bound is what
212+ // makes this safe at all: nothing stops an .aff file from having a class
213+ // continue to itself, and following that faithfully would not terminate.
214+ const maxAffixDepth = 2
215+
216+ // appendForms adds each generated form to out, then follows any continuation
217+ // flags it carries.
218+ //
219+ // This is the step Hunspell calls twofold affixation: `SFX 1 0 t/34,22 e` says
220+ // that after the rule builds its form, classes 34 and 22 apply to *that*. Not
221+ // following them leaves the further-inflected words unrecognized, which reads
222+ // to a user as their own dictionary not knowing an ordinary word -- most
223+ // visibly in Danish, Dutch and Hungarian, where inflection is built this way.
224+ func (a dictConfig ) appendForms (forms []form , out []string , depth int ) []string {
225+ for _ , f := range forms {
226+ out = append (out , f .Word )
227+ if f .Cont == "" || depth >= maxAffixDepth {
228+ continue
229+ }
230+ // The continuation is expressed exactly like a dictionary entry, so
231+ // it is expanded as one.
232+ more , err := a .expandDepth (f .Word + "/" + f .Cont , nil , depth + 1 )
233+ if err != nil {
234+ continue
235+ }
236+ // expandDepth re-emits the word it was given; it is already in out.
237+ for _ , w := range more {
238+ if w != f .Word {
239+ out = append (out , w )
240+ }
241+ }
242+ }
243+ return out
244+ }
245+
187246// allDigits reports whether s is non-empty and contains only ASCII digits. It
188247// distinguishes a PFX/SFX header's count field from a rule's affix text when
189248// both lines have four fields. See #776.
@@ -370,23 +429,26 @@ func newDictConfig(file io.Reader) (*dictConfig, error) { //nolint:funlen
370429 // See #499.
371430 //
372431 // TODO: Is this safe to do in all cases?
373- affixText := parts [3 ]
432+ affixText , cont := parts [3 ], ""
374433 if affixText == "0" {
375434 affixText = ""
376435 } else if i := strings .Index (affixText , "/" ); i >= 0 {
377- // Strip the affix's own continuation flags, e.g. the
378- // "/34,22" in `SFX 1 0 t/34,22 e`. Otherwise they'd be
379- // appended to the generated word ("stavet/34,22"), so the
380- // real form ("stavet") is never recognized. See #1065.
436+ // Split off the affix's own continuation flags, e.g. the
437+ // "/34,22" in `SFX 1 0 t/34,22 e`. Left in place they would
438+ // be appended to the generated word ("stavet/34,22"), so
439+ // the real form ("stavet") is never recognized. See #1065.
381440 //
382- // NOTE: We don't yet recursively apply continuation classes,
383- // so some further-inflected forms remain unrecognized.
384- affixText = affixText [:i ]
441+ // They are kept rather than dropped: the flags name further
442+ // classes that apply to the form this rule produces, which
443+ // is how Hunspell builds a word like `stavets` from
444+ // `stave` in two steps. See expand.
445+ affixText , cont = affixText [:i ], affixText [i + 1 :]
385446 }
386447
387448 a .Rules = append (a .Rules , rule {
388449 Strip : strip ,
389450 AffixText : affixText ,
451+ Cont : cont ,
390452 Pattern : cond ,
391453 matcher : matcher ,
392454 })
0 commit comments