use crate::Plugin;
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
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 RemoveDescConfig {
#[serde(default)]
pub remove_any: bool,
}
pub struct RemoveDescPlugin {
config: RemoveDescConfig,
}
static STANDARD_DESCS: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(Created with|Created using)").unwrap());
impl RemoveDescPlugin {
pub fn new() -> Self {
Self {
config: RemoveDescConfig::default(),
}
}
pub fn with_config(config: RemoveDescConfig) -> Self {
Self { config }
}
fn parse_config(params: &Value) -> Result<RemoveDescConfig> {
if params.is_null() {
Ok(RemoveDescConfig::default())
} else {
serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("Invalid plugin configuration: {}", e))
}
}
fn process_element(&self, element: &mut Element) {
element.children.retain(|child| {
match child {
Node::Element(child_element) if child_element.name == "desc" => {
!self.should_remove_desc(child_element)
}
_ => true,
}
});
let mut i = 0;
while i < element.children.len() {
if let Node::Element(child) = &mut element.children[i] {
self.process_element(child);
}
i += 1;
}
}
fn should_remove_desc(&self, desc_element: &Element) -> bool {
if self.config.remove_any {
return true;
}
if desc_element.children.is_empty() {
return true;
}
if desc_element.children.len() == 1 {
if let Some(Node::Text(text)) = desc_element.children.first() {
if STANDARD_DESCS.is_match(text) {
return true;
}
}
}
false
}
}
impl Default for RemoveDescPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for RemoveDescPlugin {
fn name(&self) -> &'static str {
"removeDesc"
}
fn description(&self) -> &'static str {
"removes <desc> element"
}
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 super::*;
use vexy_vsvg::parser::Parser;
#[test]
fn test_plugin_info() {
let plugin = RemoveDescPlugin::new();
assert_eq!(plugin.name(), "removeDesc");
assert_eq!(plugin.description(), "removes <desc> element");
}
#[test]
fn test_param_validation() {
let plugin = RemoveDescPlugin::new();
assert!(plugin.validate_params(&Value::Null).is_ok());
assert!(plugin
.validate_params(&serde_json::json!({
"removeAny": true
}))
.is_ok());
assert!(plugin
.validate_params(&serde_json::json!({
"invalidParam": true
}))
.is_err());
}
#[test]
fn test_remove_empty_desc() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
<desc></desc>
<rect width="100" height="100"/>
</svg>"#;
let parser = Parser::new();
let mut document = parser.parse(svg).unwrap();
let plugin = RemoveDescPlugin::new();
plugin.apply(&mut document).unwrap();
assert!(!has_desc_element(&document.root));
}
#[test]
fn test_remove_standard_desc() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
<desc>Created with Sketch.</desc>
<rect width="100" height="100"/>
</svg>"#;
let parser = Parser::new();
let mut document = parser.parse(svg).unwrap();
let plugin = RemoveDescPlugin::new();
plugin.apply(&mut document).unwrap();
assert!(!has_desc_element(&document.root));
}
#[test]
fn test_preserve_custom_desc() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
<desc>This is a custom description for accessibility</desc>
<rect width="100" height="100"/>
</svg>"#;
let parser = Parser::new();
let mut document = parser.parse(svg).unwrap();
let plugin = RemoveDescPlugin::new();
plugin.apply(&mut document).unwrap();
assert!(has_desc_element(&document.root));
}
#[test]
fn test_remove_any() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg">
<desc>This is a custom description for accessibility</desc>
<rect width="100" height="100"/>
</svg>"#;
let parser = Parser::new();
let mut document = parser.parse(svg).unwrap();
let plugin = RemoveDescPlugin::with_config(RemoveDescConfig { remove_any: true });
plugin.apply(&mut document).unwrap();
assert!(!has_desc_element(&document.root));
}
fn has_desc_element(element: &Element) -> bool {
for child in &element.children {
if let Node::Element(child_element) = child {
if child_element.name == "desc" {
return true;
}
if has_desc_element(child_element) {
return true;
}
}
}
false
}
}