use crate::Plugin;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vexy_vsvg::ast::{Document, Element, Node};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[derive(Default)]
pub struct ConvertEllipseToCircleConfig {
}
pub struct ConvertEllipseToCirclePlugin {
#[allow(dead_code)]
config: ConvertEllipseToCircleConfig,
}
impl ConvertEllipseToCirclePlugin {
pub fn new() -> Self {
Self {
config: ConvertEllipseToCircleConfig::default(),
}
}
pub fn with_config(config: ConvertEllipseToCircleConfig) -> Self {
Self { config }
}
fn parse_config(params: &Value) -> Result<ConvertEllipseToCircleConfig> {
if params.is_null() || (params.is_object() && params.as_object().unwrap().is_empty()) {
Ok(ConvertEllipseToCircleConfig::default())
} else if params.is_object() {
serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("Invalid configuration: {}", e))
} else {
Ok(ConvertEllipseToCircleConfig::default())
}
}
fn convert_ellipse_to_circle_recursive(&self, element: &mut Element) {
if element.name == "ellipse" {
let rx = element
.attributes
.get("rx")
.cloned()
.unwrap_or_else(|| "0".into());
let ry = element
.attributes
.get("ry")
.cloned()
.unwrap_or_else(|| "0".into());
if rx == ry || rx == "auto" || ry == "auto" {
element.name = "circle".into();
let radius = if rx == "auto" { ry } else { rx };
element.attributes.shift_remove("rx");
element.attributes.shift_remove("ry");
element.attributes.insert("r".into(), radius);
}
}
for child in &mut element.children {
if let Node::Element(elem) = child {
self.convert_ellipse_to_circle_recursive(elem);
}
}
}
}
impl Default for ConvertEllipseToCirclePlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for ConvertEllipseToCirclePlugin {
fn name(&self) -> &'static str {
"convertEllipseToCircle"
}
fn description(&self) -> &'static str {
"converts non-eccentric <ellipse>s to <circle>s"
}
fn validate_params(&self, params: &Value) -> Result<()> {
Self::parse_config(params)?;
Ok(())
}
fn apply(&self, document: &mut Document) -> Result<()> {
self.convert_ellipse_to_circle_recursive(&mut document.root);
Ok(())
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
use serde_json::json;
use std::borrow::Cow;
use vexy_vsvg::ast::{Document, Element, Node};
fn create_element(name: &'static str) -> Element<'static> {
let mut element = Element::new(name);
element.name = Cow::Borrowed(name);
element
}
fn count_elements_by_name(element: &Element, name: &str) -> usize {
let mut count = 0;
for child in &element.children {
if let Node::Element(elem) = child {
if elem.name == name {
count += 1;
}
count += count_elements_by_name(elem, name);
}
}
count
}
#[test]
fn test_plugin_creation() {
let plugin = ConvertEllipseToCirclePlugin::new();
assert_eq!(plugin.name(), "convertEllipseToCircle");
assert_eq!(
plugin.description(),
"converts non-eccentric <ellipse>s to <circle>s"
);
}
#[test]
fn test_parameter_validation() {
let plugin = ConvertEllipseToCirclePlugin::new();
assert!(plugin.validate_params(&json!({})).is_ok());
assert!(plugin.validate_params(&json!({"param": "value"})).is_err());
}
#[test]
fn test_convert_ellipse_equal_radii() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let mut ellipse = create_element("ellipse");
ellipse.attributes.insert("rx".into(), "10".into());
ellipse.attributes.insert("ry".into(), "10".into());
ellipse.attributes.insert("cx".into(), "50".into());
ellipse.attributes.insert("cy".into(), "50".into());
doc.root.children.push(Node::Element(ellipse));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 0);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "circle");
assert_eq!(elem.attributes.get("r"), Some(&"10".into()));
assert_eq!(elem.attributes.get("cx"), Some(&"50".into()));
assert_eq!(elem.attributes.get("cy"), Some(&"50".into()));
assert!(!elem.attributes.contains_key("rx"));
assert!(!elem.attributes.contains_key("ry"));
}
}
#[test]
fn test_convert_ellipse_auto_rx() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let mut ellipse = create_element("ellipse");
ellipse.attributes.insert("rx".into(), "auto".into());
ellipse.attributes.insert("ry".into(), "15".into());
doc.root.children.push(Node::Element(ellipse));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 0);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "circle");
assert_eq!(elem.attributes.get("r"), Some(&"15".into()));
assert!(!elem.attributes.contains_key("rx"));
assert!(!elem.attributes.contains_key("ry"));
}
}
#[test]
fn test_convert_ellipse_auto_ry() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let mut ellipse = create_element("ellipse");
ellipse.attributes.insert("rx".into(), "20".into());
ellipse.attributes.insert("ry".into(), "auto".into());
doc.root.children.push(Node::Element(ellipse));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 0);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "circle");
assert_eq!(elem.attributes.get("r"), Some(&"20".into()));
assert!(!elem.attributes.contains_key("rx"));
assert!(!elem.attributes.contains_key("ry"));
}
}
#[test]
fn test_keep_ellipse_different_radii() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let mut ellipse = create_element("ellipse");
ellipse.attributes.insert("rx".into(), "10".into());
ellipse.attributes.insert("ry".into(), "20".into());
doc.root.children.push(Node::Element(ellipse));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 1);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 0);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "ellipse");
assert_eq!(elem.attributes.get("rx"), Some(&"10".into()));
assert_eq!(elem.attributes.get("ry"), Some(&"20".into()));
}
}
#[test]
fn test_ellipse_default_attributes() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let ellipse = create_element("ellipse");
doc.root.children.push(Node::Element(ellipse));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 0);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "circle");
assert_eq!(elem.attributes.get("r"), Some(&"0".into()));
}
}
#[test]
fn test_ellipse_with_only_rx() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let mut ellipse = create_element("ellipse");
ellipse.attributes.insert("rx".into(), "0".into());
doc.root.children.push(Node::Element(ellipse));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 0);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "circle");
assert_eq!(elem.attributes.get("r"), Some(&"0".into()));
}
}
#[test]
fn test_nested_ellipses() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let mut group = create_element("g");
let mut ellipse1 = create_element("ellipse");
ellipse1.attributes.insert("rx".into(), "5".into());
ellipse1.attributes.insert("ry".into(), "5".into());
group.children.push(Node::Element(ellipse1));
let mut ellipse2 = create_element("ellipse");
ellipse2.attributes.insert("rx".into(), "5".into());
ellipse2.attributes.insert("ry".into(), "10".into());
group.children.push(Node::Element(ellipse2));
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 1);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 1);
}
#[test]
fn test_no_ellipses() {
let plugin = ConvertEllipseToCirclePlugin::new();
let mut doc = Document::new();
let rect = create_element("rect");
doc.root.children.push(Node::Element(rect));
let circle = create_element("circle");
doc.root.children.push(Node::Element(circle));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "ellipse"), 0);
assert_eq!(count_elements_by_name(&doc.root, "circle"), 1);
assert_eq!(count_elements_by_name(&doc.root, "rect"), 1);
}
#[test]
fn test_config_parsing() {
let config = ConvertEllipseToCirclePlugin::parse_config(&json!({})).unwrap();
let _ = config;
}
}
#[cfg(test)]
#[cfg(test)]
vexy_vsvg_test_utils::plugin_fixture_tests!(ConvertEllipseToCirclePlugin, "convertEllipseToCircle");