use crate::Plugin;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use vexy_vsvg::ast::{Document, Element};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[derive(Default)]
pub struct AddClassesToSVGElementConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub class_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "classes")]
pub class_names: Option<Vec<String>>,
}
pub struct AddClassesToSVGElementPlugin {
config: AddClassesToSVGElementConfig,
}
impl AddClassesToSVGElementPlugin {
pub fn new() -> Self {
Self {
config: AddClassesToSVGElementConfig::default(),
}
}
pub fn with_config(config: AddClassesToSVGElementConfig) -> Self {
Self { config }
}
fn parse_config(params: &Value) -> Result<AddClassesToSVGElementConfig, anyhow::Error> {
if params.is_null() {
Ok(AddClassesToSVGElementConfig::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 apply_classes(&self, element: &mut Element) {
let mut classes_to_add = Vec::new();
if let Some(ref class_name) = self.config.class_name {
classes_to_add.push(class_name.clone());
}
if let Some(ref class_names) = self.config.class_names {
classes_to_add.extend(class_names.iter().cloned());
}
let mut class_list: Vec<String> = Vec::new();
let mut class_set: HashSet<String> = HashSet::new();
if let Some(existing_class) = element.attributes.get("class") {
for class_name in existing_class.split_whitespace() {
if class_set.insert(class_name.to_string()) {
class_list.push(class_name.to_string());
}
}
}
for class_name in classes_to_add {
if !class_name.is_empty() && class_set.insert(class_name.clone()) {
class_list.push(class_name);
}
}
if !class_list.is_empty() {
let class_string = class_list.join(" ");
element
.attributes
.insert("class".into(), class_string.into());
}
}
}
impl Default for AddClassesToSVGElementPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for AddClassesToSVGElementPlugin {
fn name(&self) -> &'static str {
"addClassesToSVGElement"
}
fn description(&self) -> &'static str {
"adds classnames to an outer <svg> element"
}
fn validate_params(&self, params: &Value) -> anyhow::Result<()> {
let config = Self::parse_config(params)?;
if config.class_name.is_none()
&& (config.class_names.is_none() || config.class_names.as_ref().unwrap().is_empty())
{
return Err(anyhow::anyhow!(
"Error in plugin \"addClassesToSVGElement\": absent parameters.\n\
It should have a list of classes in \"classNames\" or one \"className\"."
));
}
Ok(())
}
fn configure(&mut self, params: &Value) -> anyhow::Result<()> {
self.config = Self::parse_config(params)?;
Ok(())
}
fn apply(&self, document: &mut Document) -> anyhow::Result<()> {
if document.root.name == "svg" {
self.apply_classes(&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};
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 = AddClassesToSVGElementPlugin::new();
assert_eq!(plugin.name(), "addClassesToSVGElement");
assert_eq!(
plugin.description(),
"adds classnames to an outer <svg> element"
);
}
#[test]
fn test_parameter_validation_missing_params() {
let plugin = AddClassesToSVGElementPlugin::new();
assert!(plugin.validate_params(&json!({})).is_err());
assert!(plugin
.validate_params(&json!({
"classNames": []
}))
.is_err());
}
#[test]
fn test_parameter_validation_single_class() {
let plugin = AddClassesToSVGElementPlugin::new();
assert!(plugin
.validate_params(&json!({
"className": "myClass"
}))
.is_ok());
}
#[test]
fn test_parameter_validation_multiple_classes() {
let plugin = AddClassesToSVGElementPlugin::new();
assert!(plugin
.validate_params(&json!({
"classNames": ["class1", "class2"]
}))
.is_ok());
}
#[test]
fn test_add_single_class() {
let config = AddClassesToSVGElementConfig {
class_name: Some("myClass".to_string()),
class_names: None,
};
let plugin = AddClassesToSVGElementPlugin::with_config(config);
let mut doc = Document::new();
doc.root = create_element("svg");
plugin.apply(&mut doc).unwrap();
assert_eq!(doc.root.attr("class"), Some("myClass"));
}
#[test]
fn test_add_multiple_classes() {
let config = AddClassesToSVGElementConfig {
class_name: None,
class_names: Some(vec![
"class1".to_string(),
"class2".to_string(),
"class3".to_string(),
]),
};
let plugin = AddClassesToSVGElementPlugin::with_config(config);
let mut doc = Document::new();
doc.root = create_element("svg");
plugin.apply(&mut doc).unwrap();
let class_attr = doc.root.attr("class").unwrap();
let classes: HashSet<&str> = class_attr.split_whitespace().collect();
assert!(classes.contains("class1"));
assert!(classes.contains("class2"));
assert!(classes.contains("class3"));
}
#[test]
fn test_preserves_existing_classes() {
let config = AddClassesToSVGElementConfig {
class_name: Some("newClass".to_string()),
class_names: None,
};
let plugin = AddClassesToSVGElementPlugin::with_config(config);
let mut doc = Document::new();
doc.root = create_element("svg");
doc.root.set_attr("class", "existingClass1 existingClass2");
plugin.apply(&mut doc).unwrap();
let class_attr = doc.root.attr("class").unwrap();
let classes: HashSet<&str> = class_attr.split_whitespace().collect();
assert!(classes.contains("existingClass1"));
assert!(classes.contains("existingClass2"));
assert!(classes.contains("newClass"));
}
#[test]
fn test_deduplicates_classes() {
let config = AddClassesToSVGElementConfig {
class_name: Some("duplicateClass".to_string()),
class_names: Some(vec![
"duplicateClass".to_string(),
"uniqueClass".to_string(),
]),
};
let plugin = AddClassesToSVGElementPlugin::with_config(config);
let mut doc = Document::new();
doc.root = create_element("svg");
doc.root.set_attr("class", "duplicateClass");
plugin.apply(&mut doc).unwrap();
let class_attr = doc.root.attr("class").unwrap();
let classes: Vec<&str> = class_attr.split_whitespace().collect();
assert_eq!(
classes.iter().filter(|&&c| c == "duplicateClass").count(),
1
);
assert!(classes.contains(&"uniqueClass"));
}
#[test]
fn test_only_applies_to_svg_element() {
let config = AddClassesToSVGElementConfig {
class_name: Some("myClass".to_string()),
class_names: None,
};
let plugin = AddClassesToSVGElementPlugin::with_config(config);
let mut doc = Document::new();
doc.root = create_element("div");
plugin.apply(&mut doc).unwrap();
assert!(!doc.root.attributes.contains_key("class"));
}
#[test]
fn test_both_class_name_and_class_names() {
let config = AddClassesToSVGElementConfig {
class_name: Some("single".to_string()),
class_names: Some(vec!["multiple1".to_string(), "multiple2".to_string()]),
};
let plugin = AddClassesToSVGElementPlugin::with_config(config);
let mut doc = Document::new();
doc.root = create_element("svg");
plugin.apply(&mut doc).unwrap();
let class_attr = doc.root.attr("class").unwrap();
let classes: HashSet<&str> = class_attr.split_whitespace().collect();
assert!(classes.contains("single"));
assert!(classes.contains("multiple1"));
assert!(classes.contains("multiple2"));
}
#[test]
fn test_empty_class_names_are_ignored() {
let config = AddClassesToSVGElementConfig {
class_name: Some("".to_string()), class_names: Some(vec!["valid".to_string(), "".to_string()]), };
let plugin = AddClassesToSVGElementPlugin::with_config(config);
let mut doc = Document::new();
doc.root = create_element("svg");
plugin.apply(&mut doc).unwrap();
let class_attr = doc.root.attr("class").unwrap();
assert_eq!(class_attr, "valid");
}
#[test]
fn test_config_parsing() {
let config = AddClassesToSVGElementPlugin::parse_config(&json!({
"className": "test"
}))
.unwrap();
assert_eq!(config.class_name, Some("test".to_string()));
let config = AddClassesToSVGElementPlugin::parse_config(&json!({
"classNames": ["class1", "class2"]
}))
.unwrap();
assert_eq!(
config.class_names,
Some(vec!["class1".to_string(), "class2".to_string()])
);
}
}
#[cfg(test)]
#[cfg(test)]
vexy_vsvg_test_utils::plugin_fixture_tests_with_params!(
AddClassesToSVGElementPlugin,
"addClassesToSVGElement"
);