utpm 0.3.0

UTPM is a package manager for local and remote Typst packages. Quickly create and manage projects and templates on your system, and publish them directly to Typst Universe.
Documentation
use std::fs::read_to_string;
use std::path::PathBuf;
use std::{fs, path::Path};

use regex::Regex;

use std::io;
use typst_kit::download::{DownloadState, Progress};
use typst_syntax::package::PackageManifest;

pub mod dryrun;
pub mod git;
pub mod macros;
pub mod output;
pub mod paths;
pub mod specs;
pub mod state;

use crate::utpm_bail;

use self::state::Result;

/// Recursively copies a directory from a source to a destination.
///
/// This function is based on the solution from:
/// <https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust>
/// It has been edited to fit the needs of the CI environment.
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
    fn inner(src: &mut PathBuf, dst: &mut PathBuf) -> io::Result<()> {
        fs::create_dir_all(&dst)?;
        for entry in fs::read_dir(&src)? {
            let entry = entry?;
            let ty = entry.file_type()?;
            let name = entry.file_name();

            src.push(&name);
            dst.push(&name);
            if ty.is_dir() && name != ".utpm" {
                inner(src, dst)?;
            } else {
                fs::copy(&src, &dst)?;
            }
            src.pop();
            dst.pop();
        }
        Ok(())
    }
    inner(
        &mut src.as_ref().to_path_buf(),
        &mut dst.as_ref().to_path_buf(),
    )
}

/// Finds the path to a `typst.toml` manifest file in the given directory.
///
/// Returns an error if the manifest file does not exist.
///
/// # Arguments
/// * `s` - The directory to search in
///
/// # Errors
/// Returns `UtpmError::Manifest` if `typst.toml` is not found.
pub fn try_find_path(s: impl AsRef<Path>) -> Result<PathBuf> {
    let manifest_path = PathBuf::from_iter([s.as_ref(), "typst.toml".as_ref()]);

    if !manifest_path.try_exists()? {
        utpm_bail!(Manifest);
    }
    Ok(manifest_path)
}

/// Finds and parses a `typst.toml` manifest file in the given directory.
///
/// Returns the parsed `PackageManifest` structure.
///
/// # Arguments
/// * `s` - The directory containing the manifest file
///
/// # Errors
/// Returns an error if:
/// - The manifest file is not found
/// - The file cannot be read
/// - The TOML content is invalid
pub fn try_find(s: impl AsRef<Path>) -> Result<PackageManifest> {
    let e = read_to_string(try_find_path(s)?)?;
    let f: PackageManifest = toml::from_str(&e)?;
    Ok(f)
}

/// Creates a symlink. This function is platform-specific.
///
/// On Unix systems, it creates a standard symbolic link.
#[cfg(not(windows))]
pub fn symlink_all(origin: impl AsRef<Path>, new_path: impl AsRef<Path>) -> io::Result<()> {
    use std::os::unix::fs::symlink;
    symlink(origin, new_path)
}

/// Creates a symlink. This function is platform-specific.
///
/// On Windows, it creates a directory symlink.
#[cfg(windows)]
pub fn symlink_all(origin: impl AsRef<Path>, new_path: impl AsRef<Path>) -> io::Result<()> {
    use std::os::windows::fs::symlink_dir;
    symlink_dir(origin, new_path)
}

/// Returns a regex for matching typst package specifications (`@namespace/name:version`).
pub fn regex_package() -> Regex {
    Regex::new(r"^@([a-zA-Z]+)\/([a-zA-Z]+(?:\-[a-zA-Z]+)?)\:(\d+)\.(\d+)\.(\d+)$").unwrap()
}

/// Returns a regex for matching typst import statements (`#import "@namespace/name:version"`).
pub fn regex_import() -> Regex {
    Regex::new(r#"\#import \"@([a-zA-Z]+)\/([a-zA-Z]+(?:\-[a-zA-Z]+)?)\:(\d+)\.(\d+)\.(\d+)\""#)
        .unwrap()
}

/// Writes a `PackageManifest` to `./typst.toml` in pretty TOML format.
///
/// Respects dry-run mode - if dry-run is enabled, the file is not actually written.
///
/// # Arguments
/// * `data` - The package manifest to write
///
/// # Errors
/// Returns an error if:
/// - The manifest cannot be serialized to TOML
/// - The file cannot be written (if not in dry-run mode)
pub fn write_manifest(data: &PackageManifest) -> Result<()> {
    let tomlfy: String = toml::to_string_pretty(data)?;
    if !crate::utils::dryrun::get_dry_run() {
        std::fs::write(Path::new("./typst.toml"), tomlfy)?;
    }
    Ok(())
}

//todo: impl
/// A progress indicator for package downloads.
///
/// (Warning) This is not fully implemented yet.
/// It is intended to provide feedback to the user during package downloads.
pub struct ProgressPrint {}

impl Progress for ProgressPrint {
    fn print_start(&mut self) {}

    fn print_progress(&mut self, _state: &DownloadState) {}

    fn print_finish(&mut self, _state: &DownloadState) {}
}

mod tests {
    #[test]
    fn regex() {
        let re = super::regex_package();
        assert!(re.is_match("@preview/package:2.0.1"));
        assert!(!re.is_match("@preview/package-:2.0.1"));
        assert!(re.is_match("@local/package-A:2.0.1"));
        assert!(re.is_match("@local/package-a:2.0.1"));
        assert!(re.is_match("@local/AAAAAAAAAAAAAA:2.0.1"));
        assert!(!re.is_match("@local/p:1..1"));
        assert!(re.is_match("@a/p:1.0.1"));
        assert!(!re.is_match("@a/p:v1.0.1"));
        assert!(!re.is_match("@/p:1.0.1"));
        assert!(!re.is_match("p:1.0.1"));
        assert!(!re.is_match("@a/p"));
    }
}