mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-09-22 18:43:33 +00:00
2e89586a11
Transcripts flattened links to "text ( url )", so the model copied that shape and drafts came back with the URL in brackets instead of a link. HTML to text for the LLM now writes "[text](url)", which Markdown2HTML turns back into an anchor.
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(HTML2TextMarkdownLinks(htmlContent))
|
|
}
|
|
pruneQuotedNodes(doc)
|
|
var b strings.Builder
|
|
if err := html.Render(&b, doc); err != nil {
|
|
return TrimPlainTextQuotes(HTML2TextMarkdownLinks(htmlContent))
|
|
}
|
|
return TrimPlainTextQuotes(HTML2TextMarkdownLinks(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
|
|
}
|