From b861469319ac962394d63b40f8ac97488c157e1a Mon Sep 17 00:00:00 2001 From: Thomas Gideon Date: Sun, 16 Jul 2023 08:22:28 -0400 Subject: [PATCH] Apply block quote to multiline content --- Cargo.lock | 1 + Cargo.toml | 1 + src/format.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 18 +++++++----- 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d44745..7f1d35a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,6 +932,7 @@ dependencies = [ "html2md", "log", "megalodon", + "regex", "rss", "rustyline", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 945b07c..0d41947 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ env_logger = "0.10.0" html2md = "0.2.14" log = "0.4.19" megalodon = "0.8.3" +regex = "1.9.1" rss = "2.0.4" rustyline = "12.0.0" tokio = { version = "1.28.2", features = ["default", "full"] } diff --git a/src/format.rs b/src/format.rs index 405561b..a6b50c3 100644 --- a/src/format.rs +++ b/src/format.rs @@ -6,6 +6,7 @@ use megalodon::{ response::Response, Megalodon, }; +use regex::{Captures, Regex}; use rustyline::DefaultEditor; use url::Url; use url_open::UrlOpen; @@ -19,7 +20,7 @@ pub(super) async fn format_status( "{} > {}{}", status.created_at.with_timezone(&Local).format("%H:%M"), - parse_html(&status.content).trim(), + apply_block_quote(1, (&status.content).trim()), format_attachments(1, &status.media_attachments) ); let Response { json, .. } = client.get_status_context(status.id.clone(), None).await?; @@ -35,7 +36,7 @@ pub(super) async fn format_status( > {} >> {}{}", status.created_at.with_timezone(&Local).format("%H:%M"), - parse_html(&status.content).trim(), + apply_block_quote(2, parse_html(&status.content).trim()), format_attachments(2, &status.media_attachments) ) }) @@ -52,8 +53,11 @@ pub(super) async fn format_status( )) } +fn quote_prefix(depth: usize) -> String { + vec![">"; depth].join("") +} + fn format_attachments(depth: usize, attachments: &[Attachment]) -> String { - let prefix = vec![">"; depth].join(""); if attachments.is_empty() { String::default() } else { @@ -69,10 +73,76 @@ fn format_attachments(depth: usize, attachments: &[Attachment]) -> String { } else { a.url.clone() }; - format!("{} ", prefix, src) + format!("{} ", quote_prefix(depth), src) }) .collect::>() .join("\n") ) } } + +// without processing the content per line, the block quote markdown is only added to the first +// line, breaking the desired formatting +fn apply_block_quote>(depth: usize, content: S) -> String { + let prefix = quote_prefix(depth); + let empty = Regex::new("\n\\s").expect("Failed to compiled regex for nested empty lines!"); + let non_empty = + Regex::new("\n(?P[^>\\s])").expect("Failed to compile regex for nested lines!"); + // parsing some multiline content adds paragraph tags, convert those back to new lines to apply + // the block quote level to each line + let content = content.as_ref().replace("

", "\n\n"); + let content = content.replace("

", ""); + let content = content.replace("

", ""); + // replace separately to avoid trailing spaces when replacing empty lines with the prefix + let content = empty.replace(content.as_ref(), format!("\n{}\n", prefix)); + let content = non_empty.replace_all(&content, |c: &Captures| { + format!("\n{} {}", prefix, c["initial"].to_string()) + }); + content.to_string() +} + +#[cfg(test)] +mod test { + use crate::format::apply_block_quote; + + #[test] + fn test_apply_bq() { + assert_eq!( + "Foo +> +> Bar" + .to_owned(), + apply_block_quote( + 1, "Foo + +Bar" + ) + ); + } + + #[test] + fn test_apply_bq_deeper() { + assert_eq!( + "Foo +>> +>> Bar" + .to_owned(), + apply_block_quote( + 2, "Foo + +Bar" + ) + ); + } + + #[test] + fn test_apply_bq_strip_ps() { + assert_eq!( + "Foo +>> +>> Bar" + .to_owned(), + apply_block_quote(2, "

Foo

Bar

") + ); + } +} diff --git a/src/main.rs b/src/main.rs index f8adbee..be22d03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,12 +27,12 @@ mod range; #[derive(Debug, Parser)] #[command()] struct Config { - #[arg(short, long, env = "MASTODON_URL")] + #[arg(short, long, env = "MASTODON_URL", required = true)] url: String, - #[arg(short, long, env = "MASTODON_ACCESS_TOKEN")] + #[arg(short, long, env = "MASTODON_ACCESS_TOKEN", required = true)] access_token: String, #[arg(short, long)] - output_dir: String, + output_dir: Option, #[arg(required = true)] date: String, #[arg(short, long)] @@ -108,10 +108,14 @@ async fn main() -> Result<()> { } } reversed.reverse(); - let output = format!("{}{}.md", output_dir, date); - let mut f = File::options().append(true).open(&output)?; - f.write_all(&reversed.join("\n\n").as_bytes())?; - println!("Appended matching posts to {}.", output); + if let Some(output_dir) = output_dir { + let output = format!("{}{}.md", output_dir, date); + let mut f = File::options().append(true).open(&output)?; + f.write_all(&reversed.join("\n\n").as_bytes())?; + println!("Appended matching posts to {}.", output); + } else { + println!("{}", reversed.join("\n\n")); + } Ok(()) }