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"),
)
.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"),
)
.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();
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()
}
};
}
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, ®ex, 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(())
}