use std::{
collections::HashSet,
iter::Peekable,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use oxvg_actions::Actor;
use oxvg_ast::{
parse::roxmltree::parse_with_options,
xmlwriter::{Indent, Options, Space},
};
use oxvg_collections::atom::Atom;
use oxvg_parse::Parse as _;
use roxmltree::ParsingOptions;
use crate::{
args::RunCommand,
config::Config,
walk::{Output, Walk},
};
#[derive(clap::Args, Debug)]
#[command(args_conflicts_with_subcommands = true)]
#[command(flatten_help = true)]
pub struct Action {
#[command(subcommand)]
command: Option<ActionCommands>,
#[command(flatten)]
run: ActionRun,
}
#[derive(clap::Subcommand, Debug)]
pub enum ActionCommands {
Run(ActionRun),
List(ActionList),
}
#[derive(clap::Args, Debug)]
pub struct ActionRun {
#[clap(flatten)]
walk: Walk,
#[clap(long, short, default_value = "false")]
pub derive_state: bool,
#[arg(last = true)]
pub command_list: Vec<String>,
#[clap(long, short, default_value = "4")]
pub pretty: Indent,
#[clap(long, short, default_value = "auto")]
pub space: Space,
}
#[derive(clap::Args, Debug)]
#[command(ignore_errors(true))]
pub struct ActionList {
#[arg(last = true)]
pub command_list: Vec<String>,
}
impl RunCommand for Action {
async fn run(self, config: Config) -> anyhow::Result<()> {
if let Some(subcommand) = self.command {
subcommand.run(config).await
} else {
self.run.run(config).await
}
}
}
impl RunCommand for ActionCommands {
async fn run(self, config: Config) -> anyhow::Result<()> {
match self {
Self::Run(args) => args.run(config).await,
Self::List(args) => args.run(config).await,
}
}
}
impl RunCommand for ActionRun {
async fn run(self, _: Config) -> anyhow::Result<()> {
let actions = parse(self.command_list)?;
let error = Arc::new(AtomicBool::new(false));
self.walk.run(|| {
let actions = actions.clone();
let error = Arc::clone(&error);
let format_options = Options {
indent: self.pretty,
trim_whitespace: self.space,
..Options::default()
};
Box::new(move |source, path, output| {
let result = parse_with_options(
source,
ParsingOptions {
allow_dtd: true,
..ParsingOptions::default()
},
#[allow(clippy::cast_precision_loss)]
|dom, allocator| -> anyhow::Result<()> {
let mut actor =
Actor::new(dom, allocator).map_err(|err| anyhow::anyhow!("{err}"))?;
for action in actions.clone() {
actor
.dispatch(action.clone())
.map_err(|err| anyhow::anyhow!("{err}"))?;
}
let output = Output {
options: format_options,
dom,
input: path,
destination: output,
input_bytes: source.len() as f64,
quiet: self.walk.quiet,
};
output.output()?;
Ok(())
},
);
if matches!(result, Err(_) | Ok(Err(_))) {
error.store(true, Ordering::Relaxed);
}
match result {
Err(err) => eprintln!("{err}"),
Ok(Err(err)) => eprintln!("{err}"),
Ok(Ok(())) => {}
}
})
})
}
}
impl RunCommand for ActionList {
async fn run(self, _: Config) -> anyhow::Result<()> {
let parts: HashSet<_> = self.command_list.into_iter().collect();
if parts.is_empty() || parts.contains(ATTR) {
println!("# Attribute\n");
println!(include_str!("../spec/manipulate/attr.md"));
}
if parts.is_empty() || parts.contains(CLASS) {
println!("# Class\n");
println!(include_str!("../spec/manipulate/class.md"));
}
if parts.is_empty() || parts.contains(PATH_INTERSECT) {
println!("# Path Intersect\n");
println!(include_str!("../spec/manipulate/path_intersect.md"));
}
if parts.is_empty() || parts.contains(STYLE) {
println!("# Style\n");
println!(include_str!("../spec/manipulate/style.md"));
}
if parts.is_empty() || parts.contains(MATRIX) {
println!("# Matrix\n");
println!(include_str!("../spec/manipulate/matrix.md"));
}
if parts.is_empty() || parts.contains(TRANSLATE) {
println!("# Translate\n");
println!(include_str!("../spec/manipulate/translate.md"));
}
if parts.is_empty() || parts.contains(SCALE) {
println!("# Scale\n");
println!(include_str!("../spec/manipulate/scale.md"));
}
if parts.is_empty() || parts.contains(ROTATE) {
println!("# Rotate\n");
println!(include_str!("../spec/manipulate/rotate.md"));
}
if parts.is_empty() || parts.contains(SKEW_X) {
println!("# Skew X\n");
println!(include_str!("../spec/manipulate/skewX.md"));
}
if parts.is_empty() || parts.contains(SKEW_Y) {
println!("# Skew Y\n");
println!(include_str!("../spec/manipulate/skewY.md"));
}
if parts.is_empty() || parts.contains(FORGET) {
println!("# Forget\n");
println!(include_str!("../spec/state/forget.md"));
}
if parts.is_empty() || parts.contains(SELECT) {
println!("# Select\n");
println!(include_str!("../spec/state/select.md"));
}
if parts.is_empty() || parts.contains(SELECT_MORE) {
println!("# Select More\n");
println!(include_str!("../spec/state/select-more.md"));
}
if parts.is_empty() || parts.contains(DESELECT) {
println!("# Deselect\n");
println!(include_str!("../spec/state/deselect.md"));
}
Ok(())
}
}
const ATTR: &str = "-attr";
const CLASS: &str = "-class";
const PATH_INTERSECT: &str = "-path-intersect";
const STYLE: &str = "-style";
const MATRIX: &str = "-matrix";
const TRANSLATE: &str = "-translate";
const SCALE: &str = "-scale";
const ROTATE: &str = "-rotate";
const SKEW_X: &str = "-skewX";
const SKEW_Y: &str = "-skewY";
const FORGET: &str = "-forget";
const SELECT: &str = "-select";
const SELECT_MORE: &str = "-select-more";
const DESELECT: &str = "-deselect";
fn parse(command_list: Vec<String>) -> anyhow::Result<Vec<oxvg_actions::Action<'static>>> {
let mut actions = Vec::with_capacity(
command_list
.iter()
.filter(|part| part.starts_with('-'))
.count(),
);
let mut parts = command_list.into_iter().peekable();
while let Some(action) = parts.next() {
let get_part = |parts: &mut Peekable<std::vec::IntoIter<String>>| {
parts
.next()
.ok_or_else(|| anyhow::anyhow!("`{action}` missing query"))
.map(Atom::from)
};
let get_part_f32 =
|parts: &mut Peekable<std::vec::IntoIter<String>>| -> anyhow::Result<f32> {
f32::parse_string(get_part(parts)?.as_str()).map_err(|err| anyhow::anyhow!("{err}"))
};
let get_part_f32_peek = |parts: &mut Peekable<std::vec::IntoIter<String>>| {
let n = {
let part = parts.peek()?;
if let Ok(n) = f32::parse_string(part) {
n
} else {
return None;
}
};
parts.next();
Some(n)
};
if !action.starts_with('-') {
return Err(anyhow::anyhow!("Expected command name, found {action}"));
}
actions.push(match action.as_str() {
ATTR => oxvg_actions::Action::Attr {
name: get_part(&mut parts)?,
value: get_part(&mut parts)?,
},
CLASS => oxvg_actions::Action::Class(get_part(&mut parts)?),
PATH_INTERSECT => oxvg_actions::Action::PathIntersect,
STYLE => oxvg_actions::Action::Style {
property: get_part(&mut parts)?,
value: get_part(&mut parts)?,
},
MATRIX => oxvg_actions::Action::Matrix(
get_part_f32(&mut parts)?,
get_part_f32(&mut parts)?,
get_part_f32(&mut parts)?,
get_part_f32(&mut parts)?,
get_part_f32(&mut parts)?,
get_part_f32(&mut parts)?,
),
TRANSLATE => oxvg_actions::Action::Translate(
get_part_f32(&mut parts)?,
get_part_f32_peek(&mut parts),
),
SCALE => oxvg_actions::Action::Scale(
get_part_f32(&mut parts)?,
get_part_f32_peek(&mut parts),
),
ROTATE => oxvg_actions::Action::Rotate(
get_part_f32(&mut parts)?,
if let Some(x) = get_part_f32_peek(&mut parts) {
Some((x, get_part_f32(&mut parts)?))
} else {
None
},
),
SKEW_X => oxvg_actions::Action::SkewX(get_part_f32(&mut parts)?),
SKEW_Y => oxvg_actions::Action::SkewY(get_part_f32(&mut parts)?),
FORGET => oxvg_actions::Action::Forget,
SELECT => oxvg_actions::Action::Select(get_part(&mut parts)?),
SELECT_MORE => oxvg_actions::Action::SelectMore(get_part(&mut parts)?),
DESELECT => oxvg_actions::Action::Deselect,
_ => return Err(anyhow::anyhow!("Unknown action `{action}`")),
});
}
Ok(actions)
}