Files

105 lines
3.0 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package punctuation
import (
"strconv"
"strings"
"unicode"
"unicode/utf8"
"git.stormux.org/storm/fenrir/internal/i18n"
)
type TextOptions struct {
Emoticons bool
RepeatedSymbols bool
RepeatedSymbolsMin int
}
// Process_text substitutes complete tokens before ordinary punctuation. Each
// generated name is emitted once, without processing punctuation inside it.
// Whitespace belongs to neither neighboring token, so adjacent emoticons work.
func (p *Processor) Process_text(text, level string, options TextOptions) string {
if !options.Emoticons && !options.RepeatedSymbols {
return p.Text(text, level)
}
var result strings.Builder
tokenStart := 0
for index, char := range text {
if !unicode.IsSpace(char) {
continue
}
result.WriteString(p.process_token(text[tokenStart:index], level, options))
result.WriteString(p.Text(string(char), level))
tokenStart = index + utf8.RuneLen(char)
}
result.WriteString(p.process_token(text[tokenStart:], level, options))
return result.String()
}
func (p *Processor) process_token(token, level string, options TextOptions) string {
if options.Emoticons {
if name, exists := emoticonNames[token]; exists {
return i18n.T(name)
}
}
if options.RepeatedSymbols && options.RepeatedSymbolsMin >= 2 && token != "" {
repeated := token
prefix, suffix := "", ""
// Progress bars commonly enclose repeated runs in brackets. Keep
// those delimiters under ordinary punctuation handling. Symbol-only
// tokens, including diffstats such as +++++-----, summarize each run.
if len(token) >= 2 {
for _, pair := range []string{"[]", "()", "{}", "<>"} {
if token[0] == pair[0] && token[len(token)-1] == pair[1] {
prefix, suffix = token[:1], token[len(token)-1:]
repeated = token[1 : len(token)-1]
break
}
}
}
chars := []rune(repeated)
for _, char := range chars {
if !(unicode.IsPunct(char) || unicode.IsSymbol(char)) {
return p.Text(token, level)
}
}
var result strings.Builder
summarized := false
for start := 0; start < len(chars); {
char := chars[start]
end := start + 1
for end < len(chars) && chars[end] == char {
end++
}
count := end - start
if start > 0 {
result.WriteByte(' ')
}
if p.levels[level][char] && count >= options.RepeatedSymbolsMin {
name := p.Character(char)
// G13's requested English summary is "5 number". Keep ordinary
// hash feedback, custom names and localized profile names intact.
if char == '#' && p.symbols[char] == "hash" {
name = "number"
if p.translate {
name = i18n.T(name)
}
}
result.WriteString(i18n.Format("{0} {1}", strconv.Itoa(count), name))
summarized = true
} else {
result.WriteString(p.Text(string(chars[start:end]), level))
}
start = end
}
if summarized {
return p.Text(prefix, level) + result.String() + p.Text(suffix, level)
}
}
return p.Text(token, level)
}