lean_ctx/dashboard/
base_path.rs1pub fn normalize(input: &str) -> String {
21 let trimmed = input.trim().trim_matches('/');
22 if trimmed.is_empty() {
23 String::new()
24 } else {
25 format!("/{trimmed}")
26 }
27}
28
29pub fn strip<'a>(path: &'a str, base: &str) -> &'a str {
34 if base.is_empty() {
35 return path;
36 }
37 if path == base {
38 return "/";
39 }
40 match path.strip_prefix(base) {
41 Some(rest) if rest.starts_with('/') => rest,
42 _ => path,
43 }
44}
45
46pub fn rewrite_asset_urls(body: &str, base: &str) -> String {
51 if base.is_empty() {
52 return body.to_string();
53 }
54 const PREFIXES: &[&str] = &["/static/", "/api/", "/favicon"];
57 const DELIMS: &[char] = &['"', '\'', '`', '('];
60
61 let mut out = body.to_string();
62 for prefix in PREFIXES {
63 for &d in DELIMS {
64 let from = format!("{d}{prefix}");
65 if out.contains(&from) {
66 let to = format!("{d}{base}{prefix}");
67 out = out.replace(&from, &to);
68 }
69 }
70 }
71 out
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn normalize_canonicalizes() {
80 assert_eq!(normalize(""), "");
81 assert_eq!(normalize("/"), "");
82 assert_eq!(normalize(" "), "");
83 assert_eq!(normalize("dashboard"), "/dashboard");
84 assert_eq!(normalize("/dashboard"), "/dashboard");
85 assert_eq!(normalize("/dashboard/"), "/dashboard");
86 assert_eq!(normalize("//dashboard//"), "/dashboard");
87 assert_eq!(normalize("/lean/ctx"), "/lean/ctx");
88 }
89
90 #[test]
91 fn strip_empty_base_is_noop() {
92 assert_eq!(strip("/api/stats", ""), "/api/stats");
93 assert_eq!(strip("/", ""), "/");
94 }
95
96 #[test]
97 fn strip_removes_prefix() {
98 assert_eq!(strip("/dashboard", "/dashboard"), "/");
99 assert_eq!(strip("/dashboard/", "/dashboard"), "/");
100 assert_eq!(strip("/dashboard/api/stats", "/dashboard"), "/api/stats");
101 assert_eq!(
102 strip("/dashboard/static/style.css", "/dashboard"),
103 "/static/style.css"
104 );
105 }
106
107 #[test]
108 fn strip_accepts_already_rootrelative() {
109 assert_eq!(strip("/api/stats", "/dashboard"), "/api/stats");
111 assert_eq!(strip("/", "/dashboard"), "/");
112 }
113
114 #[test]
115 fn strip_does_not_match_partial_segment() {
116 assert_eq!(strip("/dashboardx/api", "/dashboard"), "/dashboardx/api");
118 }
119
120 #[test]
121 fn rewrite_empty_base_is_noop() {
122 let html = r#"<script src="/static/lib/api.js"></script>"#;
123 assert_eq!(rewrite_asset_urls(html, ""), html);
124 }
125
126 #[test]
127 fn rewrite_html_attributes() {
128 let html = r#"<link href="/static/style.css"><script src="/static/lib/api.js"></script><link rel="icon" href="/favicon.svg">"#;
129 let out = rewrite_asset_urls(html, "/dashboard");
130 assert!(out.contains(r#"href="/dashboard/static/style.css""#));
131 assert!(out.contains(r#"src="/dashboard/static/lib/api.js""#));
132 assert!(out.contains(r#"href="/dashboard/favicon.svg""#));
133 assert!(!out.contains(r#"href="/static/"#));
134 }
135
136 #[test]
137 fn rewrite_js_string_and_template_literals() {
138 let js = "fetch('/api/stats'); const u = `/api/search?q=${q}`; api('/api/pulse');";
139 let out = rewrite_asset_urls(js, "/dashboard");
140 assert!(out.contains("fetch('/dashboard/api/stats')"));
141 assert!(out.contains("`/dashboard/api/search?q=${q}`"));
142 assert!(out.contains("api('/dashboard/api/pulse')"));
143 }
144
145 #[test]
146 fn rewrite_css_url() {
147 let css = "src: url('/static/fonts/inter-variable.woff2');";
148 let out = rewrite_asset_urls(css, "/dashboard");
149 assert!(out.contains("url('/dashboard/static/fonts/inter-variable.woff2')"));
150 }
151
152 #[test]
153 fn rewrite_fetch_interceptor_guard() {
154 let js = "if (url.startsWith('/api/')) attachToken();";
157 let out = rewrite_asset_urls(js, "/dashboard");
158 assert!(out.contains("url.startsWith('/dashboard/api/')"));
159 }
160
161 #[test]
162 fn rewrite_does_not_double_prefix() {
163 let html = r#"<script src="/static/x.js"></script>"#;
164 let once = rewrite_asset_urls(html, "/dashboard");
165 assert_eq!(once.matches("/dashboard/static/x.js").count(), 1);
168 assert!(!once.contains("/dashboard/dashboard/"));
169 }
170}