diff --git a/sql/funcs.go b/sql/funcs.go index 72cd46d7..64ba7a99 100644 --- a/sql/funcs.go +++ b/sql/funcs.go @@ -166,6 +166,7 @@ var funcs = map[string]map[int]interface{}{ "string.reverse": {1: nil}, "string.search": {2: nil}, "string.slice": {3: nil}, + "string.slug": {1: nil, 2: nil}, "string.split": {2: nil}, "string.startsWith": {2: nil}, "string.substr": {3: nil}, diff --git a/util/fncs/fnc.go b/util/fncs/fnc.go index 12c4deef..8b4f529b 100644 --- a/util/fncs/fnc.go +++ b/util/fncs/fnc.go @@ -198,6 +198,8 @@ func Run(ctx context.Context, name string, args ...interface{}) (interface{}, er return stringSearch(ctx, args...) case "string.slice": return stringSlice(ctx, args...) + case "string.slug": + return stringSlug(ctx, args...) case "string.split": return stringSplit(ctx, args...) case "string.startsWith": diff --git a/util/fncs/string.go b/util/fncs/string.go index da6c6734..2577947f 100644 --- a/util/fncs/string.go +++ b/util/fncs/string.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/abcum/surreal/util/ints" + "github.com/abcum/surreal/util/slug" "github.com/abcum/surreal/util/text" ) @@ -138,6 +139,19 @@ func stringSlice(ctx context.Context, args ...interface{}) (interface{}, error) return s, nil } +func stringSlug(ctx context.Context, args ...interface{}) (interface{}, error) { + switch len(args) { + case 1: + s, _ := ensureString(args[0]) + return slug.Make(s), nil + case 2: + s, _ := ensureString(args[0]) + l, _ := ensureString(args[1]) + return slug.MakeLang(s, l), nil + } + return nil, nil +} + func stringSplit(ctx context.Context, args ...interface{}) (interface{}, error) { s, _ := ensureString(args[0]) p, _ := ensureString(args[1]) diff --git a/util/slug/lang.go b/util/slug/lang.go new file mode 100644 index 00000000..d64ae78b --- /dev/null +++ b/util/slug/lang.go @@ -0,0 +1,76 @@ +// Copyright © 2016 Abcum Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slug + +func init() { + // Merge language subs with the default one + for _, sub := range []*map[rune]string{&en, &de, &fr, &es, &nl, &pl, &gr} { + for key, value := range defaults { + (*sub)[key] = value + } + } +} + +var defaults = map[rune]string{ + '"': "", + '\'': "", + '’': "", + '‒': "-", // figure dash + '–': "-", // en dash + '—': "-", // em dash + '―': "-", // horizontal bar +} + +var en = map[rune]string{ + '&': "and", + '@': "at", +} + +var de = map[rune]string{ + '&': "und", + '@': "an", +} + +var fr = map[rune]string{ + '&': "et", + '@': "a", +} + +var es = map[rune]string{ + '&': "y", + '@': "en", +} + +var nl = map[rune]string{ + '&': "en", + '@': "at", +} + +var pl = map[rune]string{ + '&': "i", + '@': "na", +} + +var gr = map[rune]string{ + '&': "kai", + 'η': "i", + 'ή': "i", + 'Η': "i", + 'ι': "i", + 'ί': "i", + 'Ι': "i", + 'χ': "x", + 'Χ': "x", +} diff --git a/util/slug/slug.go b/util/slug/slug.go new file mode 100644 index 00000000..009d9e2f --- /dev/null +++ b/util/slug/slug.go @@ -0,0 +1,84 @@ +// Copyright © 2016 Abcum Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package slug + +import ( + "bytes" + "regexp" + "strings" + + "github.com/rainycape/unidecode" +) + +var ( + regexpUnicode = regexp.MustCompile("[^a-z0-9-_]") + regexpHyphens = regexp.MustCompile("-+") +) + +func Make(s string) (slug string) { + return MakeLang(s, "en") +} + +func MakeLang(s string, l string) (slug string) { + + slug = strings.TrimSpace(s) + + switch l { + case "de": + slug = substitute(slug, de) + case "en": + slug = substitute(slug, en) + case "pl": + slug = substitute(slug, pl) + case "es": + slug = substitute(slug, es) + case "gr": + slug = substitute(slug, gr) + case "nl": + slug = substitute(slug, nl) + default: + slug = substitute(slug, en) + } + + // Process all non ASCII symbols + slug = unidecode.Unidecode(slug) + + // Format the text as lower case + slug = strings.ToLower(slug) + + // Process remaining symbols + slug = regexpUnicode.ReplaceAllString(slug, "-") + + // Process duplicated hyphens + slug = regexpHyphens.ReplaceAllString(slug, "-") + + // Trim leading hyphens + slug = strings.Trim(slug, "-") + + return slug + +} + +func substitute(s string, sub map[rune]string) string { + var buf bytes.Buffer + for _, c := range s { + if d, ok := sub[c]; ok { + buf.WriteString(d) + } else { + buf.WriteRune(c) + } + } + return buf.String() +}