Init
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type ContentChunkType string
|
||||
|
||||
const (
|
||||
ContentChunkTypeText ContentChunkType = "text"
|
||||
ContentChunkTypeImage ContentChunkType = "image"
|
||||
)
|
||||
|
||||
type ContentChunk struct {
|
||||
Type ContentChunkType //
|
||||
Content string // text content or image url
|
||||
}
|
||||
|
||||
func SplitHTMLContentChunks(content string) ([]ContentChunk, error) {
|
||||
root, err := html.Parse(strings.NewReader("<div>" + strings.TrimSpace(content) + "</div>"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
chunks []ContentChunk
|
||||
buffer strings.Builder
|
||||
)
|
||||
flushText := func() {
|
||||
text := normalizeContentChunkText(buffer.String())
|
||||
buffer.Reset()
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
chunks = append(chunks, ContentChunk{
|
||||
Type: ContentChunkTypeText,
|
||||
Content: text,
|
||||
})
|
||||
}
|
||||
|
||||
var walk func(*html.Node)
|
||||
walk = func(node *html.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
switch node.Type {
|
||||
case html.TextNode:
|
||||
buffer.WriteString(node.Data)
|
||||
case html.ElementNode:
|
||||
switch node.Data {
|
||||
case "br":
|
||||
buffer.WriteString("\n")
|
||||
case "p", "div", "li", "blockquote":
|
||||
buffer.WriteString("\n")
|
||||
case "img":
|
||||
flushText()
|
||||
src := strings.TrimSpace(findContentChunkHTMLAttr(node, "src"))
|
||||
if src == "" {
|
||||
return
|
||||
}
|
||||
chunks = append(chunks, ContentChunk{
|
||||
Type: ContentChunkTypeImage,
|
||||
Content: src,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
walk(child)
|
||||
}
|
||||
if node.Type == html.ElementNode {
|
||||
switch node.Data {
|
||||
case "p", "div", "li", "blockquote":
|
||||
buffer.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
flushText()
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
func normalizeContentChunkText(value string) string {
|
||||
value = strings.ReplaceAll(value, "\u00a0", " ")
|
||||
lines := strings.Split(value, "\n")
|
||||
normalizedLines := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
line = strings.Join(strings.Fields(strings.TrimSpace(line)), " ")
|
||||
if line == "" {
|
||||
if len(normalizedLines) > 0 && normalizedLines[len(normalizedLines)-1] != "" {
|
||||
normalizedLines = append(normalizedLines, "")
|
||||
}
|
||||
continue
|
||||
}
|
||||
normalizedLines = append(normalizedLines, line)
|
||||
}
|
||||
for len(normalizedLines) > 0 && normalizedLines[0] == "" {
|
||||
normalizedLines = normalizedLines[1:]
|
||||
}
|
||||
for len(normalizedLines) > 0 && normalizedLines[len(normalizedLines)-1] == "" {
|
||||
normalizedLines = normalizedLines[:len(normalizedLines)-1]
|
||||
}
|
||||
return strings.Join(normalizedLines, "\n")
|
||||
}
|
||||
|
||||
func findContentChunkHTMLAttr(node *html.Node, key string) string {
|
||||
for _, attr := range node.Attr {
|
||||
if strings.EqualFold(attr.Key, key) {
|
||||
return attr.Val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitHTMLContentChunks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
want []ContentChunk
|
||||
}{
|
||||
{
|
||||
name: "plain text paragraph",
|
||||
content: "<p>你好,微信用户</p>",
|
||||
want: []ContentChunk{
|
||||
{Type: ContentChunkTypeText, Content: "你好,微信用户"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single image",
|
||||
content: `<p><img src="https://example.com/a.png" alt="a"></p>`,
|
||||
want: []ContentChunk{
|
||||
{Type: ContentChunkTypeImage, Content: "https://example.com/a.png"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed text and images keep order",
|
||||
content: `<p>第一段</p><p><img src="https://example.com/1.png"></p><p>第二段<img src="https://example.com/2.png">第三段</p>`,
|
||||
want: []ContentChunk{
|
||||
{Type: ContentChunkTypeText, Content: "第一段"},
|
||||
{Type: ContentChunkTypeImage, Content: "https://example.com/1.png"},
|
||||
{Type: ContentChunkTypeText, Content: "第二段"},
|
||||
{Type: ContentChunkTypeImage, Content: "https://example.com/2.png"},
|
||||
{Type: ContentChunkTypeText, Content: "第三段"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "normalize blank lines and spaces",
|
||||
content: "<div> 第一行 <br><br> 第二行 </div>",
|
||||
want: []ContentChunk{
|
||||
{Type: ContentChunkTypeText, Content: "第一行\n\n第二行"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore image without src",
|
||||
content: `<p>前文</p><img alt="missing-src"><p>后文</p>`,
|
||||
want: []ContentChunk{
|
||||
{Type: ContentChunkTypeText, Content: "前文"},
|
||||
{Type: ContentChunkTypeText, Content: "后文"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty html returns empty chunks",
|
||||
content: "<p><br></p>",
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := SplitHTMLContentChunks(tt.content)
|
||||
if err != nil {
|
||||
t.Fatalf("SplitHTMLContentChunks() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("SplitHTMLContentChunks() = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cs-agent/internal/models"
|
||||
"cs-agent/internal/pkg/dto"
|
||||
"cs-agent/internal/pkg/errorsx"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func NormalizeNullableString(value *string) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
v := strings.TrimSpace(*value)
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
func BuildAuditFields(operator *dto.AuthPrincipal) models.AuditFields {
|
||||
now := time.Now()
|
||||
fields := models.AuditFields{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if operator != nil {
|
||||
fields.CreateUserID = operator.UserID
|
||||
fields.CreateUserName = operator.Username
|
||||
fields.UpdateUserID = operator.UserID
|
||||
fields.UpdateUserName = operator.Username
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func FormatTimePtr(t *time.Time) string {
|
||||
if t == nil || t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format(time.DateTime)
|
||||
}
|
||||
|
||||
func FormatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format(time.DateTime)
|
||||
}
|
||||
|
||||
func GenerateRandomPassword(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", errorsx.InvalidParam("密码长度不合法")
|
||||
}
|
||||
|
||||
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
|
||||
buf := make([]byte, length)
|
||||
random := make([]byte, length)
|
||||
if _, err := rand.Read(random); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := range buf {
|
||||
buf[i] = charset[int(random[i])%len(charset)]
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func JoinInt64s(values []int64) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
parts = append(parts, cast.ToString(value))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func SplitInt64s(raw string) []int64 {
|
||||
parts := strings.Split(strings.TrimSpace(raw), ",")
|
||||
ret := make([]int64, 0, len(parts))
|
||||
seen := make(map[int64]struct{})
|
||||
for _, part := range parts {
|
||||
value := strings.TrimSpace(part)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[id]; exists {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
ret = append(ret, id)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
Reference in New Issue
Block a user