1use std::borrow::Cow;
2
3use crate::history::{AnyHistory, History, HistoryError, HistoryResult};
4use crate::query::ToQuery;
5use crate::routable::Routable;
6
7pub type NavigationError = HistoryError;
8pub type NavigationResult<T> = HistoryResult<T>;
9
10#[derive(Debug, PartialEq, Eq, Clone, Copy)]
12pub enum NavigatorKind {
13 Browser,
15 Hash,
17 Memory,
19}
20
21#[derive(Debug, PartialEq, Clone)]
23pub struct Navigator {
24 inner: AnyHistory,
25 basename: Option<String>,
26}
27
28impl Navigator {
29 pub(crate) fn new(history: AnyHistory, basename: Option<String>) -> Self {
30 Self {
31 inner: history,
32 basename,
33 }
34 }
35
36 pub fn basename(&self) -> Option<&str> {
38 self.basename.as_deref()
39 }
40
41 pub fn back(&self) {
43 self.go(-1);
44 }
45
46 pub fn forward(&self) {
48 self.go(1);
49 }
50
51 pub fn go(&self, delta: isize) {
55 self.inner.go(delta);
56 }
57
58 pub fn push<R>(&self, route: &R)
60 where
61 R: Routable,
62 {
63 self.inner.push(self.prefix_basename(&route.to_path()));
64 }
65
66 pub fn replace<R>(&self, route: &R)
68 where
69 R: Routable,
70 {
71 self.inner.replace(self.prefix_basename(&route.to_path()));
72 }
73
74 pub fn push_with_state<R, T>(&self, route: &R, state: T)
76 where
77 R: Routable,
78 T: 'static,
79 {
80 self.inner
81 .push_with_state(self.prefix_basename(&route.to_path()), state);
82 }
83
84 pub fn replace_with_state<R, T>(&self, route: &R, state: T)
86 where
87 R: Routable,
88 T: 'static,
89 {
90 self.inner
91 .replace_with_state(self.prefix_basename(&route.to_path()), state);
92 }
93
94 pub fn push_with_query<R, Q>(&self, route: &R, query: Q) -> Result<(), Q::Error>
96 where
97 R: Routable,
98 Q: ToQuery,
99 {
100 self.inner
101 .push_with_query(self.prefix_basename(&route.to_path()), query)
102 }
103
104 pub fn replace_with_query<R, Q>(&self, route: &R, query: Q) -> Result<(), Q::Error>
106 where
107 R: Routable,
108 Q: ToQuery,
109 {
110 self.inner
111 .replace_with_query(self.prefix_basename(&route.to_path()), query)
112 }
113
114 pub fn push_with_query_and_state<R, Q, T>(
116 &self,
117 route: &R,
118 query: Q,
119 state: T,
120 ) -> Result<(), Q::Error>
121 where
122 R: Routable,
123 Q: ToQuery,
124 T: 'static,
125 {
126 self.inner
127 .push_with_query_and_state(self.prefix_basename(&route.to_path()), query, state)
128 }
129
130 pub fn replace_with_query_and_state<R, Q, T>(
132 &self,
133 route: &R,
134 query: Q,
135 state: T,
136 ) -> Result<(), Q::Error>
137 where
138 R: Routable,
139 Q: ToQuery,
140 T: 'static,
141 {
142 self.inner.replace_with_query_and_state(
143 self.prefix_basename(&route.to_path()),
144 query,
145 state,
146 )
147 }
148
149 pub fn kind(&self) -> NavigatorKind {
151 match &self.inner {
152 AnyHistory::Browser(_) => NavigatorKind::Browser,
153 AnyHistory::Hash(_) => NavigatorKind::Hash,
154 AnyHistory::Memory(_) => NavigatorKind::Memory,
155 }
156 }
157
158 pub(crate) fn prefix_basename<'a>(&self, route_s: &'a str) -> Cow<'a, str> {
159 match self.basename() {
160 Some(base) => {
161 if base.is_empty() && route_s.is_empty() {
162 Cow::from("/")
163 } else {
164 Cow::from(format!("{base}{route_s}"))
165 }
166 }
167 None => route_s.into(),
168 }
169 }
170
171 pub(crate) fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {
172 match self.basename() {
173 Some(m) => {
174 let mut path = strip_basename_prefix(&path, m)
175 .map(|rest| Cow::Owned(rest.to_owned()))
176 .unwrap_or(path);
177
178 if !path.starts_with('/') {
179 path = format!("/{path}").into();
180 }
181
182 path
183 }
184 None => path,
185 }
186 }
187}
188
189fn strip_basename_prefix<'a>(path: &'a str, basename: &str) -> Option<&'a str> {
195 path.strip_prefix(basename)
196 .filter(|rest| rest.is_empty() || rest.starts_with('/'))
197}
198
199#[cfg(test)]
200mod tests {
201 use std::borrow::Cow;
202
203 use super::Navigator;
204 use crate::history::{AnyHistory, MemoryHistory};
205
206 fn make_nav(basename: Option<&str>) -> Navigator {
207 Navigator::new(
208 AnyHistory::from(MemoryHistory::default()),
209 basename.map(str::to_owned),
210 )
211 }
212
213 #[test]
214 fn strip_exact_match() {
215 let nav = make_nav(Some("/base"));
216 assert_eq!(nav.strip_basename(Cow::from("/base")), "/");
217 }
218
219 #[test]
220 fn strip_with_trailing_path() {
221 let nav = make_nav(Some("/base"));
222 assert_eq!(nav.strip_basename(Cow::from("/base/page")), "/page");
223 assert_eq!(nav.strip_basename(Cow::from("/base/a/b/c")), "/a/b/c");
224 }
225
226 #[test]
227 fn strip_partial_segment_rejected() {
228 let nav = make_nav(Some("/base"));
229 assert_eq!(nav.strip_basename(Cow::from("/baseball")), "/baseball");
231 assert_eq!(nav.strip_basename(Cow::from("/base-2")), "/base-2");
232 assert_eq!(nav.strip_basename(Cow::from("/basefoo")), "/basefoo");
233 }
234
235 #[test]
236 fn strip_no_match() {
237 let nav = make_nav(Some("/base"));
238 assert_eq!(nav.strip_basename(Cow::from("/other/page")), "/other/page");
239 }
240
241 #[test]
242 fn strip_no_basename() {
243 let nav = make_nav(None);
244 assert_eq!(nav.strip_basename(Cow::from("/any/path")), "/any/path");
245 }
246}