// 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) }