use core::ops::{Deref, DerefMut};
#[must_use]
pub fn hyphenate(s: &str) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_ascii_uppercase() {
if i != 0 {
out.push('-');
}
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
out
}
#[must_use]
pub fn escape(text: &str) -> String {
text.chars()
.map(|c| match c {
'&' => "&".to_owned(),
'>' => ">".to_owned(),
'<' => "<".to_owned(),
'"' => """.to_owned(),
'\'' => "'".to_owned(),
_ => c.to_string(),
})
.collect()
}
#[must_use]
pub fn protocol_from_url(url: &str) -> Option<String> {
let mut s = url;
while let Some(first) = s.chars().next() {
if first <= '\u{20}' {
s = &s[first.len_utf8()..];
} else {
break;
}
}
for (i, ch) in s.char_indices() {
if ch == ':' {
return validate_and_return(&s[..i], ":", &s[i + 1..]);
} else if ch == '&' {
let rest = &s[i..];
if let Some((entity, skip)) = match_html_colon_entity(rest) {
return validate_and_return(&s[..i], entity, &s[i + skip..]);
}
} else if ch == '\\' || ch == '/' || ch == '#' || ch == '?' {
return Some("_relative".into());
}
}
Some("_relative".into())
}
fn match_html_colon_entity(s: &str) -> Option<(&'static str, usize)> {
if let Some(decimal) = s.strip_prefix("&#") {
let mut idx = 0;
while decimal[idx..].starts_with('0') {
idx += 1;
}
if decimal[idx..].starts_with("58") {
let after = idx + 2;
return Some(("�*58", 2 + after));
}
if decimal[idx..].starts_with('x') || decimal[idx..].starts_with('X') {
let hexpart = &decimal[idx + 1..];
let mut idx2 = 0;
while hexpart[idx2..].starts_with('0') {
idx2 += 1;
}
if hexpart[idx2..].to_ascii_lowercase().starts_with("3a") {
let after = idx + 1 + idx2 + 2;
return Some(("�*3a", 2 + after));
}
}
}
if s.to_ascii_lowercase().starts_with("&colon") {
return Some(("&colon", 6));
}
None
}
fn validate_and_return(scheme: &str, colon_match: &str, _rest: &str) -> Option<String> {
if colon_match != ":" {
return None;
}
let mut chars = scheme.chars();
if !matches!(chars.next(), Some(c) if c.is_ascii_alphabetic()) {
return None;
}
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') {
return None;
}
Some(scheme.to_lowercase())
}
pub fn push_and_get_ref<T>(vec: &mut Vec<T>, value: T) -> (&T, &Vec<T>) {
vec.push(value);
let idx = vec.len() - 1;
(&vec[idx], vec)
}
pub fn push_and_get_mut<T>(vec: &mut Vec<T>, value: T) -> &mut T {
vec.push(value);
let idx = vec.len() - 1;
&mut vec[idx]
}
pub enum OwnedOrMut<'a, T> {
Owned {
idx: usize,
val: T,
},
Borrowed(&'a mut T),
}
impl<T> Deref for OwnedOrMut<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
OwnedOrMut::Owned { val: arr, .. } => arr,
OwnedOrMut::Borrowed(r) => r,
}
}
}
impl<T> DerefMut for OwnedOrMut<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
OwnedOrMut::Owned { val: arr, .. } => arr,
OwnedOrMut::Borrowed(r) => r,
}
}
}
pub fn set_panic_hook() {
#[cfg(feature = "wasm")]
console_error_panic_hook::set_once();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hyphenate() {
assert_eq!(hyphenate("camelCase"), "camel-case");
assert_eq!(hyphenate("XMLHttpRequest"), "x-m-l-http-request");
}
#[test]
fn test_escape() {
assert_eq!(escape("a & b"), "a & b");
assert_eq!(escape("a > b"), "a > b");
assert_eq!(escape("a < b"), "a < b");
assert_eq!(escape("a \" b"), "a " b");
assert_eq!(escape("a ' b"), "a ' b");
}
#[test]
fn test_protocol_from_url() {
assert_eq!(
protocol_from_url("https://example.com"),
Some("https".to_owned())
);
assert_eq!(
protocol_from_url("http://example.com"),
Some("http".to_owned())
);
assert_eq!(
protocol_from_url("ftp://example.com"),
Some("ftp".to_owned())
);
assert_eq!(
protocol_from_url("/path/to/file"),
Some("_relative".to_owned())
);
assert_eq!(protocol_from_url("1invalid://example.com"), None);
assert_eq!(protocol_from_url("weird:colon"), Some("weird".to_owned()));
}
#[test]
fn test_protocol_from_url_misc() {
assert_eq!(protocol_from_url("/path/to/file"), Some("_relative".into()));
assert_eq!(
protocol_from_url(" \u{0007}../rel"),
Some("_relative".into())
);
assert_eq!(
protocol_from_url("https://example.com"),
Some("https".into())
);
assert_eq!(protocol_from_url("FTP://example.com"), Some("ftp".into()));
assert_eq!(
protocol_from_url("mailto:user@example.com"),
Some("mailto".into())
);
assert_eq!(protocol_from_url("http://foo"), None);
assert_eq!(protocol_from_url("http://foo"), None);
assert_eq!(protocol_from_url("http&colon//foo"), None);
assert_eq!(protocol_from_url("1abc://foo"), None);
assert_eq!(protocol_from_url("abc^://foo"), None);
assert_eq!(protocol_from_url("ht tp://foo"), None);
}
}