More work on repeated string summarization.
This commit is contained in:
1 parent
3cf41e662c
commit
47f18fdd28
7 files changed
+122
-34
No files matched your search
@@ -76,7 +76,7 @@ punctuation_level = "some"
|
|||||||
# Whole whitespace-delimited emoticons, matching Python's names/boundaries.
|
# Whole whitespace-delimited emoticons, matching Python's names/boundaries.
|
||||||
emoticons = true
|
emoticons = true
|
||||||
# Optional summaries for repeated-symbol tokens, e.g. ##### -> 5 number.
|
# Optional summaries for repeated-symbol tokens, e.g. ##### -> 5 number.
|
||||||
# Matching brackets around a run are allowed, e.g. [#####].
|
# Matching brackets may enclose multiple runs, e.g. [#####-----].
|
||||||
# Only symbols named at the selected punctuation level are summarized.
|
# Only symbols named at the selected punctuation level are summarized.
|
||||||
repeated_symbols = false
|
repeated_symbols = false
|
||||||
# Minimum run length, 2–10000; default 4 means more than three repeats.
|
# Minimum run length, 2–10000; default 4 means more than three repeats.
|
||||||
|
|||||||
+4
-1
@@ -119,7 +119,10 @@ Punctuation levels are `none`, `some`, `most`, and `all`. At `some` and `most`,
|
|||||||
punctuation inside words, URLs, email addresses, and filenames is also named.
|
punctuation inside words, URLs, email addresses, and filenames is also named.
|
||||||
Apostrophes within words and single numeric separators such as those in `3.14`,
|
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
|
`1,000`, and `12:30` follow the selected level. Sentence punctuation follows the
|
||||||
selected level too. Character review always names punctuation.
|
selected level too. A percent sign immediately after a digit is named at `some`
|
||||||
|
and `most`, so `100%` is read as "100 percent". Character review always names
|
||||||
|
punctuation. Repeated-symbol summaries also handle multiple runs inside matching
|
||||||
|
brackets, such as `[#####-----]`.
|
||||||
Capital indicators, emoticons, repeated-symbol summaries,
|
Capital indicators, emoticons, repeated-symbol summaries,
|
||||||
indentation feedback, and attribute feedback are configurable in Reading.
|
indentation feedback, and attribute feedback are configurable in Reading.
|
||||||
|
|
||||||
|
|||||||
@@ -75,16 +75,24 @@ func TestPackageProgressBarSpeechKeepsOriginalClipboardText(t *testing.T) {
|
|||||||
settings.Reading.RepeatedSymbols = true
|
settings.Reading.RepeatedSymbols = true
|
||||||
voice := &fakeVoice{}
|
voice := &fakeVoice{}
|
||||||
echo := &echoSpeaker{Speaker: voice, punctuation: processor, punctuationLevel: "some", settings: &settings}
|
echo := &echoSpeaker{Speaker: voice, punctuation: processor, punctuationLevel: "some", settings: &settings}
|
||||||
text := "(123/189) upgrading nodejs-lts-jod" + strings.Repeat(" ", 157) + "[" + strings.Repeat("#", 120) + "] 100%"
|
for _, trial := range []struct{ bar, want string }{
|
||||||
echo.Say(text)
|
{strings.Repeat("#", 120), "120 number"},
|
||||||
if len(voice.messages) != 1 {
|
{strings.Repeat("#", 110) + strings.Repeat("-", 10), "110 number 10 dash"},
|
||||||
t.Fatal("missing progress speech", voice.messages)
|
{strings.Repeat("#", 90) + strings.Repeat("-", 10), "90 number 10 dash"},
|
||||||
}
|
} {
|
||||||
if got := strings.Join(strings.Fields(voice.messages[0]), " "); got != "123 slash 189 upgrading nodejs dash lts dash jod 120 number 100" {
|
voice.messages = nil
|
||||||
t.Fatal("progress bar did not summarize", got)
|
text := "(123/189) upgrading nodejs-lts-jod" + strings.Repeat(" ", 157) + "[" + trial.bar + "] 100%"
|
||||||
}
|
echo.Say(text)
|
||||||
if echo.last != text {
|
if len(voice.messages) != 1 {
|
||||||
t.Fatal("clipboard echo lost original progress bar", echo.last)
|
t.Fatal("missing progress speech", voice.messages)
|
||||||
|
}
|
||||||
|
want := "123 slash 189 upgrading nodejs dash lts dash jod " + trial.want + " 100 percent"
|
||||||
|
if got := strings.Join(strings.Fields(voice.messages[0]), " "); got != want {
|
||||||
|
t.Fatal("progress bar did not summarize", got)
|
||||||
|
}
|
||||||
|
if echo.last != text {
|
||||||
|
t.Fatal("clipboard echo lost original progress bar", echo.last)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -159,8 +159,8 @@ func (p *Processor) Character(char rune) string {
|
|||||||
|
|
||||||
// Text retains Python's levels and pause handling. At some and most, symbols
|
// Text retains Python's levels and pause handling. At some and most, symbols
|
||||||
// embedded between letters or numbers are also named, except apostrophes in
|
// embedded between letters or numbers are also named, except apostrophes in
|
||||||
// contractions and single numeric separators. Sentence punctuation still follows
|
// contractions and single numeric separators. A percent sign after a digit is
|
||||||
// the selected level.
|
// also named at some and most. Sentence punctuation follows the selected level.
|
||||||
func (p *Processor) Text(text, level string) string {
|
func (p *Processor) Text(text, level string) string {
|
||||||
selected := p.levels[level]
|
selected := p.levels[level]
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
@@ -187,7 +187,8 @@ func (p *Processor) Text(text, level string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, known := p.symbols[char]
|
_, known := p.symbols[char]
|
||||||
if selected[char] || embedded && known {
|
percentage := (level == "some" || level == "most") && char == '%' && index > 0 && unicode.IsDigit(chars[index-1])
|
||||||
|
if selected[char] || (embedded || percentage) && known {
|
||||||
result.WriteByte(' ')
|
result.WriteByte(' ')
|
||||||
result.WriteString(p.Character(char))
|
result.WriteString(p.Character(char))
|
||||||
if strings.ContainsRune("',.;:?!", char) {
|
if strings.ContainsRune("',.;:?!", char) {
|
||||||
|
|||||||
@@ -98,6 +98,52 @@ func TestEmbeddedPunctuation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNumericPercentSpeech(t *testing.T) {
|
||||||
|
processor, _, err := Load([]string{"../../config/punctuation/default.toml"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, trial := range []struct{ text, want string }{
|
||||||
|
{"100%", "100 percent"},
|
||||||
|
{"0%", "0 percent"},
|
||||||
|
{"12.5%", "12.5 percent"},
|
||||||
|
{"-5%", "dash 5 percent"},
|
||||||
|
{"(100%)", "100 percent"},
|
||||||
|
{"100%, done", "100 percent , done"},
|
||||||
|
{"100%", "100 percent"},
|
||||||
|
{"%", ""},
|
||||||
|
{"text%", "text"},
|
||||||
|
{"100 %", "100"},
|
||||||
|
{"don't 3.14 1,000 12:30", "don't 3.14 1,000 12:30"},
|
||||||
|
} {
|
||||||
|
for _, options := range []TextOptions{{}, {Emoticons: true, RepeatedSymbols: true, RepeatedSymbolsMin: 4}} {
|
||||||
|
if got := strings.Join(strings.Fields(processor.Process_text(trial.text, "some", options)), " "); got != trial.want {
|
||||||
|
t.Errorf("%q, options %+v: got %q, want %q", trial.text, options, got, trial.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, level := range Levels() {
|
||||||
|
want := "100 percent"
|
||||||
|
if level == "none" {
|
||||||
|
want = "100"
|
||||||
|
}
|
||||||
|
if got := strings.Join(strings.Fields(processor.Text("100%", level)), " "); got != want {
|
||||||
|
t.Errorf("level %s: got %q, want %q", level, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile("../../config/punctuation/default.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
custom, err := Decode([]byte(strings.Replace(string(data), `"%" = "percent"`, `"%" = "per/cent"`, 1)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := custom.Text("100%", "some"); got != "100 per/cent " {
|
||||||
|
t.Fatal("percent suffix lost or reprocessed profile name", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCustomNamesAndInvalidProfiles(t *testing.T) {
|
func TestCustomNamesAndInvalidProfiles(t *testing.T) {
|
||||||
data, err := os.ReadFile("../../config/punctuation/default.toml")
|
data, err := os.ReadFile("../../config/punctuation/default.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ func (p *Processor) process_token(token, level string, options TextOptions) stri
|
|||||||
repeated := token
|
repeated := token
|
||||||
prefix, suffix := "", ""
|
prefix, suffix := "", ""
|
||||||
// Progress bars commonly enclose a repeated run in brackets. Keep
|
// Progress bars commonly enclose a repeated run in brackets. Keep
|
||||||
// those delimiters under ordinary punctuation handling, and require
|
// those delimiters under ordinary punctuation handling. Inside them,
|
||||||
// the entire interior to repeat so paths and mixed tokens stay literal.
|
// summarize each run, including partially filled bars such as [####----].
|
||||||
if len(token) >= 2 {
|
if len(token) >= 2 {
|
||||||
for _, pair := range []string{"[]", "()", "{}", "<>"} {
|
for _, pair := range []string{"[]", "()", "{}", "<>"} {
|
||||||
if token[0] == pair[0] && token[len(token)-1] == pair[1] {
|
if token[0] == pair[0] && token[len(token)-1] == pair[1] {
|
||||||
@@ -61,16 +61,25 @@ func (p *Processor) process_token(token, level string, options TextOptions) stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
char, _ := utf8.DecodeRuneInString(repeated)
|
chars := []rune(repeated)
|
||||||
if p.levels[level][char] && (unicode.IsPunct(char) || unicode.IsSymbol(char)) {
|
for _, char := range chars {
|
||||||
count := 0
|
if !(unicode.IsPunct(char) || unicode.IsSymbol(char)) || prefix == "" && char != chars[0] {
|
||||||
for _, next := range repeated {
|
return p.Text(token, level)
|
||||||
if next != char {
|
|
||||||
return p.Text(token, level)
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
}
|
}
|
||||||
if count >= options.RepeatedSymbolsMin {
|
}
|
||||||
|
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)
|
name := p.Character(char)
|
||||||
// G13's requested English summary is "5 number". Keep ordinary
|
// G13's requested English summary is "5 number". Keep ordinary
|
||||||
// hash feedback, custom names and localized profile names intact.
|
// hash feedback, custom names and localized profile names intact.
|
||||||
@@ -80,8 +89,15 @@ func (p *Processor) process_token(token, level string, options TextOptions) stri
|
|||||||
name = i18n.T(name)
|
name = i18n.T(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p.Text(prefix, level) + i18n.Format("{0} {1}", strconv.Itoa(count), name) + p.Text(suffix, level)
|
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)
|
return p.Text(token, level)
|
||||||
|
|||||||
@@ -32,13 +32,17 @@ func TestTextSubstitutionsAndBoundaries(t *testing.T) {
|
|||||||
{"(#####)", " 5 number "},
|
{"(#####)", " 5 number "},
|
||||||
{"{#####}", " 5 number "},
|
{"{#####}", " 5 number "},
|
||||||
{"<#####>", " 5 number "},
|
{"<#####>", " 5 number "},
|
||||||
|
{"[#####-----]", " 5 number 5 dash "},
|
||||||
|
{"[-----#####]", " 5 dash 5 number "},
|
||||||
|
{"[#####----#####]", " 5 number 4 dash 5 number "},
|
||||||
|
{"[#####--]", " 5 number dash dash "},
|
||||||
{"Oo", "WTF?"},
|
{"Oo", "WTF?"},
|
||||||
} {
|
} {
|
||||||
if got := processor.Process_text(trial.text, "some", options); got != trial.want {
|
if got := processor.Process_text(trial.text, "some", options); got != trial.want {
|
||||||
t.Errorf("%q: %q, want %q", trial.text, got, trial.want)
|
t.Errorf("%q: %q, want %q", trial.text, got, trial.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, text := range []string{"hello :)!", "path/to:)file", "https://example.invalid/:)", "https://example.invalid/:-/", "(o_O)", "###", "##--##", "a####b", "../....", "https://example.invalid/####", "10000", "aaaa", "🙂🙂🙂🙂", "", "[]", "[###]", "[####)", "[##--##]", "[a####b]", "[../....]", "[https://example.invalid/####]"} {
|
for _, text := range []string{"hello :)!", "path/to:)file", "https://example.invalid/:)", "https://example.invalid/:-/", "(o_O)", "###", "##--##", "a####b", "../....", "https://example.invalid/####", "10000", "aaaa", "🙂🙂🙂🙂", "", "[]", "[###]", "[####)", "[##--##]", "[a####b]", "[https://example.invalid/####]"} {
|
||||||
for _, level := range Levels() {
|
for _, level := range Levels() {
|
||||||
if got, want := processor.Process_text(text, level, options), processor.Text(text, level); got != want {
|
if got, want := processor.Process_text(text, level, options), processor.Text(text, level); got != want {
|
||||||
t.Errorf("%q, %s changed ordinary processing: %q, want %q", text, level, got, want)
|
t.Errorf("%q, %s changed ordinary processing: %q, want %q", text, level, got, want)
|
||||||
@@ -93,6 +97,9 @@ func TestSubstitutionOptionsAndProfileNames(t *testing.T) {
|
|||||||
if got := custom.Process_text("////", "some", options); got != "4 path/separator" {
|
if got := custom.Process_text("////", "some", options); got != "4 path/separator" {
|
||||||
t.Fatal("custom name changed or processed twice", got)
|
t.Fatal("custom name changed or processed twice", got)
|
||||||
}
|
}
|
||||||
|
if got := custom.Process_text("[////####]", "some", options); got != " 4 path/separator 4 number " {
|
||||||
|
t.Fatal("mixed summary reprocessed custom name", got)
|
||||||
|
}
|
||||||
french, _, err := Load([]string{"../../config/punctuation/fr.toml"})
|
french, _, err := Load([]string{"../../config/punctuation/fr.toml"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -118,12 +125,19 @@ func TestBracketedRepeatedSymbolsRespectOptionsAndLevels(t *testing.T) {
|
|||||||
{RepeatedSymbols: true, RepeatedSymbolsMin: 6},
|
{RepeatedSymbols: true, RepeatedSymbolsMin: 6},
|
||||||
{RepeatedSymbols: true, RepeatedSymbolsMin: 4},
|
{RepeatedSymbols: true, RepeatedSymbolsMin: 4},
|
||||||
} {
|
} {
|
||||||
want := processor.Text("[#####]", level)
|
for _, interior := range []string{"#####", "#####-----"} {
|
||||||
if options.RepeatedSymbols && options.RepeatedSymbolsMin == 4 && level != "none" {
|
text := "[" + interior + "]"
|
||||||
want = processor.Text("[", level) + "5 number" + processor.Text("]", level)
|
want := processor.Text(text, level)
|
||||||
}
|
if options.RepeatedSymbols && options.RepeatedSymbolsMin == 4 && level != "none" {
|
||||||
if got := processor.Process_text("[#####]", level, options); got != want {
|
summary := "5 number"
|
||||||
t.Errorf("level %s, options %+v: got %q, want %q", level, options, got, want)
|
if interior == "#####-----" {
|
||||||
|
summary += " 5 dash"
|
||||||
|
}
|
||||||
|
want = processor.Text("[", level) + summary + processor.Text("]", level)
|
||||||
|
}
|
||||||
|
if got := processor.Process_text(text, level, options); got != want {
|
||||||
|
t.Errorf("%q, level %s, options %+v: got %q, want %q", text, level, options, got, want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user