calepin 0.0.14

A Rust CLI for preprocessing Typst documents with executable code chunks
use anyhow::{anyhow, Context, Result};
use serde_json::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, OnceLock};

use super::{commands, split_page_meta, PreprocessMetadata};
use crate::typst::model::LayoutPaths;
use crate::typst::run::{TypstInput, INPUT_MODE, INPUT_RESULTS, INPUT_TARGET};

static EVAL_AVAILABLE: OnceLock<Mutex<HashMap<PathBuf, bool>>> = OnceLock::new();

pub fn is_available(typst: &Path) -> bool {
    let cache = EVAL_AVAILABLE.get_or_init(|| Mutex::new(HashMap::new()));
    if let Some(available) = cache
        .lock()
        .ok()
        .and_then(|cache| cache.get(typst).copied())
    {
        return available;
    }

    let available = Command::new(typst)
        .arg("eval")
        .arg("1")
        .arg("--format")
        .arg("json")
        .output()
        .is_ok_and(|output| output.status.success());
    if let Ok(mut cache) = cache.lock() {
        cache.insert(typst.to_path_buf(), available);
    }
    available
}

pub fn preprocess_metadata(
    typst: &Path,
    layout: &LayoutPaths,
    input: &Path,
    results_input: &str,
) -> Result<PreprocessMetadata> {
    let output = commands::typst_eval(
        typst,
        layout,
        input,
        r#"(
  setup: query(selector(<calepin-config>).or(<website-metadata>)),
  chunks: query(raw.where(block: true).or(<calepin-fence-label>).or(<calepin-chunk>)),
)"#,
        &[
            TypstInput::new(INPUT_MODE, "query"),
            TypstInput::new(INPUT_RESULTS, results_input),
            TypstInput::new(INPUT_TARGET, "paged"),
        ],
    )?;
    let root: Value =
        serde_json::from_str(&output).context("failed to parse typst eval metadata output")?;
    let setup = root
        .get("setup")
        .cloned()
        .ok_or_else(|| anyhow!("typst eval metadata output is missing `setup`"))?;
    let chunks = root
        .get("chunks")
        .cloned()
        .ok_or_else(|| anyhow!("typst eval metadata output is missing `chunks`"))?;
    let setup_array = setup
        .as_array()
        .ok_or_else(|| anyhow!("typst eval setup output must be an array"))?;
    let (setup_json, page_meta) = split_page_meta(setup_array)?;

    Ok(PreprocessMetadata {
        setup_json,
        page_meta,
        chunks_json: serde_json::to_string(&chunks)?,
    })
}

pub fn page_anchors(typst: &Path, layout: &LayoutPaths) -> Result<HashMap<String, usize>> {
    let results_input = super::results_input(layout);
    let output = commands::typst_eval(
        typst,
        layout,
        &layout.render_input,
        r#"query(<calepin-page>).map(it => (
  label: it.value.label,
  page: it.value.page,
))"#,
        &[
            TypstInput::new(INPUT_MODE, "render"),
            TypstInput::new(INPUT_RESULTS, results_input),
            TypstInput::new(INPUT_TARGET, "paged"),
        ],
    )?;
    let root: Value =
        serde_json::from_str(&output).context("failed to parse typst eval page sync output")?;
    super::parse_page_anchor_entries(&root)
}

#[cfg(test)]
mod tests {}