mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-12 16:08:01 +00:00
72c3f29830
AI grammar fix rewrote the draft as plain text, so links in the reply box were dropped. It now sends the editor HTML and asks the model to keep tags. Conversation transcripts, the AI agent history and copilot context also stripped link URLs when converting HTML to text, so the model never saw them. They now keep links as "text ( url )".
120 lines
3.0 KiB
Go
120 lines
3.0 KiB
Go
package stringutil
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
// Marker sets mirror the frontend's .hide-quoted-text selectors in
|
|
// frontend/shared-ui/assets/styles/main.scss - keep the two in sync.
|
|
var (
|
|
// Outlook/Hotmail markers: the quoted history follows as trailing siblings, so the marker AND
|
|
// everything after it at the same level is quote.
|
|
quoteMarkerIDs = map[string]bool{"divRplyFwdMsg": true, "appendonsend": true, "OLK_SRC_BODY_SECTION": true}
|
|
quoteMarkerClasses = []string{"OutlookMessageHeader"}
|
|
// Containers that wrap the whole quote.
|
|
quoteContainerClasses = []string{"yahoo_quoted", "gmail_quote_container", "protonmail_quote"}
|
|
|
|
attributionRegex = regexp.MustCompile(`(?i)^on\b.{0,200}\bwrote:$`)
|
|
originalMessageRegex = regexp.MustCompile(`(?i)^-{2,}\s*original message\s*-{2,}$`)
|
|
)
|
|
|
|
// HTML2TextNoQuotes converts email HTML to plain text with quoted reply chains removed, returning "" when the message was quote-only.
|
|
func HTML2TextNoQuotes(htmlContent string) string {
|
|
doc, err := html.Parse(strings.NewReader(htmlContent))
|
|
if err != nil {
|
|
return TrimPlainTextQuotes(HTML2TextWithLinks(htmlContent))
|
|
}
|
|
pruneQuotedNodes(doc)
|
|
var b strings.Builder
|
|
if err := html.Render(&b, doc); err != nil {
|
|
return TrimPlainTextQuotes(HTML2TextWithLinks(htmlContent))
|
|
}
|
|
return TrimPlainTextQuotes(HTML2TextWithLinks(b.String()))
|
|
}
|
|
|
|
// TrimPlainTextQuotes strips a trailing quoted-reply block (">" lines, "On ... wrote:" and "Original Message" markers) from plain text.
|
|
func TrimPlainTextQuotes(text string) string {
|
|
lines := strings.Split(text, "\n")
|
|
end := len(lines)
|
|
for end > 0 {
|
|
l := strings.TrimSpace(lines[end-1])
|
|
if l == "" || strings.HasPrefix(l, ">") || attributionRegex.MatchString(l) || originalMessageRegex.MatchString(l) {
|
|
end--
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return strings.TrimSpace(strings.Join(lines[:end], "\n"))
|
|
}
|
|
|
|
func pruneQuotedNodes(n *html.Node) {
|
|
var next *html.Node
|
|
for child := n.FirstChild; child != nil; child = next {
|
|
next = child.NextSibling
|
|
if isQuoteNode(child) {
|
|
n.RemoveChild(child)
|
|
continue
|
|
}
|
|
if isQuoteMarkerNode(child) {
|
|
for sib := child; sib != nil; {
|
|
after := sib.NextSibling
|
|
n.RemoveChild(sib)
|
|
sib = after
|
|
}
|
|
return
|
|
}
|
|
pruneQuotedNodes(child)
|
|
}
|
|
}
|
|
|
|
func isQuoteNode(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
if n.Data == "blockquote" {
|
|
return true
|
|
}
|
|
for _, c := range quoteContainerClasses {
|
|
if hasClass(n, c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isQuoteMarkerNode(n *html.Node) bool {
|
|
if n.Type != html.ElementNode {
|
|
return false
|
|
}
|
|
if quoteMarkerIDs[attrValue(n, "id")] {
|
|
return true
|
|
}
|
|
for _, c := range quoteMarkerClasses {
|
|
if hasClass(n, c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func attrValue(n *html.Node, key string) string {
|
|
for _, a := range n.Attr {
|
|
if a.Key == key {
|
|
return a.Val
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func hasClass(n *html.Node, want string) bool {
|
|
for _, class := range strings.Fields(attrValue(n, "class")) {
|
|
if class == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|