Slugify: Replace gosimple/slug with a simple function (#59517)

pull/59596/head
Ryan McKinley 3 years ago committed by GitHub
parent 000de83eb4
commit 5b71a16acf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      go.mod
  2. 4
      go.sum
  3. 370
      pkg/infra/slugify/slugify.go
  4. 54
      pkg/infra/slugify/slugify_test.go
  5. 22
      pkg/models/dashboards.go
  6. 5
      pkg/models/dashboards_test.go
  7. 5
      pkg/plugins/manager/loader/loader.go
  8. 3
      pkg/services/folder/folderimpl/sqlstore.go
  9. 3
      pkg/services/folder/model.go
  10. 3
      pkg/services/provisioning/alerting/rules_provisioner.go
  11. 3
      pkg/services/provisioning/dashboards/file_reader.go
  12. 22
      pkg/services/sqlstore/migrations/ualert/dashboard.go
  13. 3
      pkg/services/store/kind/dashboard/summary.go

@ -56,7 +56,6 @@ require (
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/gorilla/websocket v1.5.0
github.com/gosimple/slug v1.12.0
github.com/grafana/cuetsy v0.1.1
github.com/grafana/grafana-aws-sdk v0.11.0
github.com/grafana/grafana-azure-sdk-go v1.3.1
@ -288,7 +287,6 @@ require (
github.com/google/go-querystring v1.1.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/memberlist v0.4.0 // indirect
github.com/huandu/xstrings v1.3.1 // indirect

@ -1353,10 +1353,6 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc=
github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
github.com/grafana/cuetsy v0.1.1 h1:+1jaDDYCpvKlcOWJgBRbkc5+VZIClCEn5mbI+4PLZqM=

@ -0,0 +1,370 @@
/*
* This file evolved from the MIT licensed: https://github.com/machiel/slugify
*/
/*
The MIT License (MIT)
Copyright (c) 2015 Machiel Molenaar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package slugify
import (
"bytes"
"encoding/base64"
"strings"
"unicode/utf8"
"github.com/gofrs/uuid"
)
var (
simpleSlugger = &slugger{
isValidCharacter: validCharacter,
replaceCharacter: '-',
replacementMap: getDefaultReplacements(),
}
)
// Slugify creates a URL safe latin slug for a given value
func Slugify(value string) string {
s := simpleSlugger.Slugify(value)
if s == "" {
s = base64.RawURLEncoding.EncodeToString([]byte(value))
if len(s) > 50 || s == "" {
s = uuid.NewV5(uuid.NamespaceOID, value).String()
}
}
return s
}
func validCharacter(c rune) bool {
if c >= 'a' && c <= 'z' {
return true
}
if c >= '0' && c <= '9' {
return true
}
return false
}
// Slugifier based on settings
type slugger struct {
isValidCharacter func(c rune) bool
replaceCharacter rune
replacementMap map[rune]string
}
// Slugify creates a slug for a string
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 {
buffer.WriteString(newCharacter)
lastCharacterWasInvalid = false
continue
}
if s.isValidCharacter(c) {
buffer.WriteRune(c)
lastCharacterWasInvalid = false
} else if !lastCharacterWasInvalid {
buffer.WriteRune(s.replaceCharacter)
lastCharacterWasInvalid = true
}
}
return strings.Trim(buffer.String(), string(s.replaceCharacter))
}
func getDefaultReplacements() map[rune]string {
return map[rune]string{
'&': "and",
'@': "at",
'©': "c",
'®': "r",
'Æ': "ae",
'ß': "ss",
'à': "a",
'á': "a",
'â': "a",
'ä': "a", // or "ae"
'å': "a",
'æ': "ae",
'ç': "c",
'è': "e",
'é': "e",
'ê': "e",
'ë': "e",
'ì': "i",
'í': "i",
'î': "i",
'ï': "i",
'ò': "o",
'ó': "o",
'ô': "o",
'õ': "o",
'ö': "o", // or "oe"?
'ø': "o",
'ù': "u",
'ú': "u",
'û': "u",
'ü': "ue",
'ý': "y",
'þ': "p",
'ÿ': "y",
'ā': "a",
'ă': "a",
'Ą': "a",
'ą': "a",
'ć': "c",
'ĉ': "c",
'ċ': "c",
'č': "c",
'ď': "d",
'đ': "d",
'ē': "e",
'ĕ': "e",
'ė': "e",
'ę': "e",
'ě': "e",
'ĝ': "g",
'ğ': "g",
'ġ': "g",
'ģ': "g",
'ĥ': "h",
'ħ': "h",
'ĩ': "i",
'ī': "i",
'ĭ': "i",
'į': "i",
'ı': "i",
'ij': "ij",
'ĵ': "j",
'ķ': "k",
'ĸ': "k",
'Ĺ': "l",
'ĺ': "l",
'ļ': "l",
'ľ': "l",
'ŀ': "l",
'ł': "l",
'ń': "n",
'ņ': "n",
'ň': "n",
'ʼn': "n",
'ŋ': "n",
'ō': "o",
'ŏ': "o",
'ő': "o",
'Œ': "oe",
'œ': "oe",
'ŕ': "r",
'ŗ': "r",
'ř': "r",
'ś': "s",
'ŝ': "s",
'ş': "s",
'š': "s",
'ţ': "t",
'ť': "t",
'ŧ': "t",
'ũ': "u",
'ū': "u",
'ŭ': "u",
'ů': "u",
'ű': "u",
'ų': "u",
'ŵ': "w",
'ŷ': "y",
'ź': "z",
'ż': "z",
'ž': "z",
'ſ': "z",
'Ə': "e",
'ƒ': "f",
'Ơ': "o",
'ơ': "o",
'Ư': "u",
'ư': "u",
'ǎ': "a",
'ǐ': "i",
'ǒ': "o",
'ǔ': "u",
'ǖ': "u",
'ǘ': "u",
'ǚ': "u",
'ǜ': "u",
'ǻ': "a",
'Ǽ': "ae",
'ǽ': "ae",
'Ǿ': "o",
'ǿ': "o",
'ə': "e",
'Є': "e",
'Б': "b",
'Г': "g",
'Д': "d",
'Ж': "zh",
'З': "z",
'У': "u",
'Ф': "f",
'Х': "h",
'Ц': "c",
'Ч': "ch",
'Ш': "sh",
'Щ': "sch",
'Ъ': "-",
'Ы': "y",
'Ь': "-",
'Э': "je",
'Ю': "ju",
'Я': "ja",
'а': "a",
'б': "b",
'в': "v",
'г': "g",
'д': "d",
'е': "e",
'ж': "zh",
'з': "z",
'и': "i",
'й': "j",
'к': "k",
'л': "l",
'м': "m",
'н': "n",
'о': "o",
'п': "p",
'р': "r",
'с': "s",
'т': "t",
'у': "u",
'ф': "f",
'х': "h",
'ц': "c",
'ч': "ch",
'ш': "sh",
'щ': "sch",
'ъ': "-",
'ы': "y",
'ь': "-",
'э': "je",
'ю': "ju",
'я': "ja",
'ё': "jo",
'є': "e",
'і': "i",
'ї': "i",
'Ґ': "g",
'ґ': "g",
'א': "a",
'ב': "b",
'ג': "g",
'ד': "d",
'ה': "h",
'ו': "v",
'ז': "z",
'ח': "h",
'ט': "t",
'י': "i",
'ך': "k",
'כ': "k",
'ל': "l",
'ם': "m",
'מ': "m",
'ן': "n",
'נ': "n",
'ס': "s",
'ע': "e",
'ף': "p",
'פ': "p",
'ץ': "C",
'צ': "c",
'ק': "q",
'ר': "r",
'ש': "w",
'ת': "t",
'™': "tm",
'ả': "a",
'ã': "a",
'ạ': "a",
'ắ': "a",
'ằ': "a",
'ẳ': "a",
'ẵ': "a",
'ặ': "a",
'ấ': "a",
'ầ': "a",
'ẩ': "a",
'ẫ': "a",
'ậ': "a",
'ẻ': "e",
'ẽ': "e",
'ẹ': "e",
'ế': "e",
'ề': "e",
'ể': "e",
'ễ': "e",
'ệ': "e",
'ỉ': "i",
'ị': "i",
'ỏ': "o",
'ọ': "o",
'ố': "o",
'ồ': "o",
'ổ': "o",
'ỗ': "o",
'ộ': "o",
'ớ': "o",
'ờ': "o",
'ở': "o",
'ỡ': "o",
'ợ': "o",
'ủ': "u",
'ụ': "u",
'ứ': "u",
'ừ': "u",
'ử': "u",
'ữ': "u",
'ự': "u",
'ỳ': "y",
'ỷ': "y",
'ỹ': "y",
'ỵ': "y",
}
}

@ -0,0 +1,54 @@
package slugify
import (
"testing"
)
func TestSlugify(t *testing.T) {
results := make(map[string]string)
results["hello-playground"] = "Hello, playground"
results["hello-it-s-paradise"] = "😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise"
results["61db60b5-f1e7-5853-9b81-0f074fc268ea"] = "😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬"
results["8J-YoiAt"] = "😢 -"
results["a"] = "?,a . \n "
results["0a68eb57-c88a-5f34-9e9d-27f85e68af4f"] = "" // empty input has a slug!
results["hi-this-is-a-test"] = "方向盤後面 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
for slug, original := range results {
actual := Slugify(original)
if actual != slug {
t.Errorf("Expected '%s', got: %s", slug, actual)
}
}
}
func BenchmarkSlugify(b *testing.B) {
for i := 0; i < b.N; i++ {
Slugify("Hello, world!")
}
}
func BenchmarkSlugifyLongString(b *testing.B) {
for i := 0; i < b.N; i++ {
Slugify(`
😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise
😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise
😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise
😢 😣 😤 😥 😦 😧 😨 😩 😪 😫 😬 Hello, it's paradise
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam sapien nisl, laoreet quis vestibulum ut, cursus
in turpis. Sed magna mi, blandit id nisi vel, imperdiet
mollis turpis. Fusce vel fringilla mauris. Donec cursus
rhoncus bibendum. Aliquam erat volutpat. Maecenas
faucibus turpis ex, quis lacinia ligula ultrices non.
Sed gravida justo augue. Nulla bibendum dignissim tellus
vitae lobortis. Suspendisse fermentum vel purus in pulvinar.
Vivamus eu fermentum purus, sit amet tempor orci.
Praesent congue convallis turpis, ac ullamcorper lorem
semper id.
`)
}
}

@ -1,14 +1,11 @@
package models
import (
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/setting"
)
@ -147,22 +144,7 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
// UpdateSlug updates the slug
func (d *Dashboard) UpdateSlug() {
title := d.Data.Get("title").MustString()
d.Slug = SlugifyTitle(title)
}
func SlugifyTitle(title string) string {
s := slug.Make(strings.ToLower(title))
if s == "" {
// If the dashboard name is only characters outside of the
// sluggable characters, the slug creation will return an
// empty string which will mess up URLs. This failsafe picks
// that up and creates the slug as a base64 identifier instead.
s = base64.RawURLEncoding.EncodeToString([]byte(title))
if slug.MaxLength != 0 && len(s) > slug.MaxLength {
s = s[:slug.MaxLength]
}
}
return s
d.Slug = slugify.Slugify(title)
}
// GetUrl return the html url for a folder if it's folder, otherwise for a dashboard

@ -4,6 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -72,14 +73,14 @@ func TestSlugifyTitle(t *testing.T) {
testCases := map[string]string{
"Grafana Play Home": "grafana-play-home",
"snöräv-över-ån": "snorav-over-an",
"漢字": "han-zi", // Hanzi for hanzi
"漢字": "5ryi5a2X", // "han-zi", // Hanzi for hanzi
"🇦🇶": "8J-HpvCfh7Y", // flag of Antarctica-emoji, using fallback
"𒆠": "8JKGoA", // cuneiform Ki, using fallback
}
for input, expected := range testCases {
t.Run(input, func(t *testing.T) {
slug := SlugifyTitle(input)
slug := slugify.Slugify(input)
assert.Equal(t, expected, slug)
})
}

@ -12,11 +12,10 @@ import (
"runtime"
"strings"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
@ -343,7 +342,7 @@ func setDefaultNavURL(p *plugins.Plugin) {
// slugify pages
for _, include := range p.Includes {
if include.Slug == "" {
include.Slug = slug.Make(include.Name)
include.Slug = slugify.Slugify(include.Name)
}
if !include.DefaultNav {

@ -10,6 +10,7 @@ import (
"github.com/go-sql-driver/mysql"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
@ -170,7 +171,7 @@ func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.F
}
return nil
})
foldr.Url = models.GetFolderUrl(foldr.UID, models.SlugifyTitle(foldr.Title))
foldr.Url = models.GetFolderUrl(foldr.UID, slugify.Slugify(foldr.Title))
return foldr, err
}

@ -3,6 +3,7 @@ package folder
import (
"time"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util/errutil"
@ -141,7 +142,7 @@ func (f *Folder) ToLegacyModel() *models.Folder {
Id: f.ID,
Uid: f.UID,
Title: f.Title,
Url: models.GetFolderUrl(f.UID, models.SlugifyTitle(f.Title)),
Url: models.GetFolderUrl(f.UID, slugify.Slugify(f.Title)),
Version: 0,
Created: f.Created,
Updated: f.Updated,

@ -6,6 +6,7 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
alert_models "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -99,7 +100,7 @@ func (prov *defaultAlertRuleProvisioner) provisionRule(
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID(
ctx context.Context, folderName string, orgID int64) (string, error) {
cmd := &models.GetDashboardQuery{
Slug: models.SlugifyTitle(folderName),
Slug: slugify.Slugify(folderName),
OrgId: orgID,
}
err := prov.dashboardService.GetDashboard(ctx, cmd)

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -299,7 +300,7 @@ func (fr *FileReader) getOrCreateFolderID(ctx context.Context, cfg *config, serv
return 0, ErrFolderNameMissing
}
cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(folderName), OrgId: cfg.OrgID}
cmd := &models.GetDashboardQuery{Slug: slugify.Slugify(folderName), OrgId: cfg.OrgID}
err := fr.dashboardStore.GetDashboard(ctx, cmd)
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {

@ -1,13 +1,10 @@
package ualert
import (
"encoding/base64"
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/gosimple/slug"
"github.com/grafana/grafana/pkg/infra/slugify"
)
type dashboard struct {
@ -45,22 +42,7 @@ func (d *dashboard) setVersion(version int) {
// UpdateSlug updates the slug
func (d *dashboard) updateSlug() {
title := d.Data.Get("title").MustString()
d.Slug = slugifyTitle(title)
}
func slugifyTitle(title string) string {
s := slug.Make(strings.ToLower(title))
if s == "" {
// If the dashboard name is only characters outside of the
// sluggable characters, the slug creation will return an
// empty string which will mess up URLs. This failsafe picks
// that up and creates the slug as a base64 identifier instead.
s = base64.RawURLEncoding.EncodeToString([]byte(title))
if slug.MaxLength != 0 && len(s) > slug.MaxLength {
s = s[:slug.MaxLength]
}
}
return s
d.Slug = slugify.Slugify(title)
}
func newDashboardFromJson(data *simplejson.Json) *dashboard {

@ -7,6 +7,7 @@ import (
"fmt"
"strconv"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
@ -57,7 +58,7 @@ func NewStaticDashboardSummaryBuilder(lookup DatasourceLookup, sanitize bool) mo
}
dashboardRefs := NewReferenceAccumulator()
url := fmt.Sprintf("/d/%s/%s", uid, models.SlugifyTitle(dash.Title))
url := fmt.Sprintf("/d/%s/%s", uid, slugify.Slugify(dash.Title))
summary.Name = dash.Title
summary.Description = dash.Description
summary.URL = url

Loading…
Cancel
Save