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)]
pub struct RemoveEmptyTextConfig {
#[serde(default = "default_true")]
pub text: bool,
#[serde(default = "default_true")]
pub tspan: bool,
#[serde(default = "default_true")]
pub tref: bool,
}
fn default_true() -> bool {
true
}
impl Default for RemoveEmptyTextConfig {
fn default() -> Self {
Self {
text: true,
tspan: true,
tref: true,
}
}
}
pub struct RemoveEmptyTextPlugin {
config: RemoveEmptyTextConfig,
}
impl RemoveEmptyTextPlugin {
pub fn new() -> Self {
Self {
config: RemoveEmptyTextConfig::default(),
}
}
pub fn with_config(config: RemoveEmptyTextConfig) -> Self {
Self { config }
}
fn parse_config(params: &Value) -> Result<RemoveEmptyTextConfig> {
if params.is_null() || (params.is_object() && params.as_object().unwrap().is_empty()) {
Ok(RemoveEmptyTextConfig::default())
} else if params.is_object() {
serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("Invalid configuration: {}", e))
} else {
Ok(RemoveEmptyTextConfig::default())
}
}
fn remove_empty_text_recursive(&self, element: &mut Element) {
element.children.retain(|child| {
if let Node::Element(elem) = child {
if self.config.text && elem.name == "text" && elem.children.is_empty() {
return false;
}
if self.config.tspan && elem.name == "tspan" && elem.children.is_empty() {
return false;
}
if self.config.tref && elem.name == "tref" {
if let Some(href) = elem.attributes.get("xlink:href") {
if href.is_empty() {
return false;
}
} else {
return false;
}
}
}
true });
for child in &mut element.children {
if let Node::Element(elem) = child {
self.remove_empty_text_recursive(elem);
}
}
}
}
impl Default for RemoveEmptyTextPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for RemoveEmptyTextPlugin {
fn name(&self) -> &'static str {
"removeEmptyText"
}
fn description(&self) -> &'static str {
"removes empty <text> elements"
}
fn validate_params(&self, params: &Value) -> Result<()> {
Self::parse_config(params)?;
Ok(())
}
fn apply(&self, document: &mut Document) -> Result<()> {
self.remove_empty_text_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 = RemoveEmptyTextPlugin::new();
assert_eq!(plugin.name(), "removeEmptyText");
assert_eq!(plugin.description(), "removes empty <text> elements");
}
#[test]
fn test_parameter_validation() {
let plugin = RemoveEmptyTextPlugin::new();
assert!(plugin.validate_params(&json!({})).is_ok());
assert!(plugin.validate_params(&json!({"text": true})).is_ok());
assert!(plugin
.validate_params(&json!({"text": false, "tspan": true}))
.is_ok());
assert!(plugin.validate_params(&json!({"text": "invalid"})).is_err());
assert!(plugin
.validate_params(&json!({"unknownParam": true}))
.is_err());
}
#[test]
fn test_remove_empty_text() {
let plugin = RemoveEmptyTextPlugin::new();
let mut doc = Document::new();
let empty_text = create_element("text");
doc.root.children.push(Node::Element(empty_text));
let mut text_with_content = create_element("text");
text_with_content
.children
.push(Node::Text("Hello".to_string().into()));
doc.root.children.push(Node::Element(text_with_content));
let rect = create_element("rect");
doc.root.children.push(Node::Element(rect));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "text"), 1);
assert_eq!(doc.root.children.len(), 2);
}
#[test]
fn test_remove_empty_tspan() {
let plugin = RemoveEmptyTextPlugin::new();
let mut doc = Document::new();
let empty_tspan = create_element("tspan");
doc.root.children.push(Node::Element(empty_tspan));
let mut tspan_with_content = create_element("tspan");
tspan_with_content
.children
.push(Node::Text("World".to_string().into()));
doc.root.children.push(Node::Element(tspan_with_content));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "tspan"), 1);
assert_eq!(doc.root.children.len(), 1);
}
#[test]
fn test_remove_tref_empty_href() {
let plugin = RemoveEmptyTextPlugin::new();
let mut doc = Document::new();
let mut tref_empty = create_element("tref");
tref_empty.set_attr("xlink:href", "");
doc.root.children.push(Node::Element(tref_empty));
let tref_no_href = create_element("tref");
doc.root.children.push(Node::Element(tref_no_href));
let mut tref_valid = create_element("tref");
tref_valid.set_attr("xlink:href", "#validref");
doc.root.children.push(Node::Element(tref_valid));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "tref"), 1);
assert_eq!(doc.root.children.len(), 1);
}
#[test]
fn test_config_text_disabled() {
let config = RemoveEmptyTextConfig {
text: false,
tspan: true,
tref: true,
};
let plugin = RemoveEmptyTextPlugin::with_config(config);
let mut doc = Document::new();
let empty_text = create_element("text");
doc.root.children.push(Node::Element(empty_text));
let empty_tspan = create_element("tspan");
doc.root.children.push(Node::Element(empty_tspan));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "text"), 1);
assert_eq!(count_elements_by_name(&doc.root, "tspan"), 0);
}
#[test]
fn test_config_tspan_disabled() {
let config = RemoveEmptyTextConfig {
text: true,
tspan: false,
tref: true,
};
let plugin = RemoveEmptyTextPlugin::with_config(config);
let mut doc = Document::new();
let empty_text = create_element("text");
doc.root.children.push(Node::Element(empty_text));
let empty_tspan = create_element("tspan");
doc.root.children.push(Node::Element(empty_tspan));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "text"), 0);
assert_eq!(count_elements_by_name(&doc.root, "tspan"), 1);
}
#[test]
fn test_config_tref_disabled() {
let config = RemoveEmptyTextConfig {
text: true,
tspan: true,
tref: false,
};
let plugin = RemoveEmptyTextPlugin::with_config(config);
let mut doc = Document::new();
let mut tref_empty = create_element("tref");
tref_empty.set_attr("xlink:href", "");
doc.root.children.push(Node::Element(tref_empty));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "tref"), 1);
}
#[test]
fn test_nested_empty_text() {
let plugin = RemoveEmptyTextPlugin::new();
let mut doc = Document::new();
let mut group = create_element("g");
let empty_text = create_element("text");
group.children.push(Node::Element(empty_text));
let rect = create_element("rect");
group.children.push(Node::Element(rect));
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "text"), 0);
assert_eq!(doc.root.children.len(), 1);
if let Node::Element(group_elem) = &doc.root.children[0] {
assert_eq!(group_elem.name, "g");
assert_eq!(group_elem.children.len(), 1);
}
}
#[test]
fn test_no_empty_text_elements() {
let plugin = RemoveEmptyTextPlugin::new();
let mut doc = Document::new();
let mut text_with_content = create_element("text");
text_with_content
.children
.push(Node::Text("Hello".to_string().into()));
doc.root.children.push(Node::Element(text_with_content));
let rect = create_element("rect");
doc.root.children.push(Node::Element(rect));
plugin.apply(&mut doc).unwrap();
assert_eq!(count_elements_by_name(&doc.root, "text"), 1);
assert_eq!(doc.root.children.len(), 2);
}
#[test]
fn test_config_parsing() {
let config = RemoveEmptyTextPlugin::parse_config(&json!({
"text": false,
"tspan": true,
"tref": false
}))
.unwrap();
assert!(!config.text);
assert!(config.tspan);
assert!(!config.tref);
}
}
#[cfg(test)]
#[cfg(test)]
vexy_vsvg_test_utils::plugin_fixture_tests!(RemoveEmptyTextPlugin, "removeEmptyText");