markdown-code-runner 0.1.0

Automatically update Markdown files with code block output
Documentation
//! Markdown Code Runner - Automatically update Markdown files with code block output.
//!
//! This crate provides functionality to execute code blocks in Markdown files
//! and insert their output back into the document.
//!
//! # Example
//!
//! Add code blocks between `<!-- CODE:START -->` and `<!-- CODE:END -->` markers:
//!
//! ```markdown
//! <!-- CODE:START -->
//! <!-- print('Hello, world!') -->
//! <!-- CODE:END -->
//! <!-- OUTPUT:START -->
//! This will be replaced by the output.
//! <!-- OUTPUT:END -->
//! ```
//!
//! Or use triple backticks with the `markdown-code-runner` modifier:
//!
//! ```markdown
//! ```python markdown-code-runner
//! print('Hello, world!')
//! ```
//! <!-- OUTPUT:START -->
//! This will be replaced by the output.
//! <!-- OUTPUT:END -->
//! ```

pub mod executor;
pub mod markers;
pub mod parser;
pub mod standardize;

use anyhow::{Context, Result};
use std::fs;
use std::path::Path;

pub use executor::Language;
pub use markers::WARNING;
pub use parser::{process_markdown, BacktickOptions, ProcessingState, Section};
pub use standardize::standardize_code_fences;

/// Update a Markdown file by executing code blocks and inserting their output.
///
/// # Arguments
///
/// * `input_filepath` - Path to the input Markdown file
/// * `output_filepath` - Optional path to the output file (defaults to overwriting input)
/// * `verbose` - Enable verbose output
/// * `backtick_standardize` - Remove `markdown-code-runner` from executed code fences
/// * `execute` - Whether to execute code blocks
/// * `standardize` - Post-process to standardize ALL code fences
///
/// # Errors
///
/// Returns an error if the file cannot be read, written, or if code execution fails.
pub fn update_markdown_file(
    input_filepath: &Path,
    output_filepath: Option<&Path>,
    verbose: bool,
    backtick_standardize: bool,
    execute: bool,
    standardize: bool,
) -> Result<()> {
    // Read the input file
    let content = fs::read_to_string(input_filepath)
        .with_context(|| format!("Failed to read input file: {:?}", input_filepath))?;

    if verbose {
        eprintln!("Processing input file: {:?}", input_filepath);
    }

    // Split into lines (removing trailing newlines)
    let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();

    // Process the markdown
    let new_lines = process_markdown(&lines, verbose, backtick_standardize, execute)?;

    // Join lines and ensure trailing newline
    let mut updated_content = new_lines.join("\n");
    // Trim trailing whitespace/newlines and add exactly one newline
    updated_content = updated_content.trim_end().to_string();
    updated_content.push('\n');

    // Post-process to standardize all code fences if requested
    if standardize {
        if verbose {
            eprintln!("Standardizing all code fences...");
        }
        updated_content = standardize_code_fences(&updated_content);
    }

    // Determine output path
    let output_path = output_filepath.unwrap_or(input_filepath);

    if verbose {
        eprintln!("Writing output to: {:?}", output_path);
    }

    // Write the output
    fs::write(output_path, &updated_content)
        .with_context(|| format!("Failed to write output file: {:?}", output_path))?;

    if verbose {
        eprintln!("Done!");
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_update_markdown_file() {
        let dir = tempdir().unwrap();
        let input_path = dir.path().join("test.md");
        let output_path = dir.path().join("output.md");

        let content = r#"# Test
```python markdown-code-runner
print('Hello')
```
<!-- OUTPUT:START -->
old
<!-- OUTPUT:END -->
"#;

        fs::write(&input_path, content).unwrap();

        update_markdown_file(&input_path, Some(&output_path), false, false, true, false).unwrap();

        let result = fs::read_to_string(&output_path).unwrap();
        assert!(result.contains("Hello"));
        assert!(!result.contains("old"));
    }

    #[test]
    fn test_update_markdown_file_no_execute() {
        let dir = tempdir().unwrap();
        let input_path = dir.path().join("test.md");

        let content = r#"# Test
```python markdown-code-runner
print('Hello')
```
<!-- OUTPUT:START -->
old
<!-- OUTPUT:END -->
"#;

        fs::write(&input_path, content).unwrap();

        update_markdown_file(&input_path, None, false, false, false, false).unwrap();

        let result = fs::read_to_string(&input_path).unwrap();
        assert!(result.contains("old"));
    }

    #[test]
    fn test_update_markdown_file_standardize() {
        let dir = tempdir().unwrap();
        let input_path = dir.path().join("test.md");
        let output_path = dir.path().join("output.md");

        let content = r#"# Test
```python markdown-code-runner
print('Hello')
```
<!-- OUTPUT:START -->
old
<!-- OUTPUT:END -->
"#;

        fs::write(&input_path, content).unwrap();

        update_markdown_file(&input_path, Some(&output_path), false, false, true, true).unwrap();

        let result = fs::read_to_string(&output_path).unwrap();
        assert!(result.contains("```python\n"));
        // Should not contain markdown-code-runner in code fence (but warning comment is ok)
        assert!(!result.contains("```python markdown-code-runner"));
    }
}