Honor :results header args on code execution
Parse the :results switch from a src block's header arguments and apply
Emacs org-babel semantics when writing results:
- silent/none: execute and report, but do not write a #+RESULTS: block
- raw: insert output verbatim, no prefix or wrapping
- drawer: wrap output in a :results: ... 🔚 drawer
- default: fixed-width for <10 lines, #+begin_example for 10+
The insert_results detection loop now recognizes and replaces all four
wrapping styles in place, so re-execution and cross-mode transitions
(e.g. drawer to fixed-width) update cleanly without duplication. Raw
results, which have no delimiters, terminate at a blank line or heading.
This commit is contained in:
parent
a13f76cd40
commit
d073c8fa5e
|
|
@ -416,10 +416,13 @@ impl OrkState {
|
|||
blocks_with_results.sort_by(|a, b| b.3.end.offset.cmp(&a.3.end.offset));
|
||||
|
||||
for (section_id, idx, block, span, result) in &blocks_with_results {
|
||||
let params = parse_results_params(block.arguments.as_deref());
|
||||
match result {
|
||||
Ok(exec_result) => {
|
||||
// Insert results into document
|
||||
content = self.insert_results(&content, *span, &exec_result.stdout);
|
||||
// Insert results into document unless the block is silent.
|
||||
if !params.silent {
|
||||
content = self.insert_results(&content, *span, &exec_result.stdout, params.wrap);
|
||||
}
|
||||
|
||||
results_output.push(format!(
|
||||
"---\nsection: {}\nindex: {}\nlang: {}\nstatus: {}\nstdout: |\n{}\nstderr: |\n{}",
|
||||
|
|
@ -452,7 +455,7 @@ impl OrkState {
|
|||
}
|
||||
|
||||
/// Insert #+RESULTS: block after a source block.
|
||||
fn insert_results(&self, content: &str, block_span: Span, stdout: &str) -> String {
|
||||
fn insert_results(&self, content: &str, block_span: Span, stdout: &str, wrap: ResultWrap) -> String {
|
||||
let end_offset = block_span.end.offset.min(content.len());
|
||||
|
||||
// The block span end already points to the start of the line following
|
||||
|
|
@ -487,34 +490,56 @@ impl OrkState {
|
|||
let abs_start = insert_pos;
|
||||
let mut abs_end = abs_start;
|
||||
|
||||
// Track state: are we in results, and are we in an example block?
|
||||
// Track state across the possible wrapping styles:
|
||||
// - fixed-width (`: ` prefixed lines)
|
||||
// - #+begin_example ... #+end_example
|
||||
// - :results: ... :end: drawer
|
||||
// - raw (bare contiguous lines terminated by a blank line)
|
||||
let mut in_results = false;
|
||||
let mut in_example_block = false;
|
||||
let mut in_drawer = false;
|
||||
|
||||
for line in after_block.lines() {
|
||||
let trimmed = line.trim();
|
||||
let upper = trimmed.to_uppercase();
|
||||
|
||||
if !in_results {
|
||||
if upper.starts_with(results_prefix) {
|
||||
in_results = true;
|
||||
abs_end += line.len() + 1; // +1 for newline
|
||||
} else if in_results && upper.starts_with("#+BEGIN_EXAMPLE") {
|
||||
in_example_block = true;
|
||||
abs_end += line.len() + 1;
|
||||
} else if in_example_block {
|
||||
continue;
|
||||
}
|
||||
// Haven't reached the results keyword yet; nothing to replace.
|
||||
break;
|
||||
}
|
||||
|
||||
// We are inside the results block.
|
||||
if in_example_block {
|
||||
abs_end += line.len() + 1;
|
||||
if upper.starts_with("#+END_EXAMPLE") {
|
||||
in_example_block = false;
|
||||
}
|
||||
} else if in_results && (trimmed.starts_with(':') || trimmed.is_empty()) {
|
||||
// Fixed-width format or blank line continuation
|
||||
} else if in_drawer {
|
||||
abs_end += line.len() + 1;
|
||||
} else if in_results {
|
||||
// End of results block
|
||||
if upper == ":END:" {
|
||||
in_drawer = false;
|
||||
}
|
||||
} else if upper.starts_with("#+BEGIN_EXAMPLE") {
|
||||
in_example_block = true;
|
||||
abs_end += line.len() + 1;
|
||||
} else if upper == ":RESULTS:" {
|
||||
in_drawer = true;
|
||||
abs_end += line.len() + 1;
|
||||
} else if trimmed.is_empty() {
|
||||
// A blank line terminates the results block (consume it).
|
||||
abs_end += line.len() + 1;
|
||||
break;
|
||||
} else if trimmed.starts_with('*') {
|
||||
// A heading starts a new element; stop without consuming it.
|
||||
break;
|
||||
} else {
|
||||
// No results block found
|
||||
break;
|
||||
// Content line: fixed-width (`: ...`) or raw. Consume it.
|
||||
abs_end += line.len() + 1;
|
||||
}
|
||||
}
|
||||
(abs_start, abs_end.min(content.len()))
|
||||
|
|
@ -524,7 +549,7 @@ impl OrkState {
|
|||
};
|
||||
|
||||
// Format the new results
|
||||
let formatted_results = format_results(stdout);
|
||||
let formatted_results = format_results(stdout, wrap);
|
||||
|
||||
// Build new content
|
||||
let mut new_content = String::with_capacity(content.len() + formatted_results.len());
|
||||
|
|
@ -540,16 +565,92 @@ impl OrkState {
|
|||
/// Matches Emacs `org-babel-min-lines-for-block-output` default.
|
||||
const MIN_LINES_FOR_BLOCK_OUTPUT: usize = 10;
|
||||
|
||||
/// Format stdout as org #+RESULTS: block.
|
||||
/// Uses fixed-width (`: ` prefix) for <10 lines, #+begin_example block for ≥10.
|
||||
fn format_results(stdout: &str) -> String {
|
||||
/// How to wrap the output under the `#+RESULTS:` keyword.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ResultWrap {
|
||||
/// Fixed-width (`: ` prefix) for small output, #+begin_example for large.
|
||||
Default,
|
||||
/// Insert output verbatim, no prefix or wrapping.
|
||||
Raw,
|
||||
/// Wrap output in a `:results:` ... `:end:` drawer.
|
||||
Drawer,
|
||||
}
|
||||
|
||||
/// Parsed `:results` header-argument options relevant to insertion.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ResultsParams {
|
||||
/// `silent`/`none` — execute but do not write a results block.
|
||||
silent: bool,
|
||||
/// How to wrap the inserted output.
|
||||
wrap: ResultWrap,
|
||||
}
|
||||
|
||||
impl Default for ResultsParams {
|
||||
fn default() -> Self {
|
||||
ResultsParams { silent: false, wrap: ResultWrap::Default }
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the `:results` switch from a source block's raw header arguments.
|
||||
///
|
||||
/// `arguments` is the full header string, e.g. `"python :results output drawer"`.
|
||||
/// Only the tokens following `:results` (up to the next `:switch` or end) are
|
||||
/// interpreted. Unknown keywords (table, html, value, ...) are ignored, which
|
||||
/// leaves the default behavior intact.
|
||||
fn parse_results_params(arguments: Option<&str>) -> ResultsParams {
|
||||
let mut params = ResultsParams::default();
|
||||
let Some(args) = arguments else { return params };
|
||||
|
||||
let tokens: Vec<&str> = args.split_whitespace().collect();
|
||||
// Find the ":results" switch (case-insensitive).
|
||||
let Some(start) = tokens.iter().position(|t| t.eq_ignore_ascii_case(":results")) else {
|
||||
return params;
|
||||
};
|
||||
|
||||
// Collect values until the next switch (token starting with ':').
|
||||
for tok in &tokens[start + 1..] {
|
||||
if tok.starts_with(':') {
|
||||
break;
|
||||
}
|
||||
match tok.to_ascii_lowercase().as_str() {
|
||||
"silent" | "none" => params.silent = true,
|
||||
"raw" => params.wrap = ResultWrap::Raw,
|
||||
"drawer" => params.wrap = ResultWrap::Drawer,
|
||||
// output/value and other type keywords don't change insertion here.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
params
|
||||
}
|
||||
|
||||
/// Format stdout as org #+RESULTS: block, honoring the wrap mode.
|
||||
/// Default: fixed-width (`: ` prefix) for <10 lines, #+begin_example for ≥10.
|
||||
fn format_results(stdout: &str, wrap: ResultWrap) -> String {
|
||||
if stdout.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let line_count = stdout.lines().count();
|
||||
let mut result = String::from("#+RESULTS:\n");
|
||||
|
||||
match wrap {
|
||||
ResultWrap::Raw => {
|
||||
// Verbatim: no prefix, no wrapping.
|
||||
result.push_str(stdout);
|
||||
if !stdout.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
ResultWrap::Drawer => {
|
||||
result.push_str(":results:\n");
|
||||
result.push_str(stdout);
|
||||
if !stdout.ends_with('\n') {
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str(":end:\n");
|
||||
}
|
||||
ResultWrap::Default => {
|
||||
let line_count = stdout.lines().count();
|
||||
if line_count < MIN_LINES_FOR_BLOCK_OUTPUT {
|
||||
// Fixed-width format: prefix each line with ": "
|
||||
for line in stdout.lines() {
|
||||
|
|
@ -566,6 +667,8 @@ fn format_results(stdout: &str) -> String {
|
|||
}
|
||||
result.push_str("#+end_example\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add blank line after results for separation
|
||||
result.push('\n');
|
||||
|
|
@ -2037,14 +2140,14 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_format_results_empty() {
|
||||
assert_eq!(format_results(""), "");
|
||||
assert_eq!(format_results("", ResultWrap::Default), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_results_short() {
|
||||
// < 10 lines uses fixed-width format (: prefix)
|
||||
let stdout = "line1\nline2\nline3\n";
|
||||
let result = format_results(stdout);
|
||||
let result = format_results(stdout, ResultWrap::Default);
|
||||
assert!(result.starts_with("#+RESULTS:\n"));
|
||||
assert!(result.contains(": line1\n"));
|
||||
assert!(result.contains(": line2\n"));
|
||||
|
|
@ -2057,7 +2160,7 @@ mod tests {
|
|||
// >= 10 lines uses #+begin_example block
|
||||
let lines: Vec<_> = (1..=15).map(|i| format!("line{}", i)).collect();
|
||||
let stdout = lines.join("\n");
|
||||
let result = format_results(&stdout);
|
||||
let result = format_results(&stdout, ResultWrap::Default);
|
||||
assert!(result.starts_with("#+RESULTS:\n"));
|
||||
assert!(result.contains("#+begin_example\n"));
|
||||
assert!(result.contains("#+end_example\n"));
|
||||
|
|
@ -2066,4 +2169,61 @@ mod tests {
|
|||
// Should NOT have : prefix in example block
|
||||
assert!(!result.contains(": line1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_results_raw() {
|
||||
// raw: verbatim, no prefix, no wrapping
|
||||
let stdout = "a\nb\nc\n";
|
||||
let result = format_results(stdout, ResultWrap::Raw);
|
||||
assert_eq!(result, "#+RESULTS:\na\nb\nc\n\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_results_drawer() {
|
||||
// drawer: :results: ... :end: wrapper
|
||||
let stdout = "x\ny\n";
|
||||
let result = format_results(stdout, ResultWrap::Drawer);
|
||||
assert_eq!(result, "#+RESULTS:\n:results:\nx\ny\n:end:\n\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_results_params() {
|
||||
// No arguments → default
|
||||
let p = parse_results_params(None);
|
||||
assert_eq!(p, ResultsParams { silent: false, wrap: ResultWrap::Default });
|
||||
|
||||
// Language only, no :results → default
|
||||
let p = parse_results_params(Some("python"));
|
||||
assert_eq!(p.wrap, ResultWrap::Default);
|
||||
assert!(!p.silent);
|
||||
|
||||
// :results output → default wrap (output doesn't change insertion)
|
||||
let p = parse_results_params(Some("python :results output"));
|
||||
assert_eq!(p.wrap, ResultWrap::Default);
|
||||
assert!(!p.silent);
|
||||
|
||||
// :results raw
|
||||
let p = parse_results_params(Some("bash :results raw"));
|
||||
assert_eq!(p.wrap, ResultWrap::Raw);
|
||||
|
||||
// :results output drawer
|
||||
let p = parse_results_params(Some("sh :results output drawer"));
|
||||
assert_eq!(p.wrap, ResultWrap::Drawer);
|
||||
|
||||
// :results silent
|
||||
let p = parse_results_params(Some("python :results silent"));
|
||||
assert!(p.silent);
|
||||
|
||||
// none is an alias for silent
|
||||
let p = parse_results_params(Some("python :results none"));
|
||||
assert!(p.silent);
|
||||
|
||||
// Values stop at the next switch
|
||||
let p = parse_results_params(Some("python :results raw :exports both"));
|
||||
assert_eq!(p.wrap, ResultWrap::Raw);
|
||||
|
||||
// Case-insensitive switch
|
||||
let p = parse_results_params(Some("python :RESULTS drawer"));
|
||||
assert_eq!(p.wrap, ResultWrap::Drawer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue