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 RemoveStyleElementConfig {
}
pub struct RemoveStyleElementPlugin {
#[allow(dead_code)]
config: RemoveStyleElementConfig,
}
impl RemoveStyleElementPlugin {
pub fn new() -> Self {
Self {
#[allow(dead_code)]
config: RemoveStyleElementConfig::default(),
}
}
pub fn with_config(config: RemoveStyleElementConfig) -> Self {
Self { config }
}
fn parse_config(params: &Value) -> Result<RemoveStyleElementConfig> {
if params.is_null() {
Ok(RemoveStyleElementConfig::default())
} else if params.is_object() {
serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("Invalid configuration: {}", e))
} else {
Err(anyhow::anyhow!("Configuration must be an object"))
}
}
fn process_element(&self, element: &mut Element) {
element.children.retain(|child| {
if let Node::Element(elem) = child {
elem.name != "style"
} else {
true
}
});
for child in &mut element.children {
if let Node::Element(elem) = child {
self.process_element(elem);
}
}
}
}
impl Default for RemoveStyleElementPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for RemoveStyleElementPlugin {
fn name(&self) -> &'static str {
"removeStyleElement"
}
fn description(&self) -> &'static str {
"removes <style> element (disabled by default)"
}
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 unit_tests {
use std::borrow::Cow;
use serde_json::json;
use vexy_vsvg::ast::{Document, Element, Node};
use super::*;
fn create_element(name: &'static str) -> Element<'static> {
let mut element = Element::new(name);
element.name = Cow::Borrowed(name);
element
}
#[test]
fn test_plugin_creation() {
let plugin = RemoveStyleElementPlugin::new();
assert_eq!(plugin.name(), "removeStyleElement");
assert_eq!(
plugin.description(),
"removes <style> element (disabled by default)"
);
}
#[test]
fn test_removes_style_elements() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
let mut style = create_element("style");
style
.children
.push(Node::Text(".red { fill: red; }".to_string().into()));
doc.root.children.push(Node::Element(style));
let rect = create_element("rect");
doc.root.children.push(Node::Element(rect));
plugin.apply(&mut doc).unwrap();
assert_eq!(doc.root.children.len(), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "rect");
} else {
panic!("Expected element node");
}
}
#[test]
fn test_removes_multiple_style_elements() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
let mut style1 = create_element("style");
style1
.children
.push(Node::Text(".class1 { fill: blue; }".to_string().into()));
doc.root.children.push(Node::Element(style1));
let circle = create_element("circle");
doc.root.children.push(Node::Element(circle));
let mut style2 = create_element("style");
style2
.children
.push(Node::Text(".class2 { stroke: green; }".to_string().into()));
doc.root.children.push(Node::Element(style2));
plugin.apply(&mut doc).unwrap();
assert_eq!(doc.root.children.len(), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "circle");
}
}
#[test]
fn test_removes_nested_style_elements() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
let mut defs = create_element("defs");
let mut style = create_element("style");
style
.children
.push(Node::Text("#id { opacity: 0.5; }".to_string().into()));
defs.children.push(Node::Element(style));
let gradient = create_element("linearGradient");
defs.children.push(Node::Element(gradient));
doc.root.children.push(Node::Element(defs));
let mut group = create_element("g");
let mut nested_style = create_element("style");
nested_style
.children
.push(Node::Text(".nested { fill: yellow; }".to_string().into()));
group.children.push(Node::Element(nested_style));
let path = create_element("path");
group.children.push(Node::Element(path));
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
if let Node::Element(defs) = &doc.root.children[0] {
assert_eq!(defs.children.len(), 1);
if let Node::Element(grad) = &defs.children[0] {
assert_eq!(grad.name, "linearGradient");
}
}
if let Node::Element(g) = &doc.root.children[1] {
assert_eq!(g.children.len(), 1);
if let Node::Element(p) = &g.children[0] {
assert_eq!(p.name, "path");
}
}
}
#[test]
fn test_preserves_style_attributes() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
let mut rect = create_element("rect");
rect.set_attr("style", "fill: red; stroke: blue;");
doc.root.children.push(Node::Element(rect));
let mut style = create_element("style");
style
.children
.push(Node::Text(".class { fill: green; }".to_string().into()));
doc.root.children.push(Node::Element(style));
plugin.apply(&mut doc).unwrap();
assert_eq!(doc.root.children.len(), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "rect");
assert_eq!(elem.attr("style"), Some("fill: red; stroke: blue;"));
}
}
#[test]
fn test_empty_document() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
}
#[test]
fn test_document_without_styles() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
doc.root
.children
.push(Node::Element(create_element("rect")));
doc.root
.children
.push(Node::Element(create_element("circle")));
doc.root
.children
.push(Node::Element(create_element("path")));
let children_before = doc.root.children.len();
plugin.apply(&mut doc).unwrap();
assert_eq!(doc.root.children.len(), children_before);
}
#[test]
fn test_style_with_attributes() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
let mut style = create_element("style");
style.set_attr("type", "text/css");
style.set_attr("media", "screen");
style.children.push(Node::Text(
"@media print { .no-print { display: none; } }".into(),
));
doc.root.children.push(Node::Element(style));
plugin.apply(&mut doc).unwrap();
assert_eq!(doc.root.children.len(), 0);
}
#[test]
fn test_parameter_validation() {
let plugin = RemoveStyleElementPlugin::new();
assert!(plugin.validate_params(&json!({})).is_ok());
assert!(plugin.validate_params(&Value::Null).is_ok());
assert!(plugin.validate_params(&json!("invalid")).is_err());
}
#[test]
fn test_cdata_style_content() {
let plugin = RemoveStyleElementPlugin::new();
let mut doc = Document::new();
doc.root = create_element("svg");
let mut style = create_element("style");
style
.children
.push(Node::Text("<![CDATA[ .class { fill: red; } ]]>".into()));
doc.root.children.push(Node::Element(style));
plugin.apply(&mut doc).unwrap();
assert_eq!(doc.root.children.len(), 0);
}
}
#[cfg(test)]
#[cfg(test)]
vexy_vsvg_test_utils::plugin_fixture_tests!(RemoveStyleElementPlugin, "removeStyleElement");