calepin 0.0.52

A Rust CLI for preprocessing Typst documents with executable code chunks
use std::path::{Path, PathBuf};

pub fn expand_home(path: PathBuf) -> PathBuf {
    let text = path.to_string_lossy();
    if text == "~" {
        return std::env::var_os("HOME").map(PathBuf::from).unwrap_or(path);
    }
    let Some(rest) = text.strip_prefix("~/") else {
        return path;
    };
    std::env::var_os("HOME")
        .map(|home| PathBuf::from(home).join(rest))
        .unwrap_or(path)
}

pub fn is_path_like(path: &Path) -> bool {
    path.components().count() > 1 || path.to_string_lossy().contains('\\')
}

pub fn normalize_path(path: &Path) -> PathBuf {
    let mut out = PathBuf::new();
    for component in path.components() {
        match component {
            std::path::Component::CurDir => {}
            std::path::Component::ParentDir => {
                out.pop();
            }
            _ => out.push(component.as_os_str()),
        }
    }
    out
}

pub fn absolutize_from(root: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        root.join(path)
    }
}

pub(crate) fn strip_query_and_fragment(value: &str) -> &str {
    value.split(&['?', '#'][..]).next().unwrap_or(value)
}

pub(crate) fn strip_leading_url_prefix(value: &str) -> &str {
    value.trim_start_matches('/').trim_start_matches("./")
}

pub(crate) fn canonical_root(path: &Path) -> Option<PathBuf> {
    path.canonicalize().ok()
}

pub(crate) fn is_within_root(root: &Path, path: &Path) -> bool {
    canonical_root(root)
        .and_then(|root| path.canonicalize().ok().map(|path| path.starts_with(root)))
        .unwrap_or(false)
}

pub fn slash_path(path: &Path) -> String {
    path.components()
        .map(|component| component.as_os_str().to_string_lossy())
        .collect::<Vec<_>>()
        .join("/")
}

#[cfg(test)]
mod tests {
    use super::{absolutize_from, expand_home, strip_leading_url_prefix, strip_query_and_fragment};
    use crate::utils::testutil::{env_lock, EnvVarGuard};
    use std::path::{Path, PathBuf};

    #[test]
    fn absolutize_from_joins_only_relative_paths() {
        assert_eq!(
            absolutize_from(Path::new("/project"), Path::new("config.toml")),
            PathBuf::from("/project/config.toml")
        );
        assert_eq!(
            absolutize_from(Path::new("/project"), Path::new("/tmp/config.toml")),
            PathBuf::from("/tmp/config.toml")
        );
    }

    #[test]
    fn expand_home_expands_tilde_and_home_relative_paths() {
        let _env_lock = env_lock();
        let _home = EnvVarGuard::set("HOME", "/users/example");

        assert_eq!(
            expand_home(PathBuf::from("~/docs")),
            PathBuf::from("/users/example/docs")
        );
        assert_eq!(
            expand_home(PathBuf::from("~")),
            PathBuf::from("/users/example")
        );
        assert_eq!(
            expand_home(PathBuf::from("/absolute")),
            PathBuf::from("/absolute")
        );
    }

    #[test]
    fn strip_query_and_fragment_stops_at_first_query_or_fragment() {
        assert_eq!(
            strip_query_and_fragment("index.html?cache=1#top"),
            "index.html"
        );
        assert_eq!(strip_query_and_fragment("notes.html#section"), "notes.html");
        assert_eq!(strip_query_and_fragment("about.md"), "about.md");
    }

    #[test]
    fn strip_leading_url_prefix_removes_root_and_dot_prefixes() {
        assert_eq!(strip_leading_url_prefix("/about.html"), "about.html");
        assert_eq!(strip_leading_url_prefix("./notes/main"), "notes/main");
        assert_eq!(strip_leading_url_prefix("notes/main"), "notes/main");
    }
}