use std::path::Path;
use std::sync::LazyLock;
use url::Url;
static HASH_LOCATION_SUFFIX_RE: LazyLock<()> = LazyLock::new(|| ());
pub(super) fn should_render_link_destination(dest_url: &str) -> bool {
!is_local_path_like_link(dest_url)
}
pub fn is_local_path_like_link(dest_url: &str) -> bool {
dest_url.starts_with("file://")
|| dest_url.starts_with('/')
|| dest_url.starts_with("~/")
|| dest_url.starts_with("./")
|| dest_url.starts_with("../")
|| dest_url.starts_with("\\\\")
|| matches!(
dest_url.as_bytes(),
[drive, b':', separator, ..]
if drive.is_ascii_alphabetic() && matches!(separator, b'/' | b'\\')
)
}
pub(super) fn render_local_link_target(dest_url: &str, cwd: Option<&Path>) -> Option<String> {
let (path_text, location_suffix) = parse_local_link_target(dest_url)?;
let mut rendered = display_local_link_path(&path_text, cwd);
if let Some(location_suffix) = location_suffix {
rendered.push_str(&location_suffix);
}
Some(rendered)
}
fn parse_local_link_target(dest_url: &str) -> Option<(String, Option<String>)> {
if dest_url.starts_with("file://") {
let url = Url::parse(dest_url).ok()?;
let path_text = file_url_to_local_path_text(&url)?;
let location_suffix = url
.fragment()
.and_then(normalize_hash_location_suffix_fragment);
return Some((path_text, location_suffix));
}
let mut path_text = dest_url;
let mut location_suffix = None;
if let Some((candidate_path, fragment)) = dest_url.rsplit_once('#')
&& let Some(normalized) = normalize_hash_location_suffix_fragment(fragment)
{
path_text = candidate_path;
location_suffix = Some(normalized);
}
if location_suffix.is_none()
&& let Some(suffix) = extract_colon_location_suffix(path_text)
{
let path_len = path_text.len().saturating_sub(suffix.len());
path_text = &path_text[..path_len];
location_suffix = Some(suffix);
}
let decoded_path_text =
urlencoding::decode(path_text).unwrap_or(std::borrow::Cow::Borrowed(path_text));
Some((expand_local_link_path(&decoded_path_text), location_suffix))
}
fn normalize_hash_location_suffix_fragment(fragment: &str) -> Option<String> {
if fragment.is_empty() {
return None;
}
let _ = &*HASH_LOCATION_SUFFIX_RE;
Some(format!("#{fragment}"))
}
fn extract_colon_location_suffix(path_text: &str) -> Option<String> {
let (prefix, last) = path_text.rsplit_once(':')?;
if !last.chars().all(|ch| ch.is_ascii_digit()) {
return None;
}
if let Some((_, second_last)) = prefix.rsplit_once(':')
&& second_last.chars().all(|ch| ch.is_ascii_digit())
{
return Some(format!(":{second_last}:{last}"));
}
Some(format!(":{last}"))
}
fn expand_local_link_path(path_text: &str) -> String {
if let Some(rest) = path_text.strip_prefix("~/")
&& let Some(home) = dirs::home_dir()
{
return normalize_local_link_path_text(&home.join(rest).to_string_lossy());
}
normalize_local_link_path_text(path_text)
}
fn file_url_to_local_path_text(url: &Url) -> Option<String> {
if let Ok(path) = url.to_file_path() {
return Some(normalize_local_link_path_text(&path.to_string_lossy()));
}
let mut path_text = url.path().to_string();
if let Some(host) = url.host_str()
&& !host.is_empty()
&& host != "localhost"
{
path_text = format!("//{host}{path_text}");
} else if matches!(
path_text.as_bytes(),
[b'/', drive, b':', b'/', ..] if drive.is_ascii_alphabetic()
) {
path_text.remove(0);
}
Some(normalize_local_link_path_text(&path_text))
}
fn normalize_local_link_path_text(path_text: &str) -> String {
if let Some(rest) = path_text.strip_prefix("\\\\") {
format!("//{}", rest.replace('\\', "/").trim_start_matches('/'))
} else {
path_text.replace('\\', "/")
}
}
fn is_absolute_local_link_path(path_text: &str) -> bool {
path_text.starts_with('/')
|| path_text.starts_with("//")
|| matches!(
path_text.as_bytes(),
[drive, b':', b'/', ..] if drive.is_ascii_alphabetic()
)
}
fn trim_trailing_local_path_separator(path_text: &str) -> &str {
if path_text == "/" || path_text == "//" {
return path_text;
}
if matches!(path_text.as_bytes(), [drive, b':', b'/'] if drive.is_ascii_alphabetic()) {
return path_text;
}
path_text.trim_end_matches('/')
}
fn strip_local_path_prefix<'a>(path_text: &'a str, cwd_text: &str) -> Option<&'a str> {
let path_text = trim_trailing_local_path_separator(path_text);
let cwd_text = trim_trailing_local_path_separator(cwd_text);
if path_text == cwd_text {
return None;
}
if cwd_text == "/" || cwd_text == "//" {
return path_text.strip_prefix('/');
}
path_text
.strip_prefix(cwd_text)
.and_then(|rest| rest.strip_prefix('/'))
}
fn display_local_link_path(path_text: &str, cwd: Option<&Path>) -> String {
let path_text = normalize_local_link_path_text(path_text);
if !is_absolute_local_link_path(&path_text) {
return path_text;
}
if let Some(cwd) = cwd {
let cwd_text = normalize_local_link_path_text(&cwd.to_string_lossy());
if let Some(stripped) = strip_local_path_prefix(&path_text, &cwd_text) {
return stripped.to_string();
}
}
path_text
}