use std::fmt::Write;
pub fn relative_link(from: &str, to: &str, fragment: Option<&str>) -> String {
let from_dir = from.rfind('/').map_or("", |at| &from[..at]);
let mut relative = posix_relative(from_dir, to);
if relative.is_empty() {
relative = to.rsplit('/').next().unwrap_or(to).to_owned();
}
let encoded: Vec<String> = relative
.split('/')
.map(|segment| {
if segment == ".." || segment == "." {
segment.to_owned()
} else {
encode_uri_component(segment)
}
})
.collect();
match fragment {
Some(fragment) => format!("{}#{}", encoded.join("/"), encode_uri_fragment(fragment)),
None => encoded.join("/"),
}
}
fn posix_relative(from_dir: &str, to: &str) -> String {
let from: Vec<&str> = from_dir.split('/').filter(|s| !s.is_empty()).collect();
let to: Vec<&str> = to.split('/').filter(|s| !s.is_empty()).collect();
let shared = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
let mut out: Vec<&str> = vec![".."; from.len() - shared];
out.extend(&to[shared..]);
out.join("/")
}
pub fn encode_uri_component(text: &str) -> String {
percent_encode(text, |b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'-' | b'_' | b'.' | b'!' | b'~' | b'*' | b'\'' | b'(' | b')'
)
})
}
pub fn encode_uri_fragment(fragment: &str) -> String {
percent_encode(fragment, |b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'-' | b'_'
| b'.'
| b'!'
| b'~'
| b'*'
| b'\''
| b'('
| b')'
| b';'
| b','
| b'/'
| b'?'
| b':'
| b'@'
| b'&'
| b'='
| b'+'
| b'$'
)
})
}
pub fn escape_label(label: &str) -> String {
let mut out = String::with_capacity(label.len());
for c in label.chars() {
if matches!(c, '\\' | '[' | ']') {
out.push('\\');
}
out.push(c);
}
out
}
fn percent_encode(text: &str, keep: impl Fn(u8) -> bool) -> String {
let mut out = String::with_capacity(text.len());
for byte in text.bytes() {
if keep(byte) {
out.push(byte as char);
} else {
let _ = write!(out, "%{byte:02X}");
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_link_climbs_out_of_unshared_folders_only() {
assert_eq!(relative_link("x.md", "a/b.md", None), "a/b.md");
assert_eq!(relative_link("a/x.md", "b.md", None), "../b.md");
assert_eq!(relative_link("a/b/x.md", "a/c/y.md", None), "../c/y.md");
}
#[test]
fn segments_are_encoded_and_dot_segments_are_not() {
assert_eq!(
relative_link("a/x.md", "My Notes/(draft) é.md", None),
"../My%20Notes/(draft)%20%C3%A9.md"
);
}
#[test]
fn a_self_link_uses_its_own_name() {
assert_eq!(relative_link("x.md", "x.md", None), "x.md");
assert_eq!(relative_link("a/x.md", "a/x.md", Some("^b1")), "x.md#%5Eb1");
}
#[test]
fn the_two_encodings_differ_on_uri_structure() {
assert_eq!(encode_uri_component("a/b?c=d"), "a%2Fb%3Fc%3Dd");
assert_eq!(encode_uri_fragment("a/b?c=d"), "a/b?c=d");
assert_eq!(encode_uri_fragment("#"), "%23");
}
}