diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml deleted file mode 100644 index 3d2ad5a..0000000 --- a/.github/workflows/rebase.yml +++ /dev/null @@ -1,22 +0,0 @@ -on: - issue_comment: - types: [created] - -name: Automatic Rebase -jobs: - rebase: - name: Rebase - if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') - runs-on: ubuntu-latest - steps: - - name: Checkout the latest code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Automatic Rebase - uses: cirrus-actions/rebase@1.7 - with: - autosquash: ${{ contains(github.event.comment.body, '/autosquash') || contains(github.event.comment.body, '/rebase-autosquash') }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9d5b625..972cfb2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,51 +20,37 @@ jobs: matrix: go-version: [1.18.x, 1.x] steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - uses: WillAbides/setup-go-faster@v1 - with: - go-version: ${{ matrix.go-version }} + - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 with: - # In order: - # * Module download cache - # * Build cache (Linux) - # * Build cache (Mac) - # * Build cache (Windows) - path: | - ~/go/pkg/mod - ~/.cache/go-build - ~/Library/Caches/go-build - %LocalAppData%\go-build - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + go-version: ${{ matrix.go-version }} - name: Test with race and coverage run: | go test -race -coverprofile=coverage.out -covermode=atomic go tool cover -func=coverage.out - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v4 with: files: ./coverage.out + token: ${{ secrets.CODECOV_TOKEN }} # required lint: name: "Run static analysis" runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 1 - - uses: WillAbides/setup-go-faster@v1 + - uses: actions/checkout@v4 + + - name: Setup Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 with: go-version: "1.x" - run: "go vet ./..." - - uses: dominikh/staticcheck-action@v1 + - name: Staticcheck + uses: dominikh/staticcheck-action@v1.3.0 with: - version: "2022.1.3" + install-go: false diff --git a/languages_substitution.go b/languages_substitution.go index e2e0ae3..7661d6a 100644 --- a/languages_substitution.go +++ b/languages_substitution.go @@ -26,6 +26,7 @@ func init() { &nlSub, &nnSub, &plSub, + &ptSub, &roSub, &slSub, &svSub, @@ -187,6 +188,25 @@ var plSub = map[rune]string{ '@': "na", } +var ptSub = map[rune]string{ + '&': "e", + '@': "em", + 'á': "a", + 'Á': "A", + 'é': "e", + 'É': "E", + 'í': "i", + 'Í': "I", + 'ó': "o", + 'Ó': "O", + 'ö': "o", + 'Ö': "O", + 'ú': "u", + 'Ú': "U", + 'ü': "u", + 'Ü': "U", +} + var roSub = map[rune]string{ '&': "si", 'Ă': "A", @@ -255,7 +275,7 @@ var bgSub = map[rune]string{ 'Ц': "Ts", 'Ч': "Ch", 'Ш': "Sh", - 'Щ': "Sh", + 'Щ': "Sht", 'Ъ': "A", 'Ь': "Y", 'Ю': "Yu", diff --git a/slug.go b/slug.go index e12b21e..763a012 100644 --- a/slug.go +++ b/slug.go @@ -39,6 +39,10 @@ var ( // Default is true. Lowercase = true + // Append timestamp to the end in order to make slug unique + // Default is false + AppendTimestamp = false + regexpNonAuthorizedChars = regexp.MustCompile("[^a-zA-Z0-9-_]") regexpMultipleDashes = regexp.MustCompile("-+") ) @@ -96,6 +100,8 @@ func MakeLang(s string, lang string) (slug string) { slug = SubstituteRune(slug, nnSub) case "pl", "pol": slug = SubstituteRune(slug, plSub) + case "pt", "prt", "pt-br", "br", "bra", "por": + slug = SubstituteRune(slug, ptSub) case "ro", "rou": slug = SubstituteRune(slug, roSub) case "sl", "slv": @@ -128,6 +134,10 @@ func MakeLang(s string, lang string) (slug string) { slug = smartTruncate(slug) } + if AppendTimestamp { + slug = slug + "-" + timestamp() + } + return slug } @@ -178,6 +188,11 @@ func smartTruncate(text string) string { return text[:MaxLength] } +// timestamp returns current timestamp as string +func timestamp() string { + return strconv.FormatInt(time.Now().Unix(), 10) +} + // IsSlug returns True if provided text does not contain white characters, // punctuation, all letters are lower case and only from ASCII range. // It could contain `-` and `_` but not at the beginning or end of the text. diff --git a/slug_test.go b/slug_test.go index 0867c0e..5c45c2c 100644 --- a/slug_test.go +++ b/slug_test.go @@ -8,6 +8,7 @@ package slug import ( "strconv" "strings" + "regexp" "testing" ) @@ -76,8 +77,8 @@ func TestSlugMakeLang(t *testing.T) { want string lowercase bool }{ - {"bg", "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя", "abvgdezhziyklmnoprstufhtschshshayyuyaabvgdezhziyklmnoprstufhtschshshtayyuya", true}, - {"bg", "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя", "ABVGDEZhZIYKLMNOPRSTUFHTsChShShAYYuYaabvgdezhziyklmnoprstufhtschshshtayyuya", false}, + {"bg", "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя", "abvgdezhziyklmnoprstufhtschshshtayyuyaabvgdezhziyklmnoprstufhtschshshtayyuya", true}, + {"bg", "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя", "ABVGDEZhZIYKLMNOPRSTUFHTsChShShtAYYuYaabvgdezhziyklmnoprstufhtschshshtayyuya", false}, {"cs", "ěščřžýáíéúůóňťĚŠČŘŽÝÁÍÉÚŮÓŇŤ", "escrzyaieuuontescrzyaieuuont", true}, {"cs", "ěščřžýáíéúůóňťĚŠČŘŽÝÁÍÉÚŮÓŇŤ", "escrzyaieuuontESCRZYAIEUUONT", false}, {"ces", "ěščřžýáíéúůóňťĚŠČŘŽÝÁÍÉÚŮÓŇŤ", "escrzyaieuuontescrzyaieuuont", true}, @@ -97,6 +98,7 @@ func TestSlugMakeLang(t *testing.T) { {"hu", "SzÉlÜtÖtt ŰrÚjsÁgírÓnŐ", "SzElUtOtt-UrUjsAgirOnO", false}, {"kk", "әғһіңөқұүӘҒҺІҢӨҚҰҮ", "aghinoquuaghinoquu", true}, {"kk", "әғһіңөқұүӘҒҺІҢӨҚҰҮ", "aghinoquuAGHINOQUU", false}, + {"pt", "áÁéÉíÍóÓöÖúÚüÜ", "aAeEiIoOoOuUuU", false}, {"ro", "ĂăÂăÎîȘșȚț", "aaaaiisstt", true}, {"ro", "ĂăÂăÎîȘșȚț", "AaAaIiSsTt", false}, {"tr", "şüöğıçŞÜÖİĞÇ", "suogicsuoigc", true}, @@ -128,6 +130,7 @@ func TestSlugMakeLang(t *testing.T) { {"kk", "This @ that", "this-that", true}, {"nl", "This & that", "this-en-that", true}, {"pl", "This & that", "this-i-that", true}, + {"pt", "This & that", "this-e-that", true}, {"pol", "This & that", "this-i-that", true}, {"sv", "This & that", "this-och-that", true}, {"sv", "This @ that", "this-snabel-a-that", true}, @@ -205,6 +208,7 @@ func TestSlugMakeUserSubstituteLang(t *testing.T) { got, smust.want) } } + CustomSub = nil } func TestSlugMakeSubstituteOrderLang(t *testing.T) { @@ -232,6 +236,8 @@ func TestSlugMakeSubstituteOrderLang(t *testing.T) { got, smsot.want) } } + CustomRuneSub = nil + CustomSub = nil } func TestSubstituteLang(t *testing.T) { @@ -342,6 +348,90 @@ func TestSlugMakeSmartTruncate(t *testing.T) { } } +func TestSlugMakeAppendTimestamp(t *testing.T) { + testCases := []struct { + in string + want string + appendTimestamp bool + }{ + {"DOBROSLAWZYBORT", "dobroslawzybort", true}, + {"Dobroslaw Zybort", "dobroslaw-zybort", true}, + {" Dobroslaw Zybort ?", "dobroslaw-zybort", true}, + {"Dobrosław Żybort", "dobroslaw-zybort", true}, + {"Ala ma 6 kotów.", "ala-ma-6-kotow", true}, + + {"áÁàÀãÃâÂäÄąĄą̊Ą̊", "aaaaaaaaaaaaaa", true}, + {"ćĆĉĈçÇčČ", "cccccccc", true}, + {"éÉèÈẽẼêÊëËęĘěĚ", "eeeeeeeeeeeeee", true}, + {"íÍìÌĩĨîÎïÏįĮ", "iiiiiiiiiiii", true}, + {"łŁ", "ll", true}, + {"ńŃ", "nn", true}, + {"óÓòÒõÕôÔöÖǫǪǭǬø", "ooooooooooooooo", true}, + {"śŚšŠ", "ssss", true}, + {"řŘ", "rr", true}, + {"ťŤ", "tt", true}, + {"úÚùÙũŨûÛüÜųŲůŮ", "uuuuuuuuuuuuuu", true}, + {"y̨Y̨ýÝ", "yyyy", true}, + {"źŹżŹžŽ", "zzzzzz", true}, + {"·/,:;`˜'\"", "", true}, + {"2000–2013", "2000-2013", true}, + {"style—not", "style-not", true}, + {"test_slug", "test_slug", true}, + {"_test_slug_", "test_slug", true}, + {"-test-slug-", "test-slug", true}, + {"Æ", "ae", true}, + {"Ich heiße", "ich-heisse", true}, + {"𐀀", "", true}, // Bug #53 + {"% 5 @ 4 $ 3 / 2 & 1 & 2 # 3 @ 4 _ 5", "5-at-4-3-2-and-1-and-2-3-at-4-_-5", true}, + + {"This & that", "this-and-that", true}, + {"fácil €", "facil-eu", true}, + {"smile ☺", "smile", true}, + {"Hellö Wörld хелло ворлд", "hello-world-khello-vorld", true}, + {"\"C'est déjà l’été.\"", "cest-deja-lete", true}, + {"jaja---lol-méméméoo--a", "jaja-lol-mememeoo-a", true}, + {"影師", "ying-shi", true}, + {"Đanković & Kožušček", "dankovic-and-kozuscek", true}, + {"ĂăÂâÎîȘșȚț", "aaaaiisstt", true}, + + // No append timestamp + {"DOBROSLAWZYBORT", "dobroslawzybort", false}, + {"Dobroslaw Zybort", "dobroslaw-zybort", false}, + {" Dobroslaw Zybort ?", "dobroslaw-zybort", false}, + {"Dobrosław Żybort", "dobroslaw-zybort", false}, + {"Ala ma 6 kotów.", "ala-ma-6-kotow", false}, + } + + MaxLength = 0 + EnableSmartTruncate = true + CustomRuneSub = nil + CustomSub = nil + Lowercase = true + for index, st := range testCases { + if st.appendTimestamp { + AppendTimestamp = true + } else { + AppendTimestamp = false + } + got := Make(st.in) + if st.appendTimestamp { + want := regexp.MustCompile(`^` + st.want + `-\d{10}$`) + if !want.MatchString(got) { + t.Errorf( + "%d. AppendTimestamp = %v; Make(%#v) = %#v; want %#v", + index, st.appendTimestamp, st.in, got, want, + ) + } + continue + } + if got != st.want { + t.Errorf( + "%d. Make(%#v) = %#v; want %#v", + index, st.in, got, st.want) + } + } +} + func TestIsSlug(t *testing.T) { MaxLength = 0 type args struct {