1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2026-06-30 23:25:16 +00:00

Add rich text / rich block DSL builder

Add a type-safe Kotlin DSL for building rich messages:
* buildRichText { } - RichTextBuilder with plain() plus a function for every
  RichTextEntity (String and nested RichTextBuilder overloads where text-bearing);
* buildRichBlocks { } / buildRichTextInfo { } - RichBlocksBuilder with the
  text-bearing and container blocks (paragraph, heading, list, blockQuotation,
  details, ...), nesting RichText or further blocks per block kind;
* RichBlockListBuilder for list items.

Container blocks expose nested block/text builders; file/cell-heavy blocks (media,
table, collage, slideshow, map) are appended via add() / unary plus. A
@DslMarker (RichTextDsl) keeps the nested scopes from leaking receivers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 17:29:47 +06:00
parent 23578d25ef
commit 6b4999095e
3 changed files with 387 additions and 0 deletions

View File

@@ -34795,6 +34795,14 @@ public final class dev/inmo/tgbotapi/types/rich/RichBlockList$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class dev/inmo/tgbotapi/types/rich/RichBlockListBuilder {
public fun <init> ()V
public final fun build ()Ljava/util/List;
public final fun item (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun item (Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun item$default (Ldev/inmo/tgbotapi/types/rich/RichBlockListBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
}
public final class dev/inmo/tgbotapi/types/rich/RichBlockListItem {
public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichBlockListItem$Companion;
public fun <init> (Ljava/lang/String;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;)V
@@ -35263,6 +35271,35 @@ public final class dev/inmo/tgbotapi/types/rich/RichBlockVoiceNote$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class dev/inmo/tgbotapi/types/rich/RichBlocksBuilder {
public fun <init> ()V
public final fun add (Ldev/inmo/tgbotapi/types/rich/RichBlock;)V
public final fun anchor (Ljava/lang/String;)V
public final fun blockQuotation (Ldev/inmo/tgbotapi/types/rich/RichText;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun blockQuotation$default (Ldev/inmo/tgbotapi/types/rich/RichBlocksBuilder;Ldev/inmo/tgbotapi/types/rich/RichText;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public final fun build ()Ljava/util/List;
public final fun details (Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)V
public final fun details (Ljava/lang/String;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun details$default (Ldev/inmo/tgbotapi/types/rich/RichBlocksBuilder;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun details$default (Ldev/inmo/tgbotapi/types/rich/RichBlocksBuilder;Ljava/lang/String;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public final fun divider ()V
public final fun footer (Ljava/lang/String;)V
public final fun footer (Lkotlin/jvm/functions/Function1;)V
public final fun heading (ILjava/lang/String;)V
public final fun heading (ILkotlin/jvm/functions/Function1;)V
public final fun list (Lkotlin/jvm/functions/Function1;)V
public final fun mathematicalExpression (Ljava/lang/String;)V
public final fun paragraph (Ljava/lang/String;)V
public final fun paragraph (Lkotlin/jvm/functions/Function1;)V
public final fun preformatted (Ljava/lang/String;Ljava/lang/String;)V
public static synthetic fun preformatted$default (Ldev/inmo/tgbotapi/types/rich/RichBlocksBuilder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
public final fun pullQuotation (Ldev/inmo/tgbotapi/types/rich/RichText;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun pullQuotation$default (Ldev/inmo/tgbotapi/types/rich/RichBlocksBuilder;Ldev/inmo/tgbotapi/types/rich/RichText;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public final fun thinking (Ljava/lang/String;)V
public final fun thinking (Lkotlin/jvm/functions/Function1;)V
public final fun unaryPlus (Ldev/inmo/tgbotapi/types/rich/RichBlock;)V
}
public abstract interface class dev/inmo/tgbotapi/types/rich/RichText {
public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichText$Companion;
}
@@ -35432,6 +35469,61 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextBotCommand$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class dev/inmo/tgbotapi/types/rich/RichTextBuilder {
public fun <init> ()V
public final fun add (Ldev/inmo/tgbotapi/types/rich/RichText;)V
public final fun anchor (Ljava/lang/String;)V
public final fun anchorLink (Ljava/lang/String;Ljava/lang/String;)V
public final fun anchorLink (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun bankCard (Ljava/lang/String;Ljava/lang/String;)V
public final fun bankCard (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun bold (Ljava/lang/String;)V
public final fun bold (Lkotlin/jvm/functions/Function1;)V
public final fun botCommand (Ljava/lang/String;Ljava/lang/String;)V
public final fun botCommand (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun build ()Ldev/inmo/tgbotapi/types/rich/RichText;
public final fun cashtag (Ljava/lang/String;Ljava/lang/String;)V
public final fun cashtag (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun code (Ljava/lang/String;)V
public final fun code (Lkotlin/jvm/functions/Function1;)V
public final fun customEmoji-R1fjqgo (Ljava/lang/String;Ljava/lang/String;)V
public final fun dateTime (JLjava/lang/String;Ljava/lang/String;)V
public final fun dateTime (JLjava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun email (Ljava/lang/String;Ljava/lang/String;)V
public final fun email (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun hashtag (Ljava/lang/String;Ljava/lang/String;)V
public final fun hashtag (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun italic (Ljava/lang/String;)V
public final fun italic (Lkotlin/jvm/functions/Function1;)V
public final fun marked (Ljava/lang/String;)V
public final fun marked (Lkotlin/jvm/functions/Function1;)V
public final fun mathematicalExpression (Ljava/lang/String;)V
public final fun mention (Ljava/lang/String;Ljava/lang/String;)V
public final fun mention (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun phone (Ljava/lang/String;Ljava/lang/String;)V
public final fun phone (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun plain (Ljava/lang/String;)V
public final fun reference (Ljava/lang/String;Ljava/lang/String;)V
public final fun reference (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun referenceLink (Ljava/lang/String;Ljava/lang/String;)V
public final fun referenceLink (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
public final fun spoiler (Ljava/lang/String;)V
public final fun spoiler (Lkotlin/jvm/functions/Function1;)V
public final fun strikethrough (Ljava/lang/String;)V
public final fun strikethrough (Lkotlin/jvm/functions/Function1;)V
public final fun subscript (Ljava/lang/String;)V
public final fun subscript (Lkotlin/jvm/functions/Function1;)V
public final fun superscript (Ljava/lang/String;)V
public final fun superscript (Lkotlin/jvm/functions/Function1;)V
public final fun textMention (Ldev/inmo/tgbotapi/types/chat/User;Ljava/lang/String;)V
public final fun textMention (Ldev/inmo/tgbotapi/types/chat/User;Lkotlin/jvm/functions/Function1;)V
public final fun unaryPlus (Ldev/inmo/tgbotapi/types/rich/RichText;)V
public final fun underline (Ljava/lang/String;)V
public final fun underline (Lkotlin/jvm/functions/Function1;)V
public final fun url (Ljava/lang/String;Ljava/lang/String;)V
public final fun url (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
}
public final class dev/inmo/tgbotapi/types/rich/RichTextCashtag : dev/inmo/tgbotapi/types/rich/RichTextEntity {
public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichTextCashtag$Companion;
public static final field TYPE Ljava/lang/String;
@@ -35564,6 +35656,16 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextDateTime$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public abstract interface annotation class dev/inmo/tgbotapi/types/rich/RichTextDsl : java/lang/annotation/Annotation {
}
public final class dev/inmo/tgbotapi/types/rich/RichTextDslKt {
public static final fun buildRichBlocks (Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun buildRichText (Lkotlin/jvm/functions/Function1;)Ldev/inmo/tgbotapi/types/rich/RichText;
public static final fun buildRichTextInfo (Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)Ldev/inmo/tgbotapi/types/rich/RichTextInfo;
public static synthetic fun buildRichTextInfo$default (Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextInfo;
}
public final class dev/inmo/tgbotapi/types/rich/RichTextEmailAddress : dev/inmo/tgbotapi/types/rich/RichTextEntity {
public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichTextEmailAddress$Companion;
public static final field TYPE Ljava/lang/String;

View File

@@ -0,0 +1,208 @@
package dev.inmo.tgbotapi.types.rich
import dev.inmo.tgbotapi.types.CustomEmojiId
import dev.inmo.tgbotapi.types.chat.User
/**
* [DslMarker] for the rich message builders, so the inner [RichTextBuilder], [RichBlocksBuilder] and
* [RichBlockListBuilder] scopes do not leak their receivers into each other.
*/
@DslMarker
annotation class RichTextDsl
/**
* Builder of a single [RichText]. Each call appends a part; [build] returns a [RichTextPlain]/[RichTextEntity] when there
* is exactly one part, a [RichTextGroup] otherwise.
*/
@RichTextDsl
class RichTextBuilder {
private val parts = mutableListOf<RichText>()
/** Appends an already built [RichText]. */
fun add(richText: RichText) {
parts.add(richText)
}
/** Appends an already built [RichText]. */
operator fun RichText.unaryPlus() = add(this)
/** Plain, non-formatted text. */
fun plain(text: String) = add(RichTextPlain(text))
fun bold(text: String) = add(RichTextBold(RichTextPlain(text)))
fun bold(block: RichTextBuilder.() -> Unit) = add(RichTextBold(buildRichText(block)))
fun italic(text: String) = add(RichTextItalic(RichTextPlain(text)))
fun italic(block: RichTextBuilder.() -> Unit) = add(RichTextItalic(buildRichText(block)))
fun underline(text: String) = add(RichTextUnderline(RichTextPlain(text)))
fun underline(block: RichTextBuilder.() -> Unit) = add(RichTextUnderline(buildRichText(block)))
fun strikethrough(text: String) = add(RichTextStrikethrough(RichTextPlain(text)))
fun strikethrough(block: RichTextBuilder.() -> Unit) = add(RichTextStrikethrough(buildRichText(block)))
fun spoiler(text: String) = add(RichTextSpoiler(RichTextPlain(text)))
fun spoiler(block: RichTextBuilder.() -> Unit) = add(RichTextSpoiler(buildRichText(block)))
fun subscript(text: String) = add(RichTextSubscript(RichTextPlain(text)))
fun subscript(block: RichTextBuilder.() -> Unit) = add(RichTextSubscript(buildRichText(block)))
fun superscript(text: String) = add(RichTextSuperscript(RichTextPlain(text)))
fun superscript(block: RichTextBuilder.() -> Unit) = add(RichTextSuperscript(buildRichText(block)))
fun marked(text: String) = add(RichTextMarked(RichTextPlain(text)))
fun marked(block: RichTextBuilder.() -> Unit) = add(RichTextMarked(buildRichText(block)))
fun code(text: String) = add(RichTextCode(RichTextPlain(text)))
fun code(block: RichTextBuilder.() -> Unit) = add(RichTextCode(buildRichText(block)))
fun dateTime(unixTime: Long, dateTimeFormat: String, text: String) =
add(RichTextDateTime(RichTextPlain(text), unixTime, dateTimeFormat))
fun dateTime(unixTime: Long, dateTimeFormat: String, block: RichTextBuilder.() -> Unit) =
add(RichTextDateTime(buildRichText(block), unixTime, dateTimeFormat))
fun textMention(user: User, text: String) = add(RichTextTextMention(RichTextPlain(text), user))
fun textMention(user: User, block: RichTextBuilder.() -> Unit) = add(RichTextTextMention(buildRichText(block), user))
fun customEmoji(customEmojiId: CustomEmojiId, alternativeText: String) =
add(RichTextCustomEmoji(customEmojiId, alternativeText))
fun mathematicalExpression(expression: String) = add(RichTextMathematicalExpression(expression))
fun url(url: String, text: String) = add(RichTextUrl(RichTextPlain(text), url))
fun url(url: String, block: RichTextBuilder.() -> Unit) = add(RichTextUrl(buildRichText(block), url))
fun email(emailAddress: String, text: String) = add(RichTextEmailAddress(RichTextPlain(text), emailAddress))
fun email(emailAddress: String, block: RichTextBuilder.() -> Unit) =
add(RichTextEmailAddress(buildRichText(block), emailAddress))
fun phone(phoneNumber: String, text: String) = add(RichTextPhoneNumber(RichTextPlain(text), phoneNumber))
fun phone(phoneNumber: String, block: RichTextBuilder.() -> Unit) =
add(RichTextPhoneNumber(buildRichText(block), phoneNumber))
fun bankCard(bankCardNumber: String, text: String) = add(RichTextBankCardNumber(RichTextPlain(text), bankCardNumber))
fun bankCard(bankCardNumber: String, block: RichTextBuilder.() -> Unit) =
add(RichTextBankCardNumber(buildRichText(block), bankCardNumber))
fun mention(username: String, text: String) = add(RichTextMention(RichTextPlain(text), username))
fun mention(username: String, block: RichTextBuilder.() -> Unit) = add(RichTextMention(buildRichText(block), username))
fun hashtag(hashtag: String, text: String) = add(RichTextHashtag(RichTextPlain(text), hashtag))
fun hashtag(hashtag: String, block: RichTextBuilder.() -> Unit) = add(RichTextHashtag(buildRichText(block), hashtag))
fun cashtag(cashtag: String, text: String) = add(RichTextCashtag(RichTextPlain(text), cashtag))
fun cashtag(cashtag: String, block: RichTextBuilder.() -> Unit) = add(RichTextCashtag(buildRichText(block), cashtag))
fun botCommand(botCommand: String, text: String) = add(RichTextBotCommand(RichTextPlain(text), botCommand))
fun botCommand(botCommand: String, block: RichTextBuilder.() -> Unit) =
add(RichTextBotCommand(buildRichText(block), botCommand))
fun anchor(name: String) = add(RichTextAnchor(name))
fun anchorLink(anchorName: String, text: String) = add(RichTextAnchorLink(RichTextPlain(text), anchorName))
fun anchorLink(anchorName: String, block: RichTextBuilder.() -> Unit) =
add(RichTextAnchorLink(buildRichText(block), anchorName))
fun reference(name: String, text: String) = add(RichTextReference(RichTextPlain(text), name))
fun reference(name: String, block: RichTextBuilder.() -> Unit) = add(RichTextReference(buildRichText(block), name))
fun referenceLink(referenceName: String, text: String) =
add(RichTextReferenceLink(RichTextPlain(text), referenceName))
fun referenceLink(referenceName: String, block: RichTextBuilder.() -> Unit) =
add(RichTextReferenceLink(buildRichText(block), referenceName))
fun build(): RichText = when (parts.size) {
0 -> RichTextGroup(emptyList())
1 -> parts.single()
else -> RichTextGroup(parts.toList())
}
}
/**
* Builder of [RichBlockListItem]s used inside [RichBlocksBuilder.list].
*/
@RichTextDsl
class RichBlockListBuilder {
private val items = mutableListOf<RichBlockListItem>()
fun item(
label: String,
hasCheckbox: Boolean? = null,
isChecked: Boolean? = null,
value: Int? = null,
labelType: String? = null,
block: RichBlocksBuilder.() -> Unit
) {
items.add(RichBlockListItem(label, buildRichBlocks(block), hasCheckbox, isChecked, value, labelType))
}
fun item(label: String, text: String) {
items.add(RichBlockListItem(label, listOf(RichBlockParagraph(RichTextPlain(text)))))
}
fun build(): List<RichBlockListItem> = items.toList()
}
/**
* Builder of a [List] of [RichBlock]s - the root of the rich message DSL. Text-bearing and container blocks have their
* own DSL functions; file/cell-heavy blocks (media, collage, slideshow, table, map) can be appended with [add] / unary
* plus.
*/
@RichTextDsl
class RichBlocksBuilder {
private val blocks = mutableListOf<RichBlock>()
/** Appends an already built [RichBlock]. */
fun add(block: RichBlock) {
blocks.add(block)
}
/** Appends an already built [RichBlock]. */
operator fun RichBlock.unaryPlus() = add(this)
fun paragraph(text: String) = add(RichBlockParagraph(RichTextPlain(text)))
fun paragraph(block: RichTextBuilder.() -> Unit) = add(RichBlockParagraph(buildRichText(block)))
fun heading(size: Int, text: String) = add(RichBlockSectionHeading(RichTextPlain(text), size))
fun heading(size: Int, block: RichTextBuilder.() -> Unit) = add(RichBlockSectionHeading(buildRichText(block), size))
fun preformatted(text: String, language: String? = null) = add(RichBlockPreformatted(RichTextPlain(text), language))
fun footer(text: String) = add(RichBlockFooter(RichTextPlain(text)))
fun footer(block: RichTextBuilder.() -> Unit) = add(RichBlockFooter(buildRichText(block)))
fun divider() = add(RichBlockDivider())
fun mathematicalExpression(expression: String) = add(RichBlockMathematicalExpression(expression))
fun anchor(name: String) = add(RichBlockAnchor(name))
fun thinking(text: String) = add(RichBlockThinking(RichTextPlain(text)))
fun thinking(block: RichTextBuilder.() -> Unit) = add(RichBlockThinking(buildRichText(block)))
fun list(block: RichBlockListBuilder.() -> Unit) = add(RichBlockList(RichBlockListBuilder().apply(block).build()))
fun blockQuotation(credit: RichText? = null, block: RichBlocksBuilder.() -> Unit) =
add(RichBlockBlockQuotation(buildRichBlocks(block), credit))
fun pullQuotation(credit: RichText? = null, block: RichTextBuilder.() -> Unit) =
add(RichBlockPullQuotation(buildRichText(block), credit))
fun details(summary: RichText, isOpen: Boolean? = null, block: RichBlocksBuilder.() -> Unit) =
add(RichBlockDetails(summary, buildRichBlocks(block), isOpen))
fun details(summary: String, isOpen: Boolean? = null, block: RichBlocksBuilder.() -> Unit) =
details(RichTextPlain(summary), isOpen, block)
fun build(): List<RichBlock> = blocks.toList()
}
/** Builds a [RichText] using the [RichTextBuilder] DSL. */
fun buildRichText(block: RichTextBuilder.() -> Unit): RichText = RichTextBuilder().apply(block).build()
/** Builds a [List] of [RichBlock]s using the [RichBlocksBuilder] DSL. */
fun buildRichBlocks(block: RichBlocksBuilder.() -> Unit): List<RichBlock> = RichBlocksBuilder().apply(block).build()
/** Builds a [RichTextInfo] using the [RichBlocksBuilder] DSL. */
fun buildRichTextInfo(isRtl: Boolean? = null, block: RichBlocksBuilder.() -> Unit): RichTextInfo =
RichTextInfo(buildRichBlocks(block), isRtl)

View File

@@ -0,0 +1,77 @@
package dev.inmo.tgbotapi.types.rich
import kotlin.test.Test
import kotlin.test.assertEquals
class RichTextDslTest {
@Test
fun buildsRichTextGroup() {
val richText = buildRichText {
plain("a ")
bold("b")
italic {
plain("c")
bold("d")
}
}
assertEquals(
RichTextGroup(
listOf(
RichTextPlain("a "),
RichTextBold(RichTextPlain("b")),
RichTextItalic(RichTextGroup(listOf(RichTextPlain("c"), RichTextBold(RichTextPlain("d")))))
)
),
richText
)
}
@Test
fun singlePartUnwraps() {
assertEquals(RichTextBold(RichTextPlain("x")), buildRichText { bold("x") })
}
@Test
fun rendersMarkdown() {
assertEquals("a **b**", buildRichText { plain("a "); bold("b") }.markdown)
}
@Test
fun buildsBlocks() {
val blocks = buildRichBlocks {
heading(1, "Title")
paragraph {
plain("Hello ")
bold("world")
}
divider()
list {
item("1", "first")
item("2", labelType = "1") { paragraph("second") }
}
blockQuotation {
paragraph("quoted")
}
}
assertEquals(5, blocks.size)
assertEquals(RichBlockSectionHeading(RichTextPlain("Title"), 1), blocks[0])
assertEquals(
RichBlockParagraph(RichTextGroup(listOf(RichTextPlain("Hello "), RichTextBold(RichTextPlain("world"))))),
blocks[1]
)
assertEquals(RichBlockDivider(), blocks[2])
val list = blocks[3] as RichBlockList
assertEquals(2, list.items.size)
assertEquals(RichBlockListItem("1", listOf(RichBlockParagraph(RichTextPlain("first")))), list.items[0])
assertEquals(RichBlockBlockQuotation(listOf(RichBlockParagraph(RichTextPlain("quoted")))), blocks[4])
}
@Test
fun buildsRichTextInfo() {
val info = buildRichTextInfo(isRtl = true) {
paragraph("p")
}
assertEquals(RichTextInfo(listOf(RichBlockParagraph(RichTextPlain("p"))), true), info)
assertEquals("p", info.markdown)
}
}