Skip to main content

yew_nav_link/utils/
path.rs

1// SPDX-FileCopyrightText: 2024-2026 RAprogramm <andrey.rozanov-vl@gmail.com>
2// SPDX-License-Identifier: MIT
3
4//! # Path Utilities
5//!
6//! Helper functions for working with URL paths: normalization,
7//! absolute path detection, and path joining.
8//!
9//! # Example
10//!
11//! ```rust
12//! use yew_nav_link::{is_absolute, join_paths, normalize_path};
13//!
14//! assert_eq!(normalize_path("/docs//api/"), "/docs/api");
15//! assert!(is_absolute("/docs"));
16//! assert_eq!(join_paths("/base", "child"), "/base/child");
17//! ```
18//!
19//! # Functions
20//!
21//! | Function | Signature | Description |
22//! |----------|-----------|-------------|
23//! | `normalize_path` | `(path: &str) -> String` | Collapse double slashes, trim trailing `/` |
24//! | `is_absolute` | `(path: &str) -> bool` | Check if path starts with `/` |
25//! | `join_paths` | `(base: &str, path: &str) -> String` | Join or replace base path |
26
27/// Normalizes a path by collapsing duplicate slashes and removing trailing
28/// slashes.
29///
30/// Preserves the root `/` path.
31#[must_use]
32pub fn normalize_path(path: &str) -> String {
33    let mut result = String::with_capacity(path.len());
34    let mut prev_was_slash = false;
35
36    for ch in path.chars() {
37        if ch == '/' {
38            if !prev_was_slash {
39                result.push(ch);
40                prev_was_slash = true;
41            }
42        } else {
43            result.push(ch);
44            prev_was_slash = false;
45        }
46    }
47
48    if result.len() > 1 && result.ends_with('/') {
49        result.pop();
50    }
51
52    result
53}
54
55/// Returns `true` if the path starts with `/` (i.e. is absolute).
56#[must_use]
57pub fn is_absolute(path: &str) -> bool {
58    path.starts_with('/')
59}
60
61/// Joins a base path with a child path.
62///
63/// If `path` is absolute, it is returned (normalized) directly.
64/// Otherwise the two paths are concatenated and normalized.
65#[must_use]
66pub fn join_paths(base: &str, path: &str) -> String {
67    if path.starts_with('/') {
68        normalize_path(path)
69    } else {
70        let base = base.trim_end_matches('/');
71        normalize_path(&format!("{base}/{path}"))
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn normalize_removes_double_slashes() {
81        assert_eq!(normalize_path("/docs//api"), "/docs/api");
82        assert_eq!(normalize_path("a//b///c"), "a/b/c");
83    }
84
85    #[test]
86    fn normalize_removes_trailing_slash() {
87        assert_eq!(normalize_path("/docs/"), "/docs");
88        assert_eq!(normalize_path("/"), "/");
89    }
90
91    #[test]
92    fn is_absolute_returns_true_for_rooted_paths() {
93        assert!(is_absolute("/"));
94        assert!(is_absolute("/docs"));
95        assert!(is_absolute("/docs/api"));
96    }
97
98    #[test]
99    fn is_absolute_returns_false_for_relative_paths() {
100        assert!(!is_absolute(""));
101        assert!(!is_absolute("docs"));
102        assert!(!is_absolute("./docs"));
103        assert!(!is_absolute("../docs"));
104    }
105
106    #[test]
107    fn join_paths_with_absolute() {
108        assert_eq!(join_paths("/base", "/docs"), "/docs");
109        assert_eq!(join_paths("/base", "/"), "/");
110    }
111
112    #[test]
113    fn join_paths_with_relative() {
114        assert_eq!(join_paths("/base", "docs"), "/base/docs");
115        assert_eq!(join_paths("/base/", "docs"), "/base/docs");
116    }
117}