oxvg_optimiser 0.0.5

The OXVG optimiser is library for optimising SVG documents.
Documentation
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))]
/// Moves some of a group's attributes to the contained elements.
///
/// # Correctness
///
/// This job should never visually change the document.
///
/// # Errors
///
/// Never.
///
/// If this job produces an error or panic, please raise an [issue](https://github.com/noahbald/oxvg/issues)
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(())
}