render_regex 0.0.0

SVG visualization of regex DFAs.
Documentation
// src/main.rs

use clap::{Arg, Command, ArgAction};
use render_regex::{render_regex_svg, Stage};
use render_regex::layout::profile::{RenderProfile, ProfileConfig, ArrowStyle, FanoutMode};
use std::error::Error;
use std::fs;
use std::path::Path;
use serde_json;

fn main() -> Result<(), Box<dyn Error>> {
    let matches = Command::new("render_regex")
        .version(env!("CARGO_PKG_VERSION"))
        .about("Render a regular expression as an SVG diagram")
        .long_about(
            "Reads a regex, compiles it through AST→NFA→DFA, lays it out, and emits an SVG.\n\
             You control metadata, layout, layering, and debug overlays via flags or profile files."
        )
        .after_help(
            "EXAMPLES:\n\
             rrx 'a(b|c)*d' --stage dfa             # prints SVG to stdout\n\
             rrx 'a(b|c)*d' --profile debug.cf      # write debug-overlay SVG via ./output\n\
             rrx 'foo|bar' --title MyPattern        # embed <title>MyPattern</title>\n\
             rrx 'loop+' --debug-overlay            # show red bend-offset labels"
        )
        .arg(
            Arg::new("regex")
                .required(true)
                .num_args(1)
                .value_name("REGEX")
                .help("Regular expression to render"),
        )
        .arg(
            Arg::new("profile")
                .long("profile")
                .num_args(1)
                .value_name("FILE")
                .help("Load a JSON-style profile file (.cf) to set metadata, layout, and layering"),
        )
        .arg(
            Arg::new("profile-preset")
                .long("profile-preset")
                .num_args(1)
                .value_name("NAME")
                .help("Pick one of the built-in profile presets (default, debug)"),
        )
        .arg(
            Arg::new("title")
                .long("title")
                .num_args(1)
                .value_name("TEXT")
                .help("Embed a <title> element in the SVG for accessibility"),
        )
        .arg(
            Arg::new("tool")
                .long("tool")
                .num_args(1)
                .value_name("NAME")
                .help("Embed a <desc> element (e.g. tool name or version)"),
        )
        .arg(
            Arg::new("debug-overlay")
                .long("debug-overlay")
                .action(ArgAction::SetTrue)
                .help("Overlay red bend-offset labels on each edge for debugging"),
        )
        .arg(
            Arg::new("layout-strategy")
                .long("layout-strategy")
                .num_args(1)
                .value_name("STRATEGY")
                .help("Override layout algorithm: TreeTopDown or Circular"),
        )
        .arg(
            Arg::new("draw-order")
                .long("draw-order")
                .num_args(1)
                .value_name("LAYERS")
                .help("Override render layers order (e.g. edges,nodes,labels)"),
        )
        .arg(
            Arg::new("output")
                .long("output")
                .num_args(1)
                .value_name("FILE")
                .help("Write the SVG file into the ./output directory"),
        )
        .arg(
            Arg::new("stage")
                .long("stage")
                .num_args(1)
                .value_name("STAGE")
                .value_parser(["ast", "nfa", "dfa"])
                .default_value("dfa")
                .help("Stop compilation at AST, NFA, or DFA stage"),
        )
        // Arrowhead controls
        .arg(
            Arg::new("no-arrows")
                .long("no-arrows")
                .action(ArgAction::SetTrue)
                .help("Disable arrowheads on all edges"),
        )
        .arg(
            Arg::new("arrow-size")
                .long("arrow-size")
                .num_args(1)
                .value_name("PX")
                .help("Set arrow marker size (markerWidth & markerHeight)"),
        )
        .arg(
            Arg::new("arrow-style")
                .long("arrow-style")
                .num_args(1)
                .value_name("STYLE")
                .help("Arrowhead shape: Triangle or Dot"),
        )
        // Fan-out controls
        .arg(
            Arg::new("no-fanout")
                .long("no-fanout")
                .action(ArgAction::SetTrue)
                .help("Disable multi-edge fan-out, rendering all edges straight"),
        )
        .arg(
            Arg::new("fanout-max-bend")
                .long("fanout-max-bend")
                .num_args(1)
                .value_name("PX")
                .help("Maximum bend offset for parallel edges"),
        )
        .arg(
            Arg::new("fanout-mode")
                .long("fanout-mode")
                .num_args(1)
                .value_name("MODE")
                .help("Parallel-edge distribution: Symmetric, Offset, or Random"),
        )
        .get_matches();

    let regex = matches.get_one::<String>("regex").unwrap();

    // Build profile from preset → file → CLI overrides
    let mut profile = if let Some(preset) = matches.get_one::<String>("profile-preset") {
        RenderProfile::preset(preset)
    } else {
        RenderProfile::default()
    };

    if let Some(path) = matches.get_one::<String>("profile") {
        let file = fs::read_to_string(path)?;
        let config: ProfileConfig = serde_json::from_str(&file)?;
        config.apply_to(&mut profile);
    }

    if let Some(t) = matches.get_one::<String>("title") {
        profile.title = Some(t.clone());
    }
    if let Some(tool_name) = matches.get_one::<String>("tool") {
        profile.tool = Some(tool_name.clone());
    }
    if matches.get_flag("debug-overlay") {
        profile.show_debug_overlay = true;
    }
    if matches.get_flag("no-arrows") {
        profile.no_arrows = true;
    }
    if let Some(sz) = matches.get_one::<String>("arrow-size") {
        if let Ok(val) = sz.parse::<f32>() {
            profile.arrow_size = val;
        }
    }
    if let Some(st) = matches.get_one::<String>("arrow-style") {
        profile.arrow_style = match st.as_str() {
            "Dot" | "dot"           => ArrowStyle::Dot,
            "Triangle" | "triangle" => ArrowStyle::Triangle,
            other => {
                eprintln!("Unknown arrow-style '{}'; using existing", other);
                profile.arrow_style.clone()
            }
        };
    }
    // Fan-out flags
    if matches.get_flag("no-fanout") {
        profile.no_fanout = true;
    }
    if let Some(v) = matches.get_one::<String>("fanout-max-bend") {
        if let Ok(val) = v.parse::<f32>() {
            profile.fanout_max_bend = val;
        }
    }
    if let Some(m) = matches.get_one::<String>("fanout-mode") {
        profile.fanout_mode = match m.as_str() {
            "Symmetric" | "symmetric" => FanoutMode::Symmetric,
            "Offset"    | "offset"    => FanoutMode::Offset,
            "Random"    | "random"    => FanoutMode::Random,
            other => {
                eprintln!("Unknown fanout-mode '{}'; using existing", other);
                profile.fanout_mode.clone()
            }
        };
    }

    let stage_str = matches.get_one::<String>("stage").unwrap().as_str();
    let stage = match stage_str {
        "ast" => Stage::Ast,
        "nfa" => Stage::Nfa,
        "dfa" => Stage::Dfa,
        _     => Stage::Dfa,
    };

    let document = render_regex_svg(stage, &regex, None, &profile)?;

    let output_dir = Path::new("output");
    fs::create_dir_all(output_dir)?;
    let output_file = matches
        .get_one::<String>("output")
        .map(|f| output_dir.join(f))
        .unwrap_or_else(|| output_dir.join("diagram.svg"));
    fs::write(&output_file, document.to_string())?;
    let abs_path = fs::canonicalize(&output_file)?;
    println!("SVG diagram written to {}", abs_path.display());

    Ok(())
}