Skip to main content

anstyle_hyperlink/
file.rs

1/// Create a URL from a given path
2pub fn path_to_url(path: &std::path::Path) -> Option<String> {
3    // Do a best-effort for getting the host in the URL
4    let hostname = if cfg!(windows) {
5        // Not supported correctly on windows
6        None
7    } else {
8        crate::hostname().ok().and_then(|os| os.into_string().ok())
9    };
10    if path.is_dir() {
11        dir_to_url(hostname.as_deref(), path)
12    } else {
13        file_to_url(hostname.as_deref(), path)
14    }
15}
16
17/// Create a URL from a given hostname and file path
18///
19/// For hyperlink escape codes, the hostname is used to avoid issues with opening a link scoped to
20/// the computer you've SSH'ed into
21/// ([reference](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#file-uris-and-the-hostname))
22pub fn file_to_url(hostname: Option<&str>, path: &std::path::Path) -> Option<String> {
23    if !path.is_absolute() {
24        return None;
25    }
26
27    let mut url = "file://".to_owned();
28    if let Some(hostname) = hostname {
29        url.push_str(hostname);
30    }
31
32    // skip the root component
33    let mut is_path_empty = true;
34    for component in path.components().skip(1) {
35        is_path_empty = false;
36        url.push_str(URL_PATH_SEP);
37        let component = component.as_os_str().to_str()?;
38        url.extend(percent_encoding::percent_encode(
39            component.as_bytes(),
40            SPECIAL_PATH_SEGMENT,
41        ));
42    }
43    if is_path_empty {
44        // An URL's path must not be empty
45        url.push_str(URL_PATH_SEP);
46    }
47
48    Some(url)
49}
50
51/// Create a URL from a given hostname and directory path
52///
53/// For hyperlink escape codes, the hostname is used to avoid issues with opening a link scoped to
54/// the computer you've SSH'ed into
55/// ([reference](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#file-uris-and-the-hostname))
56pub fn dir_to_url(hostname: Option<&str>, path: &std::path::Path) -> Option<String> {
57    let mut url = file_to_url(hostname, path)?;
58    if !url.ends_with(URL_PATH_SEP) {
59        url.push_str(URL_PATH_SEP);
60    }
61    Some(url)
62}
63
64const URL_PATH_SEP: &str = "/";
65
66/// <https://url.spec.whatwg.org/#fragment-percent-encode-set>
67const FRAGMENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
68    .add(b' ')
69    .add(b'"')
70    .add(b'<')
71    .add(b'>')
72    .add(b'`');
73
74/// <https://url.spec.whatwg.org/#path-percent-encode-set>
75const PATH: &percent_encoding::AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
76
77const PATH_SEGMENT: &percent_encoding::AsciiSet = &PATH.add(b'/').add(b'%');
78
79// The backslash (\) character is treated as a path separator in special URLs
80// so it needs to be additionally escaped in that case.
81const SPECIAL_PATH_SEGMENT: &percent_encoding::AsciiSet = &PATH_SEGMENT.add(b'\\');