use std::path::{Path, PathBuf};
#[must_use]
pub fn calculate_relative_path(from_chapter: &Path, to_glossary: &Path) -> String {
let from_dir = from_chapter
.parent()
.map_or_else(PathBuf::new, Path::to_path_buf);
let rel =
pathdiff::diff_paths(to_glossary, &from_dir).unwrap_or_else(|| to_glossary.to_path_buf());
to_url_string(&rel)
}
fn to_url_string(path: &Path) -> String {
path.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_directory_yields_bare_filename() {
assert_eq!(
calculate_relative_path(Path::new("intro.md"), Path::new("glossary.html")),
"glossary.html"
);
}
#[test]
fn one_level_deep_prepends_one_dotdot() {
assert_eq!(
calculate_relative_path(Path::new("chapter/intro.md"), Path::new("glossary.html")),
"../glossary.html"
);
}
#[test]
fn two_levels_deep_prepends_two_dotdots() {
assert_eq!(
calculate_relative_path(
Path::new("part1/chapter1/intro.md"),
Path::new("glossary.html")
),
"../../glossary.html"
);
}
#[test]
fn sibling_chapter_resolves_without_extra_dotdot() {
assert_eq!(
calculate_relative_path(
Path::new("reference/intro.md"),
Path::new("reference/glossary.html")
),
"glossary.html"
);
}
#[test]
fn deeper_sibling_walks_only_far_enough_up() {
assert_eq!(
calculate_relative_path(
Path::new("guide/api/intro.md"),
Path::new("reference/glossary.html")
),
"../../reference/glossary.html"
);
}
}