#![allow(deprecated)]
use lightningcss::{values::color::CssColor, visit_types, visitor::Visit};
use oxvg_ast::{
element::Element,
is_element,
visitor::{Context, PrepareOutcome, Visitor},
};
use oxvg_collections::{
attribute::{core::Style, Attr},
element::ElementId,
};
#[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)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub enum Method {
#[default]
Lightning,
CurrentColor,
#[doc(hidden)]
#[cfg(feature = "napi")]
Napi(),
}
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "napi", napi(object))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ConvertColors {
#[cfg_attr(feature = "wasm", tsify(optional))]
pub method: Option<Method>,
}
impl<'input, 'arena> Visitor<'input, 'arena> for ConvertColors {
type Error = JobsError<'input>;
fn prepare(
&self,
_document: &Element<'input, 'arena>,
_context: &mut Context<'input, 'arena, '_>,
) -> Result<PrepareOutcome, Self::Error> {
Ok(match self.method {
Some(Method::CurrentColor) => PrepareOutcome::none,
None | Some(Method::Lightning) => {
log::debug!("ConvertColors::prepare: skipping default behaviour");
PrepareOutcome::skip
}
#[cfg(feature = "napi")]
Some(Method::Napi()) => panic!("Napi variant is not allowed!"),
})
}
fn element(
&self,
element: &Element<'input, 'arena>,
_context: &mut Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
let is_masked = is_element!(element, Mask) || element.closest(&ElementId::Mask).is_some();
let mut method = self.method.clone().unwrap_or_default();
if is_masked && matches!(method, Method::CurrentColor) {
method = Method::Lightning;
}
for mut attr in element.attributes().into_iter_mut() {
if let Attr::Style(Style(style)) = &mut *attr {
style.visit(&mut method).ok();
} else {
attr.value_mut().visit_color(|color| {
color.visit(&mut method).ok();
});
}
}
Ok(())
}
}
impl<'input> lightningcss::visitor::Visitor<'input> for Method {
type Error = JobsError<'input>;
fn visit_types(&self) -> lightningcss::visitor::VisitTypes {
visit_types!(COLORS)
}
fn visit_color(&mut self, color: &mut CssColor) -> Result<(), Self::Error> {
match self {
Self::CurrentColor => *color = CssColor::CurrentColor,
Self::Lightning => {}
#[cfg(feature = "napi")]
Self::Napi() => panic!("Napi variant is not allowed!"),
}
Ok(())
}
}
#[test]
fn convert_colors() -> anyhow::Result<()> {
use crate::test_config;
insta::assert_snapshot!(test_config(
r#"{ "convertColors": { } }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Should convert to hex -->
<g color="black"/>
<g color="BLACK"/>
<path fill="rgb(64 64 64)"/>
<path fill="rgb(64, 64, 64)"/>
<path fill="rgb(86.27451%,86.666667%,87.058824%)"/>
<path fill="rgb(-255,100,500)"/>
</svg>"#
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "convertColors": { } }"#,
Some(
r##"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Should convert to short hex -->
<g color="#ff00aa"/>
</svg>"##
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "convertColors": { } }"#,
Some(
r##"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Should convert to named color -->
<g color="#FF0000"/>
<g color="#f00"/>
</svg>"##
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "convertColors": { "method": "currentColor" } }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Should convert to currentColor -->
<g color="black"/>
<g color="BLACK"/>
<g color="none"/>
<path fill="rgb(64, 64, 64)"/>
<path fill="rgb(86.27451%,86.666667%,87.058824%)"/>
<path fill="rgb(-255,100,500)"/>
<path fill="none"/>
</svg>"#
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "convertColors": { } }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Should preserve color-like substrings that aren't colors -->
<linearGradient id="Aa">
<stop stop-color="ReD" offset="5%"/>
</linearGradient>
<text x="0" y="32" fill="gold">uwu</text>
<text x="0" y="64" fill="GOLD">owo</text>
<text x="0" y="96" fill="url(#Aa)">eue</text>
</svg>"#
)
)?);
insta::assert_snapshot!(test_config(
r#"{ "convertColors": { "method": "currentColor" } }"#,
Some(
r##"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Should not apply `currentColor` to masks -->
<path fill="currentcolor"/>
<mask id="mask1" fill="#fff"/>
<mask id="mask2">
<path fill="rgba(255,255,255,0.75)"/>
</mask>
<mask id="mask3">
<g>
<path fill="#fff"/>
<path stroke="#000"/>
</g>
<mask id="inner-mask" fill="rgba(0,0,0,.5)"/>
</mask>
<path fill="currentcolor"/>
</svg>"##
)
)?);
Ok(())
}