use once_cell::sync::Lazy;
use crate::simplify::remove_message_footer;
#[derive(Debug)]
pub struct PlainText {
pub text: String,
pub flowed: bool,
pub delsp: bool,
}
impl PlainText {
pub fn to_html(&self) -> String {
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
static LINKIFY_URL_RE: Lazy<regex::Regex> = Lazy::new(|| {
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
});
let lines: Vec<&str> = self.text.lines().collect();
let (lines, _footer) = remove_message_footer(&lines);
let mut ret = r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
"#
.to_string();
for line in lines {
let is_quote = line.starts_with('>');
let line = line.to_string().replace('\r', "");
let mut line = LINKIFY_MAIL_RE
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
line = LINKIFY_URL_RE
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
line = escaper::encode_minimal(&line);
line = line.replace("\rLT", "<");
line = line.replace("\rGT", ">");
line = line.replace("\rQUOT", "\"");
if self.flowed {
line = line.strip_prefix(' ').unwrap_or(&line).to_string();
if is_quote {
line = "<em>".to_owned() + &line + "</em>";
}
if line.ends_with(' ') && !is_quote {
if self.delsp {
line.pop();
}
} else {
line += "<br/>\n";
}
} else {
if is_quote {
line = "<em>".to_owned() + &line + "</em>";
}
line += "<br/>\n";
}
let len_with_indentation = line.len();
let line = line.trim_start_matches(' ');
for _ in line.len()..len_with_indentation {
ret += " ";
}
ret += line;
}
ret += "</body></html>\n";
ret
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plain_to_html() {
let html = PlainText {
text: r##"line 1
line 2
line with https://link-mid-of-line.org and http://link-end-of-line.com/file?foo=bar%20
http://link-at-start-of-line.org
"##
.to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
line 1<br/>
line 2<br/>
line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a> and <a href="http://link-end-of-line.com/file?foo=bar%20">http://link-end-of-line.com/file?foo=bar%20</a><br/>
<a href="http://link-at-start-of-line.org">http://link-at-start-of-line.org</a><br/>
</body></html>
"#
);
}
#[test]
fn test_plain_remove_signature() {
let html = PlainText {
text: "Foo\nbar\n-- \nSignature here".to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
Foo<br/>
bar<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_encapsulated() {
let html = PlainText {
text: r#"line with <http://encapsulated.link/?foo=_bar> here!"#.to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
line with <<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.link/?foo=_bar</a>> here!<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_nolink() {
let html = PlainText {
text: r#"line with nohttp://no.link here"#.to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
line with nohttp://no.link here<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_mailto() {
let html = PlainText {
text: r#"just an address: foo@bar.org another@one.de"#.to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:another@one.de">another@one.de</a><br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_flowed() {
let html = PlainText {
text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
flowed: true,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
line still line<br/>
<em>>quote </em><br/>
<em>>still quote</em><br/>
>no quote<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_flowed_delsp() {
let html = PlainText {
text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
flowed: true,
delsp: true,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
linestill line<br/>
<em>>quote </em><br/>
<em>>still quote</em><br/>
>no quote<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_fixed() {
let html = PlainText {
text: "line \nstill line\n>quote \n>still quote\n >no quote".to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
line <br/>
still line<br/>
<em>>quote </em><br/>
<em>>still quote</em><br/>
>no quote<br/>
</body></html>
"#
);
}
#[test]
fn test_plain_to_html_indentation() {
let html = PlainText {
text: "def foo():\n pass\n\ndef bar(x):\n return x + 5".to_string(),
flowed: false,
delsp: false,
}
.to_html();
assert_eq!(
html,
r#"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
</head><body>
def foo():<br/>
pass<br/>
<br/>
def bar(x):<br/>
return x + 5<br/>
</body></html>
"#
);
}
}