pub mod diagram;
pub mod julia;
pub mod python;
pub mod r;
pub mod sh;
pub mod subprocess;
use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::typst::model::{EngineName, FigureSpec};
use crate::utils::tools;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum EngineResult {
Source(Vec<String>),
Output(String),
Warning(String),
Message(String),
Error(String),
Plot(PathBuf),
Asis(String),
Preamble(String),
}
pub struct EngineContext<'a> {
pub r: Option<&'a mut r::RSession>,
pub python: Option<&'a mut python::PythonSession>,
pub julia: Option<&'a mut julia::JuliaSession>,
pub sh: Option<&'a mut sh::ShSession>,
}
pub fn execute_chunk(
source: &[String],
engine: EngineName,
label: &str,
fig_dir: &Path,
figure: &FigureSpec,
ctx: &mut EngineContext,
) -> Result<Vec<EngineResult>> {
let code = source.join("\n");
let mut results = Vec::new();
let interleaved = engine != EngineName::Sh;
if !interleaved {
results.push(EngineResult::Source(source.to_vec()));
}
let is_table_chunk = label.starts_with("tbl-");
std::fs::create_dir_all(fig_dir)?;
let fig_full_path = fig_dir.join(figure.numbered_filename(label));
let fig_abs = if fig_full_path.is_relative() {
std::env::current_dir()?.join(&fig_full_path)
} else {
fig_full_path.clone()
};
let fig_full_str = if is_table_chunk {
String::new()
} else {
fig_abs.to_string_lossy().replace('\\', "/")
};
let captured =
match engine {
EngineName::Sh => {
let session = ctx
.sh
.as_mut()
.ok_or_else(|| anyhow::anyhow!("{}", tools::not_found_message(&tools::SH)))?;
session.capture(&code)?
}
EngineName::Python => {
let session = ctx.python.as_mut().ok_or_else(|| {
anyhow::anyhow!("{}", tools::not_found_message(&tools::PYTHON))
})?;
session.capture(
&code,
&fig_full_str,
figure.width,
figure.height,
f64::from(figure.dpi),
)?
}
EngineName::Julia => {
let session = ctx.julia.as_mut().ok_or_else(|| {
anyhow::anyhow!("{}", tools::not_found_message(&tools::JULIA))
})?;
session.capture(
&code,
&fig_full_str,
&figure.format,
figure.width,
figure.height,
f64::from(figure.dpi),
)?
}
EngineName::R => {
let session = ctx.r.as_mut().ok_or_else(|| {
anyhow::anyhow!("{}", tools::not_found_message(&tools::RSCRIPT))
})?;
session.capture(
&code,
&fig_full_str,
figure.r_device(),
figure.width,
figure.height,
f64::from(figure.dpi),
)?
}
other => return Err(anyhow::anyhow!("unsupported engine `{}`", other)),
};
process_results(&captured, &fig_full_path, &mut results)?;
if interleaved
&& !results
.iter()
.any(|result| matches!(result, EngineResult::Source(_)))
{
results.insert(0, EngineResult::Source(source.to_vec()));
}
Ok(results)
}
pub fn make_sentinel() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("__CALEPIN_{:x}_{:x}__", std::process::id(), seq)
}
fn process_results(raw: &str, fig_path: &Path, results: &mut Vec<EngineResult>) -> Result<()> {
let (sentinel, rest) = raw.split_once('\n').unwrap_or(("", raw));
let sep = format!("\n{}_SEP\n", sentinel);
let source_prefix = format!("{}_SOURCE:", sentinel);
let output_prefix = format!("{}_OUTPUT:", sentinel);
let asis_prefix = format!("{}_ASIS:", sentinel);
let error_prefix = format!("{}_ERROR:", sentinel);
let warning_prefix = format!("{}_WARNING:", sentinel);
let message_prefix = format!("{}_MESSAGE:", sentinel);
let plot_prefix = format!("{}_PLOT:", sentinel);
let preamble_prefix = format!("{}_PREAMBLE:", sentinel);
for part in rest.split(&sep) {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some(text) = part.strip_prefix(&source_prefix) {
if !text.is_empty() {
results.push(EngineResult::Source(
text.lines().map(ToOwned::to_owned).collect(),
));
}
} else if let Some(text) = part.strip_prefix(&error_prefix) {
if !text.is_empty() {
results.push(EngineResult::Error(text.to_string()));
}
} else if let Some(text) = part.strip_prefix(&asis_prefix) {
if !text.is_empty() {
results.push(EngineResult::Asis(text.to_string()));
}
} else if let Some(text) = part.strip_prefix(&output_prefix) {
if let Some(message) = text.strip_prefix(&error_prefix) {
results.push(EngineResult::Error(message.to_string()));
} else if !text.is_empty() {
results.push(EngineResult::Output(text.to_string()));
}
} else if let Some(text) = part.strip_prefix(&warning_prefix) {
if !text.is_empty() {
results.push(EngineResult::Warning(text.to_string()));
}
} else if let Some(text) = part.strip_prefix(&message_prefix) {
if !text.is_empty() {
results.push(EngineResult::Message(text.to_string()));
}
} else if part.starts_with(&plot_prefix) {
if fig_path.exists() {
results.push(EngineResult::Plot(fig_path.to_path_buf()));
}
} else if let Some(text) = part.strip_prefix(&preamble_prefix) {
if !text.is_empty() {
results.push(EngineResult::Preamble(text.to_string()));
}
}
}
Ok(())
}