use std::mem;
use oxvg_ast::{
element::Element,
get_attribute_mut, has_attribute, is_attribute, is_element, remove_attribute, set_attribute,
visitor::{Context, PrepareOutcome, Visitor},
};
use oxvg_collections::attribute::{
inheritable::{self, Inheritable},
AttrId,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use tsify::Tsify;
use crate::error::JobsError;
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "napi", napi(object))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct MoveGroupAttrsToElems(pub bool);
impl<'input, 'arena> Visitor<'input, 'arena> for MoveGroupAttrsToElems {
type Error = JobsError<'input>;
fn prepare(
&self,
_document: &Element<'input, 'arena>,
_context: &mut Context<'input, 'arena, '_>,
) -> Result<PrepareOutcome, Self::Error> {
Ok(if self.0 {
PrepareOutcome::none
} else {
PrepareOutcome::skip
})
}
fn element(
&self,
element: &Element<'input, 'arena>,
_context: &mut Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
if !is_element!(element, G) {
return Ok(());
}
if element.is_empty() {
return Ok(());
}
if !has_attribute!(element, Transform) {
return Ok(());
}
if element.attributes().into_iter_mut().any(|mut a| {
if is_attribute!(a, Id) {
return false;
}
let mut value = a.value_mut();
let mut references_props = false;
let mut references_url = false;
value.visit_id(|_| references_props = true);
value.visit_url(|url| references_url = references_url || url.starts_with('#'));
references_props || references_url
}) {
return Ok(());
}
if element.children_iter().any(|e| {
let name = e.qual_name();
!(is_element!(name, G | Text) || name.expected_attributes().contains(&AttrId::D))
|| has_attribute!(e, Id)
}) {
return Ok(());
}
let Some(transform) = remove_attribute!(element, Transform) else {
return Ok(());
};
let Some(transform) = transform.option_ref() else {
set_attribute!(element, Transform(transform));
return Ok(());
};
element.children_iter().for_each(|e| {
match get_attribute_mut!(e, Transform).and_then(inheritable::map_ref_mut) {
Some(mut child_attr) => {
let value = mem::replace(&mut *child_attr, transform.clone());
child_attr.0.extend(value.0);
}
None => set_attribute!(e, Transform(Inheritable::Defined(transform.clone()))),
}
});
Ok(())
}
}
impl Default for MoveGroupAttrsToElems {
fn default() -> Self {
Self(true)
}
}
#[test]
fn move_group_attrs_to_elems() -> anyhow::Result<()> {
use crate::test_config;
insta::assert_snapshot!(test_config(
r#"{ "moveGroupAttrsToElems": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- append transform to children of `g` -->
<g transform="scale(2)">
<path transform="rotate(45)" d="M0,0 L10,20"/>
<path transform="translate(10, 20)" d="M0,10 L20,30"/>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "moveGroupAttrsToElems": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- add transform to children of `g` -->
<g transform="scale(2)">
<path d="M0,0 L10,20"/>
<path d="M0,10 L20,30"/>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "moveGroupAttrsToElems": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- move transform through multiple `g`s -->
<g transform="rotate(30)">
<g transform="scale(2)">
<path d="M0,0 L10,20"/>
<path d="M0,10 L20,30"/>
</g>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "moveGroupAttrsToElems": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- move transform through multiple `g`s -->
<g transform="rotate(30)">
<g>
<g transform="scale(2)">
<path d="M0,0 L10,20"/>
<path d="M0,10 L20,30"/>
</g>
</g>
<path d="M0,10 L20,30"/>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "moveGroupAttrsToElems": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- don't move from group with reference -->
<g transform="scale(2)" clip-path="url(#a)">
<path d="M0,0 L10,20"/>
<path d="M0,10 L20,30"/>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "moveGroupAttrsToElems": true }"#,
Some(
r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- don't move for child with id -->
<g transform="translate(0 -140)">
<path id="c" transform="scale(.5)" d="M0,0 L10,20"/>
</g>
<use xlink:href="#c" transform="translate(-140)"/>
</svg>"##
),
)?);
Ok(())
}