Backend: Move string shortening functions to /pkg/txt/clip

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2024-09-05 01:45:57 +02:00
parent fd571f70b0
commit a2fd10fddd
26 changed files with 246 additions and 73 deletions

View file

@ -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 ""
}

View file

@ -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")
}

View file

@ -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())

View file

@ -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))
})
}

View file

@ -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

View file

@ -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])
}
}

View file

@ -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.

View file

@ -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 ""
}

View file

@ -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.

9
pkg/clean/length.go Normal file
View file

@ -0,0 +1,9 @@
package clean
const (
LengthType = 64
LengthShortType = 8
LengthIPv6 = 39
LengthLog = 512
LengthLimit = 4096
)

View file

@ -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 "?"
}

View file

@ -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 <quick>\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) {

View file

@ -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
}

View file

@ -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 ""
}

View file

@ -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.

View file

@ -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(""))

View file

@ -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 ""

View file

@ -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)
}

15
pkg/txt/clip/chars.go Normal file
View file

@ -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])
}
}

View file

@ -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))
})
}

25
pkg/txt/clip/clip.go Normal file
View file

@ -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"):
<https://docs.photoprism.app/license/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:
<https://www.photoprism.app/trademark>
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:
<https://docs.photoprism.app/developer-guide/>
*/
package clip

17
pkg/txt/clip/const.go Normal file
View file

@ -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.`

20
pkg/txt/clip/runes.go Normal file
View file

@ -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)
}

View file

@ -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))
})
}

16
pkg/txt/clip/shorten.go Normal file
View file

@ -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
}

View file

@ -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, ""))
})
}