package analyze
import (
"strings"
"chirami/internal/grammar"
"chirami/internal/jp"
)
// Piece is a run of an inflected word: the stem, or what one inflection
// step added, with Yomitan's name and description for that step.
type Piece struct {
Segs []Seg
Romaji string
Name string // "" for the stem
Description string
}
// Inflection splits w into its stem and the inflections Yomitan's
// deinflector finds on the way to the word's dictionary form:
// 食べ|させ|られ|なかっ|た. ok is false for words that are not inflected
// or whose inflections do not line up with the surface.
func (a *Analyzer) Inflection(w Word) ([]Piece, bool) {
if w.Key == "" || w.Key == w.Surface {
return nil, false
}
var best []grammar.Step
stemLen := 0
for _, r := range grammar.Deinflect(w.Surface) {
if r.Base != w.Key || len(r.Steps) <= len(best) {
continue
}
n, ok := stem(w.Surface, r.Steps)
if !ok || !strings.HasPrefix(w.Key, w.Surface[:n]) {
continue
}
best, stemLen = r.Steps, n
}
if best == nil {
return nil, false
}
cuts := []int{stemLen}
for _, s := range best {
cuts = append(cuts, cuts[len(cuts)-1]+len(s.Suffix))
}
parts, ok := splitSegs(w.Segs, cuts)
if !ok {
return nil, false
}
pieces := []Piece{{Segs: parts[0]}}
for i, s := range best {
pieces = append(pieces, Piece{Segs: parts[i+1], Name: s.Name, Description: s.Description})
}
readings := make([]string, len(pieces))
for i, p := range pieces {
readings[i] = segsReading(p.Segs)
}
for i := range pieces {
pieces[i].Romaji = a.pieceRomaji(readings[i:])
}
return pieces, true
}
// pieceRomaji romanizes the first reading in context: a trailing っ doubles
// the next piece's consonant (なかっ|た -> nakat|ta) and ん takes its
// apostrophe before a vowel, so it converts the piece with the next mora
// and cuts that mora's romaji off again.
func (a *Analyzer) pieceRomaji(readings []string) string {
cur := readings[0]
if len(readings) == 1 || readings[1] == "" {
return jp.Romaji(cur, a.Style)
}
next := jp.Moras(readings[1])[0]
both, tail := jp.Romaji(cur+next, a.Style), jp.Romaji(next, a.Style)
if strings.HasSuffix(both, tail) {
return strings.TrimSuffix(both, tail)
}
return jp.Romaji(cur, a.Style)
}
func segsReading(segs []Seg) string {
var b strings.Builder
for _, s := range segs {
if s.Ruby != "" {
b.WriteString(s.Ruby)
} else {
b.WriteString(jp.ToHiragana(s.Text))
}
}
return b.String()
}
// stem returns the byte length of the stem when the steps' suffixes,
// appended in order, spell the end of surface.
func stem(surface string, steps []grammar.Step) (int, bool) {
n := len(surface)
for i := len(steps) - 1; i >= 0; i-- {
sfx := steps[i].Suffix
if sfx == "" || !strings.HasSuffix(surface[:n], sfx) {
return 0, false
}
n -= len(sfx)
}
return n, n > 0
}
// splitSegs cuts segs at the byte offsets in cuts (the last is the total
// length). A cut inside a segment with ruby fails: a reading cannot be split.
func splitSegs(segs []Seg, cuts []int) ([][]Seg, bool) {
out := make([][]Seg, len(cuts))
pos, ci := 0, 0
for _, s := range segs {
text := s.Text
for text != "" {
end := cuts[ci] - pos
if end <= 0 {
ci++
if ci == len(cuts) {
return nil, false
}
continue
}
if end >= len(text) {
out[ci] = append(out[ci], Seg{text, s.Ruby})
pos += len(text)
break
}
if s.Ruby != "" {
return nil, false
}
out[ci] = append(out[ci], Seg{Text: text[:end]})
text, pos = text[end:], pos+end
}
}
return out, pos == cuts[len(cuts)-1]
}