From a0819ac72c1a76e44288349dc2a4644fbe386619 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Sat, 13 May 2023 03:20:48 +0200 Subject: [PATCH] feat: add richtext to markdown conversion --- src/model/richtext.rs | 67 +++++++++++++++++++++++++++++++++++++++++-- src/util/mod.rs | 28 ++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) 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());