use core::fmt;
use core::slice;
#[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
}
#[inline]
pub fn escape_into<W: fmt::Write>(writer: &mut W, text: &str) -> fmt::Result {
let mut last = 0;
for (idx, ch) in text.char_indices() {
let replacement = match ch {
'&' => Some("&"),
'>' => Some(">"),
'<' => Some("<"),
'"' => Some("""),
'\'' => Some("'"),
_ => None,
};
if let Some(rep) = replacement {
if last < idx {
writer.write_str(&text[last..idx])?;
}
writer.write_str(rep)?;
last = idx + ch.len_utf8();
}
}
if last < text.len() {
writer.write_str(&text[last..])
} else {
Ok(())
}
}
#[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]
}
#[allow(clippy::missing_const_for_fn)]
pub fn set_panic_hook() {
#[cfg(feature = "wasm")]
console_error_panic_hook::set_once();
}
pub trait AdvanceWhile<'a, T> {
fn advance_while(&mut self, pred: T) -> usize
where
T: Fn(u8) -> bool;
}
impl<'a, T> AdvanceWhile<'a, T> for slice::Iter<'a, u8> {
#[inline]
fn advance_while(&mut self, pred: T) -> usize
where
T: Fn(u8) -> bool,
{
let mut n = 0;
while let Some(&b) = self.as_slice().first() {
if pred(b) {
self.next();
n += 1;
} else {
break;
}
}
n
}
}
#[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_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);
}
}