Inscribe 0.0.3

A markdown preprocessor that executes code fences and embeds their output.
//! # Inscribe: A Markdown Preprocessor
//!
//! Inscribe is a command-line tool that acts as a markdown preprocessor,
//! allowing you to embed executable code blocks directly into your documents.
//! When you run Inscribe on a markdown file, it finds specially marked code
//! blocks, executes them, and replaces them with their standard output.
//!
//! This enables dynamic, self-updating documentation, tutorials where the
//! output is always correct, and literate programming styles.
//!
//! ## Basic Usage
//!
//! To mark a code block for execution, precede it with an HTML comment: `<!-- inscribe -->`.
//!
//! ### Example
//!
//! ```markdown
//! # My Document
//!
//! Here is the output of a simple Python script:
//!
//! <!-- inscribe -->
//! ```python
//! for i in range(3):
//!     print(f"Hello, world! #{i+1}")
//! ```
//!
//! After processing with `inscribe`, the file will become:
//!
//! ```markdown
//! # My Document
//!
//! Here is the output of a simple Python script:
//!
//! Hello, world! #1
//! Hello, world! #2
//! Hello, world! #3
//!
//! ```

mod config;
mod engine;

use clap::Parser as ClapParser;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::fs;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::{self, Command};
use std::sync::mpsc::channel;
use std::thread;
use std::time::{Duration, Instant};
use chrono::Local;

/// A markdown preprocessor that executes embedded code blocks.
#[derive(ClapParser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// The path to the input markdown file. If omitted, reads from stdin.
    input_file: Option<PathBuf>,

    /// The path to the output file. If omitted, writes to stdout.
    #[arg(short, long)]
    output_file: Option<PathBuf>,

    /// Watch the input file for changes and reprocess automatically.
    #[arg(long, requires("input_file"))]
    watch: bool,

    /// A command to run after processing is successfully finished.
    /// The placeholders {{input}} and {{output}} will be replaced.
    #[arg(long)]
    on_finish: Option<String>,
}

/// Reads, processes, and writes a single file.
///
/// This function orchestrates the main logic: reading the input, calling the
/// engine to process the content, writing the result to the output, and
/// finally, running a post-processing command if one was provided.
///
/// # Arguments
/// * `input_path` - Path to the source markdown file.
/// * `output_path` - Optional path for the generated output file.
/// * `on_finish_cmd` - Optional command template to execute after processing.
fn process_file(
    input_path: &Path,
    output_path: &Option<PathBuf>,
    on_finish_cmd: &Option<String>,
) -> Result<(), String> {
    let input_content = fs::read_to_string(input_path)
        .map_err(|e| format!("Error reading input file '{}': {}", input_path.display(), e))?;

    let default_runners = config::default_runners();

    // The core logic is handled by the engine.
    let output_content =
        engine::process_markdown(&input_content, &default_runners, Some(input_path))?;

    if let Some(path) = output_path {
        fs::write(path, &output_content)
            .map_err(|e| format!("Error writing to output file '{}': {}", path.display(), e))?;
        println!("✅ Successfully generated '{}'", path.display());
    } else {
        // If no output file is specified, print to standard output.
        print!("{}", output_content);
    }

    // If an `on-finish` command is provided, run it.
    if let Some(cmd) = on_finish_cmd {
        run_post_command(cmd, input_path, output_path.as_deref())?;
    }

    Ok(())
}

/// Executes a post-processing command.
///
/// This function takes a command template, replaces `{{input}}` and `{{output}}`
/// placeholders with the actual file paths, and executes it in a new shell process.
fn run_post_command(
    command_template: &str,
    input_path: &Path,
    output_path: Option<&Path>,
) -> Result<(), String> {
    let output_path_str = output_path.and_then(|p| p.to_str()).unwrap_or("");
    let input_path_str = input_path.to_str().unwrap_or("");
    let command_to_run = command_template
        .replace("{{input}}", input_path_str)
        .replace("{{output}}", output_path_str);
    println!("🚀 Running on-finish command: {}", command_to_run);

    let mut parts = command_to_run.split_whitespace();
    let command = parts.next().unwrap_or("");
    if command.is_empty() {
        return Err("The on-finish command was empty.".to_string());
    }
    let args: Vec<&str> = parts.collect();

    let status = Command::new(command)
        .args(args)
        .status()
        .map_err(|e| format!("Failed to execute on-finish command: {}", e))?;

    if !status.success() {
        eprintln!(
            "⚠️ Warning: The on-finish command failed with exit code: {}",
            status
        );
    }
    Ok(())
}

/// The main entry point of the application.
///
/// Parses command-line arguments and handles the two main modes of operation:
/// 1. Single-pass processing from stdin or a file.
/// 2. Watch mode, which continuously monitors a file for changes.
fn main() {
    let args = Args::parse();

    // If no input file is specified, read from stdin and process once.
    if args.input_file.is_none() {
        let mut buffer = String::new();
        io::stdin().read_to_string(&mut buffer).unwrap();
        let default_runners = config::default_runners();
        match engine::process_markdown(&buffer, &default_runners, None) {
            Ok(out) => print!("{}", out),
            Err(e) => {
                eprintln!("\n❌ Error: {}", e);
                process::exit(1);
            }
        }
        return;
    }

    // An input file was provided, so perform an initial processing pass.
    let input_path = args.input_file.as_ref().unwrap();
    if let Err(e) = process_file(input_path, &args.output_file, &args.on_finish) {
        eprintln!("❌ Error: {}", e);
        if !args.watch {
            process::exit(1);
        }
    }

    // If watch mode is not enabled, exit after the first pass.
    if !args.watch {
        return;
    }

    // --- Watch Mode ---
    println!(
        "\n👀 Watching for changes in '{}'. Press Ctrl+C to exit.",
        input_path.display()
    );
    let (tx, rx) = channel();

    // Create a file watcher with a short poll interval.
    let mut watcher: RecommendedWatcher = Watcher::new(
        tx,
        Config::default().with_poll_interval(Duration::from_millis(500)),
    )
    .expect("Failed to create file watcher");

    watcher
        .watch(input_path.as_ref(), RecursiveMode::NonRecursive)
        .expect("Failed to watch input file");

    // --- DEBOUNCING LOGIC ---
    let debounce_duration = Duration::from_millis(200);
    let mut last_event_time: Option<Instant> = None;

    loop {
        // Check for new events without blocking. `try_recv` is non-blocking.
        if let Ok(res) = rx.try_recv() {
            match res {
                // If we get a relevant event, just record the time. Don't act yet.
                Ok(event) if event.kind.is_modify() || event.kind.is_create() => {
                    last_event_time = Some(Instant::now());
                }
                Err(e) => eprintln!("watch error: {:?}", e),
                _ => {} // Ignore other events
            }
        }

        // After checking for events, see if we have a pending event and if enough time has passed.
        if let Some(last_event) = last_event_time {
            if last_event.elapsed() >= debounce_duration {
                let now = Local::now();
                println!(
                    "\n🔄 [{}] Detected change, reprocessing '{}'...",
                    now.format("%H:%M:%S"),
                    input_path.display()
                );

                // On change, re-run the file processing.
                if let Err(e) = process_file(input_path, &args.output_file, &args.on_finish) {
                    // Log errors but don't exit, to keep the watch process alive.
                    eprintln!("❌ Error: {}", e);
                }

                // Reset the timer so we don't re-process again until a new event arrives.
                last_event_time = None;
            }
        }

        // Sleep for a short duration to prevent this loop from spinning and eating CPU.
        thread::sleep(Duration::from_millis(50));
    }
}