Case-fold patterns.
This commit is contained in:
parent
df14a314e4
commit
96942f9acc
5 changed files with 162 additions and 80 deletions
102
file.go
102
file.go
|
@ -4,6 +4,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// SelectFile displays the file selection dialog.
|
||||
|
@ -69,6 +70,8 @@ func Filename(filename string) Option {
|
|||
|
||||
// FileFilter is an Option that sets a filename filter.
|
||||
//
|
||||
// On Windows and macOS filtering is always case-insensitive.
|
||||
//
|
||||
// macOS hides filename filters from the user,
|
||||
// and only supports filtering by extension
|
||||
// (or "uniform type identifiers").
|
||||
|
@ -78,6 +81,7 @@ func Filename(filename string) Option {
|
|||
type FileFilter struct {
|
||||
Name string // display string that describes the filter (optional)
|
||||
Patterns []string // filter patterns for the display string
|
||||
CaseFold bool // if set patterns are matched case-insensitively
|
||||
}
|
||||
|
||||
func (f FileFilter) apply(o *options) {
|
||||
|
@ -91,7 +95,7 @@ func (f FileFilters) apply(o *options) {
|
|||
o.fileFilters = append(o.fileFilters, f...)
|
||||
}
|
||||
|
||||
// Windows' patterns need a name.
|
||||
// Windows patterns need a name.
|
||||
func (f FileFilters) name() {
|
||||
for i, filter := range f {
|
||||
if filter.Name == "" {
|
||||
|
@ -100,47 +104,42 @@ func (f FileFilters) name() {
|
|||
}
|
||||
}
|
||||
|
||||
// Windows' patterns are case insensitive, don't support character classes or escaping.
|
||||
// Windows patterns are case-insensitive, don't support character classes or escaping.
|
||||
//
|
||||
// First we remove character classes, then escaping. Patterns with literal wildcards are invalid.
|
||||
// The semicolon is a separator, so we replace it with the single character wildcard.
|
||||
// Empty and invalid filters/patterns are ignored.
|
||||
func (f FileFilters) simplify() {
|
||||
var i = 0
|
||||
for _, filter := range f {
|
||||
for i := range f {
|
||||
var j = 0
|
||||
for _, pattern := range filter.Patterns {
|
||||
for _, pattern := range f[i].Patterns {
|
||||
var escape, invalid bool
|
||||
var buf strings.Builder
|
||||
for _, r := range removeClasses(pattern) {
|
||||
if !escape && r == '\\' {
|
||||
for _, b := range []byte(removeClasses(pattern)) {
|
||||
if !escape && b == '\\' {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
if escape && (r == '*' || r == '?') {
|
||||
if escape && (b == '*' || b == '?') {
|
||||
invalid = true
|
||||
break
|
||||
}
|
||||
if r == ';' {
|
||||
r = '?'
|
||||
if b == ';' {
|
||||
b = '?'
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
buf.WriteByte(b)
|
||||
escape = false
|
||||
}
|
||||
if buf.Len() > 0 && !invalid {
|
||||
filter.Patterns[j] = buf.String()
|
||||
f[i].Patterns[j] = buf.String()
|
||||
j++
|
||||
}
|
||||
}
|
||||
if j > 0 {
|
||||
filter.Patterns = filter.Patterns[:j]
|
||||
f[i] = filter
|
||||
i++
|
||||
if j != 0 {
|
||||
f[i].Patterns = f[i].Patterns[:j]
|
||||
} else {
|
||||
f[i].Patterns = nil
|
||||
}
|
||||
}
|
||||
for ; i < len(f); i++ {
|
||||
f[i] = FileFilter{}
|
||||
}
|
||||
}
|
||||
|
||||
// macOS types may be specified as extension strings without the leading period,
|
||||
|
@ -163,17 +162,17 @@ func (f FileFilters) types() []string {
|
|||
|
||||
var escape bool
|
||||
var buf strings.Builder
|
||||
for _, r := range removeClasses(ext) {
|
||||
for _, b := range []byte(removeClasses(ext)) {
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
case r == '\\':
|
||||
case b == '\\':
|
||||
escape = true
|
||||
continue
|
||||
case r == '*' || r == '?':
|
||||
case b == '*' || b == '?':
|
||||
return nil
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
buf.WriteByte(b)
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
res = append(res, buf.String())
|
||||
|
@ -187,8 +186,57 @@ func (f FileFilters) types() []string {
|
|||
return append([]string{""}, res...)
|
||||
}
|
||||
|
||||
// Unix patterns are case-sensitive. Fold them if requested.
|
||||
func (f FileFilters) casefold() {
|
||||
for i := range f {
|
||||
if !f[i].CaseFold {
|
||||
continue
|
||||
}
|
||||
for j, pattern := range f[i].Patterns {
|
||||
var class = -1
|
||||
var escape bool
|
||||
var buf strings.Builder
|
||||
for i, r := range pattern {
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
case r == '\\':
|
||||
escape = true
|
||||
case class < 0:
|
||||
if r == '[' {
|
||||
class = i
|
||||
}
|
||||
case class < i-1:
|
||||
if r == ']' {
|
||||
class = -1
|
||||
}
|
||||
}
|
||||
|
||||
nr := unicode.SimpleFold(r)
|
||||
if r == nr {
|
||||
buf.WriteRune(r)
|
||||
continue
|
||||
}
|
||||
|
||||
if class < 0 {
|
||||
buf.WriteByte('[')
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
for r != nr {
|
||||
buf.WriteRune(nr)
|
||||
nr = unicode.SimpleFold(nr)
|
||||
}
|
||||
if class < 0 {
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
}
|
||||
f[i].Patterns[j] = buf.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove character classes from pattern, assuming case insensitivity.
|
||||
// Classes of one character (case insensitive) are replaced by the character.
|
||||
// Classes of one character (case-insensitive) are replaced by the character.
|
||||
// Others are replaced by the single character wildcard.
|
||||
func removeClasses(pattern string) string {
|
||||
var res strings.Builder
|
||||
|
@ -229,7 +277,7 @@ func removeClasses(pattern string) string {
|
|||
// Find a character class in the pattern.
|
||||
func findClass(pattern string) (start, end int) {
|
||||
start = -1
|
||||
escape := false
|
||||
var escape bool
|
||||
for i, b := range []byte(pattern) {
|
||||
switch {
|
||||
case escape:
|
||||
|
@ -240,7 +288,7 @@ func findClass(pattern string) (start, end int) {
|
|||
if b == '[' {
|
||||
start = i
|
||||
}
|
||||
case 0 <= start && start < i-1:
|
||||
case start < i-1:
|
||||
if b == ']' {
|
||||
return start, i + 1
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ func TestFileFilters_name(t *testing.T) {
|
|||
data FileFilters
|
||||
want string
|
||||
}{
|
||||
{FileFilters{{"", []string{`*.png`}}}, "*.png"},
|
||||
{FileFilters{{"", []string{`*.png`, `*.jpg`}}}, "*.png *.jpg"},
|
||||
{FileFilters{{"Image files", []string{`*.png`, `*.jpg`}}}, "Image files"},
|
||||
{FileFilters{{"", []string{`*.png`}, true}}, "*.png"},
|
||||
{FileFilters{{"", []string{`*.png`, `*.jpg`}, true}}, "*.png *.jpg"},
|
||||
{FileFilters{{"Image files", []string{`*.png`, `*.jpg`}, true}}, "Image files"},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
tt.data.name()
|
||||
|
@ -24,57 +24,88 @@ func TestFileFilters_name(t *testing.T) {
|
|||
|
||||
func TestFileFilters_simplify(t *testing.T) {
|
||||
tests := []struct {
|
||||
data FileFilters
|
||||
data []string
|
||||
want []string
|
||||
}{
|
||||
{FileFilters{{"", []string{``}}}, nil},
|
||||
{FileFilters{{"", []string{`*.png`}}}, []string{"*.png"}},
|
||||
{FileFilters{{"", []string{`*.pn?`}}}, []string{"*.pn?"}},
|
||||
{FileFilters{{"", []string{`*.pn;`}}}, []string{"*.pn?"}},
|
||||
{FileFilters{{"", []string{`*.pn\?`}}}, nil},
|
||||
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, []string{"*.?"}},
|
||||
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"*.PNG"}},
|
||||
{FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"*.PNG"}},
|
||||
{FileFilters{{"", []string{`*.[PNG`}}}, []string{"*.[PNG"}},
|
||||
{FileFilters{{"", []string{`*.]PNG`}}}, []string{"*.]PNG"}},
|
||||
{FileFilters{{"", []string{`*.[[]PNG`}}}, []string{"*.[PNG"}},
|
||||
{FileFilters{{"", []string{`*.[]]PNG`}}}, []string{"*.]PNG"}},
|
||||
{FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{"*.[PNG"}},
|
||||
{FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{"*.]PNG"}},
|
||||
{FileFilters{{"", []string{`public.png`}}}, []string{"public.png"}},
|
||||
{[]string{``}, nil},
|
||||
{[]string{`*.\?`}, nil},
|
||||
{[]string{`*.png`}, []string{"*.png"}},
|
||||
{[]string{`*.pn?`}, []string{"*.pn?"}},
|
||||
{[]string{`*.pn;`}, []string{"*.pn?"}},
|
||||
{[]string{`*.[PpNnGg]`}, []string{"*.?"}},
|
||||
{[]string{`*.[Pp][Nn][Gg]`}, []string{"*.PNG"}},
|
||||
{[]string{`*.[Pp][\Nn][G\g]`}, []string{"*.PNG"}},
|
||||
{[]string{`*.[PNG`}, []string{"*.[PNG"}},
|
||||
{[]string{`*.]PNG`}, []string{"*.]PNG"}},
|
||||
{[]string{`*.[[]PNG`}, []string{"*.[PNG"}},
|
||||
{[]string{`*.[]]PNG`}, []string{"*.]PNG"}},
|
||||
{[]string{`*.[\[]PNG`}, []string{"*.[PNG"}},
|
||||
{[]string{`*.[\]]PNG`}, []string{"*.]PNG"}},
|
||||
{[]string{`public.png`}, []string{"public.png"}},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
tt.data.simplify()
|
||||
if got := tt.data[0].Patterns; !reflect.DeepEqual(got, tt.want) {
|
||||
filters := FileFilters{FileFilter{Patterns: tt.data}}
|
||||
filters.simplify()
|
||||
if got := filters[0].Patterns; !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("FileFilters.simplify[%d] = %q; want %q", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileFilters_casefold(t *testing.T) {
|
||||
tests := []struct {
|
||||
data []string
|
||||
want []string
|
||||
}{
|
||||
{[]string{`*.png`}, []string{`*.[pP][nN][gG]`}},
|
||||
{[]string{`*.pn?`}, []string{`*.[pP][nN]?`}},
|
||||
{[]string{`*.pn;`}, []string{`*.[pP][nN];`}},
|
||||
{[]string{`*.pn\?`}, []string{`*.[pP][nN]\?`}},
|
||||
{[]string{`*.[PpNnGg]`}, []string{`*.[PppPNnnNGggG]`}},
|
||||
{[]string{`*.[Pp][Nn][Gg]`}, []string{`*.[PppP][NnnN][GggG]`}},
|
||||
{[]string{`*.[Pp][\Nn][G\g]`}, []string{`*.[PppP][\NnnN][Gg\gG]`}},
|
||||
{[]string{`*.[PNG`}, []string{`*.[PpNnGg`}},
|
||||
{[]string{`*.]PNG`}, []string{`*.][Pp][Nn][Gg]`}},
|
||||
{[]string{`*.[[]PNG`}, []string{`*.[[][Pp][Nn][Gg]`}},
|
||||
{[]string{`*.[]]PNG`}, []string{`*.[]][Pp][Nn][Gg]`}},
|
||||
{[]string{`*.[\[]PNG`}, []string{`*.[\[][Pp][Nn][Gg]`}},
|
||||
{[]string{`*.[\]]PNG`}, []string{`*.[\]][Pp][Nn][Gg]`}},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
filters := FileFilters{FileFilter{Patterns: tt.data}}
|
||||
filters[0].CaseFold = true
|
||||
filters.casefold()
|
||||
if got := filters[0].Patterns; !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("FileFilters.casefold[%d] = %q; want %q", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileFilters_types(t *testing.T) {
|
||||
tests := []struct {
|
||||
data FileFilters
|
||||
data []string
|
||||
want []string
|
||||
}{
|
||||
{FileFilters{{"", []string{``}}}, nil},
|
||||
{FileFilters{{"", []string{`*.png`}}}, []string{"", "png"}},
|
||||
{FileFilters{{"", []string{`*.pn?`}}}, nil},
|
||||
{FileFilters{{"", []string{`*.pn;`}}}, []string{"", "pn;"}},
|
||||
{FileFilters{{"", []string{`*.pn\?`}}}, []string{"", "pn?"}},
|
||||
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, nil},
|
||||
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"", "PNG"}},
|
||||
{FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"", "PNG"}},
|
||||
{FileFilters{{"", []string{`*.[PNG`}}}, []string{"", "[PNG"}},
|
||||
{FileFilters{{"", []string{`*.]PNG`}}}, []string{"", "]PNG"}},
|
||||
{FileFilters{{"", []string{`*.[[]PNG`}}}, []string{"", "[PNG"}},
|
||||
{FileFilters{{"", []string{`*.[]]PNG`}}}, []string{"", "]PNG"}},
|
||||
{FileFilters{{"", []string{`*.[\[]PNG`}}}, []string{"", "[PNG"}},
|
||||
{FileFilters{{"", []string{`*.[\]]PNG`}}}, []string{"", "]PNG"}},
|
||||
{FileFilters{{"", []string{`public.png`}}}, []string{"", "public.png"}},
|
||||
{FileFilters{{"", []string{`-public-.png`}}}, []string{"", "png"}},
|
||||
{[]string{``}, nil},
|
||||
{[]string{`*.png`}, []string{"", "png"}},
|
||||
{[]string{`*.pn?`}, nil},
|
||||
{[]string{`*.pn;`}, []string{"", "pn;"}},
|
||||
{[]string{`*.pn\?`}, []string{"", "pn?"}},
|
||||
{[]string{`*.[PpNnGg]`}, nil},
|
||||
{[]string{`*.[Pp][Nn][Gg]`}, []string{"", "PNG"}},
|
||||
{[]string{`*.[Pp][\Nn][G\g]`}, []string{"", "PNG"}},
|
||||
{[]string{`*.[PNG`}, []string{"", "[PNG"}},
|
||||
{[]string{`*.]PNG`}, []string{"", "]PNG"}},
|
||||
{[]string{`*.[[]PNG`}, []string{"", "[PNG"}},
|
||||
{[]string{`*.[]]PNG`}, []string{"", "]PNG"}},
|
||||
{[]string{`*.[\[]PNG`}, []string{"", "[PNG"}},
|
||||
{[]string{`*.[\]]PNG`}, []string{"", "]PNG"}},
|
||||
{[]string{`public.png`}, []string{"", "public.png"}},
|
||||
{[]string{`-public-.png`}, []string{"", "png"}},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
if got := tt.data.types(); !reflect.DeepEqual(got, tt.want) {
|
||||
filters := FileFilters{FileFilter{Patterns: tt.data}}
|
||||
if got := filters.types(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("FileFilters.types[%d] = %v; want %v", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
|
20
file_test.go
20
file_test.go
|
@ -20,9 +20,9 @@ func ExampleSelectFile() {
|
|||
zenity.SelectFile(
|
||||
zenity.Filename(defaultPath),
|
||||
zenity.FileFilters{
|
||||
{"Go files", []string{"*.go"}},
|
||||
{"Web files", []string{"*.html", "*.js", "*.css"}},
|
||||
{"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}},
|
||||
{"Go files", []string{"*.go"}, true},
|
||||
{"Web files", []string{"*.html", "*.js", "*.css"}, true},
|
||||
{"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}, true},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -30,9 +30,9 @@ func ExampleSelectFileMultiple() {
|
|||
zenity.SelectFileMultiple(
|
||||
zenity.Filename(defaultPath),
|
||||
zenity.FileFilters{
|
||||
{"Go files", []string{"*.go"}},
|
||||
{"Web files", []string{"*.html", "*.js", "*.css"}},
|
||||
{"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}},
|
||||
{"Go files", []string{"*.go"}, true},
|
||||
{"Web files", []string{"*.html", "*.js", "*.css"}, true},
|
||||
{"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}, true},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -41,9 +41,9 @@ func ExampleSelectFileSave() {
|
|||
zenity.ConfirmOverwrite(),
|
||||
zenity.Filename(defaultName),
|
||||
zenity.FileFilters{
|
||||
{"Go files", []string{"*.go"}},
|
||||
{"Web files", []string{"*.html", "*.js", "*.css"}},
|
||||
{"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}},
|
||||
{"Go files", []string{"*.go"}, true},
|
||||
{"Web files", []string{"*.html", "*.js", "*.css"}, true},
|
||||
{"Image files", []string{"*.png", "*.gif", "*.ico", "*.jpg", "*.webp"}, true},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -232,7 +232,7 @@ func TestSelectFileSave_script(t *testing.T) {
|
|||
str, err := zenity.SelectFileSave(
|
||||
zenity.ConfirmOverwrite(),
|
||||
zenity.Filename("Χρτο.go"),
|
||||
zenity.FileFilter{"Go files", []string{"*.go"}},
|
||||
zenity.FileFilter{"Go files", []string{"*.go"}, true},
|
||||
)
|
||||
if skip, err := skip(err); skip {
|
||||
t.Skip("skipping:", err)
|
||||
|
|
|
@ -340,6 +340,9 @@ func initFilters(filters FileFilters) []uint16 {
|
|||
filters.name()
|
||||
var res []uint16
|
||||
for _, f := range filters {
|
||||
if f.Name == "" || len(f.Patterns) == 0 {
|
||||
continue
|
||||
}
|
||||
res = append(res, utf16.Encode([]rune(f.Name))...)
|
||||
res = append(res, 0)
|
||||
for _, p := range f.Patterns {
|
||||
|
|
|
@ -56,11 +56,11 @@ func Test_applyOptions(t *testing.T) {
|
|||
{name: "ConfirmCreate", args: ConfirmCreate(), want: options{confirmCreate: true}},
|
||||
{name: "ShowHidden", args: ShowHidden(), want: options{showHidden: true}},
|
||||
{name: "Filename", args: Filename("file.go"), want: options{filename: "file.go"}},
|
||||
{name: "FileFilter", args: FileFilter{"Go files", []string{"*.go"}}, want: options{
|
||||
fileFilters: FileFilters{{"Go files", []string{"*.go"}}},
|
||||
{name: "FileFilter", args: FileFilter{"Go files", []string{"*.go"}, true}, want: options{
|
||||
fileFilters: FileFilters{{"Go files", []string{"*.go"}, true}},
|
||||
}},
|
||||
{name: "FileFilters", args: FileFilters{{"Go files", []string{"*.go"}}}, want: options{
|
||||
fileFilters: FileFilters{{"Go files", []string{"*.go"}}},
|
||||
{name: "FileFilters", args: FileFilters{{"Go files", []string{"*.go"}, true}}, want: options{
|
||||
fileFilters: FileFilters{{"Go files", []string{"*.go"}, true}},
|
||||
}},
|
||||
|
||||
// Color selection options
|
||||
|
|
Loading…
Reference in a new issue