1#[must_use]
8pub fn doc_dir(path: &str) -> &str {
9 match path.rfind('/') {
10 Some(i) => &path[..=i],
11 None => "",
12 }
13}
14
15#[must_use]
20pub fn resolve_relative(target: &str, doc_dir: &str) -> String {
21 if !target.starts_with("./") && !target.starts_with("../") {
22 return target.to_owned();
23 }
24 let joined = format!("{doc_dir}{target}");
25 let mut resolved: Vec<&str> = Vec::new();
26 for p in joined.split('/') {
27 match p {
28 "." | "" => {}
29 ".." => {
30 resolved.pop();
31 }
32 seg => resolved.push(seg),
33 }
34 }
35 resolved.join("/")
36}
37
38#[must_use]
40pub fn canonical_path(path: &str) -> &str {
41 path.strip_prefix('/').unwrap_or(path)
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn directory_of_a_path() {
50 assert_eq!(doc_dir("a/b/c.md"), "a/b/");
51 assert_eq!(doc_dir("c.md"), "");
52 assert_eq!(doc_dir("a/"), "a/");
53 assert_eq!(doc_dir(""), "");
54 }
55
56 #[test]
57 fn relative_targets_join_the_directory() {
58 assert_eq!(resolve_relative("./x.md", "a/b/"), "a/b/x.md");
59 assert_eq!(resolve_relative("../x.md", "a/b/"), "a/x.md");
60 assert_eq!(resolve_relative("../../x.md", "a/b/"), "x.md");
61 assert_eq!(resolve_relative("../../../x.md", "a/b/"), "x.md");
62 assert_eq!(resolve_relative("./x.md", ""), "x.md");
63 assert_eq!(resolve_relative("./x/../../old.md", "a/"), "old.md");
64 assert_eq!(resolve_relative("./a//b/./c.md", ""), "a/b/c.md");
65 assert_eq!(resolve_relative("x.md", "a/b/"), "x.md");
67 assert_eq!(resolve_relative("/x.md", "a/b/"), "/x.md");
68 assert_eq!(resolve_relative("note", "a/"), "note");
69 assert_eq!(resolve_relative(".hidden/x.md", "a/"), ".hidden/x.md");
70 assert_eq!(resolve_relative("", "a/"), "");
71 }
72
73 #[test]
74 fn canonical_strips_one_slash() {
75 assert_eq!(canonical_path("/a.md"), "a.md");
76 assert_eq!(canonical_path("//a.md"), "/a.md");
77 assert_eq!(canonical_path("a.md"), "a.md");
78 }
79}