1use std::path::{Component, Path, PathBuf};
5
6use unicode_normalization::UnicodeNormalization;
7
8fn is_unicode_space(c: char) -> bool {
9 matches!(
10 c,
11 '\u{00A0}' | '\u{2000}'..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}'
12 )
13}
14
15pub fn home_dir() -> PathBuf {
18 std::env::var_os("HOME")
20 .map(PathBuf::from)
21 .or_else(std::env::home_dir)
22 .unwrap_or_else(|| PathBuf::from("/"))
23}
24
25pub fn normalize(input: &str) -> String {
28 let mut text: String = input
29 .chars()
30 .map(|c| if is_unicode_space(c) { ' ' } else { c })
31 .collect();
32 if let Some(rest) = text.strip_prefix('@') {
33 text = rest.to_owned();
34 }
35 expand(&text)
36}
37
38pub fn expand(text: &str) -> String {
40 if text.starts_with("file://") {
42 return url::Url::parse(text)
43 .ok()
44 .and_then(|url| url.to_file_path().ok())
45 .map_or_else(
46 || text.to_owned(),
47 |path| path.to_string_lossy().into_owned(),
48 );
49 }
50 expand_home(text)
51}
52
53pub fn file_url(path: &Path) -> String {
55 url::Url::from_file_path(path)
56 .map(String::from)
57 .unwrap_or_else(|_| format!("file://{}", path.display()))
58}
59
60pub fn file_url_path(url: &str) -> Option<PathBuf> {
63 let url = url::Url::parse(url).ok()?;
64 if url.scheme() != "file" {
65 return None;
66 }
67 url.to_file_path().ok()
68}
69
70pub fn expand_home(text: &str) -> String {
72 if text == "~" {
73 return home_dir().to_string_lossy().into_owned();
74 }
75 match text.strip_prefix("~/") {
76 Some(rest) => home_dir().join(rest).to_string_lossy().into_owned(),
77 None => text.to_owned(),
78 }
79}
80
81pub fn resolve_lexically(base: &Path, path: &Path) -> PathBuf {
83 let joined = if path.is_absolute() {
84 path.to_path_buf()
85 } else {
86 base.join(path)
87 };
88 let mut result = PathBuf::new();
89 for component in joined.components() {
90 match component {
91 Component::ParentDir => {
92 result.pop();
93 }
94 Component::CurDir => {}
95 other => result.push(other.as_os_str()),
96 }
97 }
98 result
99}
100
101pub fn relative(from: &Path, to: &Path) -> String {
104 use std::path::Component;
105 let parts = |path: &Path| -> Vec<String> {
106 resolve_lexically(Path::new("/"), path)
107 .components()
108 .filter_map(|component| match component {
109 Component::Normal(part) => Some(part.to_string_lossy().into_owned()),
110 _ => None,
111 })
112 .collect()
113 };
114 let (from, to) = (parts(from), parts(to));
115 let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
116 let mut steps: Vec<String> = vec!["..".to_owned(); from.len() - common];
117 steps.extend(to[common..].iter().cloned());
118 steps.join("/")
119}
120
121pub fn resolve_to_cwd(path: &str, cwd: &Path) -> PathBuf {
123 resolve_lexically(cwd, Path::new(&normalize(path)))
124}
125
126pub fn resolve_read_path(path: &str, cwd: &Path) -> PathBuf {
130 let resolved = resolve_to_cwd(path, cwd);
131 if resolved.exists() {
132 return resolved;
133 }
134 let text = resolved.to_string_lossy().into_owned();
135 let am_pm = text
136 .replace(" AM.", "\u{202F}AM.")
137 .replace(" PM.", "\u{202F}PM.")
138 .replace(" am.", "\u{202F}am.")
139 .replace(" pm.", "\u{202F}pm.");
140 let nfd: String = text.nfd().collect();
141 let curly = text.replace('\'', "\u{2019}");
142 let nfd_curly = nfd.replace('\'', "\u{2019}");
143 for candidate in [am_pm, nfd, curly, nfd_curly] {
144 if candidate != text && Path::new(&candidate).exists() {
145 return PathBuf::from(candidate);
146 }
147 }
148 resolved
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn resolves_like_pi() {
157 let cwd = Path::new("/work/project");
158 assert_eq!(
159 resolve_to_cwd("@src/../lib.rs", cwd),
160 PathBuf::from("/work/project/lib.rs")
161 );
162 assert_eq!(resolve_to_cwd("/tmp/./a", cwd), PathBuf::from("/tmp/a"));
163 assert_eq!(
164 resolve_to_cwd("file:///tmp/a%20b", cwd),
165 PathBuf::from("/tmp/a b")
166 );
167 assert_eq!(normalize("a\u{00A0}b"), "a b");
168 }
169
170 #[test]
171 fn relative_paths_like_node() {
172 assert_eq!(
173 relative(Path::new("/a/b"), Path::new("/a/b/c/d.txt")),
174 "c/d.txt"
175 );
176 assert_eq!(relative(Path::new("/a/b"), Path::new("/a/x")), "../x");
177 assert_eq!(relative(Path::new("/a/b/"), Path::new("/a/b")), "");
178 }
179}