From 6bd837911431ef68d23de1bcbb75893edd39a32b Mon Sep 17 00:00:00 2001
From: Bohdan Horbeshko <bodqhrohro@gmail.com>
Date: Wed, 15 Nov 2023 19:38:45 -0500
Subject: Support blockquotes in formatter

---
 telegram/formatter/formatter.go      | 231 ++++++++++++++++++++++++++++-------
 telegram/formatter/formatter_test.go | 198 +++++++++++++++++++++++-------
 telegram/utils.go                    |  20 +--
 3 files changed, 349 insertions(+), 100 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,
+		},
+	}
+}
+
+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) (*Insertion, *Insertion) {
+// 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👙_🐧_🐖 <https://narayana.im/>" {
 		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 "<empty message>"
 	}
 
-	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 {
-- 
cgit v1.2.3