diff --git a/pkg/infra/slugify/slugify.go b/pkg/infra/slugify/slugify.go index 80a1f8cce334..ef4d716d81bb 100644 --- a/pkg/infra/slugify/slugify.go +++ b/pkg/infra/slugify/slugify.go @@ -42,6 +42,7 @@ import ( var ( simpleSlugger = &slugger{ isValidCharacter: validCharacter, + replaceCharacter: '-', replacementMap: getDefaultReplacements(), omitMap: getDefaultOmitments(), } @@ -64,14 +65,12 @@ func validCharacter(c rune) bool { if c >= '0' && c <= '9' { return true } - if c == '_' || c == '-' { - return true - } return false } // Slugifier based on settings type slugger struct { + replaceCharacter rune isValidCharacter func(c rune) bool replacementMap map[rune]string omitMap map[rune]struct{} @@ -81,37 +80,52 @@ type slugger struct { func (s slugger) Slugify(value string) string { value = strings.ToLower(value) var buffer bytes.Buffer + lastCharacterWasInvalid := false for len(value) > 0 { c, size := utf8.DecodeRuneInString(value) value = value[size:] if newCharacter, ok := s.replacementMap[c]; ok { + if lastCharacterWasInvalid { + buffer.WriteRune(s.replaceCharacter) + } buffer.WriteString(newCharacter) + lastCharacterWasInvalid = false continue } if s.isValidCharacter(c) { + if lastCharacterWasInvalid { + buffer.WriteRune(s.replaceCharacter) + } buffer.WriteRune(c) + lastCharacterWasInvalid = false continue } if _, ok := s.omitMap[c]; ok { + lastCharacterWasInvalid = true continue } p := make([]byte, 4) size = utf8.EncodeRune(p, c) + if lastCharacterWasInvalid { + buffer.WriteRune(s.replaceCharacter) + } for i := 0; i < size; i++ { - buffer.WriteString(fmt.Sprintf("%%%x", p[i])) + buffer.WriteString(fmt.Sprintf("%x", p[i])) } + lastCharacterWasInvalid = true } - return buffer.String() + return strings.Trim(buffer.String(), string(s.replaceCharacter)) } func getDefaultOmitments() map[rune]struct{} { return map[rune]struct{}{ + ' ': {}, ',': {}, '"': {}, '\'': {}, @@ -122,13 +136,21 @@ func getDefaultOmitments() map[rune]struct{} { '.': {}, '(': {}, ')': {}, + '-': {}, + '_': {}, + '[': {}, + ']': {}, + '/': {}, + '\\': {}, + '!': {}, + '{': {}, + '}': {}, + '%': {}, } } func getDefaultReplacements() map[rune]string { return map[rune]string{ - ' ': "-", - '&': "and", '@': "at", '©': "c", diff --git a/pkg/infra/slugify/slugify_test.go b/pkg/infra/slugify/slugify_test.go index 77891faf10c3..d8509d5e3816 100644 --- a/pkg/infra/slugify/slugify_test.go +++ b/pkg/infra/slugify/slugify_test.go @@ -9,12 +9,16 @@ func TestSlugify(t *testing.T) { results["hello-playground"] = "Hello, playground" results["00a4bc92-3695-5702-9ddf-6719fdf11567"] = "😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise" results["61db60b5-f1e7-5853-9b81-0f074fc268ea"] = "😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬" - results["%f0%9f%98%a2--"] = "😢 -" - results["a-"] = "?,a . \n " + results["f09f98a2"] = "😢 -" + results["a"] = "?,a . \n " results["0a68eb57-c88a-5f34-9e9d-27f85e68af4f"] = "" // empty input has a slug! results["3cbb528a-0ebf-54ad-bed2-2a188cd1824e"] = "方向盤後面 hi this is a test خلف المقو" results["cong-hoa-xa-hoi-chu-nghia-viet-nam"] = "Cộng hòa xã hội chủ nghĩa Việt Nam" results["noi-nang-canh-canh-ben-long-bieng-khuay"] = "Nỗi nàng canh cánh bên lòng biếng khuây" // This line in a poem called Truyen Kieu + results["hello-playground"] = "Hello / playground" + results["hello-playground"] = "Hello % playground" + results["hello-and-playground"] = "Hello & //% playground" + results["hello-2a-23-playground"] = "Hello *# playground" for slug, original := range results { actual := Slugify(original) diff --git a/pkg/services/accesscontrol/models_test.go b/pkg/services/accesscontrol/models_test.go index fa15762535ba..64595f0ffb80 100644 --- a/pkg/services/accesscontrol/models_test.go +++ b/pkg/services/accesscontrol/models_test.go @@ -64,7 +64,7 @@ func TestSaveExternalServiceRoleCommand_Validate(t *testing.T) { Permissions: []Permission{{Action: "users:read", Scope: "users:id:1"}}, }, wantErr: false, - wantID: "thisis-a-very-strange-___-app-name", + wantID: "thisis-a-very-strange-app-name", }, { name: "invalid empty Action", diff --git a/pkg/services/dashboards/models_test.go b/pkg/services/dashboards/models_test.go index dbacb303b864..1e34a927ddda 100644 --- a/pkg/services/dashboards/models_test.go +++ b/pkg/services/dashboards/models_test.go @@ -77,9 +77,9 @@ func TestSlugifyTitle(t *testing.T) { testCases := map[string]string{ "Grafana Play Home": "grafana-play-home", "snöräv-över-ån": "snorav-over-an", - "漢字": "%e6%bc%a2%e5%ad%97", // "han-zi", // Hanzi for hanzi - "🇦🇶": "%f0%9f%87%a6%f0%9f%87%b6", // flag of Antarctica-emoji, using fallback - "𒆠": "%f0%92%86%a0", // cuneiform Ki, using fallback + "漢字": "e6bca2-e5ad97", // "han-zi", // Hanzi for hanzi + "🇦🇶": "f09f87a6-f09f87b6", // flag of Antarctica-emoji, using fallback + "𒆠": "f09286a0", // cuneiform Ki, using fallback } for input, expected := range testCases {