use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vexy_vsvg::ast::{Document, Element, Node};
use crate::Plugin;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[derive(Default)]
pub struct RemoveDimensionsConfig {}
pub struct RemoveDimensionsPlugin {
#[allow(dead_code)]
config: RemoveDimensionsConfig,
}
impl RemoveDimensionsPlugin {
pub fn new() -> Self {
Self {
#[allow(dead_code)]
config: RemoveDimensionsConfig::default(),
}
}
pub fn with_config(config: RemoveDimensionsConfig) -> Self {
Self { config }
}
fn parse_config(params: &Value) -> Result<RemoveDimensionsConfig> {
if params.is_null() {
Ok(RemoveDimensionsConfig::default())
} else {
serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("Invalid plugin configuration: {}", e))
}
}
fn process_svg_element(&self, element: &mut Element) {
if element.name != "svg" {
return;
}
let had_viewbox = element.has_attr("viewBox");
if had_viewbox {
element.remove_attr("width");
element.remove_attr("height");
} else {
let width_str = element.attr("width");
let height_str = element.attr("height");
if let (Some(width_str), Some(height_str)) = (width_str, height_str) {
if let (Ok(width), Ok(height)) =
(width_str.parse::<f64>(), height_str.parse::<f64>())
{
if !width.is_nan() && !height.is_nan() {
let viewbox = format!("0 0 {} {}", width, height);
element.set_attr("viewBox", &viewbox);
element.remove_attr("width");
element.remove_attr("height");
}
}
}
}
}
fn process_element(&self, element: &mut Element) {
self.process_svg_element(element);
let mut i = 0;
while i < element.children.len() {
if let Node::Element(child) = &mut element.children[i] {
self.process_element(child);
}
i += 1;
}
}
}
impl Default for RemoveDimensionsPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for RemoveDimensionsPlugin {
fn name(&self) -> &'static str {
"removeDimensions"
}
fn description(&self) -> &'static str {
"removes width and height in presence of viewBox (opposite to removeViewBox)"
}
fn validate_params(&self, params: &Value) -> Result<()> {
Self::parse_config(params)?;
Ok(())
}
fn apply(&self, document: &mut Document) -> Result<()> {
self.process_element(&mut document.root);
Ok(())
}
}
#[cfg(test)]
mod tests {
use indexmap::IndexMap;
use vexy_vsvg::ast::{Document, Element, Node};
use super::*;
fn create_test_document() -> Document<'static> {
Document {
root: Element {
name: "svg".into(),
attributes: IndexMap::new(),
namespaces: IndexMap::new(),
children: vec![],
},
prologue: vec![],
epilogue: vec![],
metadata: vexy_vsvg::ast::DocumentMetadata {
path: None,
encoding: None,
version: None,
..Default::default()
},
memory_budget: None,
}
}
#[test]
fn test_plugin_info() {
let plugin = RemoveDimensionsPlugin::new();
assert_eq!(plugin.name(), "removeDimensions");
assert_eq!(
plugin.description(),
"removes width and height in presence of viewBox (opposite to removeViewBox)"
);
}
#[test]
fn test_param_validation() {
let plugin = RemoveDimensionsPlugin::new();
assert!(plugin.validate_params(&Value::Null).is_ok());
assert!(plugin.validate_params(&serde_json::json!({})).is_ok());
assert!(plugin
.validate_params(&serde_json::json!({
"invalidParam": true
}))
.is_err());
}
#[test]
fn test_remove_dimensions_with_existing_viewbox() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "100");
doc.root.set_attr("height", "50");
doc.root.set_attr("viewBox", "0 0 200 100");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert!(!doc.root.has_attr("width"));
assert!(!doc.root.has_attr("height"));
assert_eq!(doc.root.attr("viewBox"), Some("0 0 200 100"));
assert!(!doc.root.has_attr("preserveAspectRatio"));
}
#[test]
fn test_create_viewbox_from_dimensions() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "100");
doc.root.set_attr("height", "50");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert!(!doc.root.has_attr("width"));
assert!(!doc.root.has_attr("height"));
assert_eq!(doc.root.attr("viewBox"), Some("0 0 100 50"));
assert!(!doc.root.has_attr("preserveAspectRatio"));
}
#[test]
fn test_decimal_dimensions() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "100.5");
doc.root.set_attr("height", "50.25");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert!(!doc.root.has_attr("width"));
assert!(!doc.root.has_attr("height"));
assert_eq!(doc.root.attr("viewBox"), Some("0 0 100.5 50.25"));
assert!(!doc.root.has_attr("preserveAspectRatio"));
}
#[test]
fn test_invalid_dimensions_ignored() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "invalid");
doc.root.set_attr("height", "50");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert_eq!(doc.root.attr("width"), Some("invalid"));
assert_eq!(doc.root.attr("height"), Some("50"));
assert!(!doc.root.has_attr("viewBox"));
}
#[test]
fn test_missing_dimension_ignored() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "100");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert_eq!(doc.root.attr("width"), Some("100"));
assert!(!doc.root.has_attr("viewBox"));
}
#[test]
fn test_only_processes_svg_elements() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
let mut rect = Element {
name: "rect".into(),
attributes: IndexMap::new(),
namespaces: IndexMap::new(),
children: vec![],
};
rect.set_attr("width", "100");
rect.set_attr("height", "50");
rect.set_attr("x", "10");
rect.set_attr("y", "10");
doc.root.children.push(Node::Element(rect));
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
if let Node::Element(rect) = &doc.root.children[0] {
assert_eq!(rect.attr("width"), Some("100"));
assert_eq!(rect.attr("height"), Some("50"));
assert!(!rect.has_attr("viewBox"));
} else {
panic!("Expected rect element");
}
}
#[test]
fn test_nested_svg_elements() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "200");
doc.root.set_attr("height", "100");
let mut nested_svg = Element {
name: "svg".into(),
attributes: IndexMap::new(),
namespaces: IndexMap::new(),
children: vec![],
};
nested_svg.set_attr("width", "100");
nested_svg.set_attr("height", "50");
doc.root.children.push(Node::Element(nested_svg));
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert!(!doc.root.has_attr("width"));
assert!(!doc.root.has_attr("height"));
assert_eq!(doc.root.attr("viewBox"), Some("0 0 200 100"));
assert!(!doc.root.has_attr("preserveAspectRatio"));
if let Node::Element(nested_svg) = &doc.root.children[0] {
assert!(!nested_svg.has_attr("width"));
assert!(!nested_svg.has_attr("height"));
assert_eq!(nested_svg.attr("viewBox"), Some("0 0 100 50"));
assert!(!nested_svg.has_attr("preserveAspectRatio"));
} else {
panic!("Expected nested SVG element");
}
}
#[test]
fn test_zero_dimensions() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "0");
doc.root.set_attr("height", "0");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert!(!doc.root.has_attr("width"));
assert!(!doc.root.has_attr("height"));
assert_eq!(doc.root.attr("viewBox"), Some("0 0 0 0"));
assert!(!doc.root.has_attr("preserveAspectRatio"));
}
#[test]
fn test_existing_preserve_aspect_ratio_is_not_overwritten() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
doc.root.set_attr("width", "100");
doc.root.set_attr("height", "50");
doc.root.set_attr("viewBox", "0 0 100 50");
doc.root.set_attr("preserveAspectRatio", "none");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert_eq!(doc.root.attr("preserveAspectRatio"), Some("none"));
}
#[test]
fn test_no_dimensions_no_change() {
let plugin = RemoveDimensionsPlugin::new();
let mut doc = create_test_document();
let original_count = doc.root.attributes.len();
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert_eq!(doc.root.attributes.len(), original_count);
assert!(!doc.root.has_attr("width"));
assert!(!doc.root.has_attr("height"));
assert!(!doc.root.has_attr("viewBox"));
}
}