chore: move runeutil and memoization into internal

This commit is contained in:
Christian Rocha
2024-10-24 15:21:20 -04:00
parent e2033f3460
commit 4e05c24fc4
6 changed files with 7 additions and 5 deletions
+102
View File
@@ -0,0 +1,102 @@
// Package runeutil provides utility functions for tidying up incoming runes
// from Key messages.
package runeutil
import (
"unicode"
"unicode/utf8"
)
// Sanitizer is a helper for bubble widgets that want to process
// Runes from input key messages.
type Sanitizer interface {
// Sanitize removes control characters from runes in a KeyRunes
// message, and optionally replaces newline/carriage return/tabs by a
// specified character.
//
// The rune array is modified in-place if possible. In that case, the
// returned slice is the original slice shortened after the control
// characters have been removed/translated.
Sanitize(runes []rune) []rune
}
// NewSanitizer constructs a rune sanitizer.
func NewSanitizer(opts ...Option) Sanitizer {
s := sanitizer{
replaceNewLine: []rune("\n"),
replaceTab: []rune(" "),
}
for _, o := range opts {
s = o(s)
}
return &s
}
// Option is the type of option that can be passed to Sanitize().
type Option func(sanitizer) sanitizer
// ReplaceTabs replaces tabs by the specified string.
func ReplaceTabs(tabRepl string) Option {
return func(s sanitizer) sanitizer {
s.replaceTab = []rune(tabRepl)
return s
}
}
// ReplaceNewlines replaces newline characters by the specified string.
func ReplaceNewlines(nlRepl string) Option {
return func(s sanitizer) sanitizer {
s.replaceNewLine = []rune(nlRepl)
return s
}
}
func (s *sanitizer) Sanitize(runes []rune) []rune {
// dstrunes are where we are storing the result.
dstrunes := runes[:0:len(runes)]
// copied indicates whether dstrunes is an alias of runes
// or a copy. We need a copy when dst moves past src.
// We use this as an optimization to avoid allocating
// a new rune slice in the common case where the output
// is smaller or equal to the input.
copied := false
for src := 0; src < len(runes); src++ {
r := runes[src]
switch {
case r == utf8.RuneError:
// skip
case r == '\r' || r == '\n':
if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
dst := len(dstrunes)
dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
copy(dstrunes, runes[:dst])
copied = true
}
dstrunes = append(dstrunes, s.replaceNewLine...)
case r == '\t':
if len(dstrunes)+len(s.replaceTab) > src && !copied {
dst := len(dstrunes)
dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
copy(dstrunes, runes[:dst])
copied = true
}
dstrunes = append(dstrunes, s.replaceTab...)
case unicode.IsControl(r):
// Other control characters: skip.
default:
// Keep the character.
dstrunes = append(dstrunes, runes[src])
}
}
return dstrunes
}
type sanitizer struct {
replaceNewLine []rune
replaceTab []rune
}
+44
View File
@@ -0,0 +1,44 @@
package runeutil
import (
"testing"
"unicode/utf8"
)
func TestSanitize(t *testing.T) {
td := []struct {
input, output string
}{
{"", ""},
{"x", "x"},
{"\n", "XX"},
{"\na\n", "XXaXX"},
{"\n\n", "XXXX"},
{"\t", ""},
{"hello", "hello"},
{"hel\nlo", "helXXlo"},
{"hel\rlo", "helXXlo"},
{"hel\tlo", "hello"},
{"he\n\nl\tlo", "heXXXXllo"},
{"he\tl\n\nlo", "helXXXXlo"},
{"hel\x1blo", "hello"},
{"hello\xc2", "hello"}, // invalid utf8
}
for _, tc := range td {
runes := make([]rune, 0, len(tc.input))
b := []byte(tc.input)
for i, w := 0, 0; i < len(b); i += w {
var r rune
r, w = utf8.DecodeRune(b[i:])
runes = append(runes, r)
}
t.Logf("input runes: %+v", runes)
s := NewSanitizer(ReplaceNewlines("XX"), ReplaceTabs(""))
result := s.Sanitize(runes)
rs := string(result)
if tc.output != rs {
t.Errorf("%q: expected %q, got %q (%+v)", tc.input, tc.output, rs, result)
}
}
}