use std::path::Path;
use std::time::Duration;
use outl_core::hlc::HlcGenerator;
use outl_core::workspace::Workspace;
use outl_md::parse::{parse, OutlineNode};
use outl_md::reconcile::reconcile_md;
use outl_md::render::render;
use crate::language::extract_fence;
use crate::registry::RuntimeRegistry;
use crate::result_block::{
render_result_body, result_source_hash, source_hash, upsert_result_child,
upsert_result_child_with_hash, upsert_result_embeds, RESULT_MARKER,
};
use crate::runtime::{ExecContext, ExecError, ExecOutput, OutputFormat};
#[cfg(target_os = "ios")]
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(not(target_os = "ios"))]
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, thiserror::Error)]
pub enum RunError {
#[error("no block at flat index {0}")]
BlockNotFound(usize),
#[error("block is not a fenced code block")]
NotACodeBlock,
#[error("code block has no language tag (e.g. ```lisp)")]
MissingLanguage,
#[error("no runtime registered for language `{0}`")]
UnknownLanguage(String),
#[error("read {path}: {source}")]
Read {
path: String,
#[source]
source: std::io::Error,
},
#[error("write {path}: {source}")]
Write {
path: String,
#[source]
source: std::io::Error,
},
#[error("reconcile: {0}")]
Reconcile(#[from] outl_md::reconcile::ReconcileError),
}
#[derive(Debug)]
pub struct RunReport {
pub language: String,
pub result: Result<ExecOutput, ExecError>,
}
pub fn run_block_at_index(
workspace: &mut Workspace,
hlc: &HlcGenerator,
md_path: &Path,
flat_index: usize,
registry: &RuntimeRegistry,
orphans_log: Option<&Path>,
) -> Result<RunReport, RunError> {
let text = std::fs::read_to_string(md_path).map_err(|source| RunError::Read {
path: md_path.display().to_string(),
source,
})?;
let mut page = parse(&text);
let block = block_at_flat_index_mut(&mut page.blocks, flat_index)
.ok_or(RunError::BlockNotFound(flat_index))?;
let parts = extract_fence(&block.text).ok_or(RunError::NotACodeBlock)?;
if parts.language.is_empty() {
return Err(RunError::MissingLanguage);
}
let language = parts.language.clone();
let body = parts.body;
let runtime = registry
.get(&language)
.ok_or_else(|| RunError::UnknownLanguage(language.clone()))?;
let ctx = ExecContext {
workspace_root: workspace
.root
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default()),
stdin: None,
timeout: DEFAULT_TIMEOUT,
mem_limit: None,
};
let result = runtime.execute(&body, &ctx);
match result.as_ref() {
Ok(o) if o.format == OutputFormat::Embeds => {
let embeds: Vec<&str> = o.stdout.lines().filter(|l| !l.is_empty()).collect();
let header = format!("{RESULT_MARKER} ({} blocks)", embeds.len());
upsert_result_embeds(block, header, &embeds);
}
_ => {
let body = render_result_body(result.as_ref());
upsert_result_child(block, body);
}
}
let rendered = render(&page);
outl_md::write_atomic(md_path, rendered.as_bytes()).map_err(|source| RunError::Write {
path: md_path.display().to_string(),
source,
})?;
reconcile_md(workspace, hlc, md_path, orphans_log)?;
Ok(RunReport { language, result })
}
pub fn run_block_at_index_if_source_changed(
workspace: &mut Workspace,
hlc: &HlcGenerator,
md_path: &Path,
flat_index: usize,
registry: &RuntimeRegistry,
orphans_log: Option<&Path>,
) -> Result<Option<RunReport>, RunError> {
let text = std::fs::read_to_string(md_path).map_err(|source| RunError::Read {
path: md_path.display().to_string(),
source,
})?;
let mut page = parse(&text);
let block = block_at_flat_index_mut(&mut page.blocks, flat_index)
.ok_or(RunError::BlockNotFound(flat_index))?;
let parts = extract_fence(&block.text).ok_or(RunError::NotACodeBlock)?;
if parts.language.is_empty() {
return Err(RunError::MissingLanguage);
}
let language = parts.language.clone();
let body = parts.body;
let want_hash = source_hash(&body);
if result_source_hash(block)
.map(|s| s == want_hash)
.unwrap_or(false)
{
return Ok(None);
}
let runtime = registry
.get(&language)
.ok_or_else(|| RunError::UnknownLanguage(language.clone()))?;
let ctx = ExecContext {
workspace_root: workspace
.root
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default()),
stdin: None,
timeout: DEFAULT_TIMEOUT,
mem_limit: None,
};
let result = runtime.execute(&body, &ctx);
match result.as_ref() {
Ok(o) if o.format == OutputFormat::Embeds => {
let embeds: Vec<&str> = o.stdout.lines().filter(|l| !l.is_empty()).collect();
let header = format!("{RESULT_MARKER} ({} blocks)", embeds.len());
upsert_result_embeds(block, header, &embeds);
}
_ => {
let body_md = render_result_body(result.as_ref());
upsert_result_child_with_hash(block, body_md, &want_hash);
}
}
let rendered = render(&page);
outl_md::write_atomic(md_path, rendered.as_bytes()).map_err(|source| RunError::Write {
path: md_path.display().to_string(),
source,
})?;
reconcile_md(workspace, hlc, md_path, orphans_log)?;
Ok(Some(RunReport { language, result }))
}
fn block_at_flat_index_mut(blocks: &mut [OutlineNode], target: usize) -> Option<&mut OutlineNode> {
fn walk<'a>(
nodes: &'a mut [OutlineNode],
target: usize,
counter: &mut usize,
) -> Option<&'a mut OutlineNode> {
for node in nodes {
if *counter == target {
return Some(node);
}
*counter += 1;
if let Some(hit) = walk(&mut node.children, target, counter) {
return Some(hit);
}
}
None
}
walk(blocks, target, &mut 0)
}
#[cfg(test)]
mod tests {
use super::*;
use outl_md::parse::ParsedPage;
fn page_with_blocks(blocks: Vec<OutlineNode>) -> ParsedPage {
ParsedPage {
properties: Vec::new(),
blocks,
warnings: Vec::new(),
}
}
fn leaf(text: &str) -> OutlineNode {
OutlineNode {
text: text.into(),
properties: Vec::new(),
children: Vec::new(),
}
}
#[test]
fn flat_index_zero_returns_first_block() {
let mut p = page_with_blocks(vec![leaf("a"), leaf("b")]);
let n = block_at_flat_index_mut(&mut p.blocks, 0).unwrap();
assert_eq!(n.text, "a");
}
#[test]
fn flat_index_descends_into_children() {
let mut p = page_with_blocks(vec![
OutlineNode {
text: "a".into(),
properties: vec![],
children: vec![leaf("a1"), leaf("a2")],
},
leaf("b"),
]);
assert_eq!(
block_at_flat_index_mut(&mut p.blocks, 1).unwrap().text,
"a1"
);
assert_eq!(
block_at_flat_index_mut(&mut p.blocks, 2).unwrap().text,
"a2"
);
assert_eq!(block_at_flat_index_mut(&mut p.blocks, 3).unwrap().text, "b");
}
#[test]
fn flat_index_past_end_returns_none() {
let mut p = page_with_blocks(vec![leaf("a")]);
assert!(block_at_flat_index_mut(&mut p.blocks, 99).is_none());
}
}