diff --git a/backend/app/notify/prune_html.go b/backend/app/notify/prune_html.go new file mode 100644 index 00000000..b2ebb7fa --- /dev/null +++ b/backend/app/notify/prune_html.go @@ -0,0 +1,77 @@ +package notify + +import ( + "fmt" + "strings" + + "golang.org/x/net/html" +) + +// pruneHTML prunes string keeping HTML closing tags. +// maxLength applies to visible text only, not HTML tags. +func pruneHTML(htmlText string, maxLength int) string { + var result strings.Builder + var endTokens []string + visibleLen := 0 + + suffix := "..." + suffixLen := len(suffix) + + tokenizer := html.NewTokenizer(strings.NewReader(htmlText)) + for { + if tokenizer.Next() == html.ErrorToken { + return result.String() + } + token := tokenizer.Token() + + switch token.Type { + case html.CommentToken, html.DoctypeToken: + continue + + case html.StartTagToken: + endTokens = append([]string{fmt.Sprintf("%s>", token.Data)}, endTokens...) + result.WriteString(token.String()) + + case html.EndTagToken: + if len(endTokens) > 0 { + endTokens = endTokens[1:] + } + result.WriteString(token.String()) + + case html.SelfClosingTagToken: + result.WriteString(token.String()) + + case html.TextToken: + text := token.String() + if visibleLen+len(text)+suffixLen > maxLength { + remaining := maxLength - visibleLen - suffixLen + text = pruneStringToWord(text, remaining) + result.WriteString(text) + result.WriteString(suffix) + for _, endTag := range endTokens { + result.WriteString(endTag) + } + return result.String() + } + visibleLen += len(text) + result.WriteString(text) + } + } +} + +// pruneStringToWord prunes string to specified length respecting word boundaries +func pruneStringToWord(text string, maxLength int) string { + if maxLength <= 0 { + return "" + } + if len(text) <= maxLength { + return text + } + + // find last space at or before maxLength to cut at word boundary + lastSpace := strings.LastIndex(text[:maxLength+1], " ") + if lastSpace <= 0 { + return "" + } + return text[:lastSpace] +} diff --git a/backend/app/notify/prune_html_test.go b/backend/app/notify/prune_html_test.go new file mode 100644 index 00000000..4ec2f6ab --- /dev/null +++ b/backend/app/notify/prune_html_test.go @@ -0,0 +1,47 @@ +package notify + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPruneHTML(t *testing.T) { + tests := []struct { + name string + html string + maxLength int + expected string + }{ + {"within limit", "
Hello
", 20, "Hello
"}, + {"exceeds limit", "Hello world, this is a long text
", 15, "Hello world,...
"}, + {"nested tags", "Hello world
More text
Hello world
More...
Hello
", 20, "Hello
"}, + {"self-closing tag", "Hello
World
Hello
...
elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ }
+ res = tb.buildMessage(Request{Comment: c})
+ assert.Equal(t, `
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut...`, res)
}
func TestTelegram_SendVerification(t *testing.T) {