calepin 0.0.20

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

use anyhow::{Context, Result};

use crate::typst::io::write_if_changed;

const RUNTIME_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/assets/typst-runtime");
const GENERATED_SYNTAX_THEME_FILE: &str = "runtime/00_syntax-theme.typ";

fn runtime_source_files() -> Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    collect_runtime_files(Path::new(RUNTIME_DIR), &mut files)?;

    files.sort_unstable_by_key(|path| {
        path.strip_prefix(RUNTIME_DIR)
            .unwrap_or(path)
            .as_os_str()
            .to_owned()
    });

    Ok(files)
}

fn collect_runtime_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
    for entry in fs::read_dir(dir).with_context(|| format!("failed to read {}", dir.display()))? {
        let entry = entry.with_context(|| format!("failed to read {}", dir.display()))?;
        let path = entry.path();
        let file_type = entry
            .file_type()
            .with_context(|| format!("failed to stat {}", path.display()))?;
        if file_type.is_dir() {
            collect_runtime_files(&path, files)?;
        } else if path.extension() == Some(OsStr::new("typ")) {
            files.push(path);
        }
    }
    Ok(())
}

fn runtime_relative_path(path: &Path) -> PathBuf {
    path.strip_prefix(RUNTIME_DIR)
        .map(Path::to_path_buf)
        .unwrap_or_else(|_| {
            path.file_name()
                .map(PathBuf::from)
                .unwrap_or_else(|| path.to_path_buf())
        })
}

fn runtime_facade_source() -> Result<String> {
    Ok(r#"// Generated by Calepin. Do not edit.

#import "runtime/core/state.typ" as state
#import "runtime/core/target.typ" as target
#import "runtime/notebook/render.typ" as render
#import "runtime/notebook/options.typ" as options
#import "runtime/notebook/chunk.typ" as chunks
#import "runtime/elements/mod.typ" as elementmod

#let pages = state.pages
#let setup = options.setup
#let chunk = chunks.chunk
#let inline = chunks.inline
#let results = chunks.results
#let chunk_from_raw_plain = chunks.chunk_from_raw_plain
#let code-block = render.code-block
#let elements = elementmod

#let _mode = target._mode
#let _is-html = target._is-html
#let _is-paged = target._is-paged
#let _is-query = target._is-query
#let _is-render = target._is-render
#let _call-defaults = state._call-defaults
#let _disable-raw-chunk-transforms = state._disable-raw-chunk-transforms
#let _resolve-options = options._resolve-options
#let _html-themed-raw-block = render._html-themed-raw-block
#let _without-raw-chunk-transforms = chunks._without-raw-chunk-transforms
#let _fenced-chunks-runs = chunks._fenced-chunks-runs
"#
    .to_string())
}

#[cfg(test)]
pub fn write_runtime(root: &Path) -> Result<PathBuf> {
    write_runtime_with_syntax_theme(root, &crate::html::HtmlSyntaxTheme::builtin())
}

pub(crate) fn write_runtime_with_syntax_theme(
    root: &Path,
    syntax_theme: &crate::html::HtmlSyntaxTheme,
) -> Result<PathBuf> {
    let calepin_dir = root.join(".calepin");
    let runtime_dir = calepin_dir.join("runtime");
    fs::create_dir_all(&runtime_dir)
        .with_context(|| format!("failed to create {}", runtime_dir.display()))?;

    write_if_changed(
        &calepin_dir.join(GENERATED_SYNTAX_THEME_FILE),
        syntax_theme.typst_runtime_source(),
    )?;

    for source_path in runtime_source_files()? {
        let rel = runtime_relative_path(&source_path);
        let destination = runtime_dir.join(&rel);
        let source = fs::read_to_string(&source_path)
            .with_context(|| format!("failed to read {}", source_path.display()))?;
        write_if_changed(&destination, source)?;
    }

    let facade = calepin_dir.join("calepin.typ");
    write_if_changed(&facade, runtime_facade_source()?)?;
    Ok(facade)
}

#[cfg(test)]
#[path = "runtime_tests.rs"]
mod runtime_tests;