use oxvg_ast::{
element::Element,
has_attribute, has_computed_style, is_element,
style::ComputedStyles,
visitor::{Context, PrepareOutcome, Visitor},
};
use oxvg_collections::element::ElementCategory;
#[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 RemoveEmptyContainers(pub bool);
impl<'input, 'arena> Visitor<'input, 'arena> for RemoveEmptyContainers {
type Error = JobsError<'input>;
fn prepare(
&self,
document: &Element<'input, 'arena>,
context: &mut Context<'input, 'arena, '_>,
) -> Result<PrepareOutcome, Self::Error> {
Ok(if self.0 {
context.query_has_stylesheet(document);
context.query_has_script(document);
PrepareOutcome::none
} else {
PrepareOutcome::skip
})
}
fn exit_element(
&self,
element: &Element<'input, 'arena>,
context: &mut Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
let name = element.qual_name();
if !name.categories().contains(ElementCategory::Container) || !element.is_empty() {
return Ok(());
}
if is_element!(element, Svg) {
return Ok(());
} else if is_element!(element, Pattern) {
if !element.attributes().is_empty() {
return Ok(());
}
} else if is_element!(element, Mask) {
if has_attribute!(element, Id) {
return Ok(());
}
} else if element
.parent_element()
.is_some_and(|e| is_element!(e, Switch))
{
return Ok(());
}
if is_element!(element, G) {
let computed_styles = ComputedStyles::default()
.with_all(element, &context.query_has_stylesheet_result)
.map_err(JobsError::ComputedStylesError)?;
if has_computed_style!(computed_styles, Filter) {
return Ok(());
}
}
element.remove();
Ok(())
}
}
impl Default for RemoveEmptyContainers {
fn default() -> Self {
Self(true)
}
}
#[test]
#[allow(clippy::too_many_lines)]
fn remove_empty_containers() -> anyhow::Result<()> {
use crate::test_config;
insta::assert_snapshot!(test_config(
r#"{ "removeEmptyContainers": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- remove empty containers -->
<pattern/>
<g>
<marker>
<a/>
</marker>
</g>
<path d="..."/>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeEmptyContainers": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- preserve non-empty containers -->
<defs>
<pattern id="a">
<rect/>
</pattern>
<pattern xlink:href="url(#a)" id="b"/>
</defs>
<g>
<marker>
<a/>
</marker>
<path d="..."/>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeEmptyContainers": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink">
<!-- preserve non-empty containers -->
<defs>
<pattern id="a">
<rect/>
</pattern>
<pattern x:href="url(#a)" id="b"/>
</defs>
<g>
<marker>
<a/>
</marker>
<path d="..."/>
</g>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeEmptyContainers": true }"#,
Some(
r#"<svg>
<!-- preserve non-empty containers -->
<defs>
<filter id="feTileFilter" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" x="115" y="40" width="250" height="250">
<feFlood x="115" y="40" width="54" height="19" flood-color="lime"/>
<feOffset x="115" y="40" width="50" height="25" dx="6" dy="6" result="offset"/>
<feTile/>
</filter>
</defs>
<g filter="url(#feTileFilter)"/>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeEmptyContainers": true }"#,
Some(
r#"<svg width="480" height="360" xmlns="http://www.w3.org/2000/svg">
<!-- preserve id'd mask -->
<mask id="testMask" />
<rect x="100" y="100" width="250" height="150" fill="green" />
<rect x="100" y="100" width="250" height="150" fill="red" mask="url(#testMask)" />
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeEmptyContainers": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 462 352">
<!-- preserve children of `switch` -->
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text>
</a>
</switch>
</svg>"#
),
)?);
insta::assert_snapshot!(test_config(
r#"{ "removeEmptyContainers": true }"#,
Some(
r##"<svg viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
<!-- preserve filtered `g`s -->
<filter id="a" x="0" y="0" width="50" height="50" filterUnits="userSpaceOnUse">
<feFlood flood-color="#aaa"/>
</filter>
<mask id="b" x="0" y="0" width="50" height="50">
<g style="filter: url(#a)"/>
</mask>
<text x="16" y="16" style="mask: url(#b)">•ᴗ•</text>
</svg>"##
),
)?);
Ok(())
}