mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI.git
synced 2026-07-03 16:46:06 +00:00
Add rich block markdown and html source builders
Add List<RichBlock>.toRichMarkdown() / toRichHtml() (and per-block RichBlock .markdown / .html plus RichTextInfo.markdown / .html convenience) that render the Rich Markdown style and Rich HTML style source strings for a whole rich message, ready to feed into InputRichMessageMarkdown / InputRichMessageHTML. All 21 block types are covered, including lists (bullet/ordered/task), tables, block/pull quotations, details, collages, slideshows, maps and media. Media blocks use the Telegram file_id as the source (documented), since Telegram only accepts HTTP(S) URLs for rich media and the parsed model carries no public URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34757,6 +34757,15 @@ public final class dev/inmo/tgbotapi/types/rich/RichBlockFooter$Companion {
|
||||
public final fun serializer ()Lkotlinx/serialization/KSerializer;
|
||||
}
|
||||
|
||||
public final class dev/inmo/tgbotapi/types/rich/RichBlockFormattingKt {
|
||||
public static final fun getHtml (Ldev/inmo/tgbotapi/types/rich/RichBlock;)Ljava/lang/String;
|
||||
public static final fun getHtml (Ldev/inmo/tgbotapi/types/rich/RichTextInfo;)Ljava/lang/String;
|
||||
public static final fun getMarkdown (Ldev/inmo/tgbotapi/types/rich/RichBlock;)Ljava/lang/String;
|
||||
public static final fun getMarkdown (Ldev/inmo/tgbotapi/types/rich/RichTextInfo;)Ljava/lang/String;
|
||||
public static final fun toRichHtml (Ljava/util/List;)Ljava/lang/String;
|
||||
public static final fun toRichMarkdown (Ljava/util/List;)Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class dev/inmo/tgbotapi/types/rich/RichBlockList : dev/inmo/tgbotapi/types/rich/RichBlock {
|
||||
public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichBlockList$Companion;
|
||||
public static final field TYPE Ljava/lang/String;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
/**
|
||||
* Builds the [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style) source string for this list
|
||||
* of [RichBlock]s. The resulting string may be passed to [InputRichMessageMarkdown].
|
||||
*
|
||||
* Media blocks ([RichBlockPhoto], [RichBlockVideo], [RichBlockAudio], [RichBlockVoiceNote] and [RichBlockAnimation]) are
|
||||
* rendered using the Telegram file_id as the media source. Telegram only accepts HTTP(S) URLs for rich media, so for
|
||||
* those blocks the output is a faithful structural representation rather than a directly sendable message.
|
||||
*/
|
||||
fun List<RichBlock>.toRichMarkdown(): String = joinToString(separator = "\n\n") { it.markdown }
|
||||
|
||||
/**
|
||||
* Builds the [Rich HTML style](https://core.telegram.org/bots/api#rich-html-style) source string for this list of
|
||||
* [RichBlock]s. The resulting string may be passed to [InputRichMessageHTML]. See [toRichMarkdown] for the media note.
|
||||
*/
|
||||
fun List<RichBlock>.toRichHtml(): String = joinToString(separator = "\n") { it.html }
|
||||
|
||||
/**
|
||||
* [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style) source of all the [RichTextInfo.blocks].
|
||||
*/
|
||||
val RichTextInfo.markdown: String
|
||||
get() = blocks.toRichMarkdown()
|
||||
|
||||
/**
|
||||
* [Rich HTML style](https://core.telegram.org/bots/api#rich-html-style) source of all the [RichTextInfo.blocks].
|
||||
*/
|
||||
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>"
|
||||
}
|
||||
|
||||
/**
|
||||
* [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>"
|
||||
}
|
||||
|
||||
private fun richOpenAttribute(isOpen: Boolean?): String = if (isOpen == true) " open" else ""
|
||||
|
||||
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 =
|
||||
list.items.mapIndexed { index, item ->
|
||||
val marker = when {
|
||||
item.hasCheckbox == true -> if (item.isChecked == true) "- [x] " else "- [ ] "
|
||||
item.labelType != null -> "${item.value ?: (index + 1)}. "
|
||||
else -> "- "
|
||||
}
|
||||
item.blocks.toRichMarkdown().lineSequence().mapIndexed { lineIndex, line ->
|
||||
if (lineIndex == 0) "$marker$line" else " $line"
|
||||
}.joinToString(separator = "\n")
|
||||
}.joinToString(separator = "\n")
|
||||
|
||||
private 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 ->
|
||||
val attributes = buildString {
|
||||
item.value?.let { append(" value=\"$it\"") }
|
||||
item.labelType?.let { append(" type=\"$it\"") }
|
||||
}
|
||||
val checkbox = if (item.hasCheckbox == true) {
|
||||
"<input type=\"checkbox\"${if (item.isChecked == true) " checked" else ""}>"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
"<li$attributes>$checkbox${item.blocks.toRichHtml()}</li>"
|
||||
}
|
||||
return "<$tag>$items</$tag>"
|
||||
}
|
||||
|
||||
private 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 {
|
||||
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 {
|
||||
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 =
|
||||
caption?.let { "" } ?: ""
|
||||
|
||||
private 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 {
|
||||
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 {
|
||||
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 {
|
||||
if (table.cells.isEmpty()) return ""
|
||||
fun renderRow(row: List<RichBlockTableCell>): String =
|
||||
row.joinToString(separator = " | ", prefix = "| ", postfix = " |") { it.text?.markdown ?: "" }
|
||||
fun alignment(cell: RichBlockTableCell): String = when (cell.align) {
|
||||
"left" -> ":---"
|
||||
"center" -> ":--:"
|
||||
"right" -> "---:"
|
||||
else -> "---"
|
||||
}
|
||||
val header = table.cells.first()
|
||||
val lines = mutableListOf(
|
||||
renderRow(header),
|
||||
header.joinToString(separator = " | ", prefix = "| ", postfix = " |") { alignment(it) }
|
||||
)
|
||||
table.cells.drop(1).forEach { lines.add(renderRow(it)) }
|
||||
return lines.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
private fun richBlockTableHtml(table: RichBlockTable): String {
|
||||
val attributes = buildString {
|
||||
if (table.isBordered == true) append(" bordered")
|
||||
if (table.isStriped == true) append(" striped")
|
||||
}
|
||||
val caption = table.caption?.let { "<caption>${it.html}</caption>" } ?: ""
|
||||
val rows = table.cells.joinToString(separator = "") { row ->
|
||||
val cells = row.joinToString(separator = "") { cell ->
|
||||
val tag = if (cell.isHeader == true) "th" else "td"
|
||||
val cellAttributes = buildString {
|
||||
cell.colspan?.let { append(" colspan=\"$it\"") }
|
||||
cell.rowspan?.let { append(" rowspan=\"$it\"") }
|
||||
append(" align=\"${cell.align}\"")
|
||||
append(" valign=\"${cell.valign}\"")
|
||||
}
|
||||
"<$tag$cellAttributes>${cell.text?.html ?: ""}</$tag>"
|
||||
}
|
||||
"<tr>$cells</tr>"
|
||||
}
|
||||
return "<table$attributes>$caption$rows</table>"
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class RichBlockFormattingTest {
|
||||
@Test
|
||||
fun paragraph() {
|
||||
val block = RichBlockParagraph(RichTextPlain("Hello"))
|
||||
assertEquals("Hello", block.markdown)
|
||||
assertEquals("<p>Hello</p>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun heading() {
|
||||
val block = RichBlockSectionHeading(RichTextPlain("Title"), 2)
|
||||
assertEquals("## Title", block.markdown)
|
||||
assertEquals("<h2>Title</h2>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun divider() {
|
||||
val block = RichBlockDivider()
|
||||
assertEquals("---", block.markdown)
|
||||
assertEquals("<hr/>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun footer() {
|
||||
val block = RichBlockFooter(RichTextPlain("f"))
|
||||
assertEquals("<footer>f</footer>", block.markdown)
|
||||
assertEquals("<footer>f</footer>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun preformatted() {
|
||||
val withLanguage = RichBlockPreformatted(RichTextPlain("code"), "kotlin")
|
||||
assertEquals("```kotlin\ncode\n```", withLanguage.markdown)
|
||||
assertEquals("<pre><code class=\"language-kotlin\">code</code></pre>", withLanguage.html)
|
||||
|
||||
val withoutLanguage = RichBlockPreformatted(RichTextPlain("c"))
|
||||
assertEquals("```\nc\n```", withoutLanguage.markdown)
|
||||
assertEquals("<pre>c</pre>", withoutLanguage.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mathematicalExpression() {
|
||||
val block = RichBlockMathematicalExpression("E=mc^2")
|
||||
assertEquals("\$\$E=mc^2\$\$", block.markdown)
|
||||
assertEquals("<tg-math-block>E=mc^2</tg-math-block>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anchor() {
|
||||
val block = RichBlockAnchor("top")
|
||||
assertEquals("<a name=\"top\"></a>", block.markdown)
|
||||
assertEquals("<a name=\"top\"></a>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bulletList() {
|
||||
val block = RichBlockList(listOf(RichBlockListItem("-", listOf(RichBlockParagraph(RichTextPlain("one"))))))
|
||||
assertEquals("- one", block.markdown)
|
||||
assertEquals("<ul><li><p>one</p></li></ul>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun orderedList() {
|
||||
val block = RichBlockList(
|
||||
listOf(RichBlockListItem("1", listOf(RichBlockParagraph(RichTextPlain("one"))), labelType = "1"))
|
||||
)
|
||||
assertEquals("1. one", block.markdown)
|
||||
assertEquals("<ol><li type=\"1\"><p>one</p></li></ol>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun taskList() {
|
||||
val block = RichBlockList(
|
||||
listOf(
|
||||
RichBlockListItem(
|
||||
"x",
|
||||
listOf(RichBlockParagraph(RichTextPlain("done"))),
|
||||
hasCheckbox = true,
|
||||
isChecked = true
|
||||
)
|
||||
)
|
||||
)
|
||||
assertEquals("- [x] done", block.markdown)
|
||||
assertEquals("<ul><li><input type=\"checkbox\" checked><p>done</p></li></ul>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun blockQuotation() {
|
||||
val block = RichBlockBlockQuotation(listOf(RichBlockParagraph(RichTextPlain("q"))))
|
||||
assertEquals("> q", block.markdown)
|
||||
assertEquals("<blockquote><p>q</p></blockquote>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun details() {
|
||||
val block = RichBlockDetails(RichTextPlain("sum"), listOf(RichBlockParagraph(RichTextPlain("body"))), isOpen = true)
|
||||
assertEquals("<details open><summary>sum</summary>\n\nbody\n\n</details>", block.markdown)
|
||||
assertEquals("<details open><summary>sum</summary><p>body</p></details>", block.html)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun table() {
|
||||
val block = RichBlockTable(
|
||||
listOf(
|
||||
listOf(
|
||||
RichBlockTableCell(text = RichTextPlain("H1"), isHeader = true, align = "left", valign = "top"),
|
||||
RichBlockTableCell(text = RichTextPlain("H2"), isHeader = true, align = "center", valign = "top")
|
||||
),
|
||||
listOf(
|
||||
RichBlockTableCell(text = RichTextPlain("a"), align = "left", valign = "top"),
|
||||
RichBlockTableCell(text = RichTextPlain("b"), align = "center", valign = "top")
|
||||
)
|
||||
)
|
||||
)
|
||||
assertEquals("| H1 | H2 |\n| :--- | :--: |\n| a | b |", block.markdown)
|
||||
assertEquals(
|
||||
"<table><tr><th align=\"left\" valign=\"top\">H1</th><th align=\"center\" valign=\"top\">H2</th></tr>" +
|
||||
"<tr><td align=\"left\" valign=\"top\">a</td><td align=\"center\" valign=\"top\">b</td></tr></table>",
|
||||
block.html
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listOfBlocksJoinsBlocks() {
|
||||
val blocks = listOf(RichBlockParagraph(RichTextPlain("a")), RichBlockDivider())
|
||||
assertEquals("a\n\n---", blocks.toRichMarkdown())
|
||||
assertEquals("<p>a</p>\n<hr/>", blocks.toRichHtml())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun richTextInfoDelegatesToBlocks() {
|
||||
val info = RichTextInfo(listOf(RichBlockParagraph(RichTextPlain("p")), RichBlockDivider()))
|
||||
assertEquals("p\n\n---", info.markdown)
|
||||
assertEquals("<p>p</p>\n<hr/>", info.html)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user