use lightningcss::{
declaration::DeclarationBlock,
properties::{
custom::{CustomProperty, Token, TokenOrValue},
Property,
},
values::percentage::DimensionPercentage,
visit_types,
visitor::{Visit, VisitTypes},
};
use oxvg_ast::{
element::Element,
get_attribute, get_attribute_mut, is_element, remove_attribute, set_attribute,
visitor::{Context, PrepareOutcome, Visitor},
};
use oxvg_collections::{
atom::Atom,
attribute::{
inheritable::Inheritable,
presentation::{EnableBackground, LengthPercentage},
},
content_type::ContentType,
element::ElementId,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::error::JobsError;
#[cfg(feature = "wasm")]
use tsify::Tsify;
#[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 CleanupEnableBackground(pub bool);
struct State {
contains_filter: bool,
}
impl<'input, 'arena> Visitor<'input, 'arena> for CleanupEnableBackground {
type Error = JobsError<'input>;
fn prepare(
&self,
document: &Element<'input, 'arena>,
context: &mut Context<'input, 'arena, '_>,
) -> Result<PrepareOutcome, Self::Error> {
if !self.0 {
return Ok(PrepareOutcome::skip);
}
if let Some(root) = document.find_element() {
State::new(&root).start_with_context(document, context)?;
}
Ok(PrepareOutcome::skip)
}
}
struct EnableBackgroundVisitor;
impl<'input> lightningcss::visitor::Visitor<'input> for EnableBackgroundVisitor {
type Error = JobsError<'input>;
fn visit_types(&self) -> VisitTypes {
visit_types!(PROPERTIES)
}
fn visit_declaration_block(
&mut self,
decls: &mut DeclarationBlock<'input>,
) -> Result<(), Self::Error> {
let first_token = TokenOrValue::Token(Token::Ident("new".into()));
let remove_enable_background_new = |property: &Property| match property {
Property::Custom(CustomProperty { name, value }) => {
name.as_ref() != "enable-background" || value.0.first() != Some(&first_token)
}
_ => true,
};
decls
.important_declarations
.retain(remove_enable_background_new);
decls.declarations.retain(remove_enable_background_new);
Ok(())
}
}
impl<'input, 'arena> Visitor<'input, 'arena> for State {
type Error = JobsError<'input>;
fn element(
&self,
element: &Element<'input, 'arena>,
_context: &mut Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
let style = get_attribute_mut!(element, Style);
if let Some(mut style) = style {
style.0.visit(&mut EnableBackgroundVisitor)?;
if style.is_empty() {
drop(style);
remove_attribute!(element, Style);
} else {
drop(style);
}
}
if !self.contains_filter {
remove_attribute!(element, EnableBackground);
return Ok(());
}
{
let enable_background = get_attribute!(element, EnableBackground);
let Some(Inheritable::Defined(enable_background)) = enable_background.as_deref() else {
return Ok(());
};
let EnableBackground::New(Some((_x, _y, eb_width, eb_height))) = enable_background
else {
return Ok(());
};
let width = element.get_attribute_local(&Atom::Static("width"));
let Some(width) = width.as_deref() else {
return Ok(());
};
let ContentType::LengthPercentage(width) = width.value() else {
return Ok(());
};
let LengthPercentage(DimensionPercentage::Dimension(width)) = &*width else {
return Ok(());
};
let height = element.get_attribute_local(&Atom::Static("height"));
let Some(height) = height.as_deref() else {
return Ok(());
};
let ContentType::LengthPercentage(height) = height.value() else {
return Ok(());
};
let LengthPercentage(DimensionPercentage::Dimension(height)) = &*height else {
return Ok(());
};
if width.to_px().is_none_or(|px| px != *eb_width)
|| height.to_px().is_none_or(|px| px != *eb_height)
{
return Ok(());
}
}
match element.qual_name().unaliased() {
ElementId::Svg => {
remove_attribute!(element, EnableBackground);
}
ElementId::Mask | ElementId::Pattern => {
set_attribute!(
element,
EnableBackground(Inheritable::Defined(EnableBackground::New(None)))
);
}
_ => {}
}
Ok(())
}
}
impl State {
fn new(root: &Element) -> Self {
Self {
contains_filter: root
.breadth_first()
.any(|element| is_element!(element, Filter)),
}
}
}
impl Default for CleanupEnableBackground {
fn default() -> Self {
Self(true)
}
}
#[test]
fn cleanup_enable_background() -> anyhow::Result<()> {
use crate::test_config;
insta::assert_snapshot!(test_config(
r#"{ "cleanupEnableBackground": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="100.5" height=".5" enable-background="new 0 0 100.5 .5">
<!-- Remove svg's enable-background on matching size -->
<defs>
<filter id="ShiftBGAndBlur">
<feOffset dx="0" dy="75"/>
</filter>
</defs>
test
</svg>"#
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "cleanupEnableBackground": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" enable-background="new 0 0 100 50">
<!-- Keep svg's enable-background on mismatching size -->
<defs>
<filter id="ShiftBGAndBlur">
<feOffset dx="0" dy="75"/>
</filter>
</defs>
test
</svg>"#
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "cleanupEnableBackground": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Replace matching mask or pattern's enable-background with "new" -->
<defs>
<filter id="ShiftBGAndBlur">
<feOffset dx="0" dy="75"/>
</filter>
</defs>
<mask width="100" height="50" enable-background="new 0 0 100 50">
test
</mask>
</svg>"#
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "cleanupEnableBackground": true }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Remove enable-background when no filter is present -->
<mask width="100" height="50" enable-background="new 0 0 100 50">
test
</mask>
</svg>"#
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "cleanupEnableBackground": true }"#,
Some(
r##"<svg height="100" width="100" style="enable-background:new 0 0 100 100">
<circle cx="50" cy="50" r="40" stroke="#000" stroke-width="3" fill="red"/>
</svg>"##
)
)?);
Ok(())
}