use alloc::string::String;
use alloc::vec::Vec;
pub fn resolve_url(base: &str, reference: &str) -> String {
let base = slashes(clean(base), None);
let base_scheme = scheme(&base);
let reference = slashes(clean(reference), base_scheme);
let b = Parts::split(&base);
if b.scheme.is_none() {
return reference;
}
let r = Parts::split(&reference);
let mut t = Parts {
scheme: b.scheme,
authority: b.authority,
path: String::new(),
query: r.query,
fragment: r.fragment,
};
if r.scheme.is_some() {
t.scheme = r.scheme;
t.authority = r.authority;
t.path = remove_dot_segments(&r.path);
} else if r.authority.is_some() {
t.authority = r.authority;
t.path = remove_dot_segments(&r.path);
} else if r.path.is_empty() {
t.path = b.path.clone();
t.query = r.query.or(b.query);
} else if r.path.starts_with('/') {
t.path = remove_dot_segments(&r.path);
} else {
let mut merged = String::new();
if b.authority.is_some() && b.path.is_empty() {
merged.push('/');
} else if let Some(dir) = b.path.rfind('/').and_then(|i| b.path.get(..=i)) {
merged.push_str(dir);
}
merged.push_str(&r.path);
t.path = remove_dot_segments(&merged);
}
t.join()
}
pub fn resolve_srcset(base: &str, srcset: &str) -> String {
let mut out = String::new();
for (url, descriptor) in srcset_candidates(srcset) {
if !out.is_empty() {
out.push_str(", ");
}
out.push_str(&resolve_url(base, url));
if !descriptor.is_empty() {
out.push(' ');
out.push_str(descriptor);
}
}
out
}
pub(crate) fn srcset_candidates(srcset: &str) -> Srcset<'_> {
Srcset { rest: srcset }
}
pub(crate) struct Srcset<'a> {
rest: &'a str,
}
impl<'a> Iterator for Srcset<'a> {
type Item = (&'a str, &'a str);
fn next(&mut self) -> Option<(&'a str, &'a str)> {
let rest = self
.rest
.trim_start_matches(|c: char| is_ascii_space(c) || c == ',');
if rest.is_empty() {
self.rest = "";
return None;
}
let end = rest.find(is_ascii_space).unwrap_or(rest.len());
let (url, after) = rest.split_at_checked(end).unwrap_or((rest, ""));
if url.ends_with(',') {
self.rest = after;
return Some((url.trim_end_matches(','), ""));
}
let mut depth = 0u32;
let mut stop = after.len();
for (i, c) in after.char_indices() {
match c {
'(' => depth = depth.saturating_add(1),
')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
stop = i;
break;
}
_ => {}
}
}
let (descriptor, tail) = after.split_at_checked(stop).unwrap_or((after, ""));
self.rest = tail.strip_prefix(',').unwrap_or(tail);
Some((url, descriptor.trim_matches(is_ascii_space)))
}
}
fn is_ascii_space(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')
}
pub(crate) fn is_javascript(url: &str) -> bool {
let url = clean(url);
url.get(..11)
.is_some_and(|s| s.eq_ignore_ascii_case("javascript:"))
}
pub(crate) fn clean(url: &str) -> String {
url.trim_matches(|c: char| c <= ' ')
.chars()
.filter(|c| !matches!(c, '\t' | '\n' | '\r'))
.collect()
}
pub(crate) fn normalize(url: &str) -> String {
slashes(clean(url), None)
}
fn path_end(url: &str) -> usize {
url.bytes()
.position(|b| matches!(b, b'?' | b'#'))
.unwrap_or(url.len())
}
fn scheme(url: &str) -> Option<&str> {
let (scheme, _) = url.get(..path_end(url))?.split_once(':')?;
is_scheme(scheme).then_some(scheme)
}
fn is_special(scheme: &str) -> bool {
["http", "https", "ftp", "ws", "wss", "file"]
.iter()
.any(|s| s.eq_ignore_ascii_case(scheme))
}
fn slashes(url: String, base_scheme: Option<&str>) -> String {
if !scheme(&url).or(base_scheme).is_some_and(is_special) {
return url;
}
match url.split_at_checked(path_end(&url)) {
Some((head, tail)) if head.contains('\\') => {
let mut out: String = head
.chars()
.map(|c| if c == '\\' { '/' } else { c })
.collect();
out.push_str(tail);
out
}
_ => url,
}
}
struct Parts<'a> {
scheme: Option<&'a str>,
authority: Option<&'a str>,
path: String,
query: Option<&'a str>,
fragment: Option<&'a str>,
}
impl<'a> Parts<'a> {
fn split(s: &'a str) -> Parts<'a> {
let (s, fragment) = match s.split_once('#') {
Some((a, f)) => (a, Some(f)),
None => (s, None),
};
let (s, query) = match s.split_once('?') {
Some((a, q)) => (a, Some(q)),
None => (s, None),
};
let (scheme, rest) = match s.split_once(':') {
Some((scheme, rest)) if is_scheme(scheme) => (Some(scheme), rest),
_ => (None, s),
};
let (authority, path) = match rest.strip_prefix("//") {
Some(r) => {
let end = r.find('/').unwrap_or(r.len());
let (authority, path) = r.split_at_checked(end).unwrap_or((r, ""));
(Some(authority), path)
}
None => (None, rest),
};
Parts {
scheme,
authority,
path: String::from(path),
query,
fragment,
}
}
fn join(&self) -> String {
let mut out = String::new();
if let Some(s) = self.scheme {
out.push_str(s);
out.push(':');
}
if let Some(a) = self.authority {
out.push_str("//");
out.push_str(a);
}
out.push_str(&self.path);
if let Some(q) = self.query {
out.push('?');
out.push_str(q);
}
if let Some(f) = self.fragment {
out.push('#');
out.push_str(f);
}
out
}
}
fn is_scheme(s: &str) -> bool {
let mut chars = s.chars();
chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
}
fn remove_dot_segments(path: &str) -> String {
let (absolute, body) = match path.strip_prefix('/') {
Some(body) => (true, body),
None => (false, path),
};
let mut out: Vec<&str> = Vec::new();
let mut segments = body.split('/').peekable();
while let Some(seg) = segments.next() {
match seg {
"." => {}
".." => {
out.pop();
}
s => {
out.push(s);
continue;
}
}
if segments.peek().is_none() {
out.push(""); }
}
let mut joined = String::with_capacity(path.len());
if absolute {
joined.push('/');
}
for (i, seg) in out.iter().enumerate() {
if i > 0 {
joined.push('/');
}
joined.push_str(seg);
}
joined
}