use std::fmt::Display;
use crate::types::MessageEntity;
#[derive(Debug, thiserror::Error)]
pub enum ErrorKind {
#[error("The text is empty")]
EmptyText,
#[error("Index out of bounds")]
IndexOutOfBounds,
}
pub(crate) fn split_by_entity(
text: &str,
entity: &MessageEntity,
) -> Result<(String, String, String), ErrorKind> {
let offset = usize::try_from(entity.offset()).map_err(|_| ErrorKind::IndexOutOfBounds)?;
let length = usize::try_from(entity.length()).map_err(|_| ErrorKind::IndexOutOfBounds)?;
let end = offset
.checked_add(length)
.ok_or(ErrorKind::IndexOutOfBounds)?;
let units: Vec<u16> = text.encode_utf16().collect();
if end > units.len() {
return Err(ErrorKind::IndexOutOfBounds);
}
let decode = |slice: &[u16]| String::from_utf16(slice).map_err(|_| ErrorKind::IndexOutOfBounds);
Ok((
decode(&units[..offset])?,
decode(&units[offset..end])?,
decode(&units[end..])?,
))
}
pub trait Formatter {
#[must_use]
fn bold<T>(&self, text: T) -> String
where
T: Display;
#[must_use]
fn italic<T>(&self, text: T) -> String
where
T: Display;
#[must_use]
fn code<C>(&self, code: C) -> String
where
C: Display;
#[must_use]
fn underline<T>(&self, text: T) -> String
where
T: Display;
#[must_use]
fn strikethrough<T>(&self, text: T) -> String
where
T: Display;
#[must_use]
fn spoiler<T>(&self, text: T) -> String
where
T: Display;
#[must_use]
fn blockquote<T>(&self, text: T) -> String
where
T: Display;
#[must_use]
fn expandable_blockquote<T>(&self, text: T) -> String
where
T: Display;
#[must_use]
fn text_link<T, U>(&self, text: T, url: U) -> String
where
T: Display,
U: Display;
#[must_use]
fn text_mention<T>(&self, text: T, user_id: i64) -> String
where
T: Display;
#[must_use]
fn custom_emoji<T, E>(&self, emoji: T, emoji_id: E) -> String
where
T: Display,
E: Display;
#[must_use]
fn pre<C>(&self, code: C) -> String
where
C: Display;
#[must_use]
fn pre_language<C, L>(&self, code: C, language: L) -> String
where
C: Display,
L: Display;
#[must_use]
fn date_time<T>(&self, text: T, unix_time: i64) -> String
where
T: Display;
#[must_use]
fn date_time_with_format<T, F>(&self, text: T, unix_time: i64, date_time_format: F) -> String
where
T: Display,
F: Display;
#[must_use]
fn quote<T>(&self, text: T) -> String
where
T: Display;
fn apply_entity<T>(&self, text: T, entity: &MessageEntity) -> Result<String, ErrorKind>
where
T: Display;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::MessageEntityBold;
struct TestFormatter;
impl Formatter for TestFormatter {
fn bold<T>(&self, text: T) -> String
where
T: Display,
{
format!("**{text}**")
}
fn apply_entity<T>(&self, text: T, entity: &MessageEntity) -> Result<String, ErrorKind>
where
T: Display,
{
let text = text.to_string();
let text_len = text.len();
if text_len == 0 {
return Err(ErrorKind::EmptyText);
}
let offset = entity.offset() as usize;
let length = entity.length() as usize;
if offset + length > text_len {
return Err(ErrorKind::IndexOutOfBounds);
}
let editable_text = &text[offset..offset + length];
if let MessageEntity::Bold(_) = entity {
} else {
unimplemented!();
}
let edited_text = self.bold(editable_text);
let mut text = text.to_owned();
text.replace_range(offset..offset + length, &edited_text);
Ok(text)
}
fn italic<T>(&self, _text: T) -> String
where
T: Display,
{
todo!()
}
fn code<C>(&self, _code: C) -> String
where
C: Display,
{
todo!()
}
fn underline<T>(&self, _text: T) -> String
where
T: Display,
{
todo!()
}
fn strikethrough<T>(&self, _text: T) -> String
where
T: Display,
{
todo!()
}
fn spoiler<T>(&self, _text: T) -> String
where
T: Display,
{
todo!()
}
fn blockquote<T>(&self, _text: T) -> String
where
T: Display,
{
todo!()
}
fn expandable_blockquote<T>(&self, _text: T) -> String
where
T: Display,
{
todo!()
}
fn text_link<T, U>(&self, _text: T, _url: U) -> String
where
T: Display,
U: Display,
{
todo!()
}
fn text_mention<T>(&self, _text: T, _user_id: i64) -> String
where
T: Display,
{
todo!()
}
fn custom_emoji<T, E>(&self, _emoji: T, _emoji_id: E) -> String
where
T: Display,
E: Display,
{
todo!()
}
fn pre<C>(&self, _code: C) -> String
where
C: Display,
{
todo!()
}
fn pre_language<C, L>(&self, _code: C, _language: L) -> String
where
C: Display,
L: Display,
{
todo!()
}
fn date_time<T>(&self, _text: T, _unix_time: i64) -> String
where
T: Display,
{
todo!()
}
fn date_time_with_format<T, F>(
&self,
_text: T,
_unix_time: i64,
_date_time_format: F,
) -> String
where
T: Display,
F: Display,
{
todo!()
}
fn quote<T>(&self, _text: T) -> String
where
T: Display,
{
todo!()
}
}
#[test]
fn test_apply_entity() {
let formatter = TestFormatter;
let text = "Hello, world!";
let entity = MessageEntity::Bold(MessageEntityBold::new(0, 5));
assert_eq!(
formatter.apply_entity(text, &entity).unwrap(),
"**Hello**, world!"
);
let entity = MessageEntity::Bold(MessageEntityBold::new(7, 5));
assert_eq!(
formatter.apply_entity(text, &entity).unwrap(),
"Hello, **world**!"
);
let entity = MessageEntity::Bold(MessageEntityBold::new(0, text.len() as i64));
assert_eq!(
formatter.apply_entity(text, &entity).unwrap(),
"**Hello, world!**"
);
}
#[test]
#[should_panic]
fn test_apply_entity_panic() {
let formatter = TestFormatter;
let text = "Hello, world!";
let entity = MessageEntity::Bold(MessageEntityBold::new(0, 15));
formatter.apply_entity(text, &entity).unwrap();
let entity = MessageEntity::Bold(MessageEntityBold::new(7, 9));
formatter.apply_entity(text, &entity).unwrap();
let entity = MessageEntity::Bold(MessageEntityBold::new(0, text.len() as i64 + 1));
formatter.apply_entity(text, &entity).unwrap();
let text = "";
formatter.apply_entity(text, &entity).unwrap();
}
#[test]
fn split_by_entity_uses_utf16_and_rejects_invalid_offsets() {
let text = "a😀b";
let entity = MessageEntity::Bold(MessageEntityBold::new(3, 1));
assert_eq!(
split_by_entity(text, &entity).unwrap(),
("a😀".to_owned(), "b".to_owned(), String::new())
);
let entity = MessageEntity::Bold(MessageEntityBold::new(-1, 1));
assert!(matches!(
split_by_entity(text, &entity),
Err(ErrorKind::IndexOutOfBounds)
));
let entity = MessageEntity::Bold(MessageEntityBold::new(0, 10));
assert!(matches!(
split_by_entity(text, &entity),
Err(ErrorKind::IndexOutOfBounds)
));
let entity = MessageEntity::Bold(MessageEntityBold::new(2, 1));
assert!(matches!(
split_by_entity(text, &entity),
Err(ErrorKind::IndexOutOfBounds)
));
}
}