Better handle punctuation inside words like email addresses, urls, file paths, etc.
This commit is contained in:
1 parent
7c53cb39d4
commit
7df1fa3ecb
4 files changed
+101
-7
No files matched your search
+6
-2
@@ -331,8 +331,12 @@ mark.
|
||||
- **Fenrir+Backspace** passes the next key through to the application.
|
||||
|
||||
Character echo, word echo, and cursor echo can be configured separately.
|
||||
Punctuation levels are `none`, `some`, `most`, and `all`. Character review still
|
||||
names punctuation. Capital indicators, emoticons, repeated-symbol summaries,
|
||||
Punctuation levels are `none`, `some`, `most`, and `all`. At `some` and `most`,
|
||||
punctuation inside words, URLs, email addresses, and filenames is also named.
|
||||
Apostrophes within words and single numeric separators such as those in `3.14`,
|
||||
`1,000`, and `12:30` follow the selected level. Sentence punctuation follows the
|
||||
selected level too. Character review always names punctuation.
|
||||
Capital indicators, emoticons, repeated-symbol summaries,
|
||||
indentation feedback, and attribute feedback are configurable in Reading.
|
||||
|
||||
**Fenrir+T** announces the time; press twice quickly for the date.
|
||||
|
||||
@@ -45,6 +45,27 @@ func TestSharedSpeechPunctuationAndPreparedCharacterFeedback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLSpeechKeepsEmbeddedPunctuationAndOriginalClipboardText(t *testing.T) {
|
||||
processor, _, err := punctuation.Load([]string{"../../config/punctuation/default.toml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, substitutions := range []bool{false, true} {
|
||||
settings := config.Defaults()
|
||||
settings.Reading.Emoticons = substitutions
|
||||
settings.Reading.RepeatedSymbols = substitutions
|
||||
voice := &fakeVoice{}
|
||||
echo := &echoSpeaker{Speaker: voice, punctuation: processor, punctuationLevel: "some", settings: &settings}
|
||||
echo.Say("https://stormux.org")
|
||||
if !reflect.DeepEqual(voice.messages, []string{"https colon: slash slash stormux dot. org"}) {
|
||||
t.Fatal("URL speech lost punctuation", substitutions, voice.messages)
|
||||
}
|
||||
if echo.last != "https://stormux.org" {
|
||||
t.Fatal("clipboard echo lost original URL", echo.last)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewCharactersUseProfileRegardlessOfLevelAndKeepBoundaries(t *testing.T) {
|
||||
processor, _, err := punctuation.Load([]string{"../../config/punctuation/fr.toml"})
|
||||
if err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.stormux.org/storm/fenrir/internal/i18n"
|
||||
@@ -156,14 +157,37 @@ func (p *Processor) Character(char rune) string {
|
||||
return presentation.Character(char)
|
||||
}
|
||||
|
||||
// Text follows Python's levels and pause handling, retaining unspoken sentence
|
||||
// punctuation and replacing other unspoken symbols with spaces. Numbers and
|
||||
// contractions therefore retain their original punctuation at lower levels.
|
||||
// Text retains Python's levels and pause handling. At some and most, symbols
|
||||
// embedded between letters or numbers are also named, except apostrophes in
|
||||
// contractions and single numeric separators. Sentence punctuation still follows
|
||||
// the selected level.
|
||||
func (p *Processor) Text(text, level string) string {
|
||||
selected := p.levels[level]
|
||||
var result strings.Builder
|
||||
for _, char := range text {
|
||||
if selected[char] {
|
||||
chars := []rune(text)
|
||||
embeddedEnd := 0
|
||||
embedded := false
|
||||
for index, char := range chars {
|
||||
if index >= embeddedEnd {
|
||||
embeddedEnd = index + 1
|
||||
embedded = false
|
||||
if (level == "some" || level == "most") && (unicode.IsPunct(char) || unicode.IsSymbol(char)) {
|
||||
for embeddedEnd < len(chars) && (unicode.IsPunct(chars[embeddedEnd]) || unicode.IsSymbol(chars[embeddedEnd])) {
|
||||
embeddedEnd++
|
||||
}
|
||||
embedded = index > 0 && embeddedEnd < len(chars) &&
|
||||
is_word_rune(chars[index-1]) && is_word_rune(chars[embeddedEnd])
|
||||
if embedded && embeddedEnd == index+1 {
|
||||
before, after := chars[index-1], chars[embeddedEnd]
|
||||
if char == '\'' && unicode.IsLetter(before) && unicode.IsLetter(after) ||
|
||||
strings.ContainsRune(".,:", char) && unicode.IsDigit(before) && unicode.IsDigit(after) {
|
||||
embedded = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_, known := p.symbols[char]
|
||||
if selected[char] || embedded && known {
|
||||
result.WriteByte(' ')
|
||||
result.WriteString(p.Character(char))
|
||||
if strings.ContainsRune("',.;:?!", char) {
|
||||
@@ -180,3 +204,7 @@ func (p *Processor) Text(text, level string) string {
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func is_word_rune(char rune) bool {
|
||||
return unicode.IsLetter(char) || unicode.IsNumber(char) || unicode.IsMark(char)
|
||||
}
|
||||
@@ -57,6 +57,47 @@ func TestTextLevelsPreservePausesAndDoNotReprocessNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedPunctuation(t *testing.T) {
|
||||
processor, _, err := Load([]string{"../../config/punctuation/default.toml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, trial := range []struct{ input, want string }{
|
||||
{"https://stormux.org", "https colon: slash slash stormux dot. org"},
|
||||
{"Visit https://example.invalid/path?q=a&n=2.", "Visit https colon: slash slash example dot. invalid slash path question?? q equals a and n equals 2."},
|
||||
{"user@example.invalid", "user at example dot. invalid"},
|
||||
{"file_name.txt", "file line name dot. txt"},
|
||||
{"你好.世界 café:menu", "你好 dot. 世界 café colon: menu"},
|
||||
{"cafe\u0301.txt", "cafe\u0301 dot. txt"},
|
||||
{"don't can't", "don't can't"},
|
||||
{"3.14 1,000 12:30", "3.14 1,000 12:30"},
|
||||
{"Hello, world! End. Next: word?", "Hello, world! End. Next: word?"},
|
||||
{"(word) [word]", "word word"},
|
||||
{"a...b", "a dot. dot. dot. b"},
|
||||
} {
|
||||
if got := strings.Join(strings.Fields(processor.Text(trial.input, "some")), " "); got != trial.want {
|
||||
t.Errorf("%q: %q, want %q", trial.input, got, trial.want)
|
||||
}
|
||||
}
|
||||
for _, trial := range []struct{ level, want string }{
|
||||
{"none", "https: stormux.org"},
|
||||
{"some", "https colon: slash slash stormux dot. org"},
|
||||
{"most", "https colon: slash slash stormux dot. org"},
|
||||
{"all", "https colon: slash slash stormux dot. org"},
|
||||
} {
|
||||
if got := processor.Text("https://stormux.org", trial.level); got != trial.want {
|
||||
t.Errorf("%s: %q, want %q", trial.level, got, trial.want)
|
||||
}
|
||||
}
|
||||
french, _, err := Load([]string{"../../config/punctuation/fr.toml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := french.Text("example.invalid", "some"); got != "example point. invalid" {
|
||||
t.Fatal("embedded punctuation lost profile name", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomNamesAndInvalidProfiles(t *testing.T) {
|
||||
data, err := os.ReadFile("../../config/punctuation/default.toml")
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user