From 4cfc0545268100001d6eb43bda9b486cbd59b4b7 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 30 Jun 2026 17:02:03 +0600 Subject: [PATCH] Implement markdown and html for RichTextEntity inheritors Add Rich Markdown style and Rich HTML style serialization for every RichTextEntity subtype, following the Bot API rich formatting spec (https://core.telegram.org/bots/api#rich-markdown-style and #rich-html-style). New RichTextFormatting.kt provides: * String.escapeRichMarkdown() escaping the rich-markdown special characters; * RichText.source - plain unformatted text of any RichText; * RichText.markdown / RichText.html - recursive dispatch over RichTextPlain, RichTextGroup and RichTextEntity so inner texts render correctly. Each of the 25 entity types now overrides markdown and html. Auto-detected entities (mention, hashtag, cashtag, bank card, bot command) emit their visible text; the rest wrap inner text in the corresponding markers/tags. Co-Authored-By: Claude Opus 4.8 --- tgbotapi.core/api/tgbotapi.core.api | 59 ++++++++ .../dev/inmo/tgbotapi/types/rich/RichText.kt | 3 + .../tgbotapi/types/rich/RichTextEntities.kt | 127 ++++++++++++++++++ .../tgbotapi/types/rich/RichTextFormatting.kt | 82 +++++++++++ 4 files changed, 271 insertions(+) create mode 100644 tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormatting.kt diff --git a/tgbotapi.core/api/tgbotapi.core.api b/tgbotapi.core/api/tgbotapi.core.api index a87db69043..f1373e71a8 100644 --- a/tgbotapi.core/api/tgbotapi.core.api +++ b/tgbotapi.core/api/tgbotapi.core.api @@ -35270,6 +35270,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextAnchor : dev/inmo/tgbota public final fun copy (Ljava/lang/String;)Ldev/inmo/tgbotapi/types/rich/RichTextAnchor; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextAnchor;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextAnchor; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35301,6 +35303,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextAnchorLink : dev/inmo/tg public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextAnchorLink;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextAnchorLink; public fun equals (Ljava/lang/Object;)Z public final fun getAnchorName ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35332,6 +35336,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextBankCardNumber : dev/inm public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextBankCardNumber;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextBankCardNumber; public fun equals (Ljava/lang/Object;)Z public final fun getBankCardNumber ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35361,6 +35367,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextBold : dev/inmo/tgbotapi public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextBold; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextBold;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextBold; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35392,6 +35400,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextBotCommand : dev/inmo/tg public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextBotCommand;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextBotCommand; public fun equals (Ljava/lang/Object;)Z public final fun getBotCommand ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35423,6 +35433,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextCashtag : dev/inmo/tgbot public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextCashtag;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextCashtag; public fun equals (Ljava/lang/Object;)Z public final fun getCashtag ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35452,6 +35464,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextCode : dev/inmo/tgbotapi public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextCode; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextCode;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextCode; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35484,6 +35498,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextCustomEmoji : dev/inmo/t public fun equals (Ljava/lang/Object;)Z public final fun getAlternativeText ()Ljava/lang/String; public final fun getCustomEmojiId-dDnjveI ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public fun getType ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -35515,6 +35531,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextDateTime : dev/inmo/tgbo public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextDateTime;Ldev/inmo/tgbotapi/types/rich/RichText;JLjava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextDateTime; public fun equals (Ljava/lang/Object;)Z public final fun getDateTimeFormat ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public final fun getUnixTime ()J @@ -35547,6 +35565,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextEmailAddress : dev/inmo/ public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextEmailAddress;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextEmailAddress; public fun equals (Ljava/lang/Object;)Z public final fun getEmailAddress ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35570,6 +35590,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextEmailAddress$Companion { public abstract interface class dev/inmo/tgbotapi/types/rich/RichTextEntity : dev/inmo/tgbotapi/types/rich/RichText { public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichTextEntity$Companion; + public abstract fun getHtml ()Ljava/lang/String; + public abstract fun getMarkdown ()Ljava/lang/String; public abstract fun getType ()Ljava/lang/String; } @@ -35581,6 +35603,13 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextEntitySerializer : kotli public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/RichTextEntitySerializer; } +public final class dev/inmo/tgbotapi/types/rich/RichTextFormattingKt { + public static final fun escapeRichMarkdown (Ljava/lang/String;)Ljava/lang/String; + public static final fun getHtml (Ldev/inmo/tgbotapi/types/rich/RichText;)Ljava/lang/String; + public static final fun getMarkdown (Ldev/inmo/tgbotapi/types/rich/RichText;)Ljava/lang/String; + public static final fun getSource (Ldev/inmo/tgbotapi/types/rich/RichText;)Ljava/lang/String; +} + public final class dev/inmo/tgbotapi/types/rich/RichTextGroup : dev/inmo/tgbotapi/types/rich/RichText { public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichTextGroup$Companion; public fun (Ljava/util/List;)V @@ -35618,6 +35647,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextHashtag : dev/inmo/tgbot public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextHashtag;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextHashtag; public fun equals (Ljava/lang/Object;)Z public final fun getHashtag ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35677,6 +35708,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextItalic : dev/inmo/tgbota public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextItalic; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextItalic;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextItalic; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35706,6 +35739,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextMarked : dev/inmo/tgbota public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextMarked; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextMarked;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextMarked; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35736,6 +35771,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextMathematicalExpression : public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextMathematicalExpression;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextMathematicalExpression; public fun equals (Ljava/lang/Object;)Z public final fun getExpression ()Ljava/lang/String; + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public fun getType ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -35765,6 +35802,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextMention : dev/inmo/tgbot public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;)Ldev/inmo/tgbotapi/types/rich/RichTextMention; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextMention;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextMention; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public final fun getUsername ()Ljava/lang/String; @@ -35796,6 +35835,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextPhoneNumber : dev/inmo/t public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;)Ldev/inmo/tgbotapi/types/rich/RichTextPhoneNumber; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextPhoneNumber;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextPhoneNumber; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getPhoneNumber ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; @@ -35854,6 +35895,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextReference : dev/inmo/tgb public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;)Ldev/inmo/tgbotapi/types/rich/RichTextReference; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextReference;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextReference; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; @@ -35885,6 +35928,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextReferenceLink : dev/inmo public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;)Ldev/inmo/tgbotapi/types/rich/RichTextReferenceLink; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextReferenceLink;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextReferenceLink; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getReferenceName ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; @@ -35924,6 +35969,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextSpoiler : dev/inmo/tgbot public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextSpoiler; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextSpoiler;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextSpoiler; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35953,6 +36000,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextStrikethrough : dev/inmo public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextStrikethrough; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextStrikethrough;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextStrikethrough; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -35982,6 +36031,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextSubscript : dev/inmo/tgb public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextSubscript; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextSubscript;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextSubscript; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -36011,6 +36062,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextSuperscript : dev/inmo/t public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextSuperscript; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextSuperscript;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextSuperscript; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -36041,6 +36094,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextTextMention : dev/inmo/t public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;Ldev/inmo/tgbotapi/types/chat/User;)Ldev/inmo/tgbotapi/types/rich/RichTextTextMention; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextTextMention;Ldev/inmo/tgbotapi/types/rich/RichText;Ldev/inmo/tgbotapi/types/chat/User;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextTextMention; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public final fun getUser ()Ldev/inmo/tgbotapi/types/chat/User; @@ -36071,6 +36126,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextUnderline : dev/inmo/tgb public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;)Ldev/inmo/tgbotapi/types/rich/RichTextUnderline; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextUnderline;Ldev/inmo/tgbotapi/types/rich/RichText;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextUnderline; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public fun hashCode ()I @@ -36101,6 +36158,8 @@ public final class dev/inmo/tgbotapi/types/rich/RichTextUrl : dev/inmo/tgbotapi/ public final fun copy (Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;)Ldev/inmo/tgbotapi/types/rich/RichTextUrl; public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/RichTextUrl;Ldev/inmo/tgbotapi/types/rich/RichText;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/RichTextUrl; public fun equals (Ljava/lang/Object;)Z + public fun getHtml ()Ljava/lang/String; + public fun getMarkdown ()Ljava/lang/String; public final fun getText ()Ldev/inmo/tgbotapi/types/rich/RichText; public fun getType ()Ljava/lang/String; public final fun getUrl ()Ljava/lang/String; diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt index d81064de88..f270d7dada 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichText.kt @@ -42,6 +42,9 @@ data class RichTextGroup( @Serializable(RichTextEntitySerializer::class) sealed interface RichTextEntity : RichText { val type: String + + val markdown: String + val html: String } object RichTextSerializer : KSerializer { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt index 251075be16..67f77caa8c 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextEntities.kt @@ -12,6 +12,7 @@ import dev.inmo.tgbotapi.types.dateTimeFormatField import dev.inmo.tgbotapi.types.emailAddressField import dev.inmo.tgbotapi.types.expressionField import dev.inmo.tgbotapi.types.hashtagField +import dev.inmo.tgbotapi.types.internalUserLinkBeginning import dev.inmo.tgbotapi.types.nameField import dev.inmo.tgbotapi.types.phoneNumberField import dev.inmo.tgbotapi.types.referenceNameField @@ -21,6 +22,7 @@ import dev.inmo.tgbotapi.types.unixTimeField import dev.inmo.tgbotapi.types.urlField import dev.inmo.tgbotapi.types.userField import dev.inmo.tgbotapi.types.usernameField +import dev.inmo.tgbotapi.utils.extensions.toHtml import kotlinx.serialization.EncodeDefault import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -39,6 +41,11 @@ data class RichTextBold( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "**${text.markdown}**" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "bold" } @@ -58,6 +65,11 @@ data class RichTextItalic( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "*${text.markdown}*" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "italic" } @@ -77,6 +89,11 @@ data class RichTextUnderline( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "${text.markdown}" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "underline" } @@ -96,6 +113,11 @@ data class RichTextStrikethrough( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "~~${text.markdown}~~" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "strikethrough" } @@ -115,6 +137,11 @@ data class RichTextSpoiler( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "||${text.markdown}||" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "spoiler" } @@ -134,6 +161,11 @@ data class RichTextSubscript( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "${text.markdown}" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "subscript" } @@ -153,6 +185,11 @@ data class RichTextSuperscript( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "${text.markdown}" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "superscript" } @@ -172,6 +209,11 @@ data class RichTextMarked( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "==${text.markdown}==" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "marked" } @@ -191,6 +233,11 @@ data class RichTextCode( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "`${text.source}`" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "code" } @@ -214,6 +261,11 @@ data class RichTextDateTime( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "![${text.markdown}](tg://time?unix=$unixTime&format=$dateTimeFormat)" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "date_time" } @@ -235,6 +287,11 @@ data class RichTextTextMention( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "[${text.markdown}]($internalUserLinkBeginning${user.id.chatId.long})" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "text_mention" } @@ -256,6 +313,11 @@ data class RichTextCustomEmoji( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "![${alternativeText.escapeRichMarkdown()}](tg://emoji?id=${customEmojiId.string})" + override val html: String + get() = "${alternativeText.toHtml()}" + companion object { const val TYPE = "custom_emoji" } @@ -275,6 +337,11 @@ data class RichTextMathematicalExpression( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "\$$expression\$" + override val html: String + get() = "$expression" + companion object { const val TYPE = "mathematical_expression" } @@ -296,6 +363,11 @@ data class RichTextUrl( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "[${text.markdown}]($url)" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "url" } @@ -317,6 +389,11 @@ data class RichTextEmailAddress( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "[${text.markdown}](mailto:$emailAddress)" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "email_address" } @@ -338,6 +415,11 @@ data class RichTextPhoneNumber( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "[${text.markdown}](tel:$phoneNumber)" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "phone_number" } @@ -359,6 +441,11 @@ data class RichTextBankCardNumber( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = text.markdown + override val html: String + get() = text.html + companion object { const val TYPE = "bank_card_number" } @@ -380,6 +467,11 @@ data class RichTextMention( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = text.markdown + override val html: String + get() = text.html + companion object { const val TYPE = "mention" } @@ -401,6 +493,11 @@ data class RichTextHashtag( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = text.markdown + override val html: String + get() = text.html + companion object { const val TYPE = "hashtag" } @@ -422,6 +519,11 @@ data class RichTextCashtag( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = text.markdown + override val html: String + get() = text.html + companion object { const val TYPE = "cashtag" } @@ -443,6 +545,11 @@ data class RichTextBotCommand( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = text.markdown + override val html: String + get() = text.html + companion object { const val TYPE = "bot_command" } @@ -462,6 +569,11 @@ data class RichTextAnchor( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "" + override val html: String + get() = "" + companion object { const val TYPE = "anchor" } @@ -483,6 +595,11 @@ data class RichTextAnchorLink( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "[${text.markdown}](#$anchorName)" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "anchor_link" } @@ -504,6 +621,11 @@ data class RichTextReference( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "${text.markdown}" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "reference" } @@ -525,6 +647,11 @@ data class RichTextReferenceLink( @SerialName(typeField) override val type: String = TYPE + override val markdown: String + get() = "[${text.markdown}](#$referenceName)" + override val html: String + get() = "${text.html}" + companion object { const val TYPE = "reference_link" } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormatting.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormatting.kt new file mode 100644 index 0000000000..69fbe2da81 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/RichTextFormatting.kt @@ -0,0 +1,82 @@ +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 + * to be represented literally. + */ +private val richMarkdownSpecialCharacters = setOf( + '\\', '`', '*', '_', '~', '|', '[', ']', '(', ')', '<', '>', '#', '=', '!', '$' +) + +/** + * Escapes all the [richMarkdownSpecialCharacters] of the receiver with a backslash so that the resulting string is + * represented literally in the [Rich Markdown style](https://core.telegram.org/bots/api#rich-markdown-style). + */ +fun String.escapeRichMarkdown(): String = buildString { + for (character in this@escapeRichMarkdown) { + if (character in richMarkdownSpecialCharacters) { + append('\\') + } + append(character) + } +} + +/** + * Plain (unformatted) text of this [RichText]. For [RichTextEntity]s without an inner [RichText] it falls back to the + * most meaningful textual representation: alternative text for custom emojis, the expression for mathematical + * expressions and an empty string for anchors. + */ +val RichText.source: String + get() = when (this) { + is RichTextPlain -> text + is RichTextGroup -> parts.joinToString(separator = "") { it.source } + is RichTextCustomEmoji -> alternativeText + is RichTextMathematicalExpression -> expression + is RichTextAnchor -> "" + is RichTextBold -> text.source + is RichTextItalic -> text.source + is RichTextUnderline -> text.source + is RichTextStrikethrough -> text.source + is RichTextSpoiler -> text.source + is RichTextSubscript -> text.source + is RichTextSuperscript -> text.source + is RichTextMarked -> text.source + is RichTextCode -> text.source + is RichTextDateTime -> text.source + is RichTextTextMention -> text.source + is RichTextUrl -> text.source + is RichTextEmailAddress -> text.source + is RichTextPhoneNumber -> text.source + is RichTextBankCardNumber -> text.source + is RichTextMention -> text.source + is RichTextHashtag -> text.source + is RichTextCashtag -> text.source + is RichTextBotCommand -> text.source + is RichTextAnchorLink -> text.source + 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 + }