Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions internal/htmlutil/htmlutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ func walkNode(b *strings.Builder, n *html.Node, depth int) {
}
return
case "action-text-attachment":
filename := getAttr(n, "filename")
if filename != "" {
if doc := parseEmbeddedActionText(n, depth); doc != nil {
walkNode(b, doc, depth+1)
} else if filename := getAttr(n, "filename"); filename != "" {
fmt.Fprintf(b, "\n[%s]\n", filename)
}
return
Expand Down Expand Up @@ -162,6 +163,13 @@ func walkMessageSourceNode(b *strings.Builder, n *html.Node, depth int) {
case "hr":
writeMessageSourceBoundary(b)
return
case "action-text-attachment":
if doc := parseEmbeddedActionText(n, depth); doc != nil {
writeMessageSourceBoundary(b)
walkMessageSourceNode(b, doc, depth+1)
writeMessageSourceBoundary(b)
}
return
case "template":
if depth == 0 || n.Parent == nil || n.Parent.Data != "shadow-content" {
return
Expand Down Expand Up @@ -196,7 +204,7 @@ func elementProvidesMessageSourceText(n *html.Node) bool {
return false
}
switch n.Data {
case "script", "style", "noscript", "head", "action-text-attachment":
case "script", "style", "noscript", "head":
return false
case "dialog":
return hasAttr(n, "open")
Expand Down Expand Up @@ -295,6 +303,21 @@ func parseTrixAttachment(n *html.Node) *trixAttachment {
return &att
}

func embeddedActionTextContent(n *html.Node) string {
if getAttr(n, "filename") != "" {
return ""
}
return getAttr(n, "content")
}

func parseEmbeddedActionText(n *html.Node, depth int) *html.Node {
content := embeddedActionTextContent(n)
if content == "" {
return nil
}
return parseEmbeddedContent(content, depth)
}

func getAttr(n *html.Node, key string) string {
for _, a := range n.Attr {
if a.Key == key {
Expand Down Expand Up @@ -401,7 +424,9 @@ func findImages(n *html.Node, urls *[]string, depth int) {
}
}
case "action-text-attachment":
if imageURL := getAttr(n, "url"); isImageContentType(getAttr(n, "content-type")) && imageURL != "" {
if doc := parseEmbeddedActionText(n, depth); doc != nil {
findImages(doc, urls, depth+1)
} else if imageURL := getAttr(n, "url"); isImageContentType(getAttr(n, "content-type")) && imageURL != "" {
*urls = append(*urls, imageURL)
}
case "figure":
Expand Down
21 changes: 21 additions & 0 deletions internal/htmlutil/htmlutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ func TestMessageSourceTextIncludesEmbeddedEmailBody(t *testing.T) {
}
}

func TestMessageSourceTextIncludesEmbeddedActionTextAttachment(t *testing.T) {
html := `<action-text-attachment content-type="text/html" content="<p>External confirmation: BLUE-42</p>"></action-text-attachment>`
if got := strings.Join(strings.Fields(MessageSourceText(html)), " "); got != "External confirmation: BLUE-42" {
t.Errorf("MessageSourceText = %q", got)
}
}

func TestMessageSourceTextFailsClosedWhenHTMLExceedsParserDepth(t *testing.T) {
html := strings.Repeat("<div>", 1_000) + "not selectable" + strings.Repeat("</div>", 1_000)
if got := MessageSourceText(html); got != "" {
Expand Down Expand Up @@ -158,6 +165,13 @@ func TestToTextActionTextAttachment(t *testing.T) {
}
}

func TestToTextRendersEmbeddedActionTextAttachment(t *testing.T) {
got := ToText(`<action-text-attachment content-type="text/html" content="<p>Inside</p>"></action-text-attachment>`)
if got != "Inside" {
t.Errorf("ToText = %q, want %q", got, "Inside")
}
}

func TestToTextTrixFigure(t *testing.T) {
got := ToText(`<p>Before</p><figure data-trix-attachment='{"filename":"photo.png","url":"/img.png","contentType":"image/png"}'></figure><p>After</p>`)
if !strings.Contains(got, "[photo.png]") {
Expand Down Expand Up @@ -187,6 +201,13 @@ func TestExtractImageURLsInsideEmbeddedHTMLAttachment(t *testing.T) {
}
}

func TestExtractImageURLsInsideEmbeddedActionTextAttachment(t *testing.T) {
urls := ExtractImageURLs(`<action-text-attachment content-type="text/html" content="<img src=&quot;https://example.com/logo.png&quot;>"></action-text-attachment>`)
if len(urls) != 1 || urls[0] != "https://example.com/logo.png" {
t.Errorf("ExtractImageURLs = %v, want the image inside the embedded body", urls)
}
}

func TestExtractAttachmentsSkipsEmbeddedHTMLAttachment(t *testing.T) {
attachments := ExtractAttachments(`<figure data-trix-attachment='{"contentType":"text/html","content":"<p>body</p>"}'></figure>`)
if len(attachments) != 0 {
Expand Down
12 changes: 11 additions & 1 deletion internal/htmlutil/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (m *markdownizer) element(n *html.Node) {
case "figure":
m.figure(n)
case "action-text-attachment":
m.attachment(getAttr(n, "filename"), getAttr(n, "url"), getAttr(n, "content-type"))
m.renderActionTextAttachment(n)
default:
m.children(n)
}
Expand Down Expand Up @@ -490,6 +490,16 @@ func (m *markdownizer) image(n *html.Node) {
}
}

// renderActionTextAttachment renders unnamed inline content as embedded HTML while
// preserving named elements as file attachments.
func (m *markdownizer) renderActionTextAttachment(n *html.Node) {
if content := embeddedActionTextContent(n); content != "" {
m.embedded(content)
Comment thread
nitrnitr marked this conversation as resolved.
return
}
m.attachment(getAttr(n, "filename"), getAttr(n, "url"), getAttr(n, "content-type"))
}

func (m *markdownizer) figure(n *html.Node) {
attachment := parseTrixAttachment(n)
switch {
Expand Down
18 changes: 18 additions & 0 deletions internal/htmlutil/markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,24 @@ func TestToMarkdownEmbeddedHTMLAttachment(t *testing.T) {
}
}

// A nested action-text-attachment carries its HTML in a content attribute.
func TestToMarkdownEmbeddedActionTextAttachment(t *testing.T) {
got := toMarkdown(`<figure data-trix-attachment='{"contentType":"text/html","content":"<action-text-attachment content-type=\"text/html\" content=\"<p>Dear customer,</p><p>Please <a href=&amp;quot;https://example.com/sign&amp;quot;>sign the document</a>.</p>\"></action-text-attachment>"}'></figure>`)

want := "Dear customer,\n\nPlease [sign the document](https://example.com/sign)."
if got != want {
t.Errorf("ToMarkdown = %q, want %q", got, want)
}
}

func TestToMarkdownNamedActionTextAttachmentIgnoresInlineContent(t *testing.T) {
got := toMarkdown(`<action-text-attachment filename="report.pdf" content-type="application/pdf" content="<p>not the body</p>"></action-text-attachment>`)
want := "馃搸 report.pdf"
if got != want {
t.Errorf("ToMarkdown = %q, want %q", got, want)
}
}

func TestToMarkdownEmbeddedContentStopsRecursing(t *testing.T) {
nested := `<figure data-trix-attachment='{"contentType":"text/html","content":"<p>innermost</p>"}'></figure>`
for range embeddedContentDepthLimit + 2 {
Expand Down