diff --git a/CHANGELOG.md b/CHANGELOG.md index 52c6becbcc..6e41dab8fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # TelegramBotAPI changelog +## 0.38.11 + +* `Common`: + * `Version`: + * `MicroUtils`: `0.9.16` -> `0.9.17` + * `Klock`: `2.6.3` -> `2.7.0` +* `Core`: + * Fixes in `TextSourcesList` creating in from `RawMessageEntities` + * Old ways to create keyboards (`matrix` and `row`) have been deprecated +* `API`: + * Add ability to `reply` with `Poll` + * Add ability to `reply` with any `MessageContent` + * Add ability to `reply` with any `TelegramMediaFile` + ## 0.38.10 * `API`: diff --git a/gradle.properties b/gradle.properties index 4ff2348dfc..a9333b0c5f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,11 +8,11 @@ kotlin.incremental.js=true kotlin_version=1.6.10 kotlin_coroutines_version=1.6.0 kotlin_serialisation_runtime_version=1.3.2 -klock_version=2.6.3 +klock_version=2.7.0 uuid_version=0.4.0 ktor_version=1.6.8 -micro_utils_version=0.9.16 +micro_utils_version=0.9.17 javax_activation_version=1.1.1 @@ -20,6 +20,6 @@ javax_activation_version=1.1.1 dokka_version=1.6.10 library_group=dev.inmo -library_version=0.38.10 +library_version=0.38.11 github_release_plugin_version=2.2.12 diff --git a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/Replies.kt b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/Replies.kt index eafb27672d..0ee552946d 100644 --- a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/Replies.kt +++ b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/Replies.kt @@ -17,10 +17,14 @@ import dev.inmo.tgbotapi.types.buttons.KeyboardMarkup import dev.inmo.tgbotapi.types.chat.abstracts.Chat import dev.inmo.tgbotapi.types.dice.DiceAnimationType import dev.inmo.tgbotapi.types.files.* +import dev.inmo.tgbotapi.types.files.abstracts.TelegramMediaFile import dev.inmo.tgbotapi.types.files.sticker.Sticker import dev.inmo.tgbotapi.types.games.Game -import dev.inmo.tgbotapi.types.location.StaticLocation +import dev.inmo.tgbotapi.types.location.* import dev.inmo.tgbotapi.types.message.abstracts.Message +import dev.inmo.tgbotapi.types.message.content.* +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile import dev.inmo.tgbotapi.types.payments.LabeledPrice import dev.inmo.tgbotapi.types.payments.abstracts.Currency import dev.inmo.tgbotapi.types.polls.* @@ -118,6 +122,7 @@ suspend inline fun TelegramBot.reply( longitude: Double, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyMarkup: KeyboardMarkup? = null ) = sendLocation( to.chat, @@ -125,6 +130,7 @@ suspend inline fun TelegramBot.reply( longitude, disableNotification, protectContent, + allowSendingWithoutReply, to.messageId, replyMarkup ) @@ -138,12 +144,14 @@ suspend inline fun TelegramBot.reply( location: StaticLocation, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyMarkup: KeyboardMarkup? = null ) = sendLocation( to.chat, location, disableNotification, protectContent, + allowSendingWithoutReply, to.messageId, replyMarkup ) @@ -887,6 +895,52 @@ suspend inline fun TelegramBot.reply( replyMarkup: KeyboardMarkup? = null ) = sendQuizPoll(to.chat, isClosed, quizPoll, question, options, correctOptionId, isAnonymous, entities, closeInfo, disableNotification, protectContent, to.messageId, allowSendingWithoutReply, replyMarkup) + +suspend inline fun TelegramBot.reply( + to: Message, + poll: Poll, + isClosed: Boolean = false, + question: String = poll.question, + options: List = poll.options.map { it.text }, + isAnonymous: Boolean = poll.isAnonymous, + closeInfo: ScheduledCloseInfo? = null, + disableNotification: Boolean = false, + protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, + replyMarkup: KeyboardMarkup? = null +) = when (poll) { + is RegularPoll -> reply( + to = to, + poll = poll, + isClosed = isClosed, + question = question, + options = options, + isAnonymous = isAnonymous, + allowMultipleAnswers = isAnonymous, + closeInfo = closeInfo, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + is UnknownPollType -> error("Unable to send poll with unknown type ($poll)") + is QuizPoll -> reply( + to = to, + quizPoll = poll, + entities = poll.textSources, + isClosed = isClosed, + question = question, + options = options, + isAnonymous = isAnonymous, + closeInfo = closeInfo, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) +} + + suspend inline fun TelegramBot.reply( to: Message, fromChatId: ChatIdentifier, @@ -921,3 +975,99 @@ suspend inline fun TelegramBot.reply( allowSendingWithoutReply: Boolean? = null, replyMarkup: KeyboardMarkup? = null ) = reply(to, copy.chat.id, copy.messageId, text, parseMode, disableNotification, protectContent, allowSendingWithoutReply, replyMarkup) + +suspend fun TelegramBot.reply( + to: Message, + content: MessageContent, + disableNotification: Boolean = false, + protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, + replyMarkup: KeyboardMarkup? = null +) { + execute( + content.createResend( + to.chat.id, + disableNotification, + protectContent, + to.messageId, + allowSendingWithoutReply, + replyMarkup + ) + ) +} + +suspend fun TelegramBot.reply( + to: Message, + mediaFile: TelegramMediaFile, + disableNotification: Boolean = false, + protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, + replyMarkup: KeyboardMarkup? = null +) { + when (mediaFile) { + is AudioFile -> reply( + to = to, + audio = mediaFile, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + is AnimationFile -> reply( + to = to, + animation = mediaFile, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + is VoiceFile -> reply( + to = to, + voice = mediaFile, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + is VideoFile -> reply( + to = to, + video = mediaFile, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + is VideoNoteFile -> reply( + to = to, + videoNote = mediaFile, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + is DocumentFile -> reply( + to = to, + document = mediaFile, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + is Sticker -> reply( + to = to, + sticker = mediaFile, + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + else -> reply( + to = to, + document = mediaFile.asDocumentFile(), + disableNotification = disableNotification, + protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, + replyMarkup = replyMarkup + ) + } +} diff --git a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/SendLocation.kt b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/SendLocation.kt index 5f6c75ffcd..69100cf049 100644 --- a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/SendLocation.kt +++ b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/SendLocation.kt @@ -18,6 +18,7 @@ suspend fun TelegramBot.sendLocation( longitude: Double, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null ) = execute( @@ -27,6 +28,7 @@ suspend fun TelegramBot.sendLocation( longitude, disableNotification = disableNotification, protectContent = protectContent, + allowSendingWithoutReply = allowSendingWithoutReply, replyToMessageId = replyToMessageId, replyMarkup = replyMarkup ) @@ -41,6 +43,7 @@ suspend fun TelegramBot.sendLocation( location: StaticLocation, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null ) = sendLocation( @@ -49,6 +52,7 @@ suspend fun TelegramBot.sendLocation( location.longitude, disableNotification, protectContent, + allowSendingWithoutReply, replyToMessageId, replyMarkup ) @@ -63,6 +67,7 @@ suspend fun TelegramBot.sendLocation( longitude: Double, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null ) = sendLocation( @@ -71,6 +76,7 @@ suspend fun TelegramBot.sendLocation( longitude, disableNotification, protectContent, + allowSendingWithoutReply, replyToMessageId, replyMarkup ) @@ -84,6 +90,7 @@ suspend fun TelegramBot.sendLocation( location: StaticLocation, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null ) = sendLocation( @@ -92,6 +99,7 @@ suspend fun TelegramBot.sendLocation( location.longitude, disableNotification, protectContent, + allowSendingWithoutReply, replyToMessageId, replyMarkup ) @@ -106,9 +114,10 @@ suspend fun TelegramBot.sendStaticLocation( longitude: Double, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null -) = sendLocation(chatId, latitude, longitude, disableNotification, protectContent, replyToMessageId, replyMarkup) +) = sendLocation(chatId, latitude, longitude, disableNotification, protectContent, allowSendingWithoutReply, replyToMessageId, replyMarkup) /** * @param replyMarkup Some of [KeyboardMarkup]. See [dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard] or @@ -119,9 +128,10 @@ suspend fun TelegramBot.sendStaticLocation( location: StaticLocation, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null -) = sendLocation(chatId, location.latitude, location.longitude, disableNotification, protectContent, replyToMessageId, replyMarkup) +) = sendLocation(chatId, location.latitude, location.longitude, disableNotification, protectContent, allowSendingWithoutReply, replyToMessageId, replyMarkup) /** * @param replyMarkup Some of [KeyboardMarkup]. See [dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard] or @@ -133,9 +143,10 @@ suspend fun TelegramBot.sendStaticLocation( longitude: Double, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null -) = sendLocation(chat.id, latitude, longitude, disableNotification, protectContent, replyToMessageId, replyMarkup) +) = sendLocation(chat.id, latitude, longitude, disableNotification, protectContent, allowSendingWithoutReply, replyToMessageId, replyMarkup) /** * @param replyMarkup Some of [KeyboardMarkup]. See [dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard] or @@ -146,6 +157,7 @@ suspend fun TelegramBot.sendStaticLocation( location: StaticLocation, disableNotification: Boolean = false, protectContent: Boolean = false, + allowSendingWithoutReply: Boolean? = null, replyToMessageId: MessageIdentifier? = null, replyMarkup: KeyboardMarkup? = null -) = sendLocation(chat.id, location.latitude, location.longitude, disableNotification, protectContent, replyToMessageId, replyMarkup) +) = sendLocation(chat.id, location.latitude, location.longitude, disableNotification, protectContent, allowSendingWithoutReply, replyToMessageId, replyMarkup) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageEntity/RawMessageEntity.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageEntity/RawMessageEntity.kt index 32289a6806..6a35f4dcd5 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageEntity/RawMessageEntity.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageEntity/RawMessageEntity.kt @@ -20,11 +20,11 @@ internal data class RawMessageEntity( internal fun RawMessageEntity.asTextSource( source: String, - subParts: TextSourcesList + subParts: List> ): TextSource { val sourceSubstring: String = source.substring(range) val subPartsWithRegulars by lazy { - subParts.fillWithRegulars(sourceSubstring) + subParts.map { (it.first - offset) to it.second }.fillWithRegulars(sourceSubstring) } return when (type) { "mention" -> MentionTextSource(sourceSubstring, subPartsWithRegulars) @@ -58,16 +58,14 @@ private inline operator fun > ClosedRange.contains(other: C return start <= other.start && endInclusive >= other.endInclusive } -internal fun TextSourcesList.fillWithRegulars(source: String): TextSourcesList { +internal fun List>.fillWithRegulars(source: String): TextSourcesList { var index = 0 val result = mutableListOf() - for (i in 0 until size) { - val textSource = get(i) - val thisSourceInStart = source.startsWith(textSource.source, index) - if (!thisSourceInStart) { - val regularEndIndex = source.indexOf(textSource.source, index) - result.add(regular(source.substring(index, regularEndIndex))) - index = regularEndIndex + for (i in indices) { + val (offset, textSource) = get(i) + if (offset - index > 0) { + result.add(regular(source.substring(index, offset))) + index = offset } result.add(textSource) index += textSource.source.length @@ -83,9 +81,9 @@ internal fun TextSourcesList.fillWithRegulars(source: String): TextSourcesList { private fun createTextSources( originalFullString: String, entities: RawMessageEntities -): TextSourcesList { +): List> { val mutableEntities = entities.toMutableList().apply { sortBy { it.offset } } - val resultList = mutableListOf() + val resultList = mutableListOf>() while (mutableEntities.isNotEmpty()) { var parent = mutableEntities.removeFirst() @@ -129,7 +127,7 @@ private fun createTextSources( emptyList() } resultList.add( - parent.asTextSource( + parent.offset to parent.asTextSource( originalFullString, subtextSources ) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/Matrix.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/Matrix.kt index 8187de14cc..902554c978 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/Matrix.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/Matrix.kt @@ -2,34 +2,69 @@ package dev.inmo.tgbotapi.utils import dev.inmo.tgbotapi.types.buttons.Matrix +/** + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardRowBuilder + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.ReplyKeyboardRowBuilder + */ +@Deprecated("This functionality will be removed soon") fun row(block: RowBuilder.() -> Unit): List { return RowBuilder().also(block).row } +/** + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardRowBuilder + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.ReplyKeyboardRowBuilder + */ +@Deprecated("This functionality will be removed soon") fun MatrixBuilder.row(block: RowBuilder.() -> Unit) { add(RowBuilder().also(block).row) } +/** + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardRowBuilder + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.ReplyKeyboardRowBuilder + */ +@Deprecated("This functionality will be removed soon") fun MatrixBuilder.row(vararg elements: T) { add(elements.toList()) } +/** + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardBuilder + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.ReplyKeyboardBuilder + */ +@Deprecated("This functionality will be removed soon") fun matrix(block: MatrixBuilder.() -> Unit): Matrix { return MatrixBuilder().also(block).matrix } +/** + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardBuilder + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.ReplyKeyboardBuilder + */ +@Deprecated("This functionality will be removed soon") fun flatMatrix(block: RowBuilder.() -> Unit): Matrix { return MatrixBuilder().apply { row(block) }.matrix } +/** + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardBuilder + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.ReplyKeyboardBuilder + */ +@Deprecated("This functionality will be removed soon") fun flatMatrix(vararg elements: T): Matrix { return MatrixBuilder().apply { row { elements.forEach { +it } } }.matrix } +/** + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardRowBuilder + * @see dev.inmo.tgbotapi.extensions.utils.types.buttons.ReplyKeyboardRowBuilder + */ +@Deprecated("This functionality will be removed soon") operator fun RowBuilder.plus(t: T) = add(t) open class RowBuilder { 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 7048fa4b6c..9b6a1d31f0 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 @@ -3,43 +3,43 @@ package dev.inmo.tgbotapi.types.MessageEntity import dev.inmo.tgbotapi.types.MessageEntity.textsources.* import kotlin.test.assertTrue -const val testText = "It is simple hello world with #tag and @mention" -const val formattedV2Text = "It *_is_ ~__simple__~* ||hello world|| with \\#tag and @mention" -const val formattedHtmlText = "It is simple hello world with #tag and @mention" +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" internal val testTextEntities = listOf( RawMessageEntity( "bold", - 3, + 9, 9 ), RawMessageEntity( "italic", - 3, + 9, 2 ), RawMessageEntity( "strikethrough", - 6, + 12, 6 ), RawMessageEntity( "underline", - 6, + 12, 6 ), RawMessageEntity( "spoiler", - 13, + 19, 11 ), RawMessageEntity( "hashtag", - 30, + 36, 4 ), RawMessageEntity( "mention", - 39, + 45, 8 ) ) 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 ba11fc9d3a..6929bf6135 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 @@ -2,8 +2,7 @@ package dev.inmo.tgbotapi.types.MessageEntity import dev.inmo.tgbotapi.extensions.utils.formatting.* import dev.inmo.tgbotapi.types.MessageEntity.textsources.* -import kotlin.test.Test -import kotlin.test.assertEquals +import kotlin.test.* class StringFormattingTests { @Test @@ -38,7 +37,7 @@ class StringFormattingTests { @Test fun testThatCreatingOfStringWithSimpleDSLWorksCorrectly() { - val sources: TextSourcesList = regular("It ") + + val sources: TextSourcesList = regular("It (is?) ") + bold(italic("is") + " " + strikethrough(underline("simple"))) + @@ -53,4 +52,43 @@ class StringFormattingTests { assertEquals(formattedV2Text, sources.toMarkdownV2Texts().first()) assertEquals(formattedHtmlText, sources.toHtmlTexts().first()) } + + @Test + fun testForRepeatingWordsInOneSentenceWithTheSecondOneFormatted() { + val sourceText = "link link" + val messageEntities = listOf( + RawMessageEntity("bold", 5, 4), + RawMessageEntity("text_link", 6, 2, "google.com") + ) + val textSources = messageEntities.asTextSources(sourceText) + val (regular, bold) = textSources + assertTrue(regular is RegularTextSource) + assertTrue(bold is BoldTextSource) + assertTrue(regular.source == "link ") + assertTrue(bold.source == "link") + assertTrue((bold.subsources[0] as? RegularTextSource) ?.source == "l") + assertTrue((bold.subsources[1] as? TextLinkTextSource) ?.source == "in") + assertTrue((bold.subsources[1] as? TextLinkTextSource) ?.url == "google.com") + assertTrue((bold.subsources[2] as? RegularTextSource) ?.source == "k") + assertTrue(bold.subsources.size == 3) + assertTrue(textSources.size == 2) + } + + @Test + fun testForRepeatingWordsInOneSentenceWithTheSecondOneFormattedInsideOfFormatting() { + val sourceText = "text" + val messageEntities = listOf( + RawMessageEntity("bold", 0, 4), + RawMessageEntity("text_link", 3, 1, "google.com") + ) + val textSources = messageEntities.asTextSources(sourceText) + val (bold) = textSources + assertTrue(bold is BoldTextSource) + assertTrue(bold.source == "text") + assertTrue((bold.subsources[0] as? RegularTextSource) ?.source == "tex") + assertTrue((bold.subsources[1] as? TextLinkTextSource) ?.source == "t") + assertTrue((bold.subsources[1] as? TextLinkTextSource) ?.url == "google.com") + assertTrue(bold.subsources.size == 2) + assertTrue(textSources.size == 1) + } }