1// Package runeutil provides utility functions for tidying up incoming runes
2// from Key messages.
3package runeutil
4
5import (
6 "unicode"
7 "unicode/utf8"
8)
9
10// Sanitizer is a helper for bubble widgets that want to process
11// Runes from input key messages.
12type Sanitizer interface {
13 // Sanitize removes control characters from runes in a KeyRunes
14 // message, and optionally replaces newline/carriage return/tabs by a
15 // specified character.
16 //
17 // The rune array is modified in-place if possible. In that case, the
18 // returned slice is the original slice shortened after the control
19 // characters have been removed/translated.
20 Sanitize(runes []rune) []rune
21}
22
23// NewSanitizer constructs a rune sanitizer.
24func NewSanitizer(opts ...Option) Sanitizer {
25 s := sanitizer{
26 replaceNewLine: []rune("\n"),
27 replaceTab: []rune(" "),
28 }
29 for _, o := range opts {
30 s = o(s)
31 }
32 return &s
33}
34
35// Option is the type of option that can be passed to Sanitize().
36type Option func(sanitizer) sanitizer
37
38// ReplaceTabs replaces tabs by the specified string.
39func ReplaceTabs(tabRepl string) Option {
40 return func(s sanitizer) sanitizer {
41 s.replaceTab = []rune(tabRepl)
42 return s
43 }
44}
45
46// ReplaceNewlines replaces newline characters by the specified string.
47func ReplaceNewlines(nlRepl string) Option {
48 return func(s sanitizer) sanitizer {
49 s.replaceNewLine = []rune(nlRepl)
50 return s
51 }
52}
53
54func (s *sanitizer) Sanitize(runes []rune) []rune {
55 // dstrunes are where we are storing the result.
56 dstrunes := runes[:0:len(runes)]
57 // copied indicates whether dstrunes is an alias of runes
58 // or a copy. We need a copy when dst moves past src.
59 // We use this as an optimization to avoid allocating
60 // a new rune slice in the common case where the output
61 // is smaller or equal to the input.
62 copied := false
63
64 for src := range runes {
65 r := runes[src]
66 switch {
67 case r == utf8.RuneError:
68 // skip
69
70 case r == '\r' || r == '\n':
71 if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
72 dst := len(dstrunes)
73 dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
74 copy(dstrunes, runes[:dst])
75 copied = true
76 }
77 dstrunes = append(dstrunes, s.replaceNewLine...)
78
79 case r == '\t':
80 if len(dstrunes)+len(s.replaceTab) > src && !copied {
81 dst := len(dstrunes)
82 dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
83 copy(dstrunes, runes[:dst])
84 copied = true
85 }
86 dstrunes = append(dstrunes, s.replaceTab...)
87
88 case unicode.IsControl(r):
89 // Other control characters: skip.
90
91 default:
92 // Keep the character.
93 dstrunes = append(dstrunes, runes[src])
94 }
95 }
96 return dstrunes
97}
98
99type sanitizer struct {
100 replaceNewLine []rune
101 replaceTab []rune
102}