From 6bd837911431ef68d23de1bcbb75893edd39a32b Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Wed, 15 Nov 2023 19:38:45 -0500 Subject: [PATCH] Support blockquotes in formatter --- telegram/formatter/formatter.go | 233 +++++++++++++++++++++------ telegram/formatter/formatter_test.go | 198 +++++++++++++++++------ telegram/utils.go | 20 +-- 3 files changed, 350 insertions(+), 101 deletions(-) diff --git a/telegram/formatter/formatter.go b/telegram/formatter/formatter.go index 740fa09..9403198 100644 --- a/telegram/formatter/formatter.go +++ b/telegram/formatter/formatter.go @@ -8,15 +8,29 @@ import ( "github.com/zelenin/go-tdlib/client" ) -// Insertion is a piece of text in given position -type Insertion struct { +type insertionType int +const ( + insertionOpening insertionType = iota + insertionClosing + insertionUnpaired +) + +type MarkupModeType int +const ( + MarkupModeXEP0393 MarkupModeType = iota + MarkupModeMarkdown +) + +// insertion is a piece of text in given position +type insertion struct { Offset int32 Runes []rune + Type insertionType } -// InsertionStack contains the sequence of insertions +// insertionStack contains the sequence of insertions // from the start or from the end -type InsertionStack []*Insertion +type insertionStack []*insertion var boldRunesMarkdown = []rune("**") var boldRunesXEP0393 = []rune("*") @@ -26,11 +40,16 @@ var strikeRunesXEP0393 = []rune("~") var codeRunes = []rune("`") var preRuneStart = []rune("```\n") var preRuneEnd = []rune("\n```") +var quoteRunes = []rune("> ") +var newlineRunes = []rune("\n") +var doubleNewlineRunes = []rune("\n\n") +var newlineCode = rune(0x0000000a) +var bmpCeil = rune(0x0000ffff) // rebalance pumps all the values until the given offset to current stack (growing // from start) from given stack (growing from end); should be called // before any insertions to the current stack at the given offset -func (s InsertionStack) rebalance(s2 InsertionStack, offset int32) (InsertionStack, InsertionStack) { +func (s insertionStack) rebalance(s2 insertionStack, offset int32) (insertionStack, insertionStack) { for len(s2) > 0 && s2[len(s2)-1].Offset <= offset { s = append(s, s2[len(s2)-1]) s2 = s2[:len(s2)-1] @@ -41,10 +60,10 @@ func (s InsertionStack) rebalance(s2 InsertionStack, offset int32) (InsertionSta // NewIterator is a second order function that sequentially scans and returns // stack elements; starts returning nil when elements are ended -func (s InsertionStack) NewIterator() func() *Insertion { +func (s insertionStack) NewIterator() func() *insertion { i := -1 - return func() *Insertion { + return func() *insertion { i++ if i < len(s) { return s[i] @@ -120,21 +139,10 @@ func MergeAdjacentEntities(entities []*client.TextEntity) []*client.TextEntity { } // ClaspDirectives to the following span as required by XEP-0393 -func ClaspDirectives(text string, entities []*client.TextEntity) []*client.TextEntity { +func ClaspDirectives(doubledRunes []rune, entities []*client.TextEntity) []*client.TextEntity { alignedEntities := make([]*client.TextEntity, len(entities)) copy(alignedEntities, entities) - // transform the source text into a form with uniform runes and code points, - // by duplicating the Basic Multilingual Plane - doubledRunes := make([]rune, 0, len(text)*2) - - for _, cp := range text { - if cp > 0x0000ffff { - doubledRunes = append(doubledRunes, cp, cp) - } else { - doubledRunes = append(doubledRunes, cp) - } - } for i, entity := range alignedEntities { var dirty bool endOffset := entity.Offset + entity.Length @@ -167,18 +175,89 @@ func ClaspDirectives(text string, entities []*client.TextEntity) []*client.TextE return alignedEntities } -func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) (*Insertion, *Insertion) { - return &Insertion{ +func markupBraces(entity *client.TextEntity, lbrace, rbrace []rune) []*insertion { + return []*insertion{ + &insertion{ Offset: entity.Offset, Runes: lbrace, - }, &Insertion{ + Type: insertionOpening, + }, + &insertion{ Offset: entity.Offset + entity.Length, Runes: rbrace, - } + Type: insertionClosing, + }, + } } -// EntityToMarkdown generates the wrapping Markdown tags -func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) { +func quotePrependNewlines(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion { + if len(doubledRunes) == 0 { + return []*insertion{} + } + + startRunes := []rune("\n> ") + if entity.Offset == 0 || doubledRunes[entity.Offset-1] == newlineCode { + startRunes = quoteRunes + } + insertions := []*insertion{ + &insertion{ + Offset: entity.Offset, + Runes: startRunes, + Type: insertionUnpaired, + }, + } + + entityEnd := entity.Offset + entity.Length + entityEndInt := int(entityEnd) + + var wasNewline bool + // last newline is omitted, there's no need to put quote mark after the quote + for i := entity.Offset; i < entityEnd-1; i++ { + isNewline := doubledRunes[i] == newlineCode + if (isNewline && markupMode == MarkupModeXEP0393) || (wasNewline && isNewline && markupMode == MarkupModeMarkdown) { + insertions = append(insertions, &insertion{ + Offset: i+1, + Runes: quoteRunes, + Type: insertionUnpaired, + }) + } + + if isNewline { + wasNewline = true + } else { + wasNewline = false + } + } + + var rbrace []rune + if len(doubledRunes) > entityEndInt { + if doubledRunes[entityEnd] == newlineCode { + if markupMode == MarkupModeMarkdown && len(doubledRunes) > entityEndInt+1 && doubledRunes[entityEndInt+1] != newlineCode { + rbrace = newlineRunes + } + } else { + if markupMode == MarkupModeMarkdown { + rbrace = doubleNewlineRunes + } else { + rbrace = newlineRunes + } + } + } + insertions = append(insertions, &insertion{ + Offset: entityEnd, + Runes: rbrace, + Type: insertionClosing, + }) + + return insertions +} + +// entityToMarkdown generates the wrapping Markdown tags +func entityToMarkdown(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion { + if entity == nil || entity.Type == nil { + return []*insertion{} + } + switch entity.Type.TextEntityTypeType() { case client.TypeTextEntityTypeBold: return markupBraces(entity, boldRunesMarkdown, boldRunesMarkdown) @@ -193,18 +272,20 @@ func EntityToMarkdown(entity *client.TextEntity) (*Insertion, *Insertion) { case client.TypeTextEntityTypePreCode: preCode, _ := entity.Type.(*client.TextEntityTypePreCode) return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes) + case client.TypeTextEntityTypeBlockQuote: + return quotePrependNewlines(entity, doubledRunes, MarkupModeMarkdown) case client.TypeTextEntityTypeTextUrl: textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl) return markupBraces(entity, []rune("["), []rune("]("+textURL.Url+")")) } - return nil, nil + return []*insertion{} } -// EntityToXEP0393 generates the wrapping XEP-0393 tags -func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) { +// entityToXEP0393 generates the wrapping XEP-0393 tags +func entityToXEP0393(entity *client.TextEntity, doubledRunes []rune, markupMode MarkupModeType) []*insertion { if entity == nil || entity.Type == nil { - return nil, nil + return []*insertion{} } switch entity.Type.TextEntityTypeType() { @@ -221,29 +302,55 @@ func EntityToXEP0393(entity *client.TextEntity) (*Insertion, *Insertion) { case client.TypeTextEntityTypePreCode: preCode, _ := entity.Type.(*client.TextEntityTypePreCode) return markupBraces(entity, []rune("\n```"+preCode.Language+"\n"), codeRunes) + case client.TypeTextEntityTypeBlockQuote: + return quotePrependNewlines(entity, doubledRunes, MarkupModeXEP0393) case client.TypeTextEntityTypeTextUrl: textURL, _ := entity.Type.(*client.TextEntityTypeTextUrl) // non-standard, Pidgin-specific return markupBraces(entity, []rune{}, []rune(" <"+textURL.Url+">")) } - return nil, nil + return []*insertion{} +} + +// transform the source text into a form with uniform runes and code points, +// by duplicating anything beyond the Basic Multilingual Plane +func textToDoubledRunes(text string) []rune { + doubledRunes := make([]rune, 0, len(text)*2) + for _, cp := range text { + if cp > bmpCeil { + doubledRunes = append(doubledRunes, cp, cp) + } else { + doubledRunes = append(doubledRunes, cp) + } + } + + return doubledRunes } // Format traverses an already sorted list of entities and wraps the text in a markup func Format( sourceText string, entities []*client.TextEntity, - entityToMarkup func(*client.TextEntity) (*Insertion, *Insertion), + markupMode MarkupModeType, ) string { if len(entities) == 0 { return sourceText } - mergedEntities := SortEntities(ClaspDirectives(sourceText, MergeAdjacentEntities(SortEntities(entities)))) + var entityToMarkup func(*client.TextEntity, []rune, MarkupModeType) []*insertion + if markupMode == MarkupModeXEP0393 { + entityToMarkup = entityToXEP0393 + } else { + entityToMarkup = entityToMarkdown + } - startStack := make(InsertionStack, 0, len(sourceText)) - endStack := make(InsertionStack, 0, len(sourceText)) + doubledRunes := textToDoubledRunes(sourceText) + + mergedEntities := SortEntities(ClaspDirectives(doubledRunes, MergeAdjacentEntities(SortEntities(entities)))) + + startStack := make(insertionStack, 0, len(sourceText)) + endStack := make(insertionStack, 0, len(sourceText)) // convert entities to a stack of brackets var maxEndOffset int32 @@ -260,36 +367,70 @@ func Format( startStack, endStack = startStack.rebalance(endStack, entity.Offset) - startInsertion, endInsertion := entityToMarkup(entity) - if startInsertion != nil { - startStack = append(startStack, startInsertion) + insertions := entityToMarkup(entity, doubledRunes, markupMode) + if len(insertions) > 1 { + startStack = append(startStack, insertions[0:len(insertions)-1]...) } - if endInsertion != nil { - endStack = append(endStack, endInsertion) + if len(insertions) > 0 { + endStack = append(endStack, insertions[len(insertions)-1]) } } // flush the closing brackets that still remain in endStack startStack, endStack = startStack.rebalance(endStack, maxEndOffset) + // sort unpaired insertions + sort.SliceStable(startStack, func(i int, j int) bool { + ins1 := startStack[i] + ins2 := startStack[j] + if ins1.Type == insertionUnpaired && ins2.Type == insertionUnpaired { + return ins1.Offset < ins2.Offset + } + if ins1.Type == insertionUnpaired { + if ins1.Offset == ins2.Offset { + if ins2.Type == insertionOpening { // > ** + return true + } else if ins2.Type == insertionClosing { // **> + return false + } + } else { + return ins1.Offset < ins2.Offset + } + } + if ins2.Type == insertionUnpaired { + if ins1.Offset == ins2.Offset { + if ins1.Type == insertionOpening { // > ** + return false + } else if ins1.Type == insertionClosing { // **> + return true + } + } else { + return ins1.Offset < ins2.Offset + } + } + return false + }) // merge brackets into text markupRunes := make([]rune, 0, len(sourceText)) nextInsertion := startStack.NewIterator() insertion := nextInsertion() - var runeI int32 + var skipNext bool - for _, cp := range sourceText { - for insertion != nil && insertion.Offset <= runeI { + for i, cp := range doubledRunes { + if skipNext { + skipNext = false + continue + } + + for insertion != nil && int(insertion.Offset) <= i { markupRunes = append(markupRunes, insertion.Runes...) insertion = nextInsertion() } markupRunes = append(markupRunes, cp) // skip two UTF-16 code units (not points actually!) if needed - if cp > 0x0000ffff { - runeI += 2 - } else { - runeI++ + if cp > bmpCeil { + skipNext = true } } for insertion != nil { diff --git a/telegram/formatter/formatter_test.go b/telegram/formatter/formatter_test.go index e4bdd23..187d486 100644 --- a/telegram/formatter/formatter_test.go +++ b/telegram/formatter/formatter_test.go @@ -7,7 +7,7 @@ import ( ) func TestNoFormatting(t *testing.T) { - markup := Format("abc\ndef", []*client.TextEntity{}, EntityToMarkdown) + markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeMarkdown) if markup != "abc\ndef" { t.Errorf("No formatting expected, but: %v", markup) } @@ -20,7 +20,7 @@ func TestFormattingSimple(t *testing.T) { Length: 4, Type: &client.TextEntityTypeBold{}, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "👙**🐧🐖**" { t.Errorf("Wrong simple formatting: %v", markup) } @@ -40,7 +40,7 @@ func TestFormattingAdjacent(t *testing.T) { Url: "https://narayana.im/", }, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "a👙_🐧_[🐖](https://narayana.im/)" { t.Errorf("Wrong adjacent formatting: %v", markup) } @@ -63,18 +63,18 @@ func TestFormattingAdjacentAndNested(t *testing.T) { Length: 2, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "```\n**👙**🐧\n```_🐖_" { t.Errorf("Wrong adjacent&nested formatting: %v", markup) } } func TestRebalanceTwoZero(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{} + s2 := insertionStack{} s1, s2 = s1.rebalance(s2, 7) if !(len(s1) == 2 && len(s2) == 0 && s1[0].Offset == 7 && s1[1].Offset == 8) { t.Errorf("Wrong rebalance 2–0: %#v %#v", s1, s2) @@ -82,13 +82,13 @@ func TestRebalanceTwoZero(t *testing.T) { } func TestRebalanceNeeded(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{ - &Insertion{Offset: 10}, - &Insertion{Offset: 9}, + s2 := insertionStack{ + &insertion{Offset: 10}, + &insertion{Offset: 9}, } s1, s2 = s1.rebalance(s2, 9) if !(len(s1) == 3 && len(s2) == 1 && @@ -99,13 +99,13 @@ func TestRebalanceNeeded(t *testing.T) { } func TestRebalanceNotNeeded(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{ - &Insertion{Offset: 10}, - &Insertion{Offset: 9}, + s2 := insertionStack{ + &insertion{Offset: 10}, + &insertion{Offset: 9}, } s1, s2 = s1.rebalance(s2, 8) if !(len(s1) == 2 && len(s2) == 2 && @@ -116,13 +116,13 @@ func TestRebalanceNotNeeded(t *testing.T) { } func TestRebalanceLate(t *testing.T) { - s1 := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s1 := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } - s2 := InsertionStack{ - &Insertion{Offset: 10}, - &Insertion{Offset: 9}, + s2 := insertionStack{ + &insertion{Offset: 10}, + &insertion{Offset: 9}, } s1, s2 = s1.rebalance(s2, 10) if !(len(s1) == 4 && len(s2) == 0 && @@ -133,7 +133,7 @@ func TestRebalanceLate(t *testing.T) { } func TestIteratorEmpty(t *testing.T) { - s := InsertionStack{} + s := insertionStack{} g := s.NewIterator() v := g() if v != nil { @@ -142,9 +142,9 @@ func TestIteratorEmpty(t *testing.T) { } func TestIterator(t *testing.T) { - s := InsertionStack{ - &Insertion{Offset: 7}, - &Insertion{Offset: 8}, + s := insertionStack{ + &insertion{Offset: 7}, + &insertion{Offset: 8}, } g := s.NewIterator() v := g() @@ -208,7 +208,7 @@ func TestSortEmpty(t *testing.T) { } func TestNoFormattingXEP0393(t *testing.T) { - markup := Format("abc\ndef", []*client.TextEntity{}, EntityToXEP0393) + markup := Format("abc\ndef", []*client.TextEntity{}, MarkupModeXEP0393) if markup != "abc\ndef" { t.Errorf("No formatting expected, but: %v", markup) } @@ -221,7 +221,7 @@ func TestFormattingXEP0393Simple(t *testing.T) { Length: 4, Type: &client.TextEntityTypeBold{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "👙*🐧🐖*" { t.Errorf("Wrong simple formatting: %v", markup) } @@ -241,7 +241,7 @@ func TestFormattingXEP0393Adjacent(t *testing.T) { Url: "https://narayana.im/", }, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "a👙_🐧_🐖 " { t.Errorf("Wrong adjacent formatting: %v", markup) } @@ -264,7 +264,7 @@ func TestFormattingXEP0393AdjacentAndNested(t *testing.T) { Length: 2, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "```\n*👙*🐧\n```_🐖_" { t.Errorf("Wrong adjacent&nested formatting: %v", markup) } @@ -287,7 +287,7 @@ func TestFormattingXEP0393AdjacentItalicBoldItalic(t *testing.T) { Length: 69, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "_раса двуногих крысолюдей, *которую так редко замечают, что многие отрицают само их существование*_" { t.Errorf("Wrong adjacent italic/bold-italic formatting: %v", markup) } @@ -315,7 +315,7 @@ func TestFormattingXEP0393MultipleAdjacent(t *testing.T) { Length: 1, Type: &client.TextEntityTypeItalic{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "a*bcd*_e_" { t.Errorf("Wrong multiple adjacent formatting: %v", markup) } @@ -343,7 +343,7 @@ func TestFormattingXEP0393Intersecting(t *testing.T) { Length: 1, Type: &client.TextEntityTypeBold{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "a*b*_*cd*e_" { t.Errorf("Wrong intersecting formatting: %v", markup) } @@ -361,7 +361,7 @@ func TestFormattingXEP0393InlineCode(t *testing.T) { Length: 25, Type: &client.TextEntityTypePre{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "Is `Gajim` a thing?\n\n```\necho 'Hello'\necho 'world'\n```\n\nhruck(" { t.Errorf("Wrong intersecting formatting: %v", markup) } @@ -374,7 +374,7 @@ func TestFormattingMarkdownStrikethrough(t *testing.T) { Length: 3, Type: &client.TextEntityTypeStrikethrough{}, }, - }, EntityToMarkdown) + }, MarkupModeMarkdown) if markup != "Everyone ~~dis~~likes cake." { t.Errorf("Wrong strikethrough formatting: %v", markup) } @@ -387,14 +387,14 @@ func TestFormattingXEP0393Strikethrough(t *testing.T) { Length: 3, Type: &client.TextEntityTypeStrikethrough{}, }, - }, EntityToXEP0393) + }, MarkupModeXEP0393) if markup != "Everyone ~dis~likes cake." { t.Errorf("Wrong strikethrough formatting: %v", markup) } } func TestClaspLeft(t *testing.T) { - text := "a b c" + text := textToDoubledRunes("a b c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -409,7 +409,7 @@ func TestClaspLeft(t *testing.T) { } func TestClaspBoth(t *testing.T) { - text := "a b c" + text := textToDoubledRunes("a b c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -424,7 +424,7 @@ func TestClaspBoth(t *testing.T) { } func TestClaspNotNeeded(t *testing.T) { - text := " abc " + text := textToDoubledRunes(" abc ") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -439,7 +439,7 @@ func TestClaspNotNeeded(t *testing.T) { } func TestClaspNested(t *testing.T) { - text := "a b c" + text := textToDoubledRunes("a b c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -459,7 +459,7 @@ func TestClaspNested(t *testing.T) { } func TestClaspEmoji(t *testing.T) { - text := "a 🐖 c" + text := textToDoubledRunes("a 🐖 c") entities := []*client.TextEntity{ &client.TextEntity{ Offset: 1, @@ -472,3 +472,111 @@ func TestClaspEmoji(t *testing.T) { t.Errorf("Wrong claspemoji: %#v", entities) } } + +func TestNoNewlineBlockquoteXEP0393(t *testing.T) { + markup := Format("yes it can i think", []*client.TextEntity{ + &client.TextEntity{ + Offset: 4, + Length: 6, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeXEP0393) + if markup != "yes \n> it can\n i think" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestNoNewlineBlockquoteMarkdown(t *testing.T) { + markup := Format("yes it can i think", []*client.TextEntity{ + &client.TextEntity{ + Offset: 4, + Length: 6, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeMarkdown) + if markup != "yes \n> it can\n\n i think" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMultilineBlockquoteXEP0393(t *testing.T) { + markup := Format("hruck\npuck\n\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 17, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeXEP0393) + if markup != "> hruck\n> puck\n> \n> shuck\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMultilineBlockquoteMarkdown(t *testing.T) { + markup := Format("hruck\npuck\n\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 17, + Type: &client.TextEntityTypeBlockQuote{}, + }, + }, MarkupModeMarkdown) + if markup != "> hruck\npuck\n\n> shuck\n\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMixedBlockquoteXEP0393(t *testing.T) { + markup := Format("hruck\npuck\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBlockQuote{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBold{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 10, + Type: &client.TextEntityTypeItalic{}, + }, + &client.TextEntity{ + Offset: 7, + Length: 2, + Type: &client.TextEntityTypeStrikethrough{}, + }, + }, MarkupModeXEP0393) + if markup != "> *_hruck\n> p~uc~k_\n> shuck*\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} + +func TestMixedBlockquoteMarkdown(t *testing.T) { + markup := Format("hruck\npuck\nshuck\ntext", []*client.TextEntity{ + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBlockQuote{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 16, + Type: &client.TextEntityTypeBold{}, + }, + &client.TextEntity{ + Offset: 0, + Length: 10, + Type: &client.TextEntityTypeItalic{}, + }, + &client.TextEntity{ + Offset: 7, + Length: 2, + Type: &client.TextEntityTypeStrikethrough{}, + }, + }, MarkupModeMarkdown) + if markup != "> **_hruck\np~~uc~~k_\nshuck**\n\ntext" { + t.Errorf("Wrong blockquote formatting: %v", markup) + } +} diff --git a/telegram/utils.go b/telegram/utils.go index b22f156..9370839 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -593,7 +593,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return "" } - markupFunction := c.getFormatter() + markupMode := c.getFormatter() switch message.Content.MessageContentType() { case client.TypeMessageSticker: sticker, _ := message.Content.(*client.MessageSticker) @@ -646,7 +646,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( photo.Caption.Text, photo.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageAudio: @@ -657,7 +657,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( audio.Caption.Text, audio.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageVideo: @@ -668,7 +668,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( video.Caption.Text, video.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageDocument: @@ -679,7 +679,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( document.Caption.Text, document.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageText: @@ -690,7 +690,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( text.Text.Text, text.Text.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageVoiceNote: @@ -701,7 +701,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( voice.Caption.Text, voice.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageVideoNote: @@ -714,7 +714,7 @@ func (c *Client) messageToText(message *client.Message, preview bool) string { return formatter.Format( animation.Caption.Text, animation.Caption.Entities, - markupFunction, + markupMode, ) } case client.TypeMessageContact: @@ -1500,8 +1500,8 @@ func (c *Client) hasLastMessageHashChanged(chatId, messageId int64, content clie return !ok || oldHash != newHash } -func (c *Client) getFormatter() func(*client.TextEntity) (*formatter.Insertion, *formatter.Insertion) { - return formatter.EntityToXEP0393 +func (c *Client) getFormatter() formatter.MarkupModeType { + return formatter.MarkupModeXEP0393 } func (c *Client) usernamesToString(usernames []string) string {