markdown_code_runner/
lib.rs

1//! Markdown Code Runner - Automatically update Markdown files with code block output.
2//!
3//! This crate provides functionality to execute code blocks in Markdown files
4//! and insert their output back into the document.
5//!
6//! # Example
7//!
8//! Add code blocks between `<!-- CODE:START -->` and `<!-- CODE:END -->` markers:
9//!
10//! ```markdown
11//! <!-- CODE:START -->
12//! <!-- print('Hello, world!') -->
13//! <!-- CODE:END -->
14//! <!-- OUTPUT:START -->
15//! This will be replaced by the output.
16//! <!-- OUTPUT:END -->
17//! ```
18//!
19//! Or use triple backticks with the `markdown-code-runner` modifier:
20//!
21//! ```markdown
22//! ```python markdown-code-runner
23//! print('Hello, world!')
24//! ```
25//! <!-- OUTPUT:START -->
26//! This will be replaced by the output.
27//! <!-- OUTPUT:END -->
28//! ```
29
30pub mod executor;
31pub mod markers;
32pub mod parser;
33pub mod standardize;
34
35use anyhow::{Context, Result};
36use std::fs;
37use std::path::Path;
38
39pub use executor::Language;
40pub use markers::WARNING;
41pub use parser::{process_markdown, BacktickOptions, ProcessingState, Section};
42pub use standardize::standardize_code_fences;
43
44/// Update a Markdown file by executing code blocks and inserting their output.
45///
46/// # Arguments
47///
48/// * `input_filepath` - Path to the input Markdown file
49/// * `output_filepath` - Optional path to the output file (defaults to overwriting input)
50/// * `verbose` - Enable verbose output
51/// * `backtick_standardize` - Remove `markdown-code-runner` from executed code fences
52/// * `execute` - Whether to execute code blocks
53/// * `standardize` - Post-process to standardize ALL code fences
54///
55/// # Errors
56///
57/// Returns an error if the file cannot be read, written, or if code execution fails.
58pub fn update_markdown_file(
59    input_filepath: &Path,
60    output_filepath: Option<&Path>,
61    verbose: bool,
62    backtick_standardize: bool,
63    execute: bool,
64    standardize: bool,
65) -> Result<()> {
66    // Read the input file
67    let content = fs::read_to_string(input_filepath)
68        .with_context(|| format!("Failed to read input file: {:?}", input_filepath))?;
69
70    if verbose {
71        eprintln!("Processing input file: {:?}", input_filepath);
72    }
73
74    // Split into lines (removing trailing newlines)
75    let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
76
77    // Process the markdown
78    let new_lines = process_markdown(&lines, verbose, backtick_standardize, execute)?;
79
80    // Join lines and ensure trailing newline
81    let mut updated_content = new_lines.join("\n");
82    // Trim trailing whitespace/newlines and add exactly one newline
83    updated_content = updated_content.trim_end().to_string();
84    updated_content.push('\n');
85
86    // Post-process to standardize all code fences if requested
87    if standardize {
88        if verbose {
89            eprintln!("Standardizing all code fences...");
90        }
91        updated_content = standardize_code_fences(&updated_content);
92    }
93
94    // Determine output path
95    let output_path = output_filepath.unwrap_or(input_filepath);
96
97    if verbose {
98        eprintln!("Writing output to: {:?}", output_path);
99    }
100
101    // Write the output
102    fs::write(output_path, &updated_content)
103        .with_context(|| format!("Failed to write output file: {:?}", output_path))?;
104
105    if verbose {
106        eprintln!("Done!");
107    }
108
109    Ok(())
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::fs;
116    use tempfile::tempdir;
117
118    #[test]
119    fn test_update_markdown_file() {
120        let dir = tempdir().unwrap();
121        let input_path = dir.path().join("test.md");
122        let output_path = dir.path().join("output.md");
123
124        let content = r#"# Test
125```python markdown-code-runner
126print('Hello')
127```
128<!-- OUTPUT:START -->
129old
130<!-- OUTPUT:END -->
131"#;
132
133        fs::write(&input_path, content).unwrap();
134
135        update_markdown_file(&input_path, Some(&output_path), false, false, true, false).unwrap();
136
137        let result = fs::read_to_string(&output_path).unwrap();
138        assert!(result.contains("Hello"));
139        assert!(!result.contains("old"));
140    }
141
142    #[test]
143    fn test_update_markdown_file_no_execute() {
144        let dir = tempdir().unwrap();
145        let input_path = dir.path().join("test.md");
146
147        let content = r#"# Test
148```python markdown-code-runner
149print('Hello')
150```
151<!-- OUTPUT:START -->
152old
153<!-- OUTPUT:END -->
154"#;
155
156        fs::write(&input_path, content).unwrap();
157
158        update_markdown_file(&input_path, None, false, false, false, false).unwrap();
159
160        let result = fs::read_to_string(&input_path).unwrap();
161        assert!(result.contains("old"));
162    }
163
164    #[test]
165    fn test_update_markdown_file_standardize() {
166        let dir = tempdir().unwrap();
167        let input_path = dir.path().join("test.md");
168        let output_path = dir.path().join("output.md");
169
170        let content = r#"# Test
171```python markdown-code-runner
172print('Hello')
173```
174<!-- OUTPUT:START -->
175old
176<!-- OUTPUT:END -->
177"#;
178
179        fs::write(&input_path, content).unwrap();
180
181        update_markdown_file(&input_path, Some(&output_path), false, false, true, true).unwrap();
182
183        let result = fs::read_to_string(&output_path).unwrap();
184        assert!(result.contains("```python\n"));
185        // Should not contain markdown-code-runner in code fence (but warning comment is ok)
186        assert!(!result.contains("```python markdown-code-runner"));
187    }
188}