Apply block quote to multiline content

This commit is contained in:
Thomas Gideon 2023-07-16 08:22:28 -04:00
parent 4dae4360cc
commit b861469319
4 changed files with 87 additions and 11 deletions

1
Cargo.lock generated
View file

@ -932,6 +932,7 @@ dependencies = [
"html2md", "html2md",
"log", "log",
"megalodon", "megalodon",
"regex",
"rss", "rss",
"rustyline", "rustyline",
"tokio", "tokio",

View file

@ -13,6 +13,7 @@ env_logger = "0.10.0"
html2md = "0.2.14" html2md = "0.2.14"
log = "0.4.19" log = "0.4.19"
megalodon = "0.8.3" megalodon = "0.8.3"
regex = "1.9.1"
rss = "2.0.4" rss = "2.0.4"
rustyline = "12.0.0" rustyline = "12.0.0"
tokio = { version = "1.28.2", features = ["default", "full"] } tokio = { version = "1.28.2", features = ["default", "full"] }

View file

@ -6,6 +6,7 @@ use megalodon::{
response::Response, response::Response,
Megalodon, Megalodon,
}; };
use regex::{Captures, Regex};
use rustyline::DefaultEditor; use rustyline::DefaultEditor;
use url::Url; use url::Url;
use url_open::UrlOpen; use url_open::UrlOpen;
@ -19,7 +20,7 @@ pub(super) async fn format_status(
"{} "{}
> {}{}", > {}{}",
status.created_at.with_timezone(&Local).format("%H:%M"), 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) format_attachments(1, &status.media_attachments)
); );
let Response { json, .. } = client.get_status_context(status.id.clone(), None).await?; 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"), 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) 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 { fn format_attachments(depth: usize, attachments: &[Attachment]) -> String {
let prefix = vec![">"; depth].join("");
if attachments.is_empty() { if attachments.is_empty() {
String::default() String::default()
} else { } else {
@ -69,10 +73,76 @@ fn format_attachments(depth: usize, attachments: &[Attachment]) -> String {
} else { } else {
a.url.clone() a.url.clone()
}; };
format!("{} <img src=\"{}\" />", prefix, src) format!("{} <img src=\"{}\" />", quote_prefix(depth), src)
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .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<S: AsRef<str>>(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<initial>[^>\\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("</p><p>", "\n\n");
let content = content.replace("<p>", "");
let content = content.replace("</p>", "");
// 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, "<p>Foo</p><p>Bar</p>")
);
}
}

View file

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