mirror of
https://github.com/go-gitea/gitea
synced 2025-01-05 07:24:25 +00:00
Refactor "string truncate" (#32984)
This commit is contained in:
parent
594edad213
commit
9bfa9f450d
@ -275,7 +275,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
run.Index = index
|
run.Index = index
|
||||||
run.Title, _ = util.SplitStringAtByteN(run.Title, 255)
|
run.Title = util.EllipsisDisplayString(run.Title, 255)
|
||||||
|
|
||||||
if err := db.Insert(ctx, run); err != nil {
|
if err := db.Insert(ctx, run); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -308,7 +308,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
|||||||
} else {
|
} else {
|
||||||
hasWaiting = true
|
hasWaiting = true
|
||||||
}
|
}
|
||||||
job.Name, _ = util.SplitStringAtByteN(job.Name, 255)
|
job.Name = util.EllipsisDisplayString(job.Name, 255)
|
||||||
runJobs = append(runJobs, &ActionRunJob{
|
runJobs = append(runJobs, &ActionRunJob{
|
||||||
RunID: run.ID,
|
RunID: run.ID,
|
||||||
RepoID: run.RepoID,
|
RepoID: run.RepoID,
|
||||||
@ -402,7 +402,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error {
|
|||||||
if len(cols) > 0 {
|
if len(cols) > 0 {
|
||||||
sess.Cols(cols...)
|
sess.Cols(cols...)
|
||||||
}
|
}
|
||||||
run.Title, _ = util.SplitStringAtByteN(run.Title, 255)
|
run.Title = util.EllipsisDisplayString(run.Title, 255)
|
||||||
affected, err := sess.Update(run)
|
affected, err := sess.Update(run)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -252,7 +252,7 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) {
|
|||||||
// UpdateRunner updates runner's information.
|
// UpdateRunner updates runner's information.
|
||||||
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
|
func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
r.Name, _ = util.SplitStringAtByteN(r.Name, 255)
|
r.Name = util.EllipsisDisplayString(r.Name, 255)
|
||||||
var err error
|
var err error
|
||||||
if len(cols) == 0 {
|
if len(cols) == 0 {
|
||||||
_, err = e.ID(r.ID).AllCols().Update(r)
|
_, err = e.ID(r.ID).AllCols().Update(r)
|
||||||
@ -279,7 +279,7 @@ func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
|||||||
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
// Remove OwnerID to avoid confusion; it's not worth returning an error here.
|
||||||
t.OwnerID = 0
|
t.OwnerID = 0
|
||||||
}
|
}
|
||||||
t.Name, _ = util.SplitStringAtByteN(t.Name, 255)
|
t.Name = util.EllipsisDisplayString(t.Name, 255)
|
||||||
return db.Insert(ctx, t)
|
return db.Insert(ctx, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
@ -52,7 +51,7 @@ func GetRunnerToken(ctx context.Context, token string) (*ActionRunnerToken, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, fmt.Errorf(`runner token "%s...": %w`, base.TruncateString(token, 3), util.ErrNotExist)
|
return nil, fmt.Errorf(`runner token "%s...": %w`, util.TruncateRunes(token, 3), util.ErrNotExist)
|
||||||
}
|
}
|
||||||
return &runnerToken, nil
|
return &runnerToken, nil
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
|
|||||||
|
|
||||||
// Loop through each schedule row
|
// Loop through each schedule row
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
row.Title, _ = util.SplitStringAtByteN(row.Title, 255)
|
row.Title = util.EllipsisDisplayString(row.Title, 255)
|
||||||
// Create new schedule row
|
// Create new schedule row
|
||||||
if err = db.Insert(ctx, row); err != nil {
|
if err = db.Insert(ctx, row); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -298,7 +298,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
if len(workflowJob.Steps) > 0 {
|
if len(workflowJob.Steps) > 0 {
|
||||||
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
||||||
for i, v := range workflowJob.Steps {
|
for i, v := range workflowJob.Steps {
|
||||||
name, _ := util.SplitStringAtByteN(v.String(), 255)
|
name := util.EllipsisDisplayString(v.String(), 255)
|
||||||
steps[i] = &ActionTaskStep{
|
steps[i] = &ActionTaskStep{
|
||||||
Name: name,
|
Name: name,
|
||||||
TaskID: task.ID,
|
TaskID: task.ID,
|
||||||
|
@ -20,12 +20,12 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
@ -226,7 +226,7 @@ func (a *Action) GetActUserName(ctx context.Context) string {
|
|||||||
// ShortActUserName gets the action's user name trimmed to max 20
|
// ShortActUserName gets the action's user name trimmed to max 20
|
||||||
// chars.
|
// chars.
|
||||||
func (a *Action) ShortActUserName(ctx context.Context) string {
|
func (a *Action) ShortActUserName(ctx context.Context) string {
|
||||||
return base.EllipsisString(a.GetActUserName(ctx), 20)
|
return util.EllipsisDisplayString(a.GetActUserName(ctx), 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
|
// GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
|
||||||
@ -260,7 +260,7 @@ func (a *Action) GetRepoUserName(ctx context.Context) string {
|
|||||||
// ShortRepoUserName returns the name of the action repository owner
|
// ShortRepoUserName returns the name of the action repository owner
|
||||||
// trimmed to max 20 chars.
|
// trimmed to max 20 chars.
|
||||||
func (a *Action) ShortRepoUserName(ctx context.Context) string {
|
func (a *Action) ShortRepoUserName(ctx context.Context) string {
|
||||||
return base.EllipsisString(a.GetRepoUserName(ctx), 20)
|
return util.EllipsisDisplayString(a.GetRepoUserName(ctx), 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoName returns the name of the action repository.
|
// GetRepoName returns the name of the action repository.
|
||||||
@ -275,7 +275,7 @@ func (a *Action) GetRepoName(ctx context.Context) string {
|
|||||||
// ShortRepoName returns the name of the action repository
|
// ShortRepoName returns the name of the action repository
|
||||||
// trimmed to max 33 chars.
|
// trimmed to max 33 chars.
|
||||||
func (a *Action) ShortRepoName(ctx context.Context) string {
|
func (a *Action) ShortRepoName(ctx context.Context) string {
|
||||||
return base.EllipsisString(a.GetRepoName(ctx), 33)
|
return util.EllipsisDisplayString(a.GetRepoName(ctx), 33)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoPath returns the virtual path to the action repository.
|
// GetRepoPath returns the virtual path to the action repository.
|
||||||
|
@ -177,7 +177,7 @@ func ChangeIssueTitle(ctx context.Context, issue *Issue, doer *user_model.User,
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
||||||
if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
|
if err = UpdateIssueCols(ctx, issue, "name"); err != nil {
|
||||||
return fmt.Errorf("updateIssueCols: %w", err)
|
return fmt.Errorf("updateIssueCols: %w", err)
|
||||||
}
|
}
|
||||||
@ -440,7 +440,7 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Index = idx
|
issue.Index = idx
|
||||||
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
||||||
|
|
||||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
|
@ -572,7 +572,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Index = idx
|
issue.Index = idx
|
||||||
issue.Title, _ = util.SplitStringAtByteN(issue.Title, 255)
|
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
||||||
|
|
||||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
|
@ -256,7 +256,7 @@ func NewProject(ctx context.Context, p *Project) error {
|
|||||||
return util.NewInvalidArgumentErrorf("project type is not valid")
|
return util.NewInvalidArgumentErrorf("project type is not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Title, _ = util.SplitStringAtByteN(p.Title, 255)
|
p.Title = util.EllipsisDisplayString(p.Title, 255)
|
||||||
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err := db.Insert(ctx, p); err != nil {
|
if err := db.Insert(ctx, p); err != nil {
|
||||||
@ -311,7 +311,7 @@ func UpdateProject(ctx context.Context, p *Project) error {
|
|||||||
p.CardType = CardTypeTextOnly
|
p.CardType = CardTypeTextOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Title, _ = util.SplitStringAtByteN(p.Title, 255)
|
p.Title = util.EllipsisDisplayString(p.Title, 255)
|
||||||
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||||
"title",
|
"title",
|
||||||
"description",
|
"description",
|
||||||
|
@ -156,7 +156,7 @@ func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, er
|
|||||||
|
|
||||||
// UpdateRelease updates all columns of a release
|
// UpdateRelease updates all columns of a release
|
||||||
func UpdateRelease(ctx context.Context, rel *Release) error {
|
func UpdateRelease(ctx context.Context, rel *Release) error {
|
||||||
rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255)
|
rel.Title = util.EllipsisDisplayString(rel.Title, 255)
|
||||||
_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
|
_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -190,9 +190,9 @@ func (u *User) BeforeUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.LowerName = strings.ToLower(u.Name)
|
u.LowerName = strings.ToLower(u.Name)
|
||||||
u.Location = base.TruncateString(u.Location, 255)
|
u.Location = util.TruncateRunes(u.Location, 255)
|
||||||
u.Website = base.TruncateString(u.Website, 255)
|
u.Website = util.TruncateRunes(u.Website, 255)
|
||||||
u.Description = base.TruncateString(u.Description, 255)
|
u.Description = util.TruncateRunes(u.Description, 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfterLoad is invoked from XORM after filling all the fields of this object.
|
// AfterLoad is invoked from XORM after filling all the fields of this object.
|
||||||
@ -501,9 +501,9 @@ func (u *User) GitName() string {
|
|||||||
// ShortName ellipses username to length
|
// ShortName ellipses username to length
|
||||||
func (u *User) ShortName(length int) string {
|
func (u *User) ShortName(length int) string {
|
||||||
if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
|
if setting.UI.DefaultShowFullName && len(u.FullName) > 0 {
|
||||||
return base.EllipsisString(u.FullName, length)
|
return util.EllipsisDisplayString(u.FullName, length)
|
||||||
}
|
}
|
||||||
return base.EllipsisString(u.Name, length)
|
return util.EllipsisDisplayString(u.Name, length)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMailable checks if a user is eligible
|
// IsMailable checks if a user is eligible
|
||||||
|
@ -16,11 +16,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
@ -35,7 +35,7 @@ func EncodeSha256(str string) string {
|
|||||||
// ShortSha is basically just truncating.
|
// ShortSha is basically just truncating.
|
||||||
// It is DEPRECATED and will be removed in the future.
|
// It is DEPRECATED and will be removed in the future.
|
||||||
func ShortSha(sha1 string) string {
|
func ShortSha(sha1 string) string {
|
||||||
return TruncateString(sha1, 10)
|
return util.TruncateRunes(sha1, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BasicAuthDecode decode basic auth string
|
// BasicAuthDecode decode basic auth string
|
||||||
@ -116,27 +116,6 @@ func FileSize(s int64) string {
|
|||||||
return humanize.IBytes(uint64(s))
|
return humanize.IBytes(uint64(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
// EllipsisString returns a truncated short string,
|
|
||||||
// it appends '...' in the end of the length of string is too large.
|
|
||||||
func EllipsisString(str string, length int) string {
|
|
||||||
if length <= 3 {
|
|
||||||
return "..."
|
|
||||||
}
|
|
||||||
if utf8.RuneCountInString(str) <= length {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return string([]rune(str)[:length-3]) + "..."
|
|
||||||
}
|
|
||||||
|
|
||||||
// TruncateString returns a truncated string with given limit,
|
|
||||||
// it returns input string if length is not reached limit.
|
|
||||||
func TruncateString(str string, limit int) string {
|
|
||||||
if utf8.RuneCountInString(str) < limit {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return string([]rune(str)[:limit])
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringsToInt64s converts a slice of string to a slice of int64.
|
// StringsToInt64s converts a slice of string to a slice of int64.
|
||||||
func StringsToInt64s(strs []string) ([]int64, error) {
|
func StringsToInt64s(strs []string) ([]int64, error) {
|
||||||
if strs == nil {
|
if strs == nil {
|
||||||
|
@ -113,36 +113,6 @@ func TestFileSize(t *testing.T) {
|
|||||||
assert.Equal(t, "2.0 EiB", FileSize(size))
|
assert.Equal(t, "2.0 EiB", FileSize(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEllipsisString(t *testing.T) {
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 0))
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 1))
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 2))
|
|
||||||
assert.Equal(t, "...", EllipsisString("foobar", 3))
|
|
||||||
assert.Equal(t, "f...", EllipsisString("foobar", 4))
|
|
||||||
assert.Equal(t, "fo...", EllipsisString("foobar", 5))
|
|
||||||
assert.Equal(t, "foobar", EllipsisString("foobar", 6))
|
|
||||||
assert.Equal(t, "foobar", EllipsisString("foobar", 10))
|
|
||||||
assert.Equal(t, "测...", EllipsisString("测试文本一二三四", 4))
|
|
||||||
assert.Equal(t, "测试...", EllipsisString("测试文本一二三四", 5))
|
|
||||||
assert.Equal(t, "测试文...", EllipsisString("测试文本一二三四", 6))
|
|
||||||
assert.Equal(t, "测试文本一二三四", EllipsisString("测试文本一二三四", 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTruncateString(t *testing.T) {
|
|
||||||
assert.Equal(t, "", TruncateString("foobar", 0))
|
|
||||||
assert.Equal(t, "f", TruncateString("foobar", 1))
|
|
||||||
assert.Equal(t, "fo", TruncateString("foobar", 2))
|
|
||||||
assert.Equal(t, "foo", TruncateString("foobar", 3))
|
|
||||||
assert.Equal(t, "foob", TruncateString("foobar", 4))
|
|
||||||
assert.Equal(t, "fooba", TruncateString("foobar", 5))
|
|
||||||
assert.Equal(t, "foobar", TruncateString("foobar", 6))
|
|
||||||
assert.Equal(t, "foobar", TruncateString("foobar", 7))
|
|
||||||
assert.Equal(t, "测试文本", TruncateString("测试文本一二三四", 4))
|
|
||||||
assert.Equal(t, "测试文本一", TruncateString("测试文本一二三四", 5))
|
|
||||||
assert.Equal(t, "测试文本一二", TruncateString("测试文本一二三四", 6))
|
|
||||||
assert.Equal(t, "测试文本一二三", TruncateString("测试文本一二三四", 7))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStringsToInt64s(t *testing.T) {
|
func TestStringsToInt64s(t *testing.T) {
|
||||||
testSuccess := func(input []string, expected []int64) {
|
testSuccess := func(input []string, expected []int64) {
|
||||||
result, err := StringsToInt64s(input)
|
result, err := StringsToInt64s(input)
|
||||||
|
@ -109,7 +109,7 @@ func unmarshal(filename string, content []byte) (*api.IssueTemplate, error) {
|
|||||||
|
|
||||||
it.Content = string(content)
|
it.Content = string(content)
|
||||||
it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath!
|
it.Name = path.Base(it.FileName) // paths in Git are always '/' separated - do not use filepath!
|
||||||
it.About, _ = util.SplitStringAtByteN(it.Content, 80)
|
it.About = util.EllipsisDisplayString(it.Content, 80)
|
||||||
} else {
|
} else {
|
||||||
it.Content = templateBody
|
it.Content = templateBody
|
||||||
if it.About == "" {
|
if it.About == "" {
|
||||||
|
@ -173,7 +173,7 @@ func linkProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
|
|
||||||
uri := node.Data[m[0]:m[1]]
|
uri := node.Data[m[0]:m[1]]
|
||||||
remaining := node.Data[m[1]:]
|
remaining := node.Data[m[1]:]
|
||||||
if util.IsLikelySplitLeftPart(remaining) {
|
if util.IsLikelyEllipsisLeftPart(remaining) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
|
replaceContent(node, m[0], m[1], createLink(ctx, uri, uri, "" /*link*/))
|
||||||
|
@ -207,12 +207,12 @@ func TestRender_links(t *testing.T) {
|
|||||||
"ftps://gitea.com",
|
"ftps://gitea.com",
|
||||||
`<p>ftps://gitea.com</p>`)
|
`<p>ftps://gitea.com</p>`)
|
||||||
|
|
||||||
t.Run("LinkSplit", func(t *testing.T) {
|
t.Run("LinkEllipsis", func(t *testing.T) {
|
||||||
input, _ := util.SplitStringAtByteN("http://10.1.2.3", 12)
|
input := util.EllipsisDisplayString("http://10.1.2.3", 12)
|
||||||
assert.Equal(t, "http://10…", input)
|
assert.Equal(t, "http://10…", input)
|
||||||
test(input, "<p>http://10…</p>")
|
test(input, "<p>http://10…</p>")
|
||||||
|
|
||||||
input, _ = util.SplitStringAtByteN("http://10.1.2.3", 13)
|
input = util.EllipsisDisplayString("http://10.1.2.3", 13)
|
||||||
assert.Equal(t, "http://10.…", input)
|
assert.Equal(t, "http://10.…", input)
|
||||||
test(input, "<p>http://10.…</p>")
|
test(input, "<p>http://10.…</p>")
|
||||||
})
|
})
|
||||||
|
@ -11,9 +11,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
texttmpl "text/template"
|
texttmpl "text/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
||||||
@ -24,7 +24,7 @@ func mailSubjectTextFuncMap() texttmpl.FuncMap {
|
|||||||
"dict": dict,
|
"dict": dict,
|
||||||
"Eval": evalTokens,
|
"Eval": evalTokens,
|
||||||
|
|
||||||
"EllipsisString": base.EllipsisString,
|
"EllipsisString": util.EllipsisDisplayString,
|
||||||
"AppName": func() string {
|
"AppName": func() string {
|
||||||
return setting.AppName
|
return setting.AppName
|
||||||
},
|
},
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StringUtils struct{}
|
type StringUtils struct{}
|
||||||
@ -54,7 +54,7 @@ func (su *StringUtils) Cut(s, sep string) []any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (su *StringUtils) EllipsisString(s string, maxLength int) string {
|
func (su *StringUtils) EllipsisString(s string, maxLength int) string {
|
||||||
return base.EllipsisString(s, maxLength)
|
return util.EllipsisDisplayString(s, maxLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (su *StringUtils) ToUpper(s string) string {
|
func (su *StringUtils) ToUpper(s string) string {
|
||||||
|
@ -14,31 +14,92 @@ const (
|
|||||||
asciiEllipsis = "..."
|
asciiEllipsis = "..."
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsLikelySplitLeftPart(s string) bool {
|
func IsLikelyEllipsisLeftPart(s string) bool {
|
||||||
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
|
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SplitStringAtByteN splits a string at byte n accounting for rune boundaries. (Combining characters are not accounted for.)
|
// EllipsisDisplayString returns a truncated short string for display purpose.
|
||||||
func SplitStringAtByteN(input string, n int) (left, right string) {
|
// The length is the approximate number of ASCII-width in the string (CJK/emoji are 2-ASCII width)
|
||||||
if len(input) <= n {
|
// It appends "…" or "..." at the end of truncated string.
|
||||||
return input, ""
|
// It guarantees the length of the returned runes doesn't exceed the limit.
|
||||||
}
|
func EllipsisDisplayString(str string, limit int) string {
|
||||||
|
s, _, _, _ := ellipsisDisplayString(str, limit)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
if !utf8.ValidString(input) {
|
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
|
||||||
if n-3 < 0 {
|
func EllipsisDisplayStringX(str string, limit int) (left, right string) {
|
||||||
return input, ""
|
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit)
|
||||||
|
if truncated {
|
||||||
|
right = str[offset:]
|
||||||
|
r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
|
||||||
|
encounterInvalid = encounterInvalid || r == utf8.RuneError
|
||||||
|
ellipsis := utf8Ellipsis
|
||||||
|
if encounterInvalid {
|
||||||
|
ellipsis = asciiEllipsis
|
||||||
}
|
}
|
||||||
return input[:n-3] + asciiEllipsis, asciiEllipsis + input[n-3:]
|
right = ellipsis + right
|
||||||
|
}
|
||||||
|
return left, right
|
||||||
|
}
|
||||||
|
|
||||||
|
func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) {
|
||||||
|
if len(str) <= limit {
|
||||||
|
return str, len(str), false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
end := 0
|
// To future maintainers: this logic must guarantee that the length of the returned runes doesn't exceed the limit,
|
||||||
for end <= n-3 {
|
// because the returned string will also be used as database value. UTF-8 VARCHAR(10) could store 10 rune characters,
|
||||||
_, size := utf8.DecodeRuneInString(input[end:])
|
// So each rune must be countered as at least 1 width.
|
||||||
if end+size > n-3 {
|
// Even if there are some special Unicode characters (zero-width, combining, etc.), they should NEVER be counted as zero.
|
||||||
|
pos, used := 0, 0
|
||||||
|
for i, r := range str {
|
||||||
|
encounterInvalid = encounterInvalid || r == utf8.RuneError
|
||||||
|
pos = i
|
||||||
|
runeWidth := 1
|
||||||
|
if r >= 128 {
|
||||||
|
runeWidth = 2 // CJK/emoji chars are considered as 2-ASCII width
|
||||||
|
}
|
||||||
|
if used+runeWidth+3 > limit {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
end += size
|
used += runeWidth
|
||||||
|
offset += utf8.RuneLen(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
|
// if the remaining are fewer than 3 runes, then maybe we could add them, no need to ellipse
|
||||||
|
if len(str)-pos <= 12 {
|
||||||
|
var nextCnt, nextWidth int
|
||||||
|
for _, r := range str[pos:] {
|
||||||
|
if nextCnt >= 4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nextWidth++
|
||||||
|
if r >= 128 {
|
||||||
|
nextWidth++ // CJK/emoji chars are considered as 2-ASCII width
|
||||||
|
}
|
||||||
|
nextCnt++
|
||||||
|
}
|
||||||
|
if nextCnt <= 3 && used+nextWidth <= limit {
|
||||||
|
return str, len(str), false, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if limit < 3 {
|
||||||
|
// if the limit is so small, do not add ellipsis
|
||||||
|
return str[:offset], offset, true, false
|
||||||
|
}
|
||||||
|
ellipsis := utf8Ellipsis
|
||||||
|
if encounterInvalid {
|
||||||
|
ellipsis = asciiEllipsis
|
||||||
|
}
|
||||||
|
return str[:offset] + ellipsis, offset, true, encounterInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateRunes returns a truncated string with given rune limit,
|
||||||
|
// it returns input string if its rune length doesn't exceed the limit.
|
||||||
|
func TruncateRunes(str string, limit int) string {
|
||||||
|
if utf8.RuneCountInString(str) < limit {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return string([]rune(str)[:limit])
|
||||||
}
|
}
|
||||||
|
@ -4,43 +4,94 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSplitString(t *testing.T) {
|
func TestEllipsisString(t *testing.T) {
|
||||||
type testCase struct {
|
cases := []struct {
|
||||||
input string
|
limit int
|
||||||
n int
|
|
||||||
leftSub string
|
input, left, right string
|
||||||
ellipsis string
|
}{
|
||||||
|
{limit: 0, input: "abcde", left: "", right: "…abcde"},
|
||||||
|
{limit: 1, input: "abcde", left: "", right: "…abcde"},
|
||||||
|
{limit: 2, input: "abcde", left: "", right: "…abcde"},
|
||||||
|
{limit: 3, input: "abcde", left: "…", right: "…abcde"},
|
||||||
|
{limit: 4, input: "abcde", left: "a…", right: "…bcde"},
|
||||||
|
{limit: 5, input: "abcde", left: "abcde", right: ""},
|
||||||
|
{limit: 6, input: "abcde", left: "abcde", right: ""},
|
||||||
|
{limit: 7, input: "abcde", left: "abcde", right: ""},
|
||||||
|
|
||||||
|
// a CJK char or emoji is considered as 2-ASCII width, the ellipsis is 3-ASCII width
|
||||||
|
{limit: 0, input: "测试文本", left: "", right: "…测试文本"},
|
||||||
|
{limit: 1, input: "测试文本", left: "", right: "…测试文本"},
|
||||||
|
{limit: 2, input: "测试文本", left: "", right: "…测试文本"},
|
||||||
|
{limit: 3, input: "测试文本", left: "…", right: "…测试文本"},
|
||||||
|
{limit: 4, input: "测试文本", left: "…", right: "…测试文本"},
|
||||||
|
{limit: 5, input: "测试文本", left: "测…", right: "…试文本"},
|
||||||
|
{limit: 6, input: "测试文本", left: "测…", right: "…试文本"},
|
||||||
|
{limit: 7, input: "测试文本", left: "测试…", right: "…文本"},
|
||||||
|
{limit: 8, input: "测试文本", left: "测试文本", right: ""},
|
||||||
|
{limit: 9, input: "测试文本", left: "测试文本", right: ""},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(fmt.Sprintf("%s(%d)", c.input, c.limit), func(t *testing.T) {
|
||||||
|
left, right := EllipsisDisplayStringX(c.input, c.limit)
|
||||||
|
assert.Equal(t, c.left, left, "left")
|
||||||
|
assert.Equal(t, c.right, right, "right")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test := func(tc []*testCase, f func(input string, n int) (left, right string)) {
|
t.Run("LongInput", func(t *testing.T) {
|
||||||
for _, c := range tc {
|
left, right := EllipsisDisplayStringX(strings.Repeat("abc", 240), 90)
|
||||||
l, r := f(c.input, c.n)
|
assert.Equal(t, strings.Repeat("abc", 29)+"…", left)
|
||||||
if c.ellipsis != "" {
|
assert.Equal(t, "…"+strings.Repeat("abc", 211), right)
|
||||||
assert.Equal(t, c.leftSub+c.ellipsis, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub)
|
})
|
||||||
assert.Equal(t, c.ellipsis+c.input[len(c.leftSub):], r, "test split %s at %d, expected rightSub: %q", c.input, c.n, c.input[len(c.leftSub):])
|
|
||||||
} else {
|
t.Run("InvalidUtf8", func(t *testing.T) {
|
||||||
assert.Equal(t, c.leftSub, l, "test split %q at %d, expected leftSub: %q", c.input, c.n, c.leftSub)
|
invalidCases := []struct {
|
||||||
assert.Empty(t, r, "test split %q at %d, expected rightSub: %q", c.input, c.n, "")
|
limit int
|
||||||
}
|
left, right string
|
||||||
|
}{
|
||||||
|
{limit: 0, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 1, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 2, left: "", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 3, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 4, left: "...", right: "...\xef\x03\xfe\xef\x03\xfe"},
|
||||||
|
{limit: 5, left: "\xef\x03\xfe...", right: "...\xef\x03\xfe"},
|
||||||
|
{limit: 6, left: "\xef\x03\xfe\xef\x03\xfe", right: ""},
|
||||||
|
{limit: 7, left: "\xef\x03\xfe\xef\x03\xfe", right: ""},
|
||||||
}
|
}
|
||||||
}
|
for _, c := range invalidCases {
|
||||||
|
t.Run(fmt.Sprintf("%d", c.limit), func(t *testing.T) {
|
||||||
|
left, right := EllipsisDisplayStringX("\xef\x03\xfe\xef\x03\xfe", c.limit)
|
||||||
|
assert.Equal(t, c.left, left, "left")
|
||||||
|
assert.Equal(t, c.right, right, "right")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
tc := []*testCase{
|
t.Run("IsLikelyEllipsisLeftPart", func(t *testing.T) {
|
||||||
{"abc123xyz", 0, "", utf8Ellipsis},
|
assert.True(t, IsLikelyEllipsisLeftPart("abcde…"))
|
||||||
{"abc123xyz", 1, "", utf8Ellipsis},
|
assert.True(t, IsLikelyEllipsisLeftPart("abcde..."))
|
||||||
{"abc123xyz", 4, "a", utf8Ellipsis},
|
})
|
||||||
{"啊bc123xyz", 4, "", utf8Ellipsis},
|
}
|
||||||
{"啊bc123xyz", 6, "啊", utf8Ellipsis},
|
|
||||||
{"啊bc", 5, "啊bc", ""},
|
func TestTruncateRunes(t *testing.T) {
|
||||||
{"啊bc", 6, "啊bc", ""},
|
assert.Equal(t, "", TruncateRunes("", 0))
|
||||||
{"abc\xef\x03\xfe", 3, "", asciiEllipsis},
|
assert.Equal(t, "", TruncateRunes("", 1))
|
||||||
{"abc\xef\x03\xfe", 4, "a", asciiEllipsis},
|
|
||||||
{"\xef\x03", 1, "\xef\x03", ""},
|
assert.Equal(t, "", TruncateRunes("ab", 0))
|
||||||
}
|
assert.Equal(t, "a", TruncateRunes("ab", 1))
|
||||||
test(tc, SplitStringAtByteN)
|
assert.Equal(t, "ab", TruncateRunes("ab", 2))
|
||||||
|
assert.Equal(t, "ab", TruncateRunes("ab", 3))
|
||||||
|
|
||||||
|
assert.Equal(t, "", TruncateRunes("测试", 0))
|
||||||
|
assert.Equal(t, "测", TruncateRunes("测试", 1))
|
||||||
|
assert.Equal(t, "测试", TruncateRunes("测试", 2))
|
||||||
|
assert.Equal(t, "测试", TruncateRunes("测试", 3))
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ func (s *Service) Register(
|
|||||||
labels := req.Msg.Labels
|
labels := req.Msg.Labels
|
||||||
|
|
||||||
// create new runner
|
// create new runner
|
||||||
name, _ := util.SplitStringAtByteN(req.Msg.Name, 255)
|
name := util.EllipsisDisplayString(req.Msg.Name, 255)
|
||||||
runner := &actions_model.ActionRunner{
|
runner := &actions_model.ActionRunner{
|
||||||
UUID: gouuid.New().String(),
|
UUID: gouuid.New().String(),
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -664,7 +664,7 @@ func PrepareCompareDiff(
|
|||||||
}
|
}
|
||||||
if len(title) > 255 {
|
if len(title) > 255 {
|
||||||
var trailer string
|
var trailer string
|
||||||
title, trailer = util.SplitStringAtByteN(title, 255)
|
title, trailer = util.EllipsisDisplayStringX(title, 255)
|
||||||
if len(trailer) > 0 {
|
if len(trailer) > 0 {
|
||||||
if ctx.Data["content"] != nil {
|
if ctx.Data["content"] != nil {
|
||||||
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
|
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
|
||||||
|
@ -109,7 +109,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode
|
|||||||
IsPrivate: issue.Repo.IsPrivate,
|
IsPrivate: issue.Repo.IsPrivate,
|
||||||
}
|
}
|
||||||
|
|
||||||
truncatedContent, truncatedRight := util.SplitStringAtByteN(comment.Content, 200)
|
truncatedContent, truncatedRight := util.EllipsisDisplayStringX(comment.Content, 200)
|
||||||
if truncatedRight != "" {
|
if truncatedRight != "" {
|
||||||
// in case the content is in a Latin family language, we remove the last broken word.
|
// in case the content is in a Latin family language, we remove the last broken word.
|
||||||
lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
|
lastSpaceIdx := strings.LastIndex(truncatedContent, " ")
|
||||||
|
@ -10,9 +10,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/jaytaylor/html2text"
|
"github.com/jaytaylor/html2text"
|
||||||
gomail "github.com/wneessen/go-mail"
|
gomail "github.com/wneessen/go-mail"
|
||||||
@ -54,7 +54,7 @@ func (m *Message) ToMessage() *gomail.Msg {
|
|||||||
|
|
||||||
plainBody, err := html2text.FromString(m.Body)
|
plainBody, err := html2text.FromString(m.Body)
|
||||||
if err != nil || setting.MailService.SendAsPlainText {
|
if err != nil || setting.MailService.SendAsPlainText {
|
||||||
if strings.Contains(base.TruncateString(m.Body, 100), "<html>") {
|
if strings.Contains(util.TruncateRunes(m.Body, 100), "<html>") {
|
||||||
log.Warn("Mail contains HTML but configured to send as plain text.")
|
log.Warn("Mail contains HTML but configured to send as plain text.")
|
||||||
}
|
}
|
||||||
msg.SetBodyString("text/plain", plainBody)
|
msg.SetBodyString("text/plain", plainBody)
|
||||||
|
@ -19,7 +19,6 @@ import (
|
|||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
base_module "code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/label"
|
"code.gitea.io/gitea/modules/label"
|
||||||
@ -409,7 +408,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
|
|||||||
RepoID: g.repo.ID,
|
RepoID: g.repo.ID,
|
||||||
Repo: g.repo,
|
Repo: g.repo,
|
||||||
Index: issue.Number,
|
Index: issue.Number,
|
||||||
Title: base_module.TruncateString(issue.Title, 255),
|
Title: util.TruncateRunes(issue.Title, 255),
|
||||||
Content: issue.Content,
|
Content: issue.Content,
|
||||||
Ref: issue.Ref,
|
Ref: issue.Ref,
|
||||||
IsClosed: issue.State == "closed",
|
IsClosed: issue.State == "closed",
|
||||||
|
@ -179,7 +179,7 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rel.Title, _ = util.SplitStringAtByteN(rel.Title, 255)
|
rel.Title = util.EllipsisDisplayString(rel.Title, 255)
|
||||||
rel.LowerTagName = strings.ToLower(rel.TagName)
|
rel.LowerTagName = strings.ToLower(rel.TagName)
|
||||||
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
|
if err = db.Insert(gitRepo.Ctx, rel); err != nil {
|
||||||
return err
|
return err
|
||||||
|
Loading…
Reference in New Issue
Block a user