use crate::Plugin;
use anyhow::Result;
use vexy_vsvg::ast::{Document, Element, Node};
use vexy_vsvg::error::VexyError;
use vexy_vsvg::visitor::Visitor;
#[derive(Default)]
pub struct RemoveUselessDefsPlugin;
impl RemoveUselessDefsPlugin {
pub fn new() -> Self {
Self
}
fn non_rendering_elements() -> &'static [&'static str] {
&[
"clipPath",
"filter",
"linearGradient",
"marker",
"mask",
"pattern",
"radialGradient",
"solidColor",
"symbol",
]
}
fn is_non_rendering_element(name: &str) -> bool {
Self::non_rendering_elements().contains(&name)
}
fn should_preserve_node(node: &Node) -> bool {
match node {
Node::Element(element) => {
if element.name.as_ref() == "style" {
return true;
}
element.attributes.contains_key("id")
}
_ => true, }
}
fn should_process_element(element: &Element) -> bool {
element.name.as_ref() == "defs"
|| (Self::is_non_rendering_element(&element.name)
&& !element.attributes.contains_key("id"))
}
fn collect_useful_nodes<'a>(children: &[Node<'a>]) -> Vec<Node<'a>> {
let mut useful_nodes = Vec::new();
for child in children {
match child {
Node::Element(element) => {
if Self::should_preserve_node(child) {
useful_nodes.push(child.clone());
} else if Self::should_process_element(element) {
let flattened = Self::collect_useful_nodes(&element.children);
useful_nodes.extend(flattened);
} else {
let flattened = Self::collect_useful_nodes(&element.children);
useful_nodes.extend(flattened);
}
}
_ => {
useful_nodes.push(child.clone());
}
}
}
useful_nodes
}
}
impl Plugin for RemoveUselessDefsPlugin {
fn name(&self) -> &'static str {
"removeUselessDefs"
}
fn description(&self) -> &'static str {
"Remove useless definitions from SVG document"
}
fn validate_params(&self, params: &serde_json::Value) -> anyhow::Result<()> {
if !params.is_null() && !params.as_object().is_some_and(|obj| obj.is_empty()) {
return Err(anyhow::anyhow!(
"removeUselessDefs plugin does not accept any parameters"
));
}
Ok(())
}
fn apply(&self, document: &mut Document) -> anyhow::Result<()> {
let mut visitor = UselessDefsRemovalVisitor::new();
vexy_vsvg::visitor::walk_document(&mut visitor, document)?;
Ok(())
}
}
struct UselessDefsRemovalVisitor;
impl UselessDefsRemovalVisitor {
fn new() -> Self {
Self
}
}
impl Visitor<'_> for UselessDefsRemovalVisitor {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<(), VexyError> {
if RemoveUselessDefsPlugin::should_process_element(element) {
element.children = RemoveUselessDefsPlugin::collect_useful_nodes(&element.children);
}
Ok(())
}
}
#[cfg(test)]
mod 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 create_element_with_id(name: &'static str, id: &str) -> Element<'static> {
let mut element = create_element(name);
element
.attributes
.insert(Cow::Borrowed("id"), Cow::Owned(id.to_string()));
element
}
#[test]
fn test_plugin_creation() {
let plugin = RemoveUselessDefsPlugin::new();
assert_eq!(plugin.name(), "removeUselessDefs");
}
#[test]
fn test_parameter_validation() {
let plugin = RemoveUselessDefsPlugin::new();
assert!(plugin.validate_params(&json!(null)).is_ok());
assert!(plugin.validate_params(&json!({})).is_ok());
assert!(plugin.validate_params(&json!({"someParam": true})).is_err());
}
#[test]
fn test_non_rendering_elements() {
assert!(RemoveUselessDefsPlugin::is_non_rendering_element(
"clipPath"
));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element("filter"));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element(
"linearGradient"
));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element("marker"));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element("mask"));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element("pattern"));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element(
"radialGradient"
));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element(
"solidColor"
));
assert!(RemoveUselessDefsPlugin::is_non_rendering_element("symbol"));
assert!(!RemoveUselessDefsPlugin::is_non_rendering_element("rect"));
assert!(!RemoveUselessDefsPlugin::is_non_rendering_element("path"));
assert!(!RemoveUselessDefsPlugin::is_non_rendering_element("defs"));
}
#[test]
fn test_should_preserve_node() {
let element_with_id = create_element_with_id("path", "mypath");
assert!(RemoveUselessDefsPlugin::should_preserve_node(
&Node::Element(element_with_id)
));
let style_element = create_element("style");
assert!(RemoveUselessDefsPlugin::should_preserve_node(
&Node::Element(style_element)
));
let element_without_id = create_element("path");
assert!(!RemoveUselessDefsPlugin::should_preserve_node(
&Node::Element(element_without_id)
));
assert!(RemoveUselessDefsPlugin::should_preserve_node(
&Node::Comment("comment".to_string().into())
));
}
#[test]
fn test_should_process_element() {
let defs_element = create_element("defs");
assert!(RemoveUselessDefsPlugin::should_process_element(
&defs_element
));
let clippath_no_id = create_element("clipPath");
assert!(RemoveUselessDefsPlugin::should_process_element(
&clippath_no_id
));
let clippath_with_id = create_element_with_id("clipPath", "clip1");
assert!(!RemoveUselessDefsPlugin::should_process_element(
&clippath_with_id
));
let rect_element = create_element("rect");
assert!(!RemoveUselessDefsPlugin::should_process_element(
&rect_element
));
}
#[test]
fn test_collect_useful_nodes_basic() {
let path_with_id = create_element_with_id("path", "path1");
let path_without_id = create_element("path");
let children = vec![
Node::Element(path_with_id.clone()),
Node::Element(path_without_id),
];
let useful = RemoveUselessDefsPlugin::collect_useful_nodes(&children);
assert_eq!(useful.len(), 1);
if let Node::Element(element) = &useful[0] {
assert!(element.attributes.contains_key("id"));
}
}
#[test]
fn test_collect_useful_nodes_style_preservation() {
let style_element = create_element("style");
let path_without_id = create_element("path");
let children = vec![
Node::Element(style_element.clone()),
Node::Element(path_without_id),
];
let useful = RemoveUselessDefsPlugin::collect_useful_nodes(&children);
assert_eq!(useful.len(), 1);
if let Node::Element(element) = &useful[0] {
assert_eq!(element.name.as_ref(), "style");
}
}
#[test]
fn test_collect_useful_nodes_flattening() {
let plugin = RemoveUselessDefsPlugin::new();
let mut doc = Document::new();
let path_with_id = create_element_with_id("path", "path1");
let mut group_no_id = create_element("g");
group_no_id
.children
.push(Node::Element(path_with_id.clone()));
let mut defs = create_element("defs");
defs.children.push(Node::Element(group_no_id));
doc.root.children.push(Node::Element(defs));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(defs_element)) = doc.root.children.first() {
assert_eq!(defs_element.name.as_ref(), "defs");
assert_eq!(defs_element.children.len(), 1);
if let Node::Element(child) = &defs_element.children[0] {
assert_eq!(child.name.as_ref(), "path");
assert!(child.attributes.contains_key("id"));
}
}
}
#[test]
fn test_plugin_apply() {
let plugin = RemoveUselessDefsPlugin::new();
let mut doc = Document::new();
let mut defs = create_element("defs");
defs.children.push(Node::Element(create_element("path"))); defs.children
.push(Node::Element(create_element_with_id("path", "keep"))); defs.children.push(Node::Element(create_element("style")));
doc.root.children.push(Node::Element(defs));
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
if let Some(Node::Element(defs_element)) = doc.root.children.first() {
assert_eq!(defs_element.name.as_ref(), "defs");
assert_eq!(defs_element.children.len(), 2);
let has_path_with_id = defs_element.children.iter().any(|child| {
if let Node::Element(elem) = child {
elem.name.as_ref() == "path" && elem.attributes.contains_key("id")
} else {
false
}
});
let has_style = defs_element.children.iter().any(|child| {
if let Node::Element(elem) = child {
elem.name.as_ref() == "style"
} else {
false
}
});
assert!(has_path_with_id);
assert!(has_style);
} else {
panic!("Expected defs element not found");
}
}
#[test]
fn test_non_rendering_element_without_id() {
let plugin = RemoveUselessDefsPlugin::new();
let mut doc = Document::new();
let mut clippath = create_element("clipPath");
clippath
.children
.push(Node::Element(create_element("path"))); clippath
.children
.push(Node::Element(create_element_with_id("circle", "keep")));
doc.root.children.push(Node::Element(clippath));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(clippath_element)) = doc.root.children.first() {
assert_eq!(clippath_element.name.as_ref(), "clipPath");
assert_eq!(clippath_element.children.len(), 1);
if let Node::Element(child) = &clippath_element.children[0] {
assert_eq!(child.name.as_ref(), "circle");
assert!(child.attributes.contains_key("id"));
}
}
}
}