149 lines
4 KiB
Rust
149 lines
4 KiB
Rust
use anyhow::Result;
|
|
use chrono::Local;
|
|
use html2md::parse_html;
|
|
use megalodon::{
|
|
entities::{Account, Attachment, Status},
|
|
response::Response,
|
|
Megalodon,
|
|
};
|
|
use regex::{Captures, Regex};
|
|
use rustyline::DefaultEditor;
|
|
use url::Url;
|
|
use url_open::UrlOpen;
|
|
|
|
pub(super) async fn format_status(
|
|
client: &Box<dyn Megalodon + Send + Sync>,
|
|
account: &Account,
|
|
status: &Status,
|
|
) -> Result<String> {
|
|
let ancestor = format!(
|
|
"{}
|
|
> {}{}",
|
|
status.created_at.with_timezone(&Local).format("%H:%M"),
|
|
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?;
|
|
let thread = json
|
|
.descendants
|
|
.into_iter()
|
|
.filter(|s| {
|
|
s.in_reply_to_account_id == Some(account.id.clone()) && s.account.id == account.id
|
|
})
|
|
.map(|status| {
|
|
format!(
|
|
">
|
|
> {}
|
|
>> {}{}",
|
|
status.created_at.with_timezone(&Local).format("%H:%M"),
|
|
apply_block_quote(2, parse_html(&status.content).trim()),
|
|
format_attachments(2, &status.media_attachments)
|
|
)
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.join("\n");
|
|
Ok(format!(
|
|
"{}{}",
|
|
ancestor,
|
|
if !thread.is_empty() {
|
|
format!("\n{}", thread)
|
|
} else {
|
|
String::default()
|
|
}
|
|
))
|
|
}
|
|
|
|
fn quote_prefix(depth: usize) -> String {
|
|
vec![">"; depth].join("")
|
|
}
|
|
|
|
fn format_attachments(depth: usize, attachments: &[Attachment]) -> String {
|
|
if attachments.is_empty() {
|
|
String::default()
|
|
} else {
|
|
format!(
|
|
"\n{}",
|
|
attachments
|
|
.iter()
|
|
.map(|a| {
|
|
Url::parse(&a.url).unwrap().open();
|
|
let mut editor = DefaultEditor::new().unwrap();
|
|
let src = if let Ok(line) = editor.readline("Filename: ") {
|
|
line
|
|
} else {
|
|
a.url.clone()
|
|
};
|
|
format!("{} <img src=\"{}\" />", quote_prefix(depth), src)
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.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>")
|
|
);
|
|
}
|
|
}
|