use askama::filters::{Escaper, Html, Safe};
use askama::{Result, Values};
#[askama::filter_fn]
pub fn linkify(text: impl std::fmt::Display, _: &dyn Values) -> Result<Safe<String>> {
Ok(Safe(linked(&text.to_string())))
}
pub(super) fn linked(text: &str) -> String {
let mut html = String::new();
let mut rest = text;
while let Some(start) = rest.find("https://") {
let (before, tail) = rest.split_at(start);
let end = tail
.find(|c: char| c.is_whitespace() || matches!(c, '<' | '>' | '"' | '\'' | '`'))
.unwrap_or(tail.len());
let mut url = &tail[..end];
loop {
let trimmed = url.trim_end_matches(['.', ',', ';', ':', '!', '?']);
match trimmed.strip_suffix(')') {
Some(open) if trimmed.matches('(').count() < trimmed.matches(')').count() => {
url = open
}
_ => {
url = trimmed;
break;
}
}
}
escape(&mut html, before);
if url.len() > "https://".len() {
html.push_str("<a href=\"");
escape(&mut html, url);
html.push_str("\">");
escape(&mut html, url);
html.push_str("</a>");
} else {
escape(&mut html, url);
}
rest = &tail[url.len()..];
}
escape(&mut html, rest);
html
}
pub(super) fn escaped(text: &str) -> String {
let mut html = String::new();
escape(&mut html, text);
html
}
fn escape(html: &mut String, text: &str) {
Html.write_escaped_str(html, text)
.expect("writing to a String cannot fail");
}
#[cfg(test)]
mod tests {
use crate::console::filters;
#[derive(askama::Template)]
#[template(source = "{{ text|linkify }}", ext = "html")]
struct Linked<'a> {
text: &'a str,
}
fn linked(text: &str) -> String {
askama::Template::render(&Linked { text }).unwrap()
}
#[test]
fn links_https_urls_and_escapes_everything_else() {
assert_eq!(
linked("PR https://github.com/a/b/pull/1 is a draft."),
"PR <a href=\"https://github.com/a/b/pull/1\">https://github.com/a/b/pull/1</a> is a draft."
);
assert_eq!(
linked("(see https://x.test/a?b=1&c=2), <b>"),
"(see <a href=\"https://x.test/a?b=1&c=2\">https://x.test/a?b=1&c=2</a>), <b>"
);
assert_eq!(
linked("https://x.test/\"onmouseover=1"),
"<a href=\"https://x.test/\">https://x.test/</a>"onmouseover=1"
);
assert_eq!(
linked("http://x.test javascript:alert(1) https://"),
"http://x.test javascript:alert(1) https://"
);
}
#[test]
fn keeps_a_closing_paren_that_belongs_to_the_url() {
assert_eq!(
linked("see https://en.wikipedia.org/wiki/Rust_(programming_language)."),
"see <a href=\"https://en.wikipedia.org/wiki/Rust_(programming_language)\">https://en.wikipedia.org/wiki/Rust_(programming_language)</a>."
);
}
}