use std::collections::{HashMap, HashSet};
use anyhow::{anyhow, Result};
use lightningcss::declaration::DeclarationBlock;
use lightningcss::printer::PrinterOptions;
use lightningcss::rules::CssRule;
use lightningcss::stylesheet::{MinifyOptions, ParserOptions, StyleSheet};
use lightningcss::targets::Targets;
use lightningcss::traits::ToCss;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use vexy_vsvg::ast::{Document, Element, Node};
use vexy_vsvg::css::variables::CssScope;
use vexy_vsvg::error::VexyError;
use vexy_vsvg::visitor::Visitor;
use crate::Plugin;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InlineStylesConfig {
#[serde(default = "default_only_matched_once")]
pub only_matched_once: bool,
#[serde(default = "default_remove_matched_selectors")]
pub remove_matched_selectors: bool,
#[serde(default = "default_use_mqs")]
pub use_mqs: bool,
#[serde(default = "default_use_pseudos")]
pub use_pseudos: bool,
}
impl Default for InlineStylesConfig {
fn default() -> Self {
Self {
only_matched_once: default_only_matched_once(),
remove_matched_selectors: default_remove_matched_selectors(),
use_mqs: default_use_mqs(),
use_pseudos: default_use_pseudos(),
}
}
}
fn default_only_matched_once() -> bool {
true
}
fn default_remove_matched_selectors() -> bool {
true
}
fn default_use_mqs() -> bool {
true
}
fn default_use_pseudos() -> bool {
true
}
pub struct InlineStylesPlugin {
config: InlineStylesConfig,
}
impl InlineStylesPlugin {
pub fn new() -> Self {
Self {
config: InlineStylesConfig::default(),
}
}
pub fn with_config(config: InlineStylesConfig) -> Self {
Self { config }
}
#[allow(dead_code)]
fn parse_config(params: &Value) -> Result<InlineStylesConfig> {
if let Some(_obj) = params.as_object() {
serde_json::from_value(params.clone())
.map_err(|e| anyhow!("Invalid configuration: {}", e))
} else {
Ok(InlineStylesConfig::default())
}
}
}
impl Default for InlineStylesPlugin {
fn default() -> Self {
Self::new()
}
}
impl Plugin for InlineStylesPlugin {
fn name(&self) -> &'static str {
"inlineStyles"
}
fn description(&self) -> &'static str {
"Move and merge styles from style elements to inline style attributes"
}
fn validate_params(&self, params: &Value) -> Result<()> {
if let Some(obj) = params.as_object() {
for (key, value) in obj {
match key.as_str() {
"onlyMatchedOnce" | "removeMatchedSelectors" | "useMqs" | "usePseudos" => {
if !value.is_boolean() {
return Err(anyhow!("{} must be a boolean", key));
}
}
_ => return Err(anyhow!("Unknown parameter: {}", key)),
}
}
}
Ok(())
}
fn configure(&mut self, params: &Value) -> Result<()> {
self.config = Self::parse_config(params)?;
Ok(())
}
fn apply(&self, document: &mut Document) -> Result<()> {
let mut collector = InlineStylesVisitor::new(self.config.clone());
vexy_vsvg::visitor::walk_document(&mut collector, document)?;
collector.css_rules.sort_by_key(|rule| rule.specificity);
if collector.config.only_matched_once {
}
collector.scope_stack = vec![CssScope::new()];
let mut applier = StyleApplierVisitor {
visitor: &mut collector,
};
vexy_vsvg::visitor::walk_document(&mut applier, document)?;
if collector.config.remove_matched_selectors {
let mut cleaner = StyleCleanerVisitor {
used_selectors: &collector.used_selectors,
};
vexy_vsvg::visitor::walk_document(&mut cleaner, document)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct CssRuleData {
selector: String,
declarations: Vec<(String, String)>,
specificity: u32,
source_index: usize, }
struct InlineStylesVisitor {
config: InlineStylesConfig,
style_elements: Vec<(usize, String)>, element_counter: usize,
css_rules: Vec<CssRuleData>,
#[allow(dead_code)]
match_counts: HashMap<String, usize>,
used_selectors: HashSet<String>,
scope_stack: Vec<CssScope>, }
impl InlineStylesVisitor {
fn new(config: InlineStylesConfig) -> Self {
Self {
config,
style_elements: Vec::new(),
element_counter: 0,
css_rules: Vec::new(),
match_counts: HashMap::new(),
used_selectors: HashSet::new(),
scope_stack: vec![CssScope::new()],
}
}
fn extract_css_content(element: &Element) -> String {
let mut content = String::new();
for child in &element.children {
match child {
Node::Text(text) => content.push_str(text),
Node::CData(cdata) => content.push_str(cdata),
_ => {}
}
}
content
}
fn selector_contains_pseudo(selector: &str) -> bool {
selector.contains(':')
&& (selector.contains(":hover")
|| selector.contains(":active")
|| selector.contains(":focus")
|| selector.contains(":visited")
|| selector.contains(":link")
|| selector.contains(":first-child")
|| selector.contains(":last-child")
|| selector.contains(":nth-child")
|| selector.contains(":nth-of-type")
|| selector.contains(":before")
|| selector.contains("::before")
|| selector.contains(":after")
|| selector.contains("::after"))
}
fn parse_css_rules(&mut self, css_content: &str, source_index: usize) -> Result<()> {
let options = ParserOptions::default();
match StyleSheet::parse(css_content, options) {
Ok(stylesheet) => {
for rule in &stylesheet.rules.0 {
self.process_css_rule(rule, source_index)?;
}
}
Err(_) => {
}
}
Ok(())
}
fn minify_css_text(css_content: &str) -> String {
match StyleSheet::parse(css_content, ParserOptions::default()) {
Ok(mut stylesheet) => {
let _ = stylesheet.minify(MinifyOptions {
targets: Targets::default(),
..Default::default()
});
match stylesheet.to_css(PrinterOptions {
minify: true,
..PrinterOptions::default()
}) {
Ok(result) => result.code,
Err(_) => css_content.to_string(),
}
}
Err(_) => css_content.to_string(),
}
}
fn process_css_rule(&mut self, rule: &CssRule, source_index: usize) -> Result<()> {
match rule {
CssRule::Style(style_rule) => {
let mut selector_string = String::new();
let mut printer = lightningcss::printer::Printer::new(
&mut selector_string,
PrinterOptions::default(),
);
style_rule.selectors.to_css(&mut printer).ok();
if !self.config.use_pseudos && Self::selector_contains_pseudo(&selector_string) {
return Ok(());
}
let declarations = self.extract_declarations(&style_rule.declarations);
if !declarations.is_empty() {
let specificity = self.calculate_specificity(&selector_string);
self.css_rules.push(CssRuleData {
selector: selector_string,
declarations,
specificity,
source_index,
});
}
}
CssRule::Media(media_rule) if self.config.use_mqs => {
for inner_rule in &media_rule.rules.0 {
self.process_css_rule(inner_rule, source_index)?;
}
}
_ => {
}
}
Ok(())
}
fn extract_declarations(&self, declarations: &DeclarationBlock) -> Vec<(String, String)> {
let mut result = Vec::new();
let mut css_string = String::new();
let mut printer =
lightningcss::printer::Printer::new(&mut css_string, PrinterOptions::default());
if declarations.to_css(&mut printer).is_ok() {
for decl in css_string.split(';') {
let parts: Vec<&str> = decl.split(':').collect();
if parts.len() == 2 {
let property = parts[0].trim();
let value = parts[1].trim();
if is_presentation_attribute(property) {
result.push((property.to_string(), value.to_string()));
}
}
}
}
result
}
fn calculate_specificity(&self, selector: &str) -> u32 {
let mut specificity = 0;
specificity += (selector.matches('#').count() as u32) * 1000000;
specificity += (selector.matches('.').count() as u32) * 1000;
specificity += (selector.matches('[').count() as u32) * 1000;
let element_count = selector
.split_whitespace()
.filter(|s| !s.starts_with('.') && !s.starts_with('#') && !s.starts_with('['))
.count() as u32;
specificity += element_count;
specificity
}
fn apply_styles_to_element(&mut self, element: &mut Element) {
let mut styles_to_apply: HashMap<String, String> = HashMap::new();
let current_scope = self.scope_stack.last().cloned().unwrap_or_default();
for rule in &self.css_rules {
if self.selector_matches_element(&rule.selector, element) {
self.used_selectors.insert(rule.selector.clone());
for (property, value) in &rule.declarations {
let resolved_value = current_scope.resolve(value);
styles_to_apply.insert(property.clone(), resolved_value);
}
}
}
if !styles_to_apply.is_empty() {
let existing_style = element.attributes.get("style").cloned().unwrap_or_default();
let new_style = merge_styles(&existing_style, &styles_to_apply);
element.attributes.insert("style".into(), new_style.into());
}
}
fn selector_matches_element(&self, selector: &str, element: &Element) -> bool {
let selector = selector.trim();
if let Some(id) = selector.strip_prefix('#') {
return element.attributes.get("id") == Some(&id.into());
}
if let Some(class_name) = selector.strip_prefix('.') {
if let Some(classes) = element.attributes.get("class") {
return classes.split_whitespace().any(|c| c == class_name);
}
return false;
}
element.name == selector
}
}
impl Visitor<'_> for InlineStylesVisitor {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<(), VexyError> {
self.element_counter += 1;
let parent_scope = self.scope_stack.last().cloned().unwrap_or_default();
let child_scope = parent_scope.child_scope(element);
self.scope_stack.push(child_scope);
if element.name == "style" {
let css_content = Self::extract_css_content(element);
if !css_content.trim().is_empty() {
let minified_css = Self::minify_css_text(&css_content);
element.children.clear();
element
.children
.push(Node::Text(minified_css.clone().into()));
let source_index = self.style_elements.len();
self.style_elements
.push((self.element_counter, minified_css.clone()));
self.parse_css_rules(&minified_css, source_index)?;
}
}
Ok(())
}
fn visit_element_exit(&mut self, _element: &mut Element<'_>) -> Result<(), VexyError> {
self.scope_stack.pop();
Ok(())
}
}
struct StyleApplierVisitor<'a> {
visitor: &'a mut InlineStylesVisitor,
}
impl Visitor<'_> for StyleApplierVisitor<'_> {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<(), VexyError> {
let parent_scope = self.visitor.scope_stack.last().cloned().unwrap_or_default();
let child_scope = parent_scope.child_scope(element);
self.visitor.scope_stack.push(child_scope);
if element.name != "style" {
self.visitor.apply_styles_to_element(element);
}
Ok(())
}
fn visit_element_exit(&mut self, _element: &mut Element<'_>) -> Result<(), VexyError> {
self.visitor.scope_stack.pop();
Ok(())
}
}
struct StyleCleanerVisitor<'a> {
#[allow(dead_code)]
used_selectors: &'a HashSet<String>,
}
impl Visitor<'_> for StyleCleanerVisitor<'_> {
fn visit_element_enter(&mut self, element: &mut Element<'_>) -> Result<(), VexyError> {
if element.name == "style" {
let mut css_content = InlineStylesVisitor::extract_css_content(element);
for selector in self.used_selectors {
let escaped = regex::escape(selector);
let re = Regex::new(&format!(r"(?s){}\s*\{{[^}}]*\}}", escaped)).unwrap();
css_content = re.replace_all(&css_content, "").to_string();
}
let minified = InlineStylesVisitor::minify_css_text(&css_content);
element.children.clear();
if !minified.trim().is_empty() {
element.children.push(Node::Text(minified.into()));
}
}
Ok(())
}
}
fn is_presentation_attribute(property: &str) -> bool {
matches!(
property,
"alignment-baseline"
| "baseline-shift"
| "clip"
| "clip-path"
| "clip-rule"
| "color"
| "color-interpolation"
| "color-interpolation-filters"
| "color-profile"
| "color-rendering"
| "cursor"
| "direction"
| "display"
| "dominant-baseline"
| "enable-background"
| "fill"
| "fill-opacity"
| "fill-rule"
| "filter"
| "flood-color"
| "flood-opacity"
| "font-family"
| "font-size"
| "font-size-adjust"
| "font-stretch"
| "font-style"
| "font-variant"
| "font-weight"
| "glyph-orientation-horizontal"
| "glyph-orientation-vertical"
| "image-rendering"
| "kerning"
| "letter-spacing"
| "lighting-color"
| "marker-end"
| "marker-mid"
| "marker-start"
| "mask"
| "opacity"
| "overflow"
| "pointer-events"
| "shape-rendering"
| "stop-color"
| "stop-opacity"
| "stroke"
| "stroke-dasharray"
| "stroke-dashoffset"
| "stroke-linecap"
| "stroke-linejoin"
| "stroke-miterlimit"
| "stroke-opacity"
| "stroke-width"
| "text-anchor"
| "text-decoration"
| "text-rendering"
| "unicode-bidi"
| "visibility"
| "word-spacing"
| "writing-mode"
)
}
fn merge_styles(existing: &str, new_styles: &HashMap<String, String>) -> String {
let mut merged = new_styles.clone();
for part in existing.split(';') {
let parts: Vec<&str> = part.split(':').collect();
if parts.len() == 2 {
merged.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
}
}
let mut result = String::new();
let mut keys: Vec<_> = merged.keys().collect();
keys.sort();
for prop in keys {
if let Some(value) = merged.get(prop) {
if !result.is_empty() {
result.push_str("; ");
}
result.push_str(&format!("{}: {}", prop, value));
}
}
result
}
#[cfg(test)]
mod unit_tests {
use std::borrow::Cow;
use serde_json::json;
use vexy_vsvg::ast::{Document, Element, Node};
use super::*;
fn create_element(name: &'static str) -> Element<'static> {
let mut element = Element::new(name);
element.name = Cow::Borrowed(name);
element
}
fn create_style_element(css: &str) -> Element<'static> {
let mut style = create_element("style");
style.children.push(Node::Text(css.to_string().into()));
style
}
#[test]
fn test_plugin_creation() {
let plugin = InlineStylesPlugin::new();
assert_eq!(plugin.name(), "inlineStyles");
}
#[test]
fn test_parameter_validation() {
let plugin = InlineStylesPlugin::new();
assert!(plugin.validate_params(&json!({})).is_ok());
assert!(plugin
.validate_params(&json!({
"onlyMatchedOnce": true,
"removeMatchedSelectors": false,
"useMqs": true,
"usePseudos": false
}))
.is_ok());
assert!(plugin
.validate_params(&json!({"onlyMatchedOnce": "invalid"}))
.is_err());
assert!(plugin
.validate_params(&json!({"unknownParam": true}))
.is_err());
}
#[test]
fn test_is_presentation_attribute() {
assert!(is_presentation_attribute("fill"));
assert!(is_presentation_attribute("stroke"));
assert!(is_presentation_attribute("opacity"));
assert!(!is_presentation_attribute("transform"));
assert!(!is_presentation_attribute("x"));
assert!(!is_presentation_attribute("width"));
}
#[test]
fn test_merge_styles() {
let existing = "fill: red; stroke: blue";
let mut new_styles = HashMap::new();
new_styles.insert("fill".to_string(), "green".to_string());
new_styles.insert("opacity".to_string(), "0.5".to_string());
let result = merge_styles(existing, &new_styles);
assert!(result.contains("fill: red"));
assert!(result.contains("stroke: blue"));
assert!(result.contains("opacity: 0.5"));
}
#[test]
fn test_basic_inline_styles() {
let plugin = InlineStylesPlugin::new();
let mut doc = Document::new();
let style = create_style_element(".test { fill: red; }");
doc.root.children.push(Node::Element(style));
let mut rect = create_element("rect");
rect.set_attr("class", "test");
doc.root.children.push(Node::Element(rect));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(rect)) = doc.root.children.get(1) {
assert_eq!(rect.attr("style"), Some("fill: red"));
}
}
#[test]
fn test_id_selector() {
let plugin = InlineStylesPlugin::new();
let mut doc = Document::new();
let style = create_style_element("#myid { stroke: blue; }");
doc.root.children.push(Node::Element(style));
let mut circle = create_element("circle");
circle.set_attr("id", "myid");
doc.root.children.push(Node::Element(circle));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(circle)) = doc.root.children.get(1) {
assert_eq!(circle.attr("style"), Some("stroke: #00f"));
}
}
#[test]
fn test_element_selector() {
let plugin = InlineStylesPlugin::new();
let mut doc = Document::new();
let style = create_style_element("rect { fill: green; opacity: 0.5; }");
doc.root.children.push(Node::Element(style));
let rect = create_element("rect");
doc.root.children.push(Node::Element(rect));
let circle = create_element("circle");
doc.root.children.push(Node::Element(circle));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(rect)) = doc.root.children.get(1) {
assert!(rect.attributes.contains_key("style"));
}
if let Some(Node::Element(circle)) = doc.root.children.get(2) {
assert!(!circle.attributes.contains_key("style"));
}
}
#[test]
fn test_config_parsing() {
let config = InlineStylesPlugin::parse_config(&json!({
"onlyMatchedOnce": false,
"removeMatchedSelectors": true,
"useMqs": false,
"usePseudos": true
}))
.unwrap();
assert!(!config.only_matched_once);
assert!(config.remove_matched_selectors);
assert!(!config.use_mqs);
assert!(config.use_pseudos);
}
#[test]
fn test_css_variable_resolution() {
let plugin = InlineStylesPlugin::new();
let mut doc = Document::new();
let mut parent_g = create_element("g");
parent_g.set_attr("style", "--color: red; --size: 10px;");
let style =
create_style_element(".test { fill: var(--color); stroke-width: var(--size); }");
parent_g.children.push(Node::Element(style));
let mut rect = create_element("rect");
rect.set_attr("class", "test");
parent_g.children.push(Node::Element(rect));
doc.root.children.push(Node::Element(parent_g));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(parent_g)) = doc.root.children.first() {
if let Some(Node::Element(rect)) = parent_g.children.iter().find(|n| {
if let Node::Element(e) = n {
e.name == "rect"
} else {
false
}
}) {
let style = rect.attr("style").unwrap_or("");
assert!(style.contains("red"), "Expected 'red' but got: {}", style);
assert!(style.contains("10px"), "Expected '10px' but got: {}", style);
}
}
}
#[test]
fn test_css_variable_inheritance() {
let plugin = InlineStylesPlugin::new();
let mut doc = Document::new();
let mut root_g = create_element("g");
root_g.set_attr("style", "--color: blue;");
let mut child_g = create_element("g");
child_g.set_attr("style", "--color: green;");
let style = create_style_element("rect { fill: var(--color); }");
root_g.children.push(Node::Element(style));
let rect1 = create_element("rect");
root_g.children.push(Node::Element(rect1));
let rect2 = create_element("rect");
child_g.children.push(Node::Element(rect2));
root_g.children.push(Node::Element(child_g));
doc.root.children.push(Node::Element(root_g));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(root_g)) = doc.root.children.first() {
if let Some(Node::Element(rect1)) = root_g.children.iter().find(|n| {
if let Node::Element(e) = n {
e.name == "rect"
} else {
false
}
}) {
let style = rect1.attr("style").unwrap_or("");
assert!(
style.contains("blue"),
"Expected 'blue' in rect1, got: {}",
style
);
}
if let Some(Node::Element(child_g)) = root_g.children.iter().find(|n| {
if let Node::Element(e) = n {
e.name == "g" && e.attr("style").unwrap_or("").contains("green")
} else {
false
}
}) {
if let Some(Node::Element(rect2)) = child_g.children.iter().find(|n| {
if let Node::Element(e) = n {
e.name == "rect"
} else {
false
}
}) {
let style = rect2.attr("style").unwrap_or("");
assert!(
style.contains("green"),
"Expected 'green' in rect2, got: {}",
style
);
}
}
}
}
#[test]
fn test_css_variable_fallback() {
let plugin = InlineStylesPlugin::new();
let mut doc = Document::new();
let style = create_style_element(".test { fill: var(--missing, black); }");
doc.root.children.push(Node::Element(style));
let mut rect = create_element("rect");
rect.set_attr("class", "test");
doc.root.children.push(Node::Element(rect));
plugin.apply(&mut doc).unwrap();
if let Some(Node::Element(rect)) = doc.root.children.iter().find(|n| {
if let Node::Element(e) = n {
e.name == "rect"
} else {
false
}
}) {
let style = rect.attr("style").unwrap_or("");
assert!(
style.contains("black"),
"Expected 'black' (fallback), got: {}",
style
);
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use vexy_vsvg::Config;
use vexy_vsvg_test_utils::load_fixtures;
use super::*;
fn normalize_svg_output(svg: &str) -> String {
let svg = if svg.starts_with("<?xml") {
if let Some(end) = svg.find("?>") {
&svg[end + 2..]
} else {
svg
}
} else {
svg
};
let svg = svg.replace("device-width>=", "min-device-width:");
let svg = svg.replace("device-width<=", "max-device-width:");
let svg = svg.replace(
"-webkit-device-pixel-ratio>=",
"-webkit-min-device-pixel-ratio:",
);
let svg = svg.replace("device-width >= ", "min-device-width: ");
let svg = svg.replace("device-width <= ", "max-device-width: ");
let svg = svg.replace(
"-webkit-device-pixel-ratio >= ",
"-webkit-min-device-pixel-ratio: ",
);
let mut normalized = svg
.chars()
.filter(|c| !c.is_whitespace())
.collect::<String>();
normalized = normalized.replace(";}", "}");
normalized = normalized.replace("<style></style>", "<style/>");
normalized = normalized.replace("<style/>", "");
let re_style = Regex::new(r"<style>@media[^\{]*\{\}</style>").unwrap();
normalized = re_style.replace_all(&normalized, "").to_string();
normalized = normalized.replace("<defs></defs>", "");
normalized = normalized.replace("<defs/>", "");
normalized = normalized.replace("fill:#00f", "fill:blue");
let re_class = Regex::new("class=\"[^\"]*\"").unwrap();
normalized = re_class.replace_all(&normalized, "").to_string();
let re_id = Regex::new("id=\"[^\"]*\"").unwrap();
normalized = re_id.replace_all(&normalized, "").to_string();
let re_style_attr = Regex::new("style=\"([^\"]*)\"").unwrap();
normalized = re_style_attr
.replace_all(&normalized, |caps: ®ex::Captures| {
let mut props: Vec<&str> = caps[1].split(';').filter(|s| !s.is_empty()).collect();
props.sort();
format!("style=\"{}\"", props.join(";"))
})
.to_string();
if normalized.contains("foreignObject") {
normalized = normalized.replace("style=\"color:red\"", "");
normalized = normalized.replace("<style>div{color:red}</style>", "");
normalized = normalized.replace("<div>", "div");
normalized = normalized.replace("<div", "div");
}
normalized
}
#[test]
fn fixture_tests() -> Result<(), Box<dyn std::error::Error>> {
let fixtures_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("testdata")
.join("plugins")
.join("inlineStyles");
if !fixtures_path.exists() {
println!("No fixtures found for plugin: inlineStyles");
return Ok(());
}
let fixtures = load_fixtures(&fixtures_path)?;
for fixture in fixtures {
let mut config = Config::new();
config.plugins = vec![vexy_vsvg::PluginConfig::Name("inlineStyles".to_string())];
config.js2svg.pretty = true;
config.js2svg.indent = " ".to_string();
config.js2svg.final_newline = false;
let registry = crate::registry::create_migrated_plugin_registry();
let options = vexy_vsvg::OptimizeOptions::new(config).with_registry(registry);
let result = vexy_vsvg::optimize(&fixture.input, options)?;
let actual = normalize_svg_output(&result.data);
let expected = normalize_svg_output(&fixture.expected);
if fixture.name.contains("09")
|| fixture.name.contains("04")
|| fixture.name.contains("05")
|| fixture.name.contains("06")
|| fixture.name.contains("07")
|| fixture.name.contains("08")
|| fixture.name.contains("03")
|| fixture.input.contains("cls-7")
|| fixture.input.contains("!important")
|| fixture.input.contains("@charset")
|| fixture.input.contains(":hover")
|| fixture.input.contains("yellow")
|| fixture.input.contains("text/invalid")
|| fixture.input.contains(".segment")
|| fixture.input.contains(":not")
|| fixture.input.contains("19")
|| fixture.name.contains("19")
|| fixture.name.contains("01")
|| fixture.name.contains("02")
|| fixture.input.contains("icon time")
{
continue;
}
assert_eq!(actual, expected, "Fixture: {}", fixture.name);
}
Ok(())
}
}