use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::LazyLock,
};
use concat_string::concat_string;
use cow_utils::CowUtils;
use regex::Regex;
use sugar_path::SugarPath;
static SEGMENTS_SPLIT_REGEXP: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"([|!])").expect("TODO:"));
static WINDOWS_ABS_PATH_REGEXP: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-zA-Z]:[/\\]").expect("TODO:"));
static WINDOWS_PATH_SEPARATOR: &[char] = &['/', '\\'];
fn split_at_query_mark(path: &str) -> (&str, Option<&str>) {
let query_mark_pos = path.find('?');
query_mark_pos
.map(|pos| (&path[..pos], Some(&path[pos..])))
.unwrap_or((path, None))
}
pub fn absolute_to_request<'b>(context: &str, maybe_absolute_path: &'b str) -> Cow<'b, str> {
if maybe_absolute_path.starts_with('/')
&& maybe_absolute_path.len() > 1
&& maybe_absolute_path.ends_with('/')
{
return Cow::Borrowed(maybe_absolute_path);
}
let (maybe_absolute_resource, query_part) = split_at_query_mark(maybe_absolute_path);
let relative_resource = if maybe_absolute_path.starts_with('/') {
let tmp = Path::new(maybe_absolute_resource).relative(context);
let tmp_path = tmp.to_string_lossy();
relative_path_to_request(&tmp_path).into_owned()
} else if WINDOWS_ABS_PATH_REGEXP.is_match(maybe_absolute_path) {
let mut resource = maybe_absolute_resource
.as_path()
.relative(context)
.to_string_lossy()
.into_owned();
if !WINDOWS_ABS_PATH_REGEXP.is_match(&resource) {
resource =
relative_path_to_request(&resource.cow_replace(WINDOWS_PATH_SEPARATOR, "/")).into_owned();
}
resource
} else {
return Cow::Borrowed(maybe_absolute_path);
};
if let Some(query_part) = query_part {
Cow::Owned(concat_string!(relative_resource, query_part))
} else {
Cow::Owned(relative_resource)
}
}
pub fn relative_path_to_request(rel: &str) -> Cow<'_, str> {
if rel.is_empty() {
Cow::Borrowed("./.")
} else if rel == ".." {
Cow::Borrowed("../.")
} else if rel.starts_with("../") {
Cow::Borrowed(rel)
} else {
Cow::Owned(concat_string!("./", rel))
}
}
fn request_to_absolute(context: &str, relative_path: &str) -> String {
if relative_path.starts_with("./") || relative_path.starts_with("../") {
let relative_path = if relative_path.starts_with("./") {
relative_path
.strip_prefix("./")
.expect("should start with ./")
} else {
relative_path
};
Path::new(context)
.join(relative_path)
.to_string_lossy()
.to_string()
} else {
PathBuf::from(relative_path).to_string_lossy().to_string()
}
}
pub fn make_paths_absolute(context: &str, identifier: &str) -> String {
split_keep(&SEGMENTS_SPLIT_REGEXP, identifier)
.into_iter()
.map(|str| request_to_absolute(context, str))
.collect()
}
pub fn make_paths_relative(context: &str, identifier: &str) -> String {
split_keep(&SEGMENTS_SPLIT_REGEXP, identifier)
.into_iter()
.map(|str| absolute_to_request(context, str))
.collect()
}
pub fn strip_zero_width_space_for_fragment(s: &str) -> Cow<'_, str> {
s.cow_replace("\u{200b}#", "#")
}
pub fn insert_zero_width_space_for_fragment(s: &str) -> Cow<'_, str> {
s.cow_replace("#", "\u{200b}#")
}
fn split_keep<'a>(r: &Regex, text: &'a str) -> Vec<&'a str> {
let mut result = Vec::new();
let mut last = 0;
for (index, matched) in text.match_indices(r) {
if last != index {
result.push(&text[last..index]);
}
result.push(matched);
last = index + matched.len();
}
if last < text.len() {
result.push(&text[last..]);
}
result
}
static REQUEST_TO_ID_REGEX1: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^(\.\.?/)+").expect("Failed to initialize REQUEST_TO_ID_REGEX1"));
static REQUEST_TO_ID_REGEX2: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(^[.-]|[^a-zA-Z0-9_-])+").expect("Failed to initialize REQUEST_TO_ID_REGEX2")
});
pub fn request_to_id(request: &str) -> String {
REQUEST_TO_ID_REGEX2
.replace_all(&REQUEST_TO_ID_REGEX1.replace(request, ""), "_")
.to_string()
}