use std::path::Path;
pub fn resolve_link(link: &str, source_path: Option<&str>) -> String {
if let Some(path) = link.strip_prefix("@/") {
return resolve_absolute_link(path);
}
if let Some(source) = source_path {
let path_part = link.split('#').next().unwrap_or(link);
if path_part.ends_with(".md")
&& !link.starts_with("http://")
&& !link.starts_with("https://")
{
return resolve_relative_link(link, source);
}
}
link.to_string()
}
fn resolve_absolute_link(path: &str) -> String {
let (path_part, fragment) = match path.find('#') {
Some(idx) => (&path[..idx], Some(&path[idx..])),
None => (path, None),
};
let mut path = path_part.to_string();
if path.ends_with(".md") {
path = path[..path.len() - 3].to_string();
}
if path.ends_with("/_index") {
path = path[..path.len() - 7].to_string();
} else if path == "_index" {
path = String::new();
}
let result = if path.is_empty() {
"/".to_string()
} else {
format!("/{}/", path)
};
match fragment {
Some(f) => format!("{}{}", result, f),
None => result,
}
}
fn resolve_relative_link(link: &str, source_path: &str) -> String {
let (link_part, fragment) = match link.find('#') {
Some(idx) => (&link[..idx], Some(&link[idx..])),
None => (link, None),
};
let source = Path::new(source_path);
let source_dir = source.parent().unwrap_or(Path::new(""));
let resolved = source_dir.join(link_part);
let normalized = normalize_path(&resolved);
let mut path = normalized.replace('\\', "/");
if path.ends_with(".md") {
path = path[..path.len() - 3].to_string();
}
if path.ends_with("/_index") {
path = path[..path.len() - 7].to_string();
} else if path == "_index" {
path = String::new();
}
let result = if path.is_empty() {
"/".to_string()
} else if path.starts_with('/') {
format!("{}/", path)
} else {
format!("/{}/", path)
};
match fragment {
Some(f) => format!("{}{}", result, f),
None => result,
}
}
fn normalize_path(path: &Path) -> String {
let mut components: Vec<&str> = Vec::new();
for component in path.components() {
match component {
std::path::Component::Normal(s) => {
if let Some(s) = s.to_str() {
components.push(s);
}
}
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::CurDir => {
}
_ => {}
}
}
components.join("/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_absolute_link_simple() {
assert_eq!(resolve_link("@/docs/intro.md", None), "/docs/intro/");
}
#[test]
fn test_absolute_link_with_fragment() {
assert_eq!(
resolve_link("@/docs/intro.md#section", None),
"/docs/intro/#section"
);
}
#[test]
fn test_absolute_link_index() {
assert_eq!(resolve_link("@/_index.md", None), "/");
assert_eq!(resolve_link("@/docs/_index.md", None), "/docs/");
}
#[test]
fn test_absolute_link_no_extension() {
assert_eq!(resolve_link("@/docs/intro", None), "/docs/intro/");
}
#[test]
fn test_relative_link() {
assert_eq!(
resolve_link("sibling.md", Some("docs/page.md")),
"/docs/sibling/"
);
}
#[test]
fn test_relative_link_with_fragment() {
assert_eq!(
resolve_link("sibling.md#section", Some("docs/page.md")),
"/docs/sibling/#section"
);
}
#[test]
fn test_relative_link_parent_dir() {
assert_eq!(
resolve_link("../other.md", Some("docs/sub/page.md")),
"/docs/other/"
);
}
#[test]
fn test_external_link_passthrough() {
assert_eq!(
resolve_link("https://example.com", None),
"https://example.com"
);
assert_eq!(
resolve_link("http://example.com/page.md", None),
"http://example.com/page.md"
);
}
#[test]
fn test_fragment_only_passthrough() {
assert_eq!(resolve_link("#section", None), "#section");
}
#[test]
fn test_non_md_link_passthrough() {
assert_eq!(resolve_link("image.png", Some("docs/page.md")), "image.png");
}
}