use crate::Plugin;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use vexy_vsvg::ast::{Document, Element, Node};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[derive(Default)]
pub struct RemoveNonInheritableGroupAttrsConfig {
}
pub struct RemoveNonInheritableGroupAttrsPlugin {
config: RemoveNonInheritableGroupAttrsConfig,
presentation_attrs: HashSet<&'static str>,
inheritable_attrs: HashSet<&'static str>,
presentation_non_inheritable_group_attrs: HashSet<&'static str>,
}
impl RemoveNonInheritableGroupAttrsPlugin {
pub fn new() -> Self {
let presentation_attrs = [
"alignment-baseline",
"baseline-shift",
"clip-path",
"clip-rule",
"clip",
"color-interpolation-filters",
"color-interpolation",
"color-profile",
"color-rendering",
"color",
"cursor",
"direction",
"display",
"dominant-baseline",
"enable-background",
"fill-opacity",
"fill-rule",
"fill",
"filter",
"flood-color",
"flood-opacity",
"font-family",
"font-size-adjust",
"font-size",
"font-stretch",
"font-style",
"font-variant",
"font-weight",
"glyph-orientation-horizontal",
"glyph-orientation-vertical",
"image-rendering",
"letter-spacing",
"lighting-color",
"marker-end",
"marker-mid",
"marker-start",
"mask",
"opacity",
"overflow",
"paint-order",
"pointer-events",
"shape-rendering",
"stop-color",
"stop-opacity",
"stroke-dasharray",
"stroke-dashoffset",
"stroke-linecap",
"stroke-linejoin",
"stroke-miterlimit",
"stroke-opacity",
"stroke-width",
"stroke",
"text-anchor",
"text-decoration",
"text-rendering",
"transform",
"unicode-bidi",
"vector-effect",
"visibility",
"word-spacing",
"writing-mode",
]
.iter()
.cloned()
.collect();
let inheritable_attrs = [
"clip-rule",
"color-interpolation-filters",
"color-interpolation",
"color-profile",
"color-rendering",
"color",
"cursor",
"direction",
"dominant-baseline",
"fill-opacity",
"fill-rule",
"fill",
"font-family",
"font-size-adjust",
"font-size",
"font-stretch",
"font-style",
"font-variant",
"font-weight",
"font",
"glyph-orientation-horizontal",
"glyph-orientation-vertical",
"image-rendering",
"letter-spacing",
"marker-end",
"marker-mid",
"marker-start",
"marker",
"paint-order",
"pointer-events",
"shape-rendering",
"stroke-dasharray",
"stroke-dashoffset",
"stroke-linecap",
"stroke-linejoin",
"stroke-miterlimit",
"stroke-opacity",
"stroke-width",
"stroke",
"text-anchor",
"text-rendering",
"visibility",
"word-spacing",
"writing-mode",
]
.iter()
.cloned()
.collect();
let presentation_non_inheritable_group_attrs = [
"clip-path",
"display",
"filter",
"mask",
"opacity",
"text-decoration",
"transform",
"unicode-bidi",
]
.iter()
.cloned()
.collect();
Self {
config: RemoveNonInheritableGroupAttrsConfig::default(),
presentation_attrs,
inheritable_attrs,
presentation_non_inheritable_group_attrs,
}
}
pub fn with_config(config: RemoveNonInheritableGroupAttrsConfig) -> Self {
let mut plugin = Self::new();
plugin.config = config;
plugin
}
fn parse_config(params: &Value) -> Result<RemoveNonInheritableGroupAttrsConfig> {
if params.is_null() || (params.is_object() && params.as_object().unwrap().is_empty()) {
Ok(RemoveNonInheritableGroupAttrsConfig::default())
} else if params.is_object() {
serde_json::from_value(params.clone())
.map_err(|e| anyhow::anyhow!("Invalid configuration: {}", e))
} else {
Ok(RemoveNonInheritableGroupAttrsConfig::default())
}
}
fn remove_non_inheritable_group_attrs_recursive(&self, element: &mut Element) {
if element.name == "g" {
let mut attrs_to_remove = Vec::new();
for attr_name in element.attributes.keys() {
if self.presentation_attrs.contains(attr_name.as_ref())
&& !self.inheritable_attrs.contains(attr_name.as_ref())
&& !self
.presentation_non_inheritable_group_attrs
.contains(attr_name.as_ref())
{
attrs_to_remove.push(attr_name.clone());
}
}
for attr_name in attrs_to_remove {
element.attributes.shift_remove(&attr_name);
}
}
for child in &mut element.children {
if let Node::Element(elem) = child {
self.remove_non_inheritable_group_attrs_recursive(elem);
}
}
}
}
impl Default for RemoveNonInheritableGroupAttrsPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for RemoveNonInheritableGroupAttrsPlugin {
fn name(&self) -> &'static str {
"removeNonInheritableGroupAttrs"
}
fn description(&self) -> &'static str {
"removes non-inheritable group's presentational attributes"
}
fn validate_params(&self, params: &Value) -> Result<()> {
Self::parse_config(params)?;
Ok(())
}
fn apply(&self, document: &mut Document) -> Result<()> {
self.remove_non_inheritable_group_attrs_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
}
#[test]
fn test_plugin_creation() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
assert_eq!(plugin.name(), "removeNonInheritableGroupAttrs");
assert_eq!(
plugin.description(),
"removes non-inheritable group's presentational attributes"
);
}
#[test]
fn test_parameter_validation() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
assert!(plugin.validate_params(&json!({})).is_ok());
assert!(plugin.validate_params(&json!({"param": "value"})).is_err());
}
#[test]
fn test_removes_non_inheritable_presentation_attrs() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let mut group = create_element("g");
group
.attributes
.insert("alignment-baseline".into(), "central".into());
group
.attributes
.insert("baseline-shift".into(), "10px".into());
group
.attributes
.insert("lighting-color".into(), "red".into());
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
if let Node::Element(elem) = &doc.root.children[0] {
assert!(!elem.attributes.contains_key("alignment-baseline"));
assert!(!elem.attributes.contains_key("baseline-shift"));
assert!(!elem.attributes.contains_key("lighting-color"));
}
}
#[test]
fn test_preserves_inheritable_attrs() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let mut group = create_element("g");
group.attributes.insert("fill".into(), "red".into());
group.attributes.insert("stroke".into(), "blue".into());
group
.attributes
.insert("font-family".into(), "Arial".into());
group.attributes.insert("color".into(), "green".into());
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.attributes.get("fill"), Some(&"red".into()));
assert_eq!(elem.attributes.get("stroke"), Some(&"blue".into()));
assert_eq!(elem.attributes.get("font-family"), Some(&"Arial".into()));
assert_eq!(elem.attributes.get("color"), Some(&"green".into()));
}
}
#[test]
fn test_preserves_allowed_group_attrs() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let mut group = create_element("g");
group.attributes.insert("opacity".into(), "0.5".into());
group
.attributes
.insert("transform".into(), "translate(10,20)".into());
group
.attributes
.insert("clip-path".into(), "url(#clip)".into());
group
.attributes
.insert("filter".into(), "url(#filter)".into());
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.attributes.get("opacity"), Some(&"0.5".into()));
assert_eq!(
elem.attributes.get("transform"),
Some(&"translate(10,20)".into())
);
assert_eq!(elem.attributes.get("clip-path"), Some(&"url(#clip)".into()));
assert_eq!(elem.attributes.get("filter"), Some(&"url(#filter)".into()));
}
}
#[test]
fn test_preserves_non_presentation_attrs() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let mut group = create_element("g");
group.attributes.insert("id".into(), "mygroup".into());
group.attributes.insert("class".into(), "groupclass".into());
group
.attributes
.insert("data-custom".into(), "value".into());
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.attributes.get("id"), Some(&"mygroup".into()));
assert_eq!(elem.attributes.get("class"), Some(&"groupclass".into()));
assert_eq!(elem.attributes.get("data-custom"), Some(&"value".into()));
}
}
#[test]
fn test_ignores_non_group_elements() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let mut rect = create_element("rect");
rect.attributes
.insert("alignment-baseline".into(), "central".into());
rect.attributes
.insert("baseline-shift".into(), "10px".into());
doc.root.children.push(Node::Element(rect));
plugin.apply(&mut doc).unwrap();
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(
elem.attributes.get("alignment-baseline"),
Some(&"central".into())
);
assert_eq!(elem.attributes.get("baseline-shift"), Some(&"10px".into()));
}
}
#[test]
fn test_mixed_attributes() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let mut group = create_element("g");
group
.attributes
.insert("alignment-baseline".into(), "central".into());
group.attributes.insert("stop-color".into(), "red".into());
group.attributes.insert("fill".into(), "blue".into());
group.attributes.insert("stroke".into(), "green".into());
group.attributes.insert("opacity".into(), "0.8".into());
group.attributes.insert("id".into(), "test".into());
doc.root.children.push(Node::Element(group));
plugin.apply(&mut doc).unwrap();
if let Node::Element(elem) = &doc.root.children[0] {
assert!(!elem.attributes.contains_key("alignment-baseline"));
assert!(!elem.attributes.contains_key("stop-color"));
assert_eq!(elem.attributes.get("fill"), Some(&"blue".into()));
assert_eq!(elem.attributes.get("stroke"), Some(&"green".into()));
assert_eq!(elem.attributes.get("opacity"), Some(&"0.8".into()));
assert_eq!(elem.attributes.get("id"), Some(&"test".into()));
}
}
#[test]
fn test_nested_groups() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let mut outer_group = create_element("g");
outer_group
.attributes
.insert("alignment-baseline".into(), "central".into());
outer_group.attributes.insert("fill".into(), "red".into());
let mut inner_group = create_element("g");
inner_group
.attributes
.insert("baseline-shift".into(), "10px".into());
inner_group
.attributes
.insert("stroke".into(), "blue".into());
outer_group.children.push(Node::Element(inner_group));
doc.root.children.push(Node::Element(outer_group));
plugin.apply(&mut doc).unwrap();
if let Node::Element(outer_elem) = &doc.root.children[0] {
assert!(!outer_elem.attributes.contains_key("alignment-baseline"));
assert_eq!(outer_elem.attributes.get("fill"), Some(&"red".into()));
if let Node::Element(inner_elem) = &outer_elem.children[0] {
assert!(!inner_elem.attributes.contains_key("baseline-shift"));
assert_eq!(inner_elem.attributes.get("stroke"), Some(&"blue".into()));
}
}
}
#[test]
fn test_empty_group() {
let plugin = RemoveNonInheritableGroupAttrsPlugin::new();
let mut doc = Document::new();
let group = create_element("g");
doc.root.children.push(Node::Element(group));
let result = plugin.apply(&mut doc);
assert!(result.is_ok());
assert_eq!(doc.root.children.len(), 1);
if let Node::Element(elem) = &doc.root.children[0] {
assert_eq!(elem.name, "g");
}
}
#[test]
fn test_config_parsing() {
let config = RemoveNonInheritableGroupAttrsPlugin::parse_config(&json!({})).unwrap();
let _ = config;
}
}
#[cfg(test)]
#[cfg(test)]
vexy_vsvg_test_utils::plugin_fixture_tests!(
RemoveNonInheritableGroupAttrsPlugin,
"removeNonInheritableGroupAttrs"
);