1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2026-07-03 16:46:06 +00:00

Move RichText/RichBlock markdown and html into members

Add markdown and html as members of the RichText and RichBlock sealed
interfaces and override them in every inheritor (RichTextPlain,
RichTextGroup and all 21 RichBlock subtypes), mirroring the existing
RichTextEntity implementation. The former RichText/RichBlock.markdown
and .html extension properties (which dispatched via a when over each
subtype) are removed; the shared RichBlock render helpers become
internal so the overrides can reuse them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 20:55:46 +06:00
parent 6b4999095e
commit e793eea943
6 changed files with 205 additions and 102 deletions

View File

@@ -18,6 +18,16 @@ import kotlinx.serialization.json.jsonPrimitive
@ClassCastsIncluded
sealed interface RichBlock {
val type: String
/**
* [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style) source of this single [RichBlock].
*/
val markdown: String
/**
* [Rich HTML style](https://core.telegram.org/bots/api#rich-html-style) source of this single [RichBlock].
*/
val html: String
}
object RichBlockSerializer : JsonContentPolymorphicSerializer<RichBlock>(RichBlock::class) {

View File

@@ -28,69 +28,13 @@ val RichTextInfo.markdown: String
val RichTextInfo.html: String
get() = blocks.toRichHtml()
/**
* [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style) source of this single [RichBlock].
*/
val RichBlock.markdown: String
get() = when (this) {
is RichBlockParagraph -> text.markdown
is RichBlockSectionHeading -> "#".repeat(size) + " " + text.markdown
is RichBlockPreformatted -> "```" + (language ?: "") + "\n" + text.source + "\n```"
is RichBlockFooter -> "<footer>${text.markdown}</footer>"
is RichBlockDivider -> "---"
is RichBlockMathematicalExpression -> "\$\$" + expression + "\$\$"
is RichBlockAnchor -> "<a name=\"$name\"></a>"
is RichBlockList -> richBlockListMarkdown(this)
is RichBlockBlockQuotation -> richBlockQuotationMarkdown(blocks, credit)
is RichBlockPullQuotation -> "<aside>${text.markdown}${creditCiteMarkdown(credit)}</aside>"
is RichBlockCollage -> richMediaContainerMarkdown("tg-collage", blocks, caption)
is RichBlockSlideshow -> richMediaContainerMarkdown("tg-slideshow", blocks, caption)
is RichBlockTable -> richBlockTableMarkdown(this)
is RichBlockDetails -> "<details${richOpenAttribute(isOpen)}><summary>${summary.markdown}</summary>\n\n${blocks.toRichMarkdown()}\n\n</details>"
is RichBlockMap -> richBlockMapMarkdown(this)
is RichBlockAnimation -> richMediaMarkdown(animation.fileId.fileId, caption)
is RichBlockAudio -> richMediaMarkdown(audio.fileId.fileId, caption)
is RichBlockPhoto -> richMediaMarkdown(photo.fileId.fileId, caption)
is RichBlockVideo -> richMediaMarkdown(video.fileId.fileId, caption)
is RichBlockVoiceNote -> richMediaMarkdown(voiceNote.fileId.fileId, caption)
is RichBlockThinking -> "<tg-thinking>${text.markdown}</tg-thinking>"
}
internal fun richOpenAttribute(isOpen: Boolean?): String = if (isOpen == true) " open" else ""
/**
* [Rich HTML style](https://core.telegram.org/bots/api#rich-html-style) source of this single [RichBlock].
*/
val RichBlock.html: String
get() = when (this) {
is RichBlockParagraph -> "<p>${text.html}</p>"
is RichBlockSectionHeading -> "<h$size>${text.html}</h$size>"
is RichBlockPreformatted -> language?.let { "<pre><code class=\"language-$it\">${text.html}</code></pre>" } ?: "<pre>${text.html}</pre>"
is RichBlockFooter -> "<footer>${text.html}</footer>"
is RichBlockDivider -> "<hr/>"
is RichBlockMathematicalExpression -> "<tg-math-block>$expression</tg-math-block>"
is RichBlockAnchor -> "<a name=\"$name\"></a>"
is RichBlockList -> richBlockListHtml(this)
is RichBlockBlockQuotation -> "<blockquote>${blocks.toRichHtml()}${creditCiteHtml(credit)}</blockquote>"
is RichBlockPullQuotation -> "<aside>${text.html}${creditCiteHtml(credit)}</aside>"
is RichBlockCollage -> richMediaContainerHtml("tg-collage", blocks, caption)
is RichBlockSlideshow -> richMediaContainerHtml("tg-slideshow", blocks, caption)
is RichBlockTable -> richBlockTableHtml(this)
is RichBlockDetails -> "<details${richOpenAttribute(isOpen)}><summary>${summary.html}</summary>${blocks.toRichHtml()}</details>"
is RichBlockMap -> richBlockMapHtml(this)
is RichBlockAnimation -> richMediaHtml("video", animation.fileId.fileId, hasSpoiler == true, selfClosing = false, caption = caption)
is RichBlockAudio -> richMediaHtml("audio", audio.fileId.fileId, spoiler = false, selfClosing = false, caption = caption)
is RichBlockPhoto -> richMediaHtml("img", photo.fileId.fileId, hasSpoiler == true, selfClosing = true, caption = caption)
is RichBlockVideo -> richMediaHtml("video", video.fileId.fileId, hasSpoiler == true, selfClosing = false, caption = caption)
is RichBlockVoiceNote -> richMediaHtml("audio", voiceNote.fileId.fileId, spoiler = false, selfClosing = false, caption = caption)
is RichBlockThinking -> "<tg-thinking>${text.html}</tg-thinking>"
}
internal fun creditCiteMarkdown(credit: RichText?): String = credit?.let { "<cite>${it.markdown}</cite>" } ?: ""
private fun richOpenAttribute(isOpen: Boolean?): String = if (isOpen == true) " open" else ""
internal fun creditCiteHtml(credit: RichText?): String = credit?.let { "<cite>${it.html}</cite>" } ?: ""
private fun creditCiteMarkdown(credit: RichText?): String = credit?.let { "<cite>${it.markdown}</cite>" } ?: ""
private fun creditCiteHtml(credit: RichText?): String = credit?.let { "<cite>${it.html}</cite>" } ?: ""
private fun richBlockListMarkdown(list: RichBlockList): String =
internal fun richBlockListMarkdown(list: RichBlockList): String =
list.items.mapIndexed { index, item ->
val marker = when {
item.hasCheckbox == true -> if (item.isChecked == true) "- [x] " else "- [ ] "
@@ -102,7 +46,7 @@ private fun richBlockListMarkdown(list: RichBlockList): String =
}.joinToString(separator = "\n")
}.joinToString(separator = "\n")
private fun richBlockListHtml(list: RichBlockList): String {
internal fun richBlockListHtml(list: RichBlockList): String {
val ordered = list.items.any { it.labelType != null }
val tag = if (ordered) "ol" else "ul"
val items = list.items.joinToString(separator = "") { item ->
@@ -120,45 +64,45 @@ private fun richBlockListHtml(list: RichBlockList): String {
return "<$tag>$items</$tag>"
}
private fun richBlockQuotationMarkdown(blocks: List<RichBlock>, credit: RichText?): String {
internal fun richBlockQuotationMarkdown(blocks: List<RichBlock>, credit: RichText?): String {
val quoted = blocks.toRichMarkdown().lineSequence().joinToString(separator = "\n") { line ->
if (line.isEmpty()) ">" else "> $line"
}
return quoted + (credit?.let { "\n> ${creditCiteMarkdown(it)}" } ?: "")
}
private fun richMediaContainerMarkdown(tag: String, blocks: List<RichBlock>, caption: RichBlockCaption?): String {
internal fun richMediaContainerMarkdown(tag: String, blocks: List<RichBlock>, caption: RichBlockCaption?): String {
val media = blocks.joinToString(separator = "\n") { it.markdown }
val captionPart = caption?.let { "\n<figcaption>${it.text.markdown}${creditCiteMarkdown(it.credit)}</figcaption>" } ?: ""
return "<$tag>\n\n$media$captionPart\n\n</$tag>"
}
private fun richMediaContainerHtml(tag: String, blocks: List<RichBlock>, caption: RichBlockCaption?): String {
internal fun richMediaContainerHtml(tag: String, blocks: List<RichBlock>, caption: RichBlockCaption?): String {
val media = blocks.joinToString(separator = "") { it.html }
val captionPart = caption?.let { "<figcaption>${it.text.html}${creditCiteHtml(it.credit)}</figcaption>" } ?: ""
return "<$tag>$media$captionPart</$tag>"
}
private fun richMediaMarkdown(source: String, caption: RichBlockCaption?): String =
internal fun richMediaMarkdown(source: String, caption: RichBlockCaption?): String =
caption?.let { "![](" + source + " \"" + it.text.source + "\")" } ?: "![]($source)"
private fun richMediaHtml(tag: String, source: String, spoiler: Boolean, selfClosing: Boolean, caption: RichBlockCaption?): String {
internal fun richMediaHtml(tag: String, source: String, spoiler: Boolean, selfClosing: Boolean, caption: RichBlockCaption?): String {
val spoilerAttribute = if (spoiler) " tg-spoiler" else ""
val element = if (selfClosing) "<$tag src=\"$source\"$spoilerAttribute/>" else "<$tag src=\"$source\"$spoilerAttribute></$tag>"
return caption?.let { "<figure>$element<figcaption>${it.text.html}${creditCiteHtml(it.credit)}</figcaption></figure>" } ?: element
}
private fun richBlockMapMarkdown(map: RichBlockMap): String {
internal fun richBlockMapMarkdown(map: RichBlockMap): String {
val element = "<tg-map lat=\"${map.location.latitude}\" long=\"${map.location.longitude}\" zoom=\"${map.zoom}\"/>"
return map.caption?.let { "<figure>$element<figcaption>${it.text.markdown}${creditCiteMarkdown(it.credit)}</figcaption></figure>" } ?: element
}
private fun richBlockMapHtml(map: RichBlockMap): String {
internal fun richBlockMapHtml(map: RichBlockMap): String {
val element = "<tg-map lat=\"${map.location.latitude}\" long=\"${map.location.longitude}\" zoom=\"${map.zoom}\"/>"
return map.caption?.let { "<figure>$element<figcaption>${it.text.html}${creditCiteHtml(it.credit)}</figcaption></figure>" } ?: element
}
private fun richBlockTableMarkdown(table: RichBlockTable): String {
internal fun richBlockTableMarkdown(table: RichBlockTable): String {
if (table.cells.isEmpty()) return ""
fun renderRow(row: List<RichBlockTableCell>): String =
row.joinToString(separator = " | ", prefix = "| ", postfix = " |") { it.text?.markdown ?: "" }
@@ -177,7 +121,7 @@ private fun richBlockTableMarkdown(table: RichBlockTable): String {
return lines.joinToString(separator = "\n")
}
private fun richBlockTableHtml(table: RichBlockTable): String {
internal fun richBlockTableHtml(table: RichBlockTable): String {
val attributes = buildString {
if (table.isBordered == true) append(" bordered")
if (table.isStriped == true) append(" striped")

View File

@@ -49,6 +49,11 @@ data class RichBlockParagraph(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = text.markdown
override val html: String
get() = "<p>${text.html}</p>"
companion object {
const val TYPE = "paragraph"
}
@@ -73,6 +78,11 @@ data class RichBlockSectionHeading(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "#".repeat(size) + " " + text.markdown
override val html: String
get() = "<h$size>${text.html}</h$size>"
companion object {
const val TYPE = "heading"
}
@@ -94,6 +104,11 @@ data class RichBlockPreformatted(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "```" + (language ?: "") + "\n" + text.source + "\n```"
override val html: String
get() = language?.let { "<pre><code class=\"language-$it\">${text.html}</code></pre>" } ?: "<pre>${text.html}</pre>"
companion object {
const val TYPE = "pre"
}
@@ -113,6 +128,11 @@ data class RichBlockFooter(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "<footer>${text.markdown}</footer>"
override val html: String
get() = "<footer>${text.html}</footer>"
companion object {
const val TYPE = "footer"
}
@@ -129,6 +149,11 @@ class RichBlockDivider : RichBlock {
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "---"
override val html: String
get() = "<hr/>"
override fun equals(other: Any?): Boolean = other is RichBlockDivider
override fun hashCode(): Int = TYPE.hashCode()
@@ -154,6 +179,11 @@ data class RichBlockMathematicalExpression(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "\$\$" + expression + "\$\$"
override val html: String
get() = "<tg-math-block>$expression</tg-math-block>"
companion object {
const val TYPE = "mathematical_expression"
}
@@ -173,6 +203,11 @@ data class RichBlockAnchor(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "<a name=\"$name\"></a>"
override val html: String
get() = "<a name=\"$name\"></a>"
companion object {
const val TYPE = "anchor"
}
@@ -192,6 +227,11 @@ data class RichBlockList(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richBlockListMarkdown(this)
override val html: String
get() = richBlockListHtml(this)
companion object {
const val TYPE = "list"
}
@@ -213,6 +253,11 @@ data class RichBlockBlockQuotation(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richBlockQuotationMarkdown(blocks, credit)
override val html: String
get() = "<blockquote>${blocks.toRichHtml()}${creditCiteHtml(credit)}</blockquote>"
companion object {
const val TYPE = "blockquote"
}
@@ -234,6 +279,11 @@ data class RichBlockPullQuotation(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "<aside>${text.markdown}${creditCiteMarkdown(credit)}</aside>"
override val html: String
get() = "<aside>${text.html}${creditCiteHtml(credit)}</aside>"
companion object {
const val TYPE = "pullquote"
}
@@ -255,6 +305,11 @@ data class RichBlockCollage(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richMediaContainerMarkdown("tg-collage", blocks, caption)
override val html: String
get() = richMediaContainerHtml("tg-collage", blocks, caption)
companion object {
const val TYPE = "collage"
}
@@ -276,6 +331,11 @@ data class RichBlockSlideshow(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richMediaContainerMarkdown("tg-slideshow", blocks, caption)
override val html: String
get() = richMediaContainerHtml("tg-slideshow", blocks, caption)
companion object {
const val TYPE = "slideshow"
}
@@ -301,6 +361,11 @@ data class RichBlockTable(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richBlockTableMarkdown(this)
override val html: String
get() = richBlockTableHtml(this)
companion object {
const val TYPE = "table"
}
@@ -324,6 +389,11 @@ data class RichBlockDetails(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "<details${richOpenAttribute(isOpen)}><summary>${summary.markdown}</summary>\n\n${blocks.toRichMarkdown()}\n\n</details>"
override val html: String
get() = "<details${richOpenAttribute(isOpen)}><summary>${summary.html}</summary>${blocks.toRichHtml()}</details>"
companion object {
const val TYPE = "details"
}
@@ -354,6 +424,11 @@ data class RichBlockMap(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richBlockMapMarkdown(this)
override val html: String
get() = richBlockMapHtml(this)
companion object {
const val TYPE = "map"
}
@@ -377,6 +452,11 @@ data class RichBlockAnimation(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richMediaMarkdown(animation.fileId.fileId, caption)
override val html: String
get() = richMediaHtml("video", animation.fileId.fileId, hasSpoiler == true, selfClosing = false, caption = caption)
companion object {
const val TYPE = "animation"
}
@@ -398,6 +478,11 @@ data class RichBlockAudio(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richMediaMarkdown(audio.fileId.fileId, caption)
override val html: String
get() = richMediaHtml("audio", audio.fileId.fileId, spoiler = false, selfClosing = false, caption = caption)
companion object {
const val TYPE = "audio"
}
@@ -421,6 +506,11 @@ data class RichBlockPhoto(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richMediaMarkdown(photo.fileId.fileId, caption)
override val html: String
get() = richMediaHtml("img", photo.fileId.fileId, hasSpoiler == true, selfClosing = true, caption = caption)
companion object {
const val TYPE = "photo"
}
@@ -444,6 +534,11 @@ data class RichBlockVideo(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richMediaMarkdown(video.fileId.fileId, caption)
override val html: String
get() = richMediaHtml("video", video.fileId.fileId, hasSpoiler == true, selfClosing = false, caption = caption)
companion object {
const val TYPE = "video"
}
@@ -465,6 +560,11 @@ data class RichBlockVoiceNote(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = richMediaMarkdown(voiceNote.fileId.fileId, caption)
override val html: String
get() = richMediaHtml("audio", voiceNote.fileId.fileId, spoiler = false, selfClosing = false, caption = caption)
companion object {
const val TYPE = "voice_note"
}
@@ -484,6 +584,11 @@ data class RichBlockThinking(
@SerialName(typeField)
override val type: String = TYPE
override val markdown: String
get() = "<tg-thinking>${text.markdown}</tg-thinking>"
override val html: String
get() = "<tg-thinking>${text.html}</tg-thinking>"
companion object {
const val TYPE = "thinking"
}

View File

@@ -1,6 +1,7 @@
package dev.inmo.tgbotapi.types.rich
import dev.inmo.tgbotapi.types.typeField
import dev.inmo.tgbotapi.utils.extensions.toHtml
import dev.inmo.tgbotapi.utils.internal.ClassCastsIncluded
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
@@ -18,7 +19,17 @@ import kotlinx.serialization.json.*
*/
@Serializable(RichTextSerializer::class)
@ClassCastsIncluded
sealed interface RichText
sealed interface RichText {
/**
* [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style) representation of this [RichText].
*/
val markdown: String
/**
* [Rich HTML style](https://core.telegram.org/bots/api#rich-html-style) representation of this [RichText].
*/
val html: String
}
/**
* A plain (non-formatted) part of a [RichText]. Serialized as a bare JSON string.
@@ -26,7 +37,12 @@ sealed interface RichText
@Serializable
data class RichTextPlain(
val text: String
) : RichText
) : RichText {
override val markdown: String
get() = text.escapeRichMarkdown()
override val html: String
get() = text.toHtml()
}
/**
* A group of [RichText]s. Serialized as a JSON array.
@@ -34,7 +50,12 @@ data class RichTextPlain(
@Serializable
data class RichTextGroup(
val parts: List<RichText>
) : RichText
) : RichText {
override val markdown: String
get() = parts.joinToString(separator = "") { it.markdown }
override val html: String
get() = parts.joinToString(separator = "") { it.html }
}
/**
* Any typed (formatted) part of a [RichText]. Serialized as a JSON object with the [type] discriminator.
@@ -43,8 +64,8 @@ data class RichTextGroup(
sealed interface RichTextEntity : RichText {
val type: String
val markdown: String
val html: String
override val markdown: String
override val html: String
}
object RichTextSerializer : KSerializer<RichText> {

View File

@@ -1,8 +1,5 @@
package dev.inmo.tgbotapi.types.rich
import dev.inmo.tgbotapi.types.internalUserLinkBeginning
import dev.inmo.tgbotapi.utils.extensions.toHtml
/**
* Characters which have a special meaning in the
* [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style) and must be escaped with a backslash
@@ -60,23 +57,3 @@ val RichText.source: String
is RichTextReference -> text.source
is RichTextReferenceLink -> text.source
}
/**
* [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style) representation of this [RichText].
*/
val RichText.markdown: String
get() = when (this) {
is RichTextPlain -> text.escapeRichMarkdown()
is RichTextGroup -> parts.joinToString(separator = "") { it.markdown }
is RichTextEntity -> markdown
}
/**
* [Rich HTML style](https://core.telegram.org/bots/api#rich-html-style) representation of this [RichText].
*/
val RichText.html: String
get() = when (this) {
is RichTextPlain -> text.toHtml()
is RichTextGroup -> parts.joinToString(separator = "") { it.html }
is RichTextEntity -> html
}