calepin 0.0.18

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

use crate::typst::model::LayoutPaths;

pub fn resolve_layout(input: &Path, root: Option<&Path>) -> Result<LayoutPaths> {
    let input_abs =
        absolutize(input).with_context(|| format!("failed to resolve {}", input.display()))?;
    let root_abs = match root {
        Some(root) => {
            absolutize(root).with_context(|| format!("failed to resolve {}", root.display()))?
        }
        None => input_abs
            .parent()
            .map(Path::to_path_buf)
            .unwrap_or_else(|| PathBuf::from(".")),
    };

    let input_rel = input_abs
        .strip_prefix(&root_abs)
        .map(Path::to_path_buf)
        .map_err(|_| {
            anyhow!(
                "input `{}` is not under root `{}`",
                input_abs.display(),
                root_abs.display()
            )
        })?;
    let stem = input_stem(&input_rel)?;
    let base = root_abs.join(".calepin").join(&stem);
    let results_path = base.join("results.json");
    let work_dir = input_abs
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| root_abs.clone());

    Ok(LayoutPaths {
        root: root_abs,
        input: input_abs,
        input_rel: input_rel.clone(),
        render_input: input_rel.clone(),
        work_dir,
        artifact_dir: base.clone(),
        results_path,
        figures_dir: base.join("figures"),
    })
}

pub fn artifact_reference(root: &Path, path: &Path) -> String {
    match path.strip_prefix(root) {
        Ok(rel) => format!("/{}", slash_path(rel)),
        Err(_) => slash_path(path),
    }
}

pub fn project_relative_path(root: &Path, path: &Path) -> String {
    path.strip_prefix(root)
        .map(slash_path)
        .unwrap_or_else(|_| path.display().to_string())
}

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

fn absolutize(path: &Path) -> Result<PathBuf> {
    if path.exists() {
        return std::fs::canonicalize(path).map_err(Into::into);
    }
    let path = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()?.join(path)
    };
    Ok(path)
}

fn input_stem(input_rel: &Path) -> Result<PathBuf> {
    let mut stem = input_rel.to_path_buf();
    if stem.extension().and_then(|extension| extension.to_str()) != Some("typ") {
        return Err(anyhow!(
            "input `{}` must have a .typ extension",
            input_rel.display()
        ));
    }
    stem.set_extension("");
    Ok(stem)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lays_out_root_relative_nested_input() {
        let dir = tempfile::tempdir().unwrap();
        let input = dir.path().join("chapters").join("intro.typ");
        std::fs::create_dir_all(input.parent().unwrap()).unwrap();
        std::fs::write(&input, "").unwrap();
        let root = std::fs::canonicalize(dir.path()).unwrap();

        let layout = resolve_layout(&input, Some(dir.path())).unwrap();

        assert_eq!(layout.input_rel, PathBuf::from("chapters/intro.typ"));
        assert_eq!(
            layout.results_path,
            root.join(".calepin/chapters/intro/results.json")
        );
        assert_eq!(
            layout.figures_dir,
            root.join(".calepin/chapters/intro/figures")
        );
    }

    #[test]
    fn defaults_root_to_input_directory() {
        let dir = tempfile::tempdir().unwrap();
        let input = dir.path().join("paper.typ");
        std::fs::write(&input, "").unwrap();
        let root = std::fs::canonicalize(dir.path()).unwrap();

        let layout = resolve_layout(&input, None).unwrap();

        assert_eq!(layout.input_rel, PathBuf::from("paper.typ"));
        assert_eq!(
            layout.results_path,
            root.join(".calepin/paper/results.json")
        );
    }

    #[test]
    fn artifact_refs_are_root_relative_with_slashes() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".calepin/paper/figures/fig.svg");
        assert_eq!(
            artifact_reference(dir.path(), &path),
            "/.calepin/paper/figures/fig.svg"
        );
    }

    #[test]
    fn project_relative_paths_are_short_for_humans() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".calepin/paper/results.json");
        assert_eq!(
            project_relative_path(dir.path(), &path),
            ".calepin/paper/results.json"
        );
    }
}