From a2fd10fddded35d745d7ca607ec983702a273daa Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Thu, 5 Sep 2024 01:45:57 +0200 Subject: [PATCH] Backend: Move string shortening functions to /pkg/txt/clip Signed-off-by: Michael Mayer --- internal/config/config_customize.go | 2 ++ internal/config/config_server.go | 5 +++++ internal/config/config_test.go | 7 +++++++ internal/entity/file_test.go | 2 +- pkg/clean/clean.go | 2 -- pkg/clean/clip.go | 21 ------------------- pkg/clean/error.go | 4 ++-- pkg/clean/header.go | 2 +- pkg/clean/ip.go | 6 +++--- pkg/clean/length.go | 9 ++++++++ pkg/clean/log.go | 8 +++++++- pkg/clean/log_test.go | 10 +++++++++ pkg/clean/search.go | 4 ++-- pkg/clean/token.go | 2 +- pkg/clean/type.go | 6 ++++-- pkg/clean/type_test.go | 20 ++++++++++-------- pkg/clean/uri.go | 2 +- pkg/txt/clip.go | 32 +++++------------------------ pkg/txt/clip/chars.go | 15 ++++++++++++++ pkg/txt/clip/chars_test.go | 30 +++++++++++++++++++++++++++ pkg/txt/clip/clip.go | 25 ++++++++++++++++++++++ pkg/txt/clip/const.go | 17 +++++++++++++++ pkg/txt/clip/runes.go | 20 ++++++++++++++++++ pkg/txt/clip/runes_test.go | 30 +++++++++++++++++++++++++++ pkg/txt/clip/shorten.go | 16 +++++++++++++++ pkg/txt/clip/shorten_test.go | 22 ++++++++++++++++++++ 26 files changed, 246 insertions(+), 73 deletions(-) delete mode 100644 pkg/clean/clip.go create mode 100644 pkg/clean/length.go create mode 100644 pkg/txt/clip/chars.go create mode 100644 pkg/txt/clip/chars_test.go create mode 100644 pkg/txt/clip/clip.go create mode 100644 pkg/txt/clip/const.go create mode 100644 pkg/txt/clip/runes.go create mode 100644 pkg/txt/clip/runes_test.go create mode 100644 pkg/txt/clip/shorten.go create mode 100644 pkg/txt/clip/shorten_test.go diff --git a/internal/config/config_customize.go b/internal/config/config_customize.go index ce19844bd..35f5b67bb 100644 --- a/internal/config/config_customize.go +++ b/internal/config/config_customize.go @@ -75,6 +75,8 @@ func (c *Config) WallpaperUri() string { wallpaperUri = c.StaticAssetUri(path.Join(wallpaperPath, fileName)) } else if fs.FileExists(c.CustomStaticFile(path.Join(wallpaperPath, fileName))) { wallpaperUri = c.CustomStaticAssetUri(path.Join(wallpaperPath, fileName)) + } else if fs.FileExists(path.Join(c.ThemePath(), fileName)) { + wallpaperUri = c.BaseUri("/" + path.Join("_theme", fileName)) } else { return "" } diff --git a/internal/config/config_server.go b/internal/config/config_server.go index eb83828da..41288a020 100644 --- a/internal/config/config_server.go +++ b/internal/config/config_server.go @@ -241,3 +241,8 @@ func (c *Config) BuildPath() string { func (c *Config) ImgPath() string { return filepath.Join(c.StaticPath(), "img") } + +// ThemePath returns the path to static theme files. +func (c *Config) ThemePath() string { + return filepath.Join(c.ConfigPath(), "theme") +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 39bb979a5..333f8ecfa 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -265,6 +265,13 @@ func TestConfig_ImgPath(t *testing.T) { assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/img", path) } +func TestConfig_ThemePath(t *testing.T) { + c := NewConfig(CliTestContext()) + + path := c.ThemePath() + assert.Equal(t, "/go/src/github.com/photoprism/photoprism/storage/testdata/config/theme", path) +} + func TestConfig_IndexWorkers(t *testing.T) { c := NewConfig(CliTestContext()) diff --git a/internal/entity/file_test.go b/internal/entity/file_test.go index 833da1714..d744cc0de 100644 --- a/internal/entity/file_test.go +++ b/internal/entity/file_test.go @@ -368,7 +368,7 @@ func TestFile_SetProjection(t *testing.T) { p := projection.New(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!") m.SetProjection(p.String()) assert.Equal(t, p.String(), m.FileProjection) - assert.GreaterOrEqual(t, clean.ClipType, len(m.FileProjection)) + assert.GreaterOrEqual(t, clean.LengthType, len(m.FileProjection)) }) } diff --git a/pkg/clean/clean.go b/pkg/clean/clean.go index 3af3806e5..862eee31c 100644 --- a/pkg/clean/clean.go +++ b/pkg/clean/clean.go @@ -26,8 +26,6 @@ package clean import "strings" -const MaxLength = 4096 - func reject(s string, maxLength int) bool { if maxLength > 0 && len(s) > maxLength { return true diff --git a/pkg/clean/clip.go b/pkg/clean/clip.go deleted file mode 100644 index 7aa766b81..000000000 --- a/pkg/clean/clip.go +++ /dev/null @@ -1,21 +0,0 @@ -package clean - -import "strings" - -const ( - ClipShortType = 8 - ClipIPv6 = 39 - ClipType = 64 -) - -// Clip shortens a string to the given number of characters, and removes all leading and trailing white space. -func Clip(s string, maxLen int) string { - s = strings.TrimSpace(s) - l := len(s) - - if l <= maxLen { - return s - } else { - return strings.TrimSpace(s[:maxLen]) - } -} diff --git a/pkg/clean/error.go b/pkg/clean/error.go index 9dc21e156..8aae22c59 100644 --- a/pkg/clean/error.go +++ b/pkg/clean/error.go @@ -10,8 +10,8 @@ func Error(err error) string { return "unknown error" } else { // Limit error message length. - if len(s) > MaxLength { - s = s[:MaxLength] + if len(s) > LengthLimit { + s = s[:LengthLimit] } // Remove non-printable and other potentially problematic characters. diff --git a/pkg/clean/header.go b/pkg/clean/header.go index cfbd11494..4236ce29d 100644 --- a/pkg/clean/header.go +++ b/pkg/clean/header.go @@ -2,7 +2,7 @@ package clean // Header sanitizes a string for use in request or response headers. func Header(s string) string { - if s == "" || len(s) > MaxLength { + if s == "" || len(s) > LengthLimit { return "" } diff --git a/pkg/clean/ip.go b/pkg/clean/ip.go index e120d0216..34f0a6e6a 100644 --- a/pkg/clean/ip.go +++ b/pkg/clean/ip.go @@ -11,7 +11,7 @@ var IpRegExp = regexp.MustCompile(`[^a-zA-Z0-9:.]`) // IP returns the sanitized and normalized network address if it is valid, or the default otherwise. func IP(s, defaultIp string) string { // Return default if invalid. - if s == "" || len(s) > MaxLength || s == defaultIp { + if s == "" || len(s) > LengthLimit || s == defaultIp { return defaultIp } @@ -21,8 +21,8 @@ func IP(s, defaultIp string) string { } // Limit string length to 39 characters. - if len(s) > ClipIPv6 { - s = s[:ClipIPv6] + if len(s) > LengthIPv6 { + s = s[:LengthIPv6] } // Parse IP address and return it as string. diff --git a/pkg/clean/length.go b/pkg/clean/length.go new file mode 100644 index 000000000..aa77ec751 --- /dev/null +++ b/pkg/clean/length.go @@ -0,0 +1,9 @@ +package clean + +const ( + LengthType = 64 + LengthShortType = 8 + LengthIPv6 = 39 + LengthLog = 512 + LengthLimit = 4096 +) diff --git a/pkg/clean/log.go b/pkg/clean/log.go index d65b665a6..62bb47051 100644 --- a/pkg/clean/log.go +++ b/pkg/clean/log.go @@ -3,13 +3,19 @@ package clean import ( "fmt" "strings" + + "github.com/photoprism/photoprism/pkg/txt/clip" ) // Log sanitizes strings created from user input in response to the log4j debacle. func Log(s string) string { if s == "" { return "''" - } else if reject(s, 512) { + } + + s = clip.Shorten(s, LengthLog, clip.Ellipsis) + + if reject(s, LengthLimit) { return "?" } diff --git a/pkg/clean/log_test.go b/pkg/clean/log_test.go index 28baa5ce8..94407dd24 100644 --- a/pkg/clean/log_test.go +++ b/pkg/clean/log_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/pkg/txt/clip" ) func TestLog(t *testing.T) { @@ -25,6 +27,14 @@ func TestLog(t *testing.T) { t.Run("SpecialChars", func(t *testing.T) { assert.Equal(t, "' The ?quick? ''brown 'fox. '", Log(" The \n\r ''brown \"fox. \t ")) }) + t.Run("LoremIpsum", func(t *testing.T) { + assert.Equal(t, "'It is a long established fact that a reader will be distracted by the readable "+ + "content of a pagewhen looking at its layout. The point of using Lorem Ipsum is that it has a "+ + "more-or-less normal distribution of letters,as opposed to using 'Content here, content here', making it "+ + "look like readable English.Many desktop publishing packages and web page editors now use Lorem Ipsum as "+ + "their default model text, and a search for'lorem ipsum' will uncover many web sites still in their "+ + "infancy. Various versions…'", Log(clip.LoremIpsum)) + }) } func TestLogQuote(t *testing.T) { diff --git a/pkg/clean/search.go b/pkg/clean/search.go index 174c94f56..5a5cb1fe1 100644 --- a/pkg/clean/search.go +++ b/pkg/clean/search.go @@ -17,7 +17,7 @@ func replace(subject string, search string, replace string) string { // SearchString replaces search operator with default symbols. func SearchString(s string) string { - if s == "" || reject(s, MaxLength) { + if s == "" || reject(s, LengthLimit) { return Empty } @@ -32,7 +32,7 @@ func SearchString(s string) string { // SearchQuery replaces search operator with default symbols. func SearchQuery(s string) string { - if s == "" || reject(s, MaxLength) { + if s == "" || reject(s, LengthLimit) { return Empty } diff --git a/pkg/clean/token.go b/pkg/clean/token.go index f4facd6e8..097ac3585 100644 --- a/pkg/clean/token.go +++ b/pkg/clean/token.go @@ -6,7 +6,7 @@ import ( // Token returns the sanitized token string with a length of up to 4096 characters. func Token(s string) string { - if s == "" || reject(s, MaxLength) { + if s == "" || reject(s, LengthLimit) { return "" } diff --git a/pkg/clean/type.go b/pkg/clean/type.go index eba101a25..8d6a74de6 100644 --- a/pkg/clean/type.go +++ b/pkg/clean/type.go @@ -2,6 +2,8 @@ package clean import ( "strings" + + "github.com/photoprism/photoprism/pkg/txt/clip" ) // Type omits invalid runes, ensures a maximum length of 32 characters, and returns the result. @@ -10,7 +12,7 @@ func Type(s string) string { return s } - return Clip(ASCII(s), ClipType) + return clip.Chars(ASCII(s), LengthType) } // TypeLower converts a type string to lowercase, omits invalid runes, and shortens it if needed. @@ -37,7 +39,7 @@ func ShortType(s string) string { return s } - return Clip(ASCII(s), ClipShortType) + return clip.Chars(ASCII(s), LengthShortType) } // ShortTypeLower converts a short type string to lowercase, omits invalid runes, and shortens it if needed. diff --git a/pkg/clean/type_test.go b/pkg/clean/type_test.go index 30fded5be..1bc99f36a 100644 --- a/pkg/clean/type_test.go +++ b/pkg/clean/type_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/pkg/txt/clip" ) func TestToASCII(t *testing.T) { @@ -14,27 +16,27 @@ func TestToASCII(t *testing.T) { func TestClip(t *testing.T) { t.Run("Foo", func(t *testing.T) { - result := Clip("Foo", 16) + result := clip.Chars("Foo", 16) assert.Equal(t, "Foo", result) assert.Equal(t, 3, len(result)) }) t.Run("TrimFoo", func(t *testing.T) { - result := Clip(" Foo ", 16) + result := clip.Chars(" Foo ", 16) assert.Equal(t, "Foo", result) assert.Equal(t, 3, len(result)) }) t.Run("TooLong", func(t *testing.T) { - result := Clip(" 幸福 Hanzi are logograms developed for the writing of Chinese! ", 16) + result := clip.Chars(" 幸福 Hanzi are logograms developed for the writing of Chinese! ", 16) assert.Equal(t, "幸福 Hanzi are", result) assert.Equal(t, 16, len(result)) }) t.Run("ToASCII", func(t *testing.T) { - result := Clip(ASCII(strings.ToLower(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")), ClipType) + result := clip.Chars(ASCII(strings.ToLower(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")), LengthType) assert.Equal(t, "hanzi are logograms developed for the writing of chinese! expres", result) assert.Equal(t, 64, len(result)) }) t.Run("Empty", func(t *testing.T) { - result := Clip("", 999) + result := clip.Chars("", 999) assert.Equal(t, "", result) assert.Equal(t, 0, len(result)) }) @@ -44,7 +46,7 @@ func TestType(t *testing.T) { t.Run("Clip", func(t *testing.T) { result := Type(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!") assert.Equal(t, "Hanzi are logograms developed for the writing of Chinese! Expres", result) - assert.Equal(t, ClipType, len(result)) + assert.Equal(t, LengthType, len(result)) }) t.Run("Empty", func(t *testing.T) { assert.Equal(t, "", Type("")) @@ -55,7 +57,7 @@ func TestTypeLower(t *testing.T) { t.Run("Clip", func(t *testing.T) { result := TypeLower(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!") assert.Equal(t, "hanzi are logograms developed for the writing of chinese! expres", result) - assert.Equal(t, ClipType, len(result)) + assert.Equal(t, LengthType, len(result)) }) t.Run("Empty", func(t *testing.T) { assert.Equal(t, "", TypeLower("")) @@ -84,7 +86,7 @@ func TestShortType(t *testing.T) { t.Run("Clip", func(t *testing.T) { result := ShortType(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!") assert.Equal(t, "Hanzi ar", result) - assert.Equal(t, ClipShortType, len(result)) + assert.Equal(t, LengthShortType, len(result)) }) t.Run("Empty", func(t *testing.T) { assert.Equal(t, "", ShortType("")) @@ -95,7 +97,7 @@ func TestShortTypeLower(t *testing.T) { t.Run("Clip", func(t *testing.T) { result := ShortTypeLower(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!") assert.Equal(t, "hanzi ar", result) - assert.Equal(t, ClipShortType, len(result)) + assert.Equal(t, LengthShortType, len(result)) }) t.Run("Empty", func(t *testing.T) { assert.Equal(t, "", ShortTypeLower("")) diff --git a/pkg/clean/uri.go b/pkg/clean/uri.go index 5db00e9e5..c02ab6c1e 100644 --- a/pkg/clean/uri.go +++ b/pkg/clean/uri.go @@ -7,7 +7,7 @@ import ( // Uri removes invalid character from an uri string. func Uri(s string) string { - if s == "" || len(s) > MaxLength { + if s == "" || len(s) > LengthLimit { return "" } else if strings.Contains(s, "..") { return "" diff --git a/pkg/txt/clip.go b/pkg/txt/clip.go index 81cd101b3..6956efd42 100644 --- a/pkg/txt/clip.go +++ b/pkg/txt/clip.go @@ -1,7 +1,7 @@ package txt import ( - "strings" + "github.com/photoprism/photoprism/pkg/txt/clip" ) const ( @@ -31,34 +31,12 @@ const ( ClipLongText = 4096 ) -// Clip shortens a string to the given number of runes, and removes all leading and trailing white space. +// Clip limits a string to the given number of runes and removes all leading and trailing spaces. func Clip(s string, size int) string { - s = strings.TrimSpace(s) - - if s == "" || size <= 0 { - return "" - } - - runes := []rune(s) - - if len(runes) > size { - s = string(runes[0:size]) - } - - return strings.TrimSpace(s) + return clip.Runes(s, size) } -// Shorten shortens a string with suffix. +// Shorten limits a character string to the specified number of runes and adds a suffix if it has been shortened. func Shorten(s string, size int, suffix string) string { - if suffix == "" { - suffix = Ellipsis - } - - l := len(suffix) - - if len(s) < size || size < l+1 { - return s - } - - return Clip(s, size-l) + suffix + return clip.Shorten(s, size, suffix) } diff --git a/pkg/txt/clip/chars.go b/pkg/txt/clip/chars.go new file mode 100644 index 000000000..1f217dea3 --- /dev/null +++ b/pkg/txt/clip/chars.go @@ -0,0 +1,15 @@ +package clip + +import "strings" + +// Chars limits a string to the specified number of characters and removes all leading and trailing spaces. +func Chars(s string, maxLen int) string { + s = strings.TrimSpace(s) + l := len(s) + + if l <= maxLen { + return s + } else { + return strings.TrimSpace(s[:maxLen]) + } +} diff --git a/pkg/txt/clip/chars_test.go b/pkg/txt/clip/chars_test.go new file mode 100644 index 000000000..c4ad1e8c9 --- /dev/null +++ b/pkg/txt/clip/chars_test.go @@ -0,0 +1,30 @@ +package clip + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChars(t *testing.T) { + t.Run("Foo", func(t *testing.T) { + result := Chars("Foo", 16) + assert.Equal(t, "Foo", result) + assert.Equal(t, 3, len(result)) + }) + t.Run("TrimFoo", func(t *testing.T) { + result := Chars(" Foo ", 16) + assert.Equal(t, "Foo", result) + assert.Equal(t, 3, len(result)) + }) + t.Run("TooLong", func(t *testing.T) { + result := Chars(" 幸福 Hanzi are logograms developed for the writing of Chinese! ", 16) + assert.Equal(t, "幸福 Hanzi are", result) + assert.Equal(t, 16, len(result)) + }) + t.Run("Empty", func(t *testing.T) { + result := Chars("", 999) + assert.Equal(t, "", result) + assert.Equal(t, 0, len(result)) + }) +} diff --git a/pkg/txt/clip/clip.go b/pkg/txt/clip/clip.go new file mode 100644 index 000000000..2570495a6 --- /dev/null +++ b/pkg/txt/clip/clip.go @@ -0,0 +1,25 @@ +/* +Package clip provides functions for limiting the length of character strings. + +Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved. + + This program is free software: you can redistribute it and/or modify + it under Version 3 of the GNU Affero General Public License (the "AGPL"): + + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + The AGPL is supplemented by our Trademark and Brand Guidelines, + which describe how our Brand Assets may be used: + + +Feel free to send an email to hello@photoprism.app if you have questions, +want to support our work, or just want to say hello. + +Additional information can be found in our Developer Guide: + +*/ +package clip diff --git a/pkg/txt/clip/const.go b/pkg/txt/clip/const.go new file mode 100644 index 000000000..12e38ae98 --- /dev/null +++ b/pkg/txt/clip/const.go @@ -0,0 +1,17 @@ +package clip + +const Ellipsis = "…" + +const LoremIpsum = `It is a long established fact that a reader will be distracted by the readable content of a page +when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, +as opposed to using 'Content here, content here', making it look like readable English. +Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for +'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, +sometimes by accident, sometimes on purpose (injected humour and the like). +There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, +by injected humour, or randomised words which don't look even slightly believable. +If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the +middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making +this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of +model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always +free from repetition, injected humour, or non-characteristic words etc.` diff --git a/pkg/txt/clip/runes.go b/pkg/txt/clip/runes.go new file mode 100644 index 000000000..15deea25e --- /dev/null +++ b/pkg/txt/clip/runes.go @@ -0,0 +1,20 @@ +package clip + +import "strings" + +// Runes limits a string to the given number of runes and removes all leading and trailing spaces. +func Runes(s string, size int) string { + s = strings.TrimSpace(s) + + if s == "" || size <= 0 { + return "" + } + + runes := []rune(s) + + if len(runes) > size { + s = string(runes[0:size]) + } + + return strings.TrimSpace(s) +} diff --git a/pkg/txt/clip/runes_test.go b/pkg/txt/clip/runes_test.go new file mode 100644 index 000000000..d3f590652 --- /dev/null +++ b/pkg/txt/clip/runes_test.go @@ -0,0 +1,30 @@ +package clip + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRunes(t *testing.T) { + t.Run("Foo", func(t *testing.T) { + result := Runes("Foo", 16) + assert.Equal(t, "Foo", result) + assert.Equal(t, 3, len(result)) + }) + t.Run("TrimFoo", func(t *testing.T) { + result := Runes(" Foo ", 16) + assert.Equal(t, "Foo", result) + assert.Equal(t, 3, len(result)) + }) + t.Run("TooLong", func(t *testing.T) { + result := Runes(" 幸福 Hanzi are logograms developed for the writing of Chinese! ", 16) + assert.Equal(t, "幸福 Hanzi are log", result) + assert.Equal(t, 20, len(result)) + }) + t.Run("Empty", func(t *testing.T) { + result := Runes("", 999) + assert.Equal(t, "", result) + assert.Equal(t, 0, len(result)) + }) +} diff --git a/pkg/txt/clip/shorten.go b/pkg/txt/clip/shorten.go new file mode 100644 index 000000000..d315f80f7 --- /dev/null +++ b/pkg/txt/clip/shorten.go @@ -0,0 +1,16 @@ +package clip + +// Shorten limits a character string to the specified number of runes and adds a suffix if it has been shortened. +func Shorten(s string, size int, suffix string) string { + if suffix == "" { + suffix = Ellipsis + } + + l := len(suffix) + + if len(s) < size || size < l+1 { + return s + } + + return Runes(s, size-l) + suffix +} diff --git a/pkg/txt/clip/shorten_test.go b/pkg/txt/clip/shorten_test.go new file mode 100644 index 000000000..96b85cae3 --- /dev/null +++ b/pkg/txt/clip/shorten_test.go @@ -0,0 +1,22 @@ +package clip + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShorten(t *testing.T) { + t.Run("ShortEnough", func(t *testing.T) { + assert.Equal(t, "fox!", Shorten("fox!", 6, "...")) + }) + t.Run("CustomSuffix", func(t *testing.T) { + assert.Equal(t, "I'm ä...", Shorten("I'm ä lazy BRoWN fox!", 8, "...")) + }) + t.Run("DefaultSuffix", func(t *testing.T) { + assert.Equal(t, "I'm…", Shorten("I'm ä lazy BRoWN fox!", 7, "")) + }) + t.Run("Empty", func(t *testing.T) { + assert.Equal(t, "", Shorten("", -1, "")) + }) +}