mirror of
https://github.com/teloxide/teloxide.git
synced 2024-10-23 17:36:54 +02:00
Merge branch 'master' into fix-stack-overflow
This commit is contained in:
commit
97319c887b
12 changed files with 657 additions and 44 deletions
|
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `req.reply_to` method to the new `crate::sugar::request::RequestReplyExt` trait
|
||||
- `req.disable_link_preview` method to the new `crate::sugar::request::RequestLinkPreviewExt` trait
|
||||
- `stack_size` setter to `DispatcherBuilder` ([PR 1185](https://github.com/teloxide/teloxide/pull/1185))
|
||||
- `utils::render` module to render HTML/Markdown-formatted output ([PR 1152](https://github.com/teloxide/teloxide/pull/1152))
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -563,4 +564,5 @@ This release was yanked because it accidentally [breaks backwards compatibility]
|
|||
## 0.1.0 - 2020-02-19
|
||||
|
||||
### Added
|
||||
|
||||
- This project.
|
||||
|
|
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -255,9 +255,9 @@ checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.30"
|
||||
version = "1.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
|
||||
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
@ -583,9 +583,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
|||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
@ -624,9 +624,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.30"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
@ -910,9 +910,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.4.1"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
|
||||
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
|
@ -1102,18 +1102,18 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.70"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
version = "0.2.161"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
|
@ -1276,9 +1276,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
|||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.66"
|
||||
version = "0.10.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
|
||||
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"cfg-if",
|
||||
|
@ -1308,9 +1308,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.103"
|
||||
version = "0.9.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
|
||||
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -1452,9 +1452,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.87"
|
||||
version = "1.0.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -1772,9 +1772,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.9.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
|
||||
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
|
@ -1789,9 +1789,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
|
@ -1875,9 +1875,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.128"
|
||||
version = "1.0.132"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
@ -2565,12 +2565,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
|||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.7.0"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
|
@ -2625,9 +2622,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
@ -2699,9 +2696,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.43"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
|
||||
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
|
@ -2753,9 +2750,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.70"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
|
||||
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
|
@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::types::{ChatAdministratorRights, RequestId};
|
||||
|
||||
/// This object defines the criteria used to request a suitable chat.
|
||||
///
|
||||
/// Information about the selected chat will be shared with the bot when the
|
||||
/// corresponding button is pressed. The bot will be granted requested rights in
|
||||
/// the chat if appropriate. [More about requesting chats »]
|
||||
|
|
|
@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::types::RequestId;
|
||||
|
||||
/// This object defines the criteria used to request a suitable users.
|
||||
///
|
||||
/// Information about the selected users will be shared with the bot when the
|
||||
/// corresponding button is pressed. More about requesting users »
|
||||
///
|
||||
|
|
|
@ -53,9 +53,6 @@ impl Future for StopFlag {
|
|||
type Output = ();
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
|
||||
self.project().0.poll(cx).map(|res| match res {
|
||||
Err(_aborted) => (),
|
||||
Ok(unreachable) => match unreachable {},
|
||||
})
|
||||
self.project().0.poll(cx).map(|_res| ())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
pub mod command;
|
||||
pub mod html;
|
||||
pub mod markdown;
|
||||
pub mod render;
|
||||
pub(crate) mod shutdown_token;
|
||||
|
||||
pub use teloxide_core::net::client_from_env;
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
use teloxide_core::types::{User, UserId};
|
||||
|
||||
pub(super) const ESCAPE_CHARS: [char; 19] = [
|
||||
'\\', '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!',
|
||||
];
|
||||
|
||||
/// Applies the bold font style to the string.
|
||||
///
|
||||
/// Passed string will not be automatically escaped because it can contain
|
||||
|
@ -119,11 +123,8 @@ pub fn code_inline(s: &str) -> String {
|
|||
#[must_use = "This function returns a new string, rather than mutating the argument, so calling it \
|
||||
without using its output does nothing useful"]
|
||||
pub fn escape(s: &str) -> String {
|
||||
const CHARS: [char; 18] =
|
||||
['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!'];
|
||||
|
||||
s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
|
||||
if CHARS.contains(&c) {
|
||||
if ESCAPE_CHARS.contains(&c) {
|
||||
s.push('\\');
|
||||
}
|
||||
s.push(c);
|
||||
|
@ -247,10 +248,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_escape() {
|
||||
assert_eq!(escape("\\!"), r"\\\!");
|
||||
assert_eq!(escape("* foobar *"), r"\* foobar \*");
|
||||
assert_eq!(
|
||||
escape(r"_ * [ ] ( ) ~ \ ` > # + - = | { } . !"),
|
||||
r"\_ \* \[ \] \( \) \~ \ \` \> \# \+ \- \= \| \{ \} \. \!",
|
||||
r"\_ \* \[ \] \( \) \~ \\ \` \> \# \+ \- \= \| \{ \} \. \!",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
250
crates/teloxide/src/utils/render.rs
Normal file
250
crates/teloxide/src/utils/render.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
//! Utils for rendering HTML and Markdown output.
|
||||
|
||||
use teloxide_core::types::{MessageEntity, MessageEntityKind as MEK};
|
||||
|
||||
use tag::*;
|
||||
|
||||
pub use helper::RenderMessageTextHelper;
|
||||
|
||||
mod helper;
|
||||
mod html;
|
||||
mod markdown;
|
||||
mod tag;
|
||||
|
||||
/// Parses text and message entities to produce the final formatted output.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct Renderer<'a> {
|
||||
text: &'a str,
|
||||
tags: Vec<Tag<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
/// Creates a new [`Renderer`] instance with given text and message
|
||||
/// entities.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `text`: The input text to be parsed.
|
||||
/// - `entities`: The message entities (formatting, links, etc.) to be
|
||||
/// applied to the text.
|
||||
#[must_use]
|
||||
pub fn new(text: &'a str, entities: &'a [MessageEntity]) -> Self {
|
||||
// get the needed size for the new tags that we want to parse from entities
|
||||
let needed_size: usize = entities
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
matches!(
|
||||
e.kind,
|
||||
MEK::Bold
|
||||
| MEK::Blockquote
|
||||
| MEK::Italic
|
||||
| MEK::Underline
|
||||
| MEK::Strikethrough
|
||||
| MEK::Spoiler
|
||||
| MEK::Code
|
||||
| MEK::Pre { .. }
|
||||
| MEK::TextLink { .. }
|
||||
| MEK::TextMention { .. }
|
||||
| MEK::CustomEmoji { .. }
|
||||
)
|
||||
})
|
||||
.count()
|
||||
* 2; // 2 because we insert two tags for each entity
|
||||
|
||||
let mut tags = Vec::with_capacity(needed_size);
|
||||
|
||||
for (index, entity) in entities.iter().enumerate() {
|
||||
let kind = match &entity.kind {
|
||||
MEK::Bold => Kind::Bold,
|
||||
MEK::Blockquote => Kind::Blockquote,
|
||||
MEK::Italic => Kind::Italic,
|
||||
MEK::Underline => Kind::Underline,
|
||||
MEK::Strikethrough => Kind::Strikethrough,
|
||||
MEK::Spoiler => Kind::Spoiler,
|
||||
MEK::Code => Kind::Code,
|
||||
MEK::Pre { language } => Kind::Pre(language.as_ref().map(String::as_str)),
|
||||
MEK::TextLink { url } => Kind::TextLink(url.as_str()),
|
||||
MEK::TextMention { user } => Kind::TextMention(user.id.0),
|
||||
MEK::CustomEmoji { custom_emoji_id } => Kind::CustomEmoji(custom_emoji_id),
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
// FIXME: maybe instead of clone store all the `kind`s in a seperate
|
||||
// vector and then just store the index here?
|
||||
tags.push(Tag::start(kind.clone(), entity.offset, index));
|
||||
tags.push(Tag::end(kind, entity.offset + entity.length, index));
|
||||
}
|
||||
|
||||
tags.sort_unstable();
|
||||
|
||||
Self { text, tags }
|
||||
}
|
||||
|
||||
/// Renders text with a given [`TagWriter`].
|
||||
///
|
||||
/// This method iterates through the text and the associated position tags
|
||||
/// and writes the text with the appropriate tags to a buffer, which is then
|
||||
/// returned as a `String`.
|
||||
///
|
||||
/// If input have no tags we just return the original text as-is.
|
||||
#[must_use]
|
||||
fn format(&self, writer: &TagWriter) -> String {
|
||||
if self.tags.is_empty() {
|
||||
return self.text.to_owned();
|
||||
}
|
||||
|
||||
let mut buffer =
|
||||
String::with_capacity(self.text.len() + writer.get_extra_size_for_tags(&self.tags));
|
||||
let mut tags = self.tags.iter();
|
||||
let mut current_tag = tags.next();
|
||||
|
||||
let mut prev_point = None;
|
||||
|
||||
for (idx, point) in self.text.encode_utf16().enumerate() {
|
||||
loop {
|
||||
match current_tag {
|
||||
Some(tag) if tag.offset == idx => {
|
||||
(writer.write_tag_fn)(tag, &mut buffer);
|
||||
current_tag = tags.next();
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
let ch = if let Some(previous) = prev_point.take() {
|
||||
char::decode_utf16([previous, point]).next().unwrap().unwrap()
|
||||
} else {
|
||||
match char::decode_utf16([point]).next().unwrap() {
|
||||
Ok(c) => c,
|
||||
Err(unpaired) => {
|
||||
prev_point = Some(unpaired.unpaired_surrogate());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(writer.write_char_fn)(ch, &mut buffer);
|
||||
}
|
||||
|
||||
for tag in current_tag.into_iter().chain(tags) {
|
||||
(writer.write_tag_fn)(tag, &mut buffer);
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Renders and returns the text as an **HTML-formatted** string.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn as_html(&self) -> String {
|
||||
self.format(&html::HTML)
|
||||
}
|
||||
|
||||
/// Renders and returns the text as a **Markdown-formatted** string.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn as_markdown(&self) -> String {
|
||||
self.format(&markdown::MARKDOWN)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_render_simple() {
|
||||
let text = "Bold italic <underline_";
|
||||
let entities = vec![
|
||||
MessageEntity { kind: MEK::Bold, offset: 0, length: 4 },
|
||||
MessageEntity { kind: MEK::Italic, offset: 5, length: 6 },
|
||||
MessageEntity { kind: MEK::Underline, offset: 12, length: 10 },
|
||||
];
|
||||
|
||||
let render = Renderer::new(text, &entities);
|
||||
|
||||
assert_eq!(render.as_html(), "<b>Bold</b> <i>italic</i> <u><underline</u>_");
|
||||
assert_eq!(render.as_markdown(), "**Bold** _\ritalic_\r __\r<underline__\r\\_");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_pre_with_lang() {
|
||||
let text = "Some pre, normal and rusty code";
|
||||
let entities = vec![
|
||||
MessageEntity { kind: MEK::Pre { language: None }, offset: 5, length: 3 },
|
||||
MessageEntity { kind: MEK::Code, offset: 10, length: 6 },
|
||||
MessageEntity {
|
||||
kind: MEK::Pre { language: Some("rust".to_owned()) },
|
||||
offset: 21,
|
||||
length: 5,
|
||||
},
|
||||
];
|
||||
|
||||
let render = Renderer::new(text, &entities);
|
||||
|
||||
assert_eq!(
|
||||
render.as_html(),
|
||||
"Some <pre>pre</pre>, <code>normal</code> and <pre><code \
|
||||
class=\"language-rust\">rusty</code></pre> code",
|
||||
);
|
||||
assert_eq!(
|
||||
render.as_markdown(),
|
||||
"Some ```\npre```\n, `normal` and ```rust\nrusty```\n code",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_nested() {
|
||||
let text = "Some bold both italics";
|
||||
let entities = vec![
|
||||
MessageEntity { kind: MEK::Bold, offset: 5, length: 9 },
|
||||
MessageEntity { kind: MEK::Italic, offset: 10, length: 12 },
|
||||
];
|
||||
|
||||
let render = Renderer::new(text, &entities);
|
||||
|
||||
assert_eq!(render.as_html(), "Some <b>bold <i>both</b> italics</i>");
|
||||
assert_eq!(render.as_markdown(), "Some **bold _\rboth** italics_\r");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_complex() {
|
||||
let text = "Hi how are you?\nnested entities are cool\nIm in a Blockquote!";
|
||||
let entities = vec![
|
||||
MessageEntity { kind: MEK::Bold, offset: 0, length: 2 },
|
||||
MessageEntity { kind: MEK::Italic, offset: 3, length: 3 },
|
||||
MessageEntity { kind: MEK::Underline, offset: 7, length: 3 },
|
||||
MessageEntity { kind: MEK::Strikethrough, offset: 11, length: 3 },
|
||||
MessageEntity { kind: MEK::Bold, offset: 16, length: 1 },
|
||||
MessageEntity { kind: MEK::Bold, offset: 17, length: 5 },
|
||||
MessageEntity { kind: MEK::Underline, offset: 17, length: 4 },
|
||||
MessageEntity { kind: MEK::Strikethrough, offset: 17, length: 4 },
|
||||
MessageEntity {
|
||||
kind: MEK::TextLink { url: reqwest::Url::parse("https://t.me/").unwrap() },
|
||||
offset: 23,
|
||||
length: 8,
|
||||
},
|
||||
MessageEntity {
|
||||
kind: MEK::TextLink { url: reqwest::Url::parse("tg://user?id=1234567").unwrap() },
|
||||
offset: 32,
|
||||
length: 3,
|
||||
},
|
||||
MessageEntity { kind: MEK::Code, offset: 36, length: 4 },
|
||||
MessageEntity { kind: MEK::Blockquote, offset: 41, length: 19 },
|
||||
];
|
||||
|
||||
let render = Renderer::new(text, &entities);
|
||||
|
||||
assert_eq!(
|
||||
render.as_html(),
|
||||
"<b>Hi</b> <i>how</i> <u>are</u> <s>you</s>?\n<b>n</b><b><u><s>este</s></u>d</b> \
|
||||
<a href=\"https://t.me/\">entities</a> <a href=\"tg://user?id=1234567\">are</a> <code>cool</code>\n\
|
||||
<blockquote>Im in a Blockquote!</blockquote>"
|
||||
);
|
||||
assert_eq!(
|
||||
render.as_markdown(),
|
||||
"**Hi** _\rhow_\r __\rare__\r ~you~?\n**n****__\r~este~__\rd** [entities](https://t.me/) \
|
||||
[are](tg://user?id=1234567) `cool`\n>Im in a Blockquote\\!"
|
||||
);
|
||||
}
|
||||
}
|
64
crates/teloxide/src/utils/render/helper.rs
Normal file
64
crates/teloxide/src/utils/render/helper.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
//! A helpful trait for rendering text/caption and entities back to HTML or
|
||||
//! Markdown.
|
||||
|
||||
use teloxide_core::types::Message;
|
||||
|
||||
use super::Renderer;
|
||||
|
||||
/// Generates HTML and Markdown representations of text and captions in a
|
||||
/// Telegram message.
|
||||
pub trait RenderMessageTextHelper {
|
||||
/// Returns the HTML representation of the message text, if the message
|
||||
/// contains text. This method will parse the text and any entities
|
||||
/// (such as bold, italic, links, etc.) and return the HTML-formatted
|
||||
/// string.
|
||||
#[must_use]
|
||||
fn html_text(&self) -> Option<String>;
|
||||
|
||||
/// Returns the Markdown representation of the message text, if the message
|
||||
/// contains text. This method will parse the text and any entities
|
||||
/// (such as bold, italic, links, etc.) and return the
|
||||
/// Markdown-formatted string.
|
||||
#[must_use]
|
||||
fn markdown_text(&self) -> Option<String>;
|
||||
|
||||
/// Returns the HTML representation of the message caption, if the message
|
||||
/// contains caption. This method will parse the caption and any
|
||||
/// entities (such as bold, italic, links, etc.) and return the
|
||||
/// HTML-formatted string.
|
||||
#[must_use]
|
||||
fn html_caption(&self) -> Option<String>;
|
||||
|
||||
/// Returns the Markdown representation of the message caption, if the
|
||||
/// message contains caption. This method will parse the caption and any
|
||||
/// entities (such as bold, italic, links, etc.) and return the
|
||||
/// Markdown-formatted string.
|
||||
#[must_use]
|
||||
fn markdown_caption(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl RenderMessageTextHelper for Message {
|
||||
fn html_text(&self) -> Option<String> {
|
||||
self.text()
|
||||
.zip(self.entities())
|
||||
.map(|(text, entities)| Renderer::new(text, entities).as_html())
|
||||
}
|
||||
|
||||
fn markdown_text(&self) -> Option<String> {
|
||||
self.text()
|
||||
.zip(self.entities())
|
||||
.map(|(text, entities)| Renderer::new(text, entities).as_markdown())
|
||||
}
|
||||
|
||||
fn html_caption(&self) -> Option<String> {
|
||||
self.caption()
|
||||
.zip(self.caption_entities())
|
||||
.map(|(text, entities)| Renderer::new(text, entities).as_html())
|
||||
}
|
||||
|
||||
fn markdown_caption(&self) -> Option<String> {
|
||||
self.caption()
|
||||
.zip(self.caption_entities())
|
||||
.map(|(text, entities)| Renderer::new(text, entities).as_markdown())
|
||||
}
|
||||
}
|
70
crates/teloxide/src/utils/render/html.rs
Normal file
70
crates/teloxide/src/utils/render/html.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use super::{ComplexTag, Kind, Place, SimpleTag, Tag, TagWriter};
|
||||
|
||||
pub static HTML: TagWriter = TagWriter {
|
||||
bold: SimpleTag::new("<b>", "</b>"),
|
||||
blockquote: SimpleTag::new("<blockquote>", "</blockquote>"),
|
||||
italic: SimpleTag::new("<i>", "</i>"),
|
||||
underline: SimpleTag::new("<u>", "</u>"),
|
||||
strikethrough: SimpleTag::new("<s>", "</s>"),
|
||||
spoiler: SimpleTag::new("<tg-spoiler>", "</tg-spoiler>"),
|
||||
code: SimpleTag::new("<code>", "</code>"),
|
||||
pre_no_lang: SimpleTag::new("<pre>", "</pre>"),
|
||||
pre: ComplexTag::new("<pre><code class=\"language-", "\">", "</code></pre>"),
|
||||
text_link: ComplexTag::new("<a href=\"", "\">", "</a>"),
|
||||
text_mention: ComplexTag::new("<a href=\"tg://user?id=", "\">", "</a>"),
|
||||
custom_emoji: ComplexTag::new("<tg-emoji emoji-id=\"", "\">", "</tg-emoji>"),
|
||||
write_tag_fn: write_tag,
|
||||
write_char_fn: write_char,
|
||||
};
|
||||
|
||||
fn write_tag(tag: &Tag, buf: &mut String) {
|
||||
match tag.kind {
|
||||
Kind::Bold => buf.push_str(HTML.bold.get_tag(tag.place)),
|
||||
Kind::Blockquote => buf.push_str(HTML.blockquote.get_tag(tag.place)),
|
||||
Kind::Italic => buf.push_str(HTML.italic.get_tag(tag.place)),
|
||||
Kind::Underline => buf.push_str(HTML.underline.get_tag(tag.place)),
|
||||
Kind::Strikethrough => buf.push_str(HTML.strikethrough.get_tag(tag.place)),
|
||||
Kind::Spoiler => buf.push_str(HTML.spoiler.get_tag(tag.place)),
|
||||
Kind::Code => buf.push_str(HTML.code.get_tag(tag.place)),
|
||||
Kind::Pre(lang) => match tag.place {
|
||||
Place::Start => match lang {
|
||||
Some(lang) => write!(buf, "{}{}{}", HTML.pre.start, lang, HTML.pre.middle).unwrap(),
|
||||
None => buf.push_str(HTML.pre_no_lang.start),
|
||||
},
|
||||
Place::End => buf.push_str(lang.map_or(HTML.pre_no_lang.end, |_| HTML.pre.end)),
|
||||
},
|
||||
Kind::TextLink(url) => match tag.place {
|
||||
Place::Start => {
|
||||
write!(buf, "{}{}{}", HTML.text_link.start, url, HTML.text_link.middle).unwrap()
|
||||
}
|
||||
Place::End => buf.push_str(HTML.text_link.end),
|
||||
},
|
||||
Kind::TextMention(id) => match tag.place {
|
||||
Place::Start => {
|
||||
write!(buf, "{}{}{}", HTML.text_mention.start, id, HTML.text_mention.middle)
|
||||
.unwrap()
|
||||
}
|
||||
Place::End => buf.push_str(HTML.text_mention.end),
|
||||
},
|
||||
Kind::CustomEmoji(custom_emoji_id) => match tag.place {
|
||||
Place::Start => write!(
|
||||
buf,
|
||||
"{}{}{}",
|
||||
HTML.custom_emoji.start, custom_emoji_id, HTML.custom_emoji.middle
|
||||
)
|
||||
.unwrap(),
|
||||
Place::End => buf.push_str(HTML.custom_emoji.end),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn write_char(ch: char, buf: &mut String) {
|
||||
match ch {
|
||||
'&' => buf.push_str("&"),
|
||||
'<' => buf.push_str("<"),
|
||||
'>' => buf.push_str(">"),
|
||||
c => buf.push(c),
|
||||
}
|
||||
}
|
73
crates/teloxide/src/utils/render/markdown.rs
Normal file
73
crates/teloxide/src/utils/render/markdown.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use crate::utils::markdown::ESCAPE_CHARS;
|
||||
|
||||
use super::{ComplexTag, Kind, Place, SimpleTag, Tag, TagWriter};
|
||||
|
||||
pub static MARKDOWN: TagWriter = TagWriter {
|
||||
bold: SimpleTag::new("**", "**"),
|
||||
blockquote: SimpleTag::new(">", ""),
|
||||
italic: SimpleTag::new("_\r", "_\r"),
|
||||
underline: SimpleTag::new("__\r", "__\r"),
|
||||
strikethrough: SimpleTag::new("~", "~"),
|
||||
spoiler: SimpleTag::new("||", "||"),
|
||||
code: SimpleTag::new("`", "`"),
|
||||
pre_no_lang: SimpleTag::new("```\n", "```\n"),
|
||||
pre: ComplexTag::new("```", "\n", "```\n"),
|
||||
text_link: ComplexTag::new("[", "](", ")"),
|
||||
text_mention: ComplexTag::new("[", "](tg://user?id=", ")"),
|
||||
custom_emoji: ComplexTag::new("[", "](tg://emoji?id=", ")"),
|
||||
write_tag_fn: write_tag,
|
||||
write_char_fn: write_char,
|
||||
};
|
||||
|
||||
fn write_tag(tag: &Tag, buf: &mut String) {
|
||||
match tag.kind {
|
||||
Kind::Bold => buf.push_str(MARKDOWN.bold.get_tag(tag.place)),
|
||||
Kind::Blockquote => buf.push_str(MARKDOWN.blockquote.get_tag(tag.place)),
|
||||
Kind::Italic => buf.push_str(MARKDOWN.italic.get_tag(tag.place)),
|
||||
Kind::Underline => buf.push_str(MARKDOWN.underline.get_tag(tag.place)),
|
||||
Kind::Strikethrough => buf.push_str(MARKDOWN.strikethrough.get_tag(tag.place)),
|
||||
Kind::Spoiler => buf.push_str(MARKDOWN.spoiler.get_tag(tag.place)),
|
||||
Kind::Code => buf.push_str(MARKDOWN.code.get_tag(tag.place)),
|
||||
Kind::Pre(lang) => match tag.place {
|
||||
Place::Start => match lang {
|
||||
Some(lang) => {
|
||||
write!(buf, "{}{}{}", MARKDOWN.pre.start, lang, MARKDOWN.pre.middle).unwrap()
|
||||
}
|
||||
None => buf.push_str(MARKDOWN.pre_no_lang.start),
|
||||
},
|
||||
Place::End => buf.push_str(lang.map_or(MARKDOWN.pre_no_lang.end, |_| MARKDOWN.pre.end)),
|
||||
},
|
||||
Kind::TextLink(url) => match tag.place {
|
||||
Place::Start => buf.push_str(MARKDOWN.text_link.start),
|
||||
Place::End => {
|
||||
write!(buf, "{}{}{}", MARKDOWN.text_link.middle, url, MARKDOWN.text_link.end)
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
Kind::TextMention(id) => match tag.place {
|
||||
Place::Start => buf.push_str(MARKDOWN.text_mention.start),
|
||||
Place::End => {
|
||||
write!(buf, "{}{}{}", MARKDOWN.text_mention.middle, id, MARKDOWN.text_mention.end)
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
Kind::CustomEmoji(custom_emoji_id) => match tag.place {
|
||||
Place::Start => buf.push_str(MARKDOWN.custom_emoji.start),
|
||||
Place::End => write!(
|
||||
buf,
|
||||
"{}{}{}",
|
||||
MARKDOWN.custom_emoji.middle, custom_emoji_id, MARKDOWN.custom_emoji.end
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn write_char(ch: char, buf: &mut String) {
|
||||
if ESCAPE_CHARS.contains(&ch) {
|
||||
buf.push('\\');
|
||||
}
|
||||
buf.push(ch);
|
||||
}
|
155
crates/teloxide/src/utils/render/tag.rs
Normal file
155
crates/teloxide/src/utils/render/tag.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Tag<'a> {
|
||||
pub place: Place,
|
||||
pub kind: Kind<'a>,
|
||||
pub offset: usize,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Tag<'a> {
|
||||
#[inline(always)]
|
||||
pub const fn start(kind: Kind<'a>, offset: usize, index: usize) -> Self {
|
||||
Self { place: Place::Start, kind, offset, index }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn end(kind: Kind<'a>, offset: usize, index: usize) -> Self {
|
||||
Self { place: Place::End, kind, offset, index }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Eq for Tag<'a> {}
|
||||
|
||||
impl<'a> PartialEq for Tag<'a> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// We don't check kind here
|
||||
self.place == other.place && self.offset == other.offset && self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Ord for Tag<'a> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.offset.cmp(&other.offset).then_with(|| self.place.cmp(&other.place)).then_with(|| {
|
||||
match other.place {
|
||||
Place::Start => self.index.cmp(&other.index),
|
||||
Place::End => other.index.cmp(&self.index),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialOrd for Tag<'a> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Place {
|
||||
// HACK: `End` needs to be first because of the `Ord` Implementation.
|
||||
// the reason is when comparing tags we want the `End` to be first if the offset
|
||||
// is the same.
|
||||
End,
|
||||
Start,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum Kind<'a> {
|
||||
Bold,
|
||||
Blockquote,
|
||||
Italic,
|
||||
Underline,
|
||||
Strikethrough,
|
||||
Spoiler,
|
||||
Code,
|
||||
Pre(Option<&'a str>),
|
||||
TextLink(&'a str),
|
||||
TextMention(u64),
|
||||
CustomEmoji(&'a str),
|
||||
}
|
||||
|
||||
pub struct SimpleTag {
|
||||
pub start: &'static str,
|
||||
pub end: &'static str,
|
||||
}
|
||||
|
||||
impl SimpleTag {
|
||||
#[inline]
|
||||
pub const fn new(start: &'static str, end: &'static str) -> Self {
|
||||
Self { start, end }
|
||||
}
|
||||
|
||||
pub const fn get_tag(&self, place: Place) -> &'static str {
|
||||
match place {
|
||||
Place::Start => self.start,
|
||||
Place::End => self.end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ComplexTag {
|
||||
pub start: &'static str,
|
||||
pub middle: &'static str,
|
||||
pub end: &'static str,
|
||||
}
|
||||
|
||||
impl ComplexTag {
|
||||
#[inline]
|
||||
pub const fn new(start: &'static str, middle: &'static str, end: &'static str) -> Self {
|
||||
Self { start, middle, end }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TagWriter {
|
||||
pub bold: SimpleTag,
|
||||
pub blockquote: SimpleTag,
|
||||
pub italic: SimpleTag,
|
||||
pub underline: SimpleTag,
|
||||
pub strikethrough: SimpleTag,
|
||||
pub spoiler: SimpleTag,
|
||||
pub code: SimpleTag,
|
||||
pub pre_no_lang: SimpleTag,
|
||||
pub pre: ComplexTag,
|
||||
pub text_link: ComplexTag,
|
||||
pub text_mention: ComplexTag,
|
||||
pub custom_emoji: ComplexTag,
|
||||
pub write_tag_fn: fn(&Tag, buf: &mut String),
|
||||
pub write_char_fn: fn(char, buf: &mut String),
|
||||
}
|
||||
|
||||
impl TagWriter {
|
||||
pub fn get_extra_size_for_tags(&self, tags: &[Tag]) -> usize {
|
||||
tags.iter()
|
||||
.map(|tag| match tag.kind {
|
||||
Kind::Bold => self.bold.get_tag(tag.place).len(),
|
||||
Kind::Blockquote => self.blockquote.get_tag(tag.place).len(),
|
||||
Kind::Italic => self.italic.get_tag(tag.place).len(),
|
||||
Kind::Underline => self.underline.get_tag(tag.place).len(),
|
||||
Kind::Strikethrough => self.strikethrough.get_tag(tag.place).len(),
|
||||
Kind::Spoiler => self.spoiler.get_tag(tag.place).len(),
|
||||
Kind::Code => self.code.get_tag(tag.place).len(),
|
||||
Kind::Pre(lang) => match tag.place {
|
||||
Place::Start => lang
|
||||
.map_or(self.pre_no_lang.start.len(), |l| self.pre.start.len() + l.len()),
|
||||
Place::End => lang.map_or(self.pre_no_lang.end.len(), |_| {
|
||||
self.pre.middle.len() + self.pre.end.len()
|
||||
}),
|
||||
},
|
||||
Kind::TextLink(url) => match tag.place {
|
||||
Place::Start => self.text_link.start.len() + url.len(),
|
||||
Place::End => self.text_link.middle.len() + self.text_link.end.len(),
|
||||
},
|
||||
Kind::TextMention(id) => match tag.place {
|
||||
Place::Start => self.text_mention.start.len() + id.ilog10() as usize + 1,
|
||||
Place::End => self.text_mention.middle.len() + self.text_mention.end.len(),
|
||||
},
|
||||
Kind::CustomEmoji(custom_emoji_id) => match tag.place {
|
||||
Place::Start => self.custom_emoji.start.len() + custom_emoji_id.len(),
|
||||
Place::End => self.custom_emoji.middle.len() + self.custom_emoji.end.len(),
|
||||
},
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue