Skip to main content

omgbase_graph/
path.rs

1//! Path resolution (§3.2): a `./` or `../` target against the source
2//! document's directory, and the canonical (no leading `/`) repo path.
3
4/// The directory of a repo path: everything up to and including the last
5/// `/` (`"a/b/"` for `a/b/c.md`, `""` at the root). The reference's
6/// `path.replace(/[^/]*$/, "")`.
7#[must_use]
8pub fn doc_dir(path: &str) -> &str {
9    match path.rfind('/') {
10        Some(i) => &path[..=i],
11        None => "",
12    }
13}
14
15/// §3.2 (`resolveRelativePath`): a target starting with `./` or `../` is
16/// joined to `doc_dir` and normalized segment-wise — `.` and empty segments
17/// dropped, `..` pops (past the root it is simply dropped); any other target
18/// is returned as is.
19#[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/// One leading `/` stripped (`resolveDocPath`'s `path.replace(/^\//, "")`).
39#[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        // Anything else is taken as is (root-relative).
66        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}