diff --git a/src/model/richtext.rs b/src/model/richtext.rs
index 63081eb..9a497a6 100644
--- a/src/model/richtext.rs
+++ b/src/model/richtext.rs
@@ -57,6 +57,18 @@ pub trait ToHtml {
fn to_html_yt_host(&self, yt_host: &str) -> String;
}
+/// Trait for converting rich text to markdown.
+pub trait ToMarkdown {
+ /// Convert rich text to markdown.
+ fn to_markdown(&self) -> String {
+ self.to_markdown_yt_host("https://www.youtube.com")
+ }
+ /// Convert rich text to markdown while changing YouTube links to a custom site.
+ ///
+ /// expected yt_host format (no trailing slash): `https://example.com`
+ fn to_markdown_yt_host(&self, yt_host: &str) -> String;
+}
+
impl TextComponent {
/// Get the text from the component
pub fn get_text(&self) -> &str {
@@ -110,6 +122,21 @@ impl ToHtml for TextComponent {
}
}
+impl ToMarkdown for TextComponent {
+ fn to_markdown_yt_host(&self, yt_host: &str) -> String {
+ match self {
+ TextComponent::Text(text) => util::escape_markdown(text),
+ TextComponent::Web { text, .. } | TextComponent::YouTube { text, .. } => {
+ format!(
+ "[{}]({})",
+ util::escape_markdown(text),
+ self.get_url(yt_host)
+ )
+ }
+ }
+ }
+}
+
impl ToPlaintext for RichText {
fn to_plaintext_yt_host(&self, yt_host: &str) -> String {
self.0
@@ -125,6 +152,15 @@ impl ToHtml for RichText {
}
}
+impl ToMarkdown for RichText {
+ fn to_markdown_yt_host(&self, yt_host: &str) -> String {
+ self.0
+ .iter()
+ .map(|c| c.to_markdown_yt_host(yt_host))
+ .collect()
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -170,7 +206,7 @@ mod tests {
});
#[test]
- fn t_to_plaintext() {
+ fn to_plaintext() {
let richtext = RichText::from(TEXT_SOURCE.clone());
let plaintext = richtext.to_plaintext_yt_host("https://piped.kavin.rocks");
assert_eq!(
@@ -197,7 +233,7 @@ aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"#
}
#[test]
- fn t_to_html() {
+ fn to_html() {
let richtext = RichText::from(TEXT_SOURCE.clone());
let html = richtext.to_html_yt_host("https://piped.kavin.rocks");
assert_eq!(
@@ -205,4 +241,31 @@ aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"#
"🎧Listen and download aespa's debut single "Black Mamba": https://smarturl.it/aespa_BlackMamba
🐍The Debut Stage https://youtu.be/Ky5RT5oGg0w
🎟\u{fe0f} aespa Showcase SYNK in LA! Tickets now on sale: https://www.ticketmaster.com/event/0A...
Subscribe to aespa Official YouTube Channel!
https://www.youtube.com/aespa?sub_con...
aespa official
https://www.youtube.com/c/aespa
https://www.instagram.com/aespa_official
https://www.tiktok.com/@aespa_official
https://twitter.com/aespa_Official
https://www.facebook.com/aespa.official
https://weibo.com/aespa
#aespa #æspa #BlackMamba #블랙맘바 #에스파
aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"
);
}
+
+ #[test]
+ fn to_markdown() {
+ let richtext = RichText::from(TEXT_SOURCE.clone());
+ let markdown = richtext.to_markdown_yt_host("https://piped.kavin.rocks");
+ assert_eq!(
+ markdown,
+ r#"🎧Listen and download aespa's debut single "Black Mamba": [https://smarturl.it/aespa\_BlackMamba](https://smarturl.it/aespa_BlackMamba)
+🐍The Debut Stage [https://youtu.be/Ky5RT5oGg0w](https://piped.kavin.rocks/watch?v=Ky5RT5oGg0w)
+
+🎟️ aespa Showcase SYNK in LA! Tickets now on sale: [https://www.ticketmaster.com/event/0A...](https://www.ticketmaster.com/event/0A005CCD9E871F6E)
+
+Subscribe to aespa Official YouTube Channel!
+[https://www.youtube.com/aespa?sub\_con...](https://www.youtube.com/aespa?sub_confirmation=1)
+
+aespa official
+[https://www.youtube.com/c/aespa](https://www.youtube.com/c/aespa)
+[https://www.instagram.com/aespa\_official](https://www.instagram.com/aespa_official)
+[https://www.tiktok.com/@aespa\_official](https://www.tiktok.com/@aespa_official)
+[https://twitter.com/aespa\_Official](https://twitter.com/aespa_Official)
+[https://www.facebook.com/aespa.official](https://www.facebook.com/aespa.official)
+[https://weibo.com/aespa](https://weibo.com/aespa)
+
+\#aespa \#æspa \#BlackMamba \#블랙맘바 \#에스파
+aespa 에스파 'Black Mamba' MV ℗ SM Entertainment"#
+ )
+ }
}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index b88292c..dfec295 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -390,6 +390,34 @@ pub fn escape_html(input: &str) -> String {
buf
}
+/// Replace all markdown control characters to make a string safe for
+/// inserting into Markdown.
+pub fn escape_markdown(input: &str) -> String {
+ let mut buf = String::with_capacity(input.len());
+ let mut chars = input.chars().peekable();
+
+ while let Some(c) = chars.next() {
+ match c {
+ '<' => buf.push_str("<"),
+ '>' => buf.push_str(">"),
+ '\n' => {
+ if chars.peek() == Some(&'\n') {
+ chars.next();
+ buf.push_str("\n\n");
+ } else {
+ buf.push_str("
\n");
+ }
+ }
+ '*' | '#' | '(' | ')' | '[' | ']' | '_' | '`' => {
+ buf.push('\\');
+ buf.push(c);
+ }
+ _ => buf.push(c),
+ };
+ }
+ buf
+}
+
pub fn video_id_from_thumbnail_url(url: &str) -> Option {
static URL_REGEX: Lazy =
Lazy::new(|| Regex::new(r"^https://i.ytimg.com/vi/([A-Za-z0-9_-]{11})/").unwrap());