diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt index b056f26c2f..dde5a1fdb7 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt @@ -22,24 +22,25 @@ internal data class RawMessageEntity( val priority by lazy { when (type) { // Types with potential subsources should have priority - "mention" -> 0 - "hashtag" -> 0 - "cashtag" -> 0 - "email" -> 0 - "phone_number" -> 0 - "bold" -> 0 - "italic" -> 0 - "text_mention" -> 0 - "strikethrough" -> 0 - "underline" -> 0 - "spoiler" -> 0 - "custom_emoji" -> 0 - "bot_command" -> 1 - "url" -> 1 - "code" -> 1 - "pre" -> 1 - "text_link" -> 1 - else -> 1 + "mention" -> 1 + "hashtag" -> 1 + "cashtag" -> 1 + "email" -> 1 + "phone_number" -> 1 + "bold" -> 1 + "blockquote" -> 0 + "italic" -> 1 + "text_mention" -> 1 + "strikethrough" -> 1 + "underline" -> 1 + "spoiler" -> 1 + "custom_emoji" -> 1 + "bot_command" -> 2 + "url" -> 2 + "code" -> 2 + "pre" -> 2 + "text_link" -> 2 + else -> 2 } } } @@ -61,6 +62,7 @@ internal fun RawMessageEntity.asTextSource( "email" -> EMailTextSource(sourceSubstring, subPartsWithRegulars) "phone_number" -> PhoneNumberTextSource(sourceSubstring, subPartsWithRegulars) "bold" -> BoldTextSource(sourceSubstring, subPartsWithRegulars) + "blockquote" -> BlockquoteTextSource(sourceSubstring, subPartsWithRegulars) "italic" -> ItalicTextSource(sourceSubstring, subPartsWithRegulars) "code" -> CodeTextSource(sourceSubstring) "pre" -> PreTextSource(sourceSubstring, language) @@ -180,6 +182,7 @@ internal fun TextSource.toRawMessageEntities(offset: Int = 0): List RawMessageEntity("email", offset, length) is PhoneNumberTextSource -> RawMessageEntity("phone_number", offset, length) is BoldTextSource -> RawMessageEntity("bold", offset, length) + is BlockquoteTextSource -> RawMessageEntity("blockquote", offset, length) is ItalicTextSource -> RawMessageEntity("italic", offset, length) is CodeTextSource -> RawMessageEntity("code", offset, length) is PreTextSource -> RawMessageEntity("pre", offset, length, language = language) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BlockquoteTextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BlockquoteTextSource.kt new file mode 100644 index 0000000000..dca58afbc9 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BlockquoteTextSource.kt @@ -0,0 +1,26 @@ +package dev.inmo.tgbotapi.types.message.textsources + +import dev.inmo.tgbotapi.utils.RiskFeature +import dev.inmo.tgbotapi.utils.extensions.makeString +import dev.inmo.tgbotapi.utils.internal.* +import kotlinx.serialization.Serializable + +/** + * @see blockquote + */ +@Serializable +data class BlockquoteTextSource @RiskFeature(DirectInvocationOfTextSourceConstructor) constructor ( + override val source: String, + override val subsources: TextSourcesList +) : MultilevelTextSource { + override val markdown: String by lazy { source.blockquoteMarkdown() } + override val markdownV2: String by lazy { blockquoteMarkdownV2() } + override val html: String by lazy { blockquoteHTML() } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun blockquote(parts: TextSourcesList) = BlockquoteTextSource(parts.makeString(), parts) +@Suppress("NOTHING_TO_INLINE") +inline fun blockquote(vararg parts: TextSource) = blockquote(parts.toList()) +@Suppress("NOTHING_TO_INLINE") +inline fun blockquote(text: String) = blockquote(regular(text)) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt index 8e6f52a42a..60930f0507 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt @@ -103,6 +103,43 @@ inline fun EntitiesBuilder.bold(text: String) = add(dev.inmo.tgbotapi.types.mess */ inline fun EntitiesBuilder.boldln(text: String) = bold(text) + newLine +/** + * Add blockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.blockquote] + */ +inline fun EntitiesBuilder.blockquote(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.message.textsources.blockquote(parts)) +/** + * Version of [EntitiesBuilder.blockquote] with new line at the end + */ +inline fun EntitiesBuilder.blockquoteln(parts: TextSourcesList) = blockquote(parts) + newLine +/** + * Add blockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.blockquote]. + * Will reuse separator config from [buildEntities] + */ +inline fun EntitiesBuilder.blockquote(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.message.textsources.blockquote( + buildEntities(separator, init) +)) +/** + * Version of [EntitiesBuilder.blockquote] with new line at the end. + * Will reuse separator config from [buildEntities] + */ +inline fun EntitiesBuilder.blockquoteln(noinline init: EntitiesBuilderBody) = blockquote(init) + newLine +/** + * Add blockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.blockquote] + */ +inline fun EntitiesBuilder.blockquote(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.message.textsources.blockquote(*parts)) +/** + * Version of [EntitiesBuilder.blockquote] with new line at the end + */ +inline fun EntitiesBuilder.blockquoteln(vararg parts: TextSource) = blockquote(*parts) + newLine +/** + * Add blockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.blockquote] + */ +inline fun EntitiesBuilder.blockquote(text: String) = add(dev.inmo.tgbotapi.types.message.textsources.blockquote(text)) +/** + * Version of [EntitiesBuilder.blockquote] with new line at the end + */ +inline fun EntitiesBuilder.blockquoteln(text: String) = blockquote(text) + newLine + /** * Add spoiler using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.spoiler] */ diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/extensions/TextSourcesList.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/extensions/TextSourcesList.kt index 1480054c1a..c8dac1c7ac 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/extensions/TextSourcesList.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/extensions/TextSourcesList.kt @@ -3,6 +3,8 @@ package dev.inmo.tgbotapi.utils.extensions import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList import dev.inmo.tgbotapi.types.message.* +val eachLineRegex = Regex("^[^\n]") + inline fun TextSourcesList.makeString( parseMode: ParseMode? = null ) = when (parseMode) { @@ -21,8 +23,23 @@ inline fun TextSourcesList.makeHtmlString() = joinToString("") { it.html } -inline fun TextSourcesList.makeMarkdownV2String() = joinToString("") { +inline fun TextSourcesList.makeMarkdownV2String(eachLineSeparator: String? = null) = joinToString("") { it.markdownV2 +}.let { + if (eachLineSeparator == null) { + it + } else { + it.let { + if (it.startsWith("\n")) { + it + } else { + "$eachLineSeparator$it" + } + }.replace( + "\n", + "\n$eachLineSeparator" + ) + } } inline fun TextSourcesList.makeMarkdownString() = joinToString("") { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt index 54338b3a7a..c1dd5c6c67 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt @@ -6,8 +6,9 @@ import dev.inmo.tgbotapi.utils.extensions.* internal fun MultilevelTextSource.markdownV2Default( openControlSymbol: String, - closeControlSymbol: String = openControlSymbol -) = "$openControlSymbol${subsources.makeMarkdownV2String()}$closeControlSymbol" + closeControlSymbol: String = openControlSymbol, + eachLineSeparator: String? = null +) = "$openControlSymbol${subsources.makeMarkdownV2String(eachLineSeparator)}$closeControlSymbol" internal fun MultilevelTextSource.htmlDefault( openControlSymbol: String, closeControlSymbol: String = openControlSymbol @@ -40,6 +41,10 @@ internal fun MultilevelTextSource.boldMarkdownV2(): String = markdownV2Default(m internal fun MultilevelTextSource.boldHTML(): String = htmlDefault(htmlBoldControl) +internal fun MultilevelTextSource.blockquoteMarkdownV2(): String = markdownV2Default("", eachLineSeparator = markdownBlockquoteControl) +internal fun MultilevelTextSource.blockquoteHTML(): String = htmlDefault(htmlBlockquoteControl) + + internal fun MultilevelTextSource.cashTagMarkdownV2(): String = subsources.makeMarkdownV2String() internal fun MultilevelTextSource.cashTagHTML(): String = subsources.makeHtmlString() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt index ce3a22f25e..a8a0e4673a 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt @@ -5,6 +5,7 @@ import dev.inmo.tgbotapi.types.message.* import dev.inmo.tgbotapi.utils.extensions.* const val markdownBoldControl = "*" +const val markdownBlockquoteControl = ">" const val markdownItalicControl = "_" const val markdownSpoilerControl = "||" const val markdownCodeControl = "`" @@ -17,6 +18,7 @@ const val markdownV2UnderlineEndControl = "$markdownV2UnderlineControl$markdownV const val markdownV2ItalicEndControl = "$markdownItalicControl$markdownV2ItalicUnderlineDelimiter" const val htmlBoldControl = "b" +const val htmlBlockquoteControl = "blockquote" const val htmlItalicControl = "i" const val htmlSpoilerControl = "span class=\"tg-spoiler\"" const val htmlSpoilerClosingControl = "span" @@ -47,6 +49,8 @@ internal fun String.linkHTML(link: String): String = "${toHtml internal fun String.boldMarkdown(): String = markdownDefault(markdownBoldControl) +internal fun String.blockquoteMarkdown(): String = regularMarkdown() + internal fun String.italicMarkdown(): String = markdownDefault(markdownItalicControl) internal fun String.spoilerMarkdown(): String = regularMarkdown() diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt index 6621ed1137..6bc3340139 100644 --- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt +++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt @@ -4,9 +4,11 @@ import dev.inmo.tgbotapi.types.message.RawMessageEntity import dev.inmo.tgbotapi.types.message.textsources.* import kotlin.test.assertTrue -const val testText = "It (is?) is simple hello world with #tag and @mention" -const val formattedV2Text = "It \\(is?\\) *_is_ ~__simple__~* ||hello world|| with \\#tag and @mention" -const val formattedHtmlText = "It (is?) is simple hello world with #tag and @mention" +const val testText = "It (is?) is simple hello world with #tag and @mention. Start of blockquote: Block quotation started\n" + + "Block quotation continued\n" + + "The last line of the block quotation" +const val formattedV2Text = "It \\(is?\\) *_is_ ~__simple__~* ||hello world|| with \\#tag and @mention\\. Start of blockquote: >Block quotation started\n>Block quotation continued\n>The last line of the block quotation" +const val formattedHtmlText = "It (is?) is simple hello world with #tag and @mention. Start of blockquote:
Block quotation started\nBlock quotation continued\nThe last line of the block quotation
" internal val testTextEntities = listOf( RawMessageEntity( "bold", @@ -42,6 +44,11 @@ internal val testTextEntities = listOf( "mention", 45, 8 + ), + RawMessageEntity( + "blockquote", + 76, + 86 ) ) @@ -54,10 +61,15 @@ fun TextSourcesList.testTextSources() { assertTrue (get(5) is HashTagTextSource) assertTrue (get(6) is RegularTextSource) assertTrue (get(7) is MentionTextSource) + assertTrue (get(8) is RegularTextSource) + assertTrue (get(9) is BlockquoteTextSource) val boldSource = get(1) as BoldTextSource assertTrue (boldSource.subsources.first() is ItalicTextSource) assertTrue (boldSource.subsources[1] is RegularTextSource) assertTrue (boldSource.subsources[2] is StrikethroughTextSource) assertTrue ((boldSource.subsources[2] as StrikethroughTextSource).subsources.first() is UnderlineTextSource) + + val blockquoteSource = get(9) as BlockquoteTextSource + assertTrue (blockquoteSource.subsources.first() is RegularTextSource) } diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt index db723156cd..8077fbede4 100644 --- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt +++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt @@ -48,7 +48,13 @@ class StringFormattingTests { " with " + hashtag("tag") + " and " + - mention("mention") + mention("mention") + + ". Start of blockquote: " + + blockquote( + "Block quotation started\n" + + "Block quotation continued\n" + + "The last line of the block quotation" + ) sources.testTextSources() assertEquals(formattedV2Text, sources.toMarkdownV2Texts().first())