use alloc::string::String;
pub fn escape_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
_ => out.push(ch),
}
}
out
}
pub fn attr_escape(s: &str) -> String {
escape_text(s)
}
pub fn attr_escape_gfm(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
out
}
fn is_href_safe(b: u8) -> bool {
matches!(b,
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
| b'!' | b'#' | b'$' | b'\'' | b'(' | b')' | b'*' | b'+'
| b',' | b'-' | b'.' | b'/' | b':' | b';' | b'=' | b'?'
| b'@' | b'_' | b'~')
}
fn is_ascii_hex(b: u8) -> bool {
b.is_ascii_hexdigit()
}
pub fn encode_href(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'&' {
out.push_str("&");
i += 1;
} else if b == b'%' {
if i + 2 < bytes.len() && is_ascii_hex(bytes[i + 1]) && is_ascii_hex(bytes[i + 2]) {
out.push('%');
} else {
out.push_str("%25");
}
i += 1;
} else if is_href_safe(b) {
out.push(b as char);
i += 1;
} else {
push_percent(&mut out, b);
i += 1;
}
}
out
}
fn push_percent(out: &mut String, b: u8) {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
fn url_scheme(dest: &str) -> Option<String> {
let mut scheme = String::new();
for ch in dest.chars() {
match ch {
':' => return Some(scheme.to_ascii_lowercase()),
'/' | '?' | '#' => return None,
_ => scheme.push(ch),
}
}
None
}
fn is_dangerous_scheme(scheme: &str) -> bool {
matches!(scheme, "javascript" | "vbscript" | "file" | "data")
}
fn is_allowed_link_scheme(scheme: &str) -> bool {
matches!(
scheme,
"http" | "https" | "irc" | "ircs" | "mailto" | "xmpp"
)
}
fn is_allowed_img_scheme(scheme: &str) -> bool {
matches!(scheme, "http" | "https")
}
fn is_allowed_data_image(dest: &str) -> bool {
let lower = dest.to_ascii_lowercase();
lower.starts_with("data:image/png")
|| lower.starts_with("data:image/gif")
|| lower.starts_with("data:image/jpeg")
|| lower.starts_with("data:image/webp")
}
pub fn filter_protocol(dest: &str, allow_dangerous_protocol: bool, gfm_denylist: bool) -> String {
if allow_dangerous_protocol {
return String::from(dest);
}
match url_scheme(dest) {
None => String::from(dest),
Some(scheme) if gfm_denylist => {
if is_dangerous_scheme(&scheme) && !is_allowed_data_image(dest) {
String::new()
} else {
String::from(dest)
}
}
Some(scheme) => {
if is_allowed_link_scheme(&scheme) || is_allowed_data_image(dest) {
String::from(dest)
} else {
String::new()
}
}
}
}
pub fn filter_img_protocol(
dest: &str,
allow_dangerous_protocol: bool,
allow_any_img_src: bool,
) -> String {
if allow_dangerous_protocol || allow_any_img_src {
return String::from(dest);
}
match url_scheme(dest) {
None => String::from(dest),
Some(scheme) if is_allowed_img_scheme(&scheme) => String::from(dest),
Some(_) if is_allowed_data_image(dest) => String::from(dest),
Some(_) => String::new(),
}
}