Fix #15.
This commit is contained in:
parent
c98b30d2e7
commit
e26955a5bb
2 changed files with 79 additions and 24 deletions
73
file.go
73
file.go
|
@ -62,7 +62,8 @@ func Filename(filename string) Option {
|
||||||
// FileFilter is an Option that sets a filename filter.
|
// FileFilter is an Option that sets a filename filter.
|
||||||
//
|
//
|
||||||
// macOS hides filename filters from the user,
|
// macOS hides filename filters from the user,
|
||||||
// and only supports filtering by extension (or "type").
|
// and only supports filtering by extension
|
||||||
|
// (or "uniform type identifiers").
|
||||||
//
|
//
|
||||||
// Patterns may use the GTK syntax on all platforms:
|
// Patterns may use the GTK syntax on all platforms:
|
||||||
// https://developer.gnome.org/pygtk/stable/class-gtkfilefilter.html#method-gtkfilefilter--add-pattern
|
// https://developer.gnome.org/pygtk/stable/class-gtkfilefilter.html#method-gtkfilefilter--add-pattern
|
||||||
|
@ -92,11 +93,14 @@ 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 (match nothing).
|
//
|
||||||
|
// 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.
|
// The semicolon is a separator, so we replace it with the single character wildcard.
|
||||||
|
// Empty and invalid patterns are ignored.
|
||||||
func (f FileFilters) simplify() {
|
func (f FileFilters) simplify() {
|
||||||
for _, filter := range f {
|
for i, filter := range f {
|
||||||
for i, pattern := range filter.Patterns {
|
var n = 0
|
||||||
|
for _, pattern := range filter.Patterns {
|
||||||
var escape, invalid bool
|
var escape, invalid bool
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
for _, r := range removeClasses(pattern) {
|
for _, r := range removeClasses(pattern) {
|
||||||
|
@ -114,22 +118,35 @@ func (f FileFilters) simplify() {
|
||||||
buf.WriteRune(r)
|
buf.WriteRune(r)
|
||||||
escape = false
|
escape = false
|
||||||
}
|
}
|
||||||
if invalid {
|
if buf.Len() > 0 && !invalid {
|
||||||
filter.Patterns[i] = ""
|
filter.Patterns[n] = buf.String()
|
||||||
} else {
|
n++
|
||||||
filter.Patterns[i] = buf.String()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if n == 0 {
|
||||||
|
f[i].Patterns = nil
|
||||||
|
} else {
|
||||||
|
f[i].Patterns = filter.Patterns[:n]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// macOS filters by "type"; the case insensitive literal extension is a good proxy.
|
// macOS types may be specified as extension strings without the leading period,
|
||||||
// So we extract the extension from each pattern, remove character classes, then escaping.
|
// or as uniform type identifiers:
|
||||||
|
// https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/PromptforaFileorFolder.html
|
||||||
|
//
|
||||||
|
// First check for uniform type identifiers.
|
||||||
|
// Then we extract the extension from each pattern, remove character classes, then escaping.
|
||||||
// If an extension contains a wildcard, any type is accepted.
|
// If an extension contains a wildcard, any type is accepted.
|
||||||
func (f FileFilters) types() []string {
|
func (f FileFilters) types() []string {
|
||||||
var res []string
|
var res []string
|
||||||
for _, filter := range f {
|
for _, filter := range f {
|
||||||
for _, pattern := range filter.Patterns {
|
for _, pattern := range filter.Patterns {
|
||||||
|
if isUniformTypeIdentifier(pattern) {
|
||||||
|
res = append(res, pattern)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
ext := pattern[strings.LastIndexByte(pattern, '.')+1:]
|
ext := pattern[strings.LastIndexByte(pattern, '.')+1:]
|
||||||
|
|
||||||
var escape bool
|
var escape bool
|
||||||
|
@ -146,10 +163,16 @@ func (f FileFilters) types() []string {
|
||||||
}
|
}
|
||||||
buf.WriteRune(r)
|
buf.WriteRune(r)
|
||||||
}
|
}
|
||||||
|
if buf.Len() > 0 {
|
||||||
res = append(res, buf.String())
|
res = append(res, buf.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res
|
}
|
||||||
|
if res == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Workaround for macOS bug: first type cannot be a four letter extension, so prepend empty string.
|
||||||
|
return append([]string{""}, res...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove character classes from pattern, assuming case insensitivity.
|
// Remove character classes from pattern, assuming case insensitivity.
|
||||||
|
@ -213,6 +236,34 @@ func findClass(pattern string) (start, end int) {
|
||||||
return -1, -1
|
return -1, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uniform type identifiers use the reverse-DNS format:
|
||||||
|
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html
|
||||||
|
func isUniformTypeIdentifier(pattern string) bool {
|
||||||
|
labels := strings.Split(pattern, ".")
|
||||||
|
if len(labels) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, label := range labels {
|
||||||
|
if len := len(label); len == 0 || label[0] == '-' || label[len-1] == '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range label {
|
||||||
|
switch {
|
||||||
|
case r == '-' || r > '\x7f' ||
|
||||||
|
'a' <= r && r <= 'z' ||
|
||||||
|
'A' <= r && r <= 'Z' ||
|
||||||
|
'0' <= r && r <= '9':
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func splitDirAndName(path string) (dir, name string) {
|
func splitDirAndName(path string) (dir, name string) {
|
||||||
if path != "" {
|
if path != "" {
|
||||||
path = filepath.Clean(path)
|
path = filepath.Clean(path)
|
||||||
|
|
|
@ -10,10 +10,11 @@ func TestFileFilters_simplify(t *testing.T) {
|
||||||
data FileFilters
|
data FileFilters
|
||||||
want []string
|
want []string
|
||||||
}{
|
}{
|
||||||
|
{FileFilters{{"", []string{``}}}, nil},
|
||||||
{FileFilters{{"", []string{`*.png`}}}, []string{"*.png"}},
|
{FileFilters{{"", []string{`*.png`}}}, []string{"*.png"}},
|
||||||
{FileFilters{{"", []string{`*.pn?`}}}, []string{"*.pn?"}},
|
{FileFilters{{"", []string{`*.pn?`}}}, []string{"*.pn?"}},
|
||||||
{FileFilters{{"", []string{`*.pn;`}}}, []string{"*.pn?"}},
|
{FileFilters{{"", []string{`*.pn;`}}}, []string{"*.pn?"}},
|
||||||
{FileFilters{{"", []string{`*.pn\?`}}}, []string{""}},
|
{FileFilters{{"", []string{`*.pn\?`}}}, nil},
|
||||||
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, []string{"*.?"}},
|
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, []string{"*.?"}},
|
||||||
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"*.PNG"}},
|
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"*.PNG"}},
|
||||||
{FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"*.PNG"}},
|
{FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []string{"*.PNG"}},
|
||||||
|
@ -23,6 +24,7 @@ func TestFileFilters_simplify(t *testing.T) {
|
||||||
{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"}},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
tt.data.simplify()
|
tt.data.simplify()
|
||||||
|
@ -37,19 +39,21 @@ func TestFileFilters_types(t *testing.T) {
|
||||||
data FileFilters
|
data FileFilters
|
||||||
want []string
|
want []string
|
||||||
}{
|
}{
|
||||||
{FileFilters{{"", []string{`*.png`}}}, []string{"png"}},
|
{FileFilters{{"", []string{``}}}, nil},
|
||||||
|
{FileFilters{{"", []string{`*.png`}}}, []string{"", "png"}},
|
||||||
{FileFilters{{"", []string{`*.pn?`}}}, nil},
|
{FileFilters{{"", []string{`*.pn?`}}}, nil},
|
||||||
{FileFilters{{"", []string{`*.pn;`}}}, []string{"pn;"}},
|
{FileFilters{{"", []string{`*.pn;`}}}, []string{"", "pn;"}},
|
||||||
{FileFilters{{"", []string{`*.pn\?`}}}, []string{"pn?"}},
|
{FileFilters{{"", []string{`*.pn\?`}}}, []string{"", "pn?"}},
|
||||||
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, nil},
|
{FileFilters{{"", []string{`*.[PpNnGg]`}}}, nil},
|
||||||
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"PNG"}},
|
{FileFilters{{"", []string{`*.[Pp][Nn][Gg]`}}}, []string{"", "PNG"}},
|
||||||
{FileFilters{{"", []string{`*.[Pp][\Nn][G\g]`}}}, []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{`*.[]]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"}},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
if got := tt.data.types(); !reflect.DeepEqual(got, tt.want) {
|
if got := tt.data.types(); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
|
Loading…
Reference in a new issue