@@ -22,6 +22,18 @@ var telegramHTMLBlockTags = map[string]struct{}{
2222 "pre" : {},
2323}
2424
25+ var telegramHTMLParagraphTags = map [string ]struct {}{
26+ "blockquote" : {},
27+ "div" : {},
28+ "h1" : {},
29+ "h2" : {},
30+ "h3" : {},
31+ "h4" : {},
32+ "h5" : {},
33+ "h6" : {},
34+ "p" : {},
35+ }
36+
2537func convertTelegramMessageFormat (content , inputFormat , outputFormat , markdownVersion string ) (string , error ) {
2638 input := normalizeNotifyFormat (inputFormat )
2739 output := normalizeNotifyFormat (outputFormat )
@@ -81,10 +93,11 @@ func telegramHTMLFromHTML(content string) string {
8193
8294func renderTelegramHTMLNode (out * strings.Builder , node * nethtml.Node , inCode bool ) {
8395 if node .Type == nethtml .TextNode {
84- if ! inCode && strings .TrimSpace (node .Data ) == "" && strings .ContainsAny (node .Data , "\r \n " ) {
96+ data := telegramTextNodeData (node )
97+ if ! inCode && strings .TrimSpace (data ) == "" && strings .ContainsAny (data , "\r \n " ) {
8598 return
8699 }
87- out .WriteString (html .EscapeString (node . Data ))
100+ out .WriteString (html .EscapeString (data ))
88101 return
89102 }
90103 if node .Type != nethtml .ElementNode {
@@ -93,8 +106,12 @@ func renderTelegramHTMLNode(out *strings.Builder, node *nethtml.Node, inCode boo
93106 }
94107
95108 tag := strings .ToLower (node .Data )
96- if _ , ok := telegramHTMLBlockTags [tag ]; ok && out .Len () > 0 {
97- ensureLineBreak (out )
109+ if out .Len () > 0 {
110+ if _ , ok := telegramHTMLParagraphTags [tag ]; ok {
111+ ensureParagraphBreak (out )
112+ } else if _ , ok := telegramHTMLBlockTags [tag ]; ok {
113+ ensureLineBreak (out )
114+ }
98115 }
99116
100117 switch tag {
@@ -150,7 +167,9 @@ func renderTelegramHTMLNode(out *strings.Builder, node *nethtml.Node, inCode boo
150167 renderTelegramHTMLChildren (out , node , inCode )
151168 }
152169
153- if _ , ok := telegramHTMLBlockTags [tag ]; ok {
170+ if _ , ok := telegramHTMLParagraphTags [tag ]; ok {
171+ ensureParagraphBreak (out )
172+ } else if _ , ok := telegramHTMLBlockTags [tag ]; ok {
154173 ensureLineBreak (out )
155174 }
156175}
@@ -177,14 +196,15 @@ func telegramMarkdownFromHTML(content, markdownMode string) string {
177196
178197func renderTelegramMarkdownNode (out * strings.Builder , node * nethtml.Node , markdownMode string , inCode bool ) {
179198 if node .Type == nethtml .TextNode {
199+ data := telegramTextNodeData (node )
180200 if inCode {
181- out .WriteString (escapeTelegramMarkdownCodeText (node . Data , markdownMode ))
201+ out .WriteString (escapeTelegramMarkdownCodeText (data , markdownMode ))
182202 return
183203 }
184- if strings .TrimSpace (node . Data ) == "" && strings .ContainsAny (node . Data , "\r \n " ) {
204+ if strings .TrimSpace (data ) == "" && strings .ContainsAny (data , "\r \n " ) {
185205 return
186206 }
187- out .WriteString (escapeTelegramMarkdownText (node . Data , markdownMode ))
207+ out .WriteString (escapeTelegramMarkdownText (data , markdownMode ))
188208 return
189209 }
190210 if node .Type != nethtml .ElementNode {
@@ -193,8 +213,12 @@ func renderTelegramMarkdownNode(out *strings.Builder, node *nethtml.Node, markdo
193213 }
194214
195215 tag := strings .ToLower (node .Data )
196- if _ , ok := telegramHTMLBlockTags [tag ]; ok && out .Len () > 0 {
197- ensureLineBreak (out )
216+ if out .Len () > 0 {
217+ if _ , ok := telegramHTMLParagraphTags [tag ]; ok {
218+ ensureParagraphBreak (out )
219+ } else if _ , ok := telegramHTMLBlockTags [tag ]; ok {
220+ ensureLineBreak (out )
221+ }
198222 }
199223
200224 switch tag {
@@ -254,7 +278,9 @@ func renderTelegramMarkdownNode(out *strings.Builder, node *nethtml.Node, markdo
254278 renderTelegramMarkdownChildren (out , node , markdownMode , inCode )
255279 }
256280
257- if _ , ok := telegramHTMLBlockTags [tag ]; ok {
281+ if _ , ok := telegramHTMLParagraphTags [tag ]; ok {
282+ ensureParagraphBreak (out )
283+ } else if _ , ok := telegramHTMLBlockTags [tag ]; ok {
258284 ensureLineBreak (out )
259285 }
260286}
@@ -332,6 +358,61 @@ func htmlAttr(node *nethtml.Node, key string) string {
332358 return ""
333359}
334360
361+ // telegramTextNodeData returns node text while trimming leading CR/LF only after
362+ // a preceding br element, as reported by previousElementTag, to compensate for
363+ // markdown parsers that emit a formatting newline after <br>. Other whitespace
364+ // is preserved in non-br contexts.
365+ func telegramTextNodeData (node * nethtml.Node ) string {
366+ if node == nil {
367+ return ""
368+ }
369+ data := node .Data
370+ if previousElementTag (node ) == "br" {
371+ data = strings .TrimLeft (data , "\r \n " )
372+ }
373+ return data
374+ }
375+
376+ // previousElementTag walks previous siblings to find the nearest non-empty
377+ // element node. It skips whitespace-only text nodes because HTML parsers often
378+ // expose formatting whitespace as siblings, returns the lowercase tag name of
379+ // the first previous element node found, and returns an empty string for nil
380+ // input or when only non-whitespace text/other nodes are found.
381+ func previousElementTag (node * nethtml.Node ) string {
382+ if node == nil {
383+ return ""
384+ }
385+ for sibling := node .PrevSibling ; sibling != nil ; sibling = sibling .PrevSibling {
386+ if sibling .Type == nethtml .TextNode && strings .TrimSpace (sibling .Data ) == "" {
387+ continue
388+ }
389+ if sibling .Type == nethtml .ElementNode {
390+ return strings .ToLower (sibling .Data )
391+ }
392+ return ""
393+ }
394+ return ""
395+ }
396+
397+ // ensureParagraphBreak mutates the provided *strings.Builder in-place so it
398+ // ends with one paragraph break. It no-ops for empty builders or existing
399+ // double-newline endings, appends one newline after a single trailing newline,
400+ // and appends two newlines otherwise, making it safe to call multiple times.
401+ func ensureParagraphBreak (out * strings.Builder ) {
402+ if out .Len () == 0 {
403+ return
404+ }
405+ value := out .String ()
406+ switch {
407+ case strings .HasSuffix (value , "\n \n " ):
408+ return
409+ case strings .HasSuffix (value , "\n " ):
410+ out .WriteByte ('\n' )
411+ default :
412+ out .WriteString ("\n \n " )
413+ }
414+ }
415+
335416func ensureLineBreak (out * strings.Builder ) {
336417 if out .Len () == 0 {
337418 return
0 commit comments