package analyze
import (
"strings"
"chirami/internal/dict"
"chirami/internal/jp"
"chirami/internal/morph"
)
// furigana segments one morpheme, preferring JmdictFurigana's per-kanji
// readings: for the surface itself, else for its dictionary form with the
// inflected ending swapped in, else the kana-matching aligner below.
func (a *Analyzer) furigana(t *morph.Token, reading string) []Seg {
if a.Dict == nil || reading == "" || !hasKanji(t.Surface) {
return Furigana(t.Surface, reading)
}
if rs, ok := a.Dict.Furigana(t.Surface, reading); ok {
return segs(rs)
}
base := t.Base
if br := baseReading(t.Surface, base, reading); br != "" && base != t.Surface {
if rs, ok := a.Dict.Furigana(base, br); ok {
if s, ok := inflect(rs, t.Surface, reading); ok {
return s
}
}
}
return Furigana(t.Surface, reading)
}
// inflect reuses a dictionary form's segments for an inflected surface: the
// kanji segments are a shared prefix, the rest is plain kana. The result must
// spell out reading, or it is rejected.
func inflect(base []dict.Ruby, surface, reading string) ([]Seg, bool) {
var out []Seg
rest, spelled := surface, ""
for _, r := range base {
if !strings.HasPrefix(rest, r.Text) {
if r.Ruby != "" {
return nil, false
}
break
}
out = append(out, Seg{r.Text, r.Ruby})
rest = rest[len(r.Text):]
spelled += cmpRuby(r)
}
if rest != "" {
out = append(out, Seg{Text: rest})
spelled += jp.ToHiragana(rest)
}
return appendSegs(nil, out), spelled == reading
}
func cmpRuby(r dict.Ruby) string {
if r.Ruby != "" {
return r.Ruby
}
return jp.ToHiragana(r.Text)
}
func segs(rs []dict.Ruby) []Seg {
out := make([]Seg, len(rs))
for i, r := range rs {
out[i] = Seg{r.Text, r.Ruby}
}
return appendSegs(nil, out)
}
// Furigana splits surface into runs and puts reading only over the kanji
// runs, matching the kana runs literally: 食べる/たべる -> 食(た) べる.
// When the kana do not line up it puts the whole reading over the whole
// surface.
func Furigana(surface, reading string) []Seg {
if reading == "" || !hasKanji(surface) {
return []Seg{{Text: surface}}
}
runs := splitRuns(surface)
if rubies, ok := align(runs, reading); ok {
segs := make([]Seg, len(runs))
for i, r := range runs {
segs[i] = Seg{Text: r.text}
if r.kanji {
segs[i].Ruby = rubies[i]
}
}
return segs
}
return []Seg{{Text: surface, Ruby: reading}}
}
type run struct {
text string
kanji bool
}
func hasKanji(s string) bool { return strings.IndexFunc(s, jp.IsKanji) >= 0 }
func splitRuns(s string) []run {
var runs []run
for _, r := range s {
k := jp.IsKanji(r)
if n := len(runs); n > 0 && runs[n-1].kanji == k {
runs[n-1].text += string(r)
continue
}
runs = append(runs, run{string(r), k})
}
return runs
}
// align matches kana runs against reading and gives each kanji run the
// reading between them, backtracking over where each kanji run ends.
func align(runs []run, reading string) ([]string, bool) {
out := make([]string, len(runs))
var walk func(i int, rest string) bool
walk = func(i int, rest string) bool {
if i == len(runs) {
return rest == ""
}
r := runs[i]
if !r.kanji {
k := jp.ToHiragana(r.text)
return strings.HasPrefix(rest, k) && walk(i+1, rest[len(k):])
}
// a kanji run reads as at least one kana
for j, c := range rest {
end := j + len(string(c))
out[i] = rest[:end]
if walk(i+1, rest[end:]) {
return true
}
}
return false
}
return out, walk(0, reading)
}