use super::{Pass, PassResult};
use crate::ast::{Document, NodeId, NodeKind};
pub struct MoveGroupAttrsToElems;
const MOVEABLE_ELEMENTS: &[&str] = &[
"circle", "ellipse", "line", "path", "polygon", "polyline", "rect", "g", "text", "use",
"image", "svg",
];
const URL_REF_ATTRS: &[&str] = &[
"clip-path",
"fill",
"filter",
"marker-end",
"marker-mid",
"marker-start",
"mask",
"stroke",
];
impl Pass for MoveGroupAttrsToElems {
fn name(&self) -> &'static str {
"moveGroupAttrsToElems"
}
fn run(&self, doc: &mut Document) -> PassResult {
let mut changed = false;
let ids = doc.traverse();
for &id in &ids {
if doc.node(id).removed {
continue;
}
let node = doc.node(id);
let group_elem = match &node.kind {
NodeKind::Element(e) if e.name == "g" && e.prefix.is_none() => e,
_ => continue,
};
let transform_value = match group_elem.attr("transform") {
Some(v) => v.to_string(),
None => continue,
};
let has_other_presentation = group_elem.attributes.iter().any(|a| {
a.prefix.is_none()
&& a.name != "transform"
&& a.name != "id"
&& !a.name.starts_with("xmlns")
});
if has_other_presentation {
continue;
}
if group_elem.attr("id").is_some() {
continue;
}
let children: Vec<NodeId> = doc.children(id).collect();
let elem_children: Vec<NodeId> = children
.iter()
.copied()
.filter(|&c| matches!(&doc.node(c).kind, NodeKind::Element(_)))
.collect();
if elem_children.len() < 2 {
continue;
}
let all_moveable = elem_children.iter().all(|&c| {
matches!(&doc.node(c).kind, NodeKind::Element(e) if MOVEABLE_ELEMENTS.contains(&e.name.as_str()))
});
if !all_moveable {
continue;
}
let has_url_refs = elem_children.iter().any(|&c| {
if let NodeKind::Element(ref e) = doc.node(c).kind {
e.attributes.iter().any(|a| {
a.prefix.is_none()
&& URL_REF_ATTRS.contains(&a.name.as_str())
&& a.value.contains("url(")
})
} else {
false
}
});
if has_url_refs {
continue;
}
let has_id = elem_children.iter().any(
|&c| matches!(&doc.node(c).kind, NodeKind::Element(e) if e.attr("id").is_some()),
);
if has_id {
continue;
}
for &child_id in &elem_children {
let child = doc.node_mut(child_id);
if let NodeKind::Element(ref mut child_elem) = child.kind {
if let Some(existing) = child_elem
.attributes
.iter_mut()
.find(|a| a.prefix.is_none() && a.name == "transform")
{
existing.value = format!("{} {}", transform_value, existing.value);
} else {
child_elem.attributes.push(crate::ast::Attribute {
prefix: None,
name: "transform".to_string(),
value: transform_value.clone(),
});
}
}
}
let node = doc.node_mut(id);
if let NodeKind::Element(ref mut elem) = node.kind {
elem.attributes
.retain(|a| !(a.prefix.is_none() && a.name == "transform"));
}
changed = true;
}
if changed {
PassResult::Changed
} else {
PassResult::Unchanged
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
use crate::serializer::serialize;
#[test]
fn moves_transform_to_children() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"><g transform="translate(10,20)"><path d="M0 0"/><rect width="5" height="5"/></g></svg>"#;
let mut doc = parse(input).unwrap();
assert_eq!(MoveGroupAttrsToElems.run(&mut doc), PassResult::Changed);
let output = serialize(&doc);
assert!(!output.contains("g transform="));
assert!(output.contains("path d=\"M0 0\" transform=\"translate(10,20)\""));
}
#[test]
fn prepends_to_existing_child_transform() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"><g transform="translate(10,20)"><path d="M0 0" transform="scale(2)"/><rect width="5" height="5"/></g></svg>"#;
let mut doc = parse(input).unwrap();
assert_eq!(MoveGroupAttrsToElems.run(&mut doc), PassResult::Changed);
let output = serialize(&doc);
assert!(output.contains("transform=\"translate(10,20) scale(2)\""));
}
#[test]
fn skips_when_child_has_url_ref() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"><g transform="translate(10,20)"><path d="M0 0" fill="url(#grad)"/><rect width="5" height="5"/></g></svg>"#;
let mut doc = parse(input).unwrap();
assert_eq!(MoveGroupAttrsToElems.run(&mut doc), PassResult::Unchanged);
}
#[test]
fn skips_when_child_has_id() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"><g transform="translate(10,20)"><path id="p" d="M0 0"/><rect width="5" height="5"/></g></svg>"#;
let mut doc = parse(input).unwrap();
assert_eq!(MoveGroupAttrsToElems.run(&mut doc), PassResult::Unchanged);
}
#[test]
fn skips_group_with_other_attrs() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"><g transform="translate(10,20)" fill="red"><path d="M0 0"/><rect width="5" height="5"/></g></svg>"#;
let mut doc = parse(input).unwrap();
assert_eq!(MoveGroupAttrsToElems.run(&mut doc), PassResult::Unchanged);
}
#[test]
fn skips_group_with_id() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"><g id="g1" transform="translate(10,20)"><path d="M0 0"/><rect width="5" height="5"/></g></svg>"#;
let mut doc = parse(input).unwrap();
assert_eq!(MoveGroupAttrsToElems.run(&mut doc), PassResult::Unchanged);
}
#[test]
fn skips_single_child() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"><g transform="translate(10,20)"><path d="M0 0"/></g></svg>"#;
let mut doc = parse(input).unwrap();
assert_eq!(MoveGroupAttrsToElems.run(&mut doc), PassResult::Unchanged);
}
}