diff --git a/Cargo.toml b/Cargo.toml index 2d528a4..406e4a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ cssparser = "0.34.0" derive-where = "1.2" itertools = "0.14" lazy_static = "1.5" -lightningcss = { version = "1.0.0-alpha.63" } +lightningcss = { version = "1.0.0-alpha.63", features = [ "visitor" ] } log = "0.4" markup5ever = "0.14" phf = { version = "0.11", features = ["macros"] } diff --git a/crates/oxvg/src/args.rs b/crates/oxvg/src/args.rs index 9af44a0..ff2cbc7 100644 --- a/crates/oxvg/src/args.rs +++ b/crates/oxvg/src/args.rs @@ -4,7 +4,7 @@ use std::{ }; use clap::{Parser, Subcommand}; -use oxvg_ast::serialize::Node; +use oxvg_ast::{serialize::Node, visitor::Info}; use crate::config::Config; @@ -82,14 +82,19 @@ impl RunCommand for Optimise { use oxvg_ast::{implementations::markup5ever::Node5Ever, parse::Node}; if self.paths.len() == 1 { - let file = std::fs::File::open(self.paths.first().unwrap())?; + let path = self.paths.first().unwrap(); + let file = std::fs::File::open(path)?; let dom = Node5Ever::parse_file(&file)?; let jobs = config.optimisation.unwrap_or_default(); let start_time = SystemTime::now().duration_since(UNIX_EPOCH)?; let prev_file_size = file.metadata()?.len(); - jobs.run(&dom)?; + let info = Info { + path: Some(path.clone()), + multipass_count: 0, + }; + jobs.run(&dom, &info)?; let mut stdout = StdoutCounter::new(); dom.serialize_into(&mut stdout)?; diff --git a/crates/oxvg_ast/src/element.rs b/crates/oxvg_ast/src/element.rs index ce953cc..fb54509 100644 --- a/crates/oxvg_ast/src/element.rs +++ b/crates/oxvg_ast/src/element.rs @@ -416,7 +416,7 @@ pub trait Element: Node + Features + Debug + std::hash::Hash + Eq + PartialEq { selector: &'a str, ) -> Result< crate::selectors::Select, - cssparser::ParseError<'_, selectors::parser::SelectorParseErrorKind<'_>>, + cssparser::ParseError<'a, selectors::parser::SelectorParseErrorKind<'a>>, > { crate::selectors::Select::new(self, selector) } diff --git a/crates/oxvg_ast/src/implementations/markup5ever.rs b/crates/oxvg_ast/src/implementations/markup5ever.rs index ee63326..acc7f3a 100644 --- a/crates/oxvg_ast/src/implementations/markup5ever.rs +++ b/crates/oxvg_ast/src/implementations/markup5ever.rs @@ -350,7 +350,7 @@ impl Debug for Attributes5Ever<'_> { } } -impl<'a> ClassList for ClassList5Ever<'a> { +impl ClassList for ClassList5Ever<'_> { type Attribute = Attribute; fn length(&self) -> usize { @@ -402,7 +402,7 @@ impl<'a> ClassList for ClassList5Ever<'a> { }; let attr = self.attr().expect("had token"); - let mut new_value = attr.value.subtendril(0, start as u32); + let mut new_value = attr.value.subtendril(0, start); new_value.push_tendril(&attr.value.subtendril(end, attr.value.len() as u32 - end)); drop(attr); if new_value.trim().is_empty() { @@ -619,6 +619,10 @@ impl Node for Node5Ever { .collect() } + fn child_node_count(&self) -> usize { + self.0.children.borrow().len() + } + #[allow(refining_impl_trait)] fn element(&self) -> Option { match self.node_type() { @@ -775,6 +779,18 @@ impl Node for Node5Ever { } } + fn set_text_content(&mut self, content: Self::Atom) { + match &self.0.data { + NodeData::Text { contents } => *contents.borrow_mut() = content, + NodeData::Element { .. } => { + let text = self.text(content); + self.empty(); + self.append_child(text); + } + _ => {} + } + } + fn text(&self, content: Self::Atom) -> Self { Node5Ever(Rc::new(rcdom::Node { parent: Cell::new(None), @@ -1169,7 +1185,7 @@ impl Element for Element5Ever { fn get_attribute_node<'a>( &'a self, attr_name: &< as Attributes<'a>>::Attribute as Attr>::Name, - ) -> Option as Attributes<'a>>::Attribute>> { + ) -> Option as Attributes<'a>>::Attribute>> { self.attributes().get_named_item(attr_name) } @@ -1185,7 +1201,7 @@ impl Element for Element5Ever { fn get_attribute_node_local<'a>( &'a self, attr_name: &<< as Attributes<'a>>::Attribute as Attr>::Name as Name>::LocalName, - ) -> Option as Attributes<'a>>::Attribute>> { + ) -> Option as Attributes<'a>>::Attribute>> { self.attributes().get_named_item_local(attr_name) } @@ -1220,7 +1236,7 @@ impl Element for Element5Ever { &'a self, namespace: &<< as Attributes<'a>>::Attribute as Attr>::Name as Name>::Namespace, name: &<< as Attributes<'a>>::Attribute as Attr>::Name as Name>::LocalName, - ) -> Option> { + ) -> Option> { self.get_attribute_node_ns(namespace, name) .map(|a| Ref::map(a, |a| &a.value)) } @@ -1303,6 +1319,10 @@ impl Node for Element5Ever { self.node.child_nodes().into_iter() } + fn child_node_count(&self) -> usize { + self.node.child_node_count() + } + fn child_nodes(&self) -> Vec { self.node.child_nodes() } @@ -1395,6 +1415,10 @@ impl Node for Element5Ever { self.node.text_content() } + fn set_text_content(&mut self, content: Self::Atom) { + self.node.set_text_content(content); + } + fn text(&self, content: Self::Atom) -> Self::Child { self.node.text(content) } @@ -1716,11 +1740,11 @@ impl selectors::Element for Element5Ever { } fn is_empty(&self) -> bool { - self.all_children(|child| match child.node_type() { - node::Type::Element => false, - node::Type::Text => child.node_value().is_some(), - _ => true, - }) + !self.has_child_nodes() + || self.all_children(|child| match &child.0.data { + NodeData::Text { contents } => contents.borrow().trim().is_empty(), + _ => false, + }) } fn is_root(&self) -> bool { diff --git a/crates/oxvg_ast/src/node.rs b/crates/oxvg_ast/src/node.rs index 161539c..2de023d 100644 --- a/crates/oxvg_ast/src/node.rs +++ b/crates/oxvg_ast/src/node.rs @@ -86,9 +86,11 @@ pub trait Node: Clone + Debug + 'static + Features { /// [MDN | childNodes](https://developer.mozilla.org/en-US/docs/Web/API/Node/childNodes) fn child_nodes(&self) -> Vec; + fn child_node_count(&self) -> usize; + /// Returns whether the node's list of children is empty or not fn has_child_nodes(&self) -> bool { - self.any_child(|_| true) + self.child_node_count() > 0 } /// Upcasts self as an element @@ -182,11 +184,7 @@ pub trait Node: Clone + Debug + 'static + Features { /// [MDN | textContent](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) fn text_content(&self) -> Option; - fn set_text_content(&mut self, content: Self::Atom) { - let text = self.text(content); - self.empty(); - self.append_child(text); - } + fn set_text_content(&mut self, content: Self::Atom); /// Creates a text node with the given content fn text(&self, content: Self::Atom) -> Self::Child; diff --git a/crates/oxvg_ast/src/style.rs b/crates/oxvg_ast/src/style.rs index 32523fb..256755f 100644 --- a/crates/oxvg_ast/src/style.rs +++ b/crates/oxvg_ast/src/style.rs @@ -585,7 +585,7 @@ impl<'i> Parse<'i> for FontVariant<'i> { } } -impl<'i> ToCss for FontVariant<'i> { +impl ToCss for FontVariant<'_> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, @@ -983,7 +983,7 @@ impl SVGTransform { } } - log::debug!(r#"converted to transform: {:?}"#, shortest); + log::debug!("converted to transform: {:?}", shortest); shortest } @@ -1745,7 +1745,7 @@ impl<'i> ComputedStyles<'i> { Some((value.mode(), string)) } - fn get_important(&'i self, id: &Id<'i>) -> Option<&Style<'i>> { + fn get_important(&'i self, id: &Id<'i>) -> Option<&'i Style<'i>> { match id { Id::CSS(id) => { if let Some(value) = self.inline_important.get(id) { @@ -1760,7 +1760,7 @@ impl<'i> ComputedStyles<'i> { } } - fn get_unimportant(&'i self, id: &Id<'i>) -> Option<&Style<'i>> { + fn get_unimportant(&'i self, id: &Id<'i>) -> Option<&'i Style<'i>> { match id { Id::CSS(id) => { if let Some(value) = self.inline.get(id) { @@ -1784,7 +1784,7 @@ impl<'i> ComputedStyles<'i> { self.inherited.get(id) } - pub fn get_static<'a>(&'i self, id: &'a Id<'a>) -> Option<&Static> + pub fn get_static<'a>(&'i self, id: &'a Id<'a>) -> Option<&'i Static<'i>> where 'a: 'i, { @@ -1794,7 +1794,7 @@ impl<'i> ComputedStyles<'i> { } } - pub fn computed(&'i self) -> HashMap { + pub fn computed(&'i self) -> HashMap, &'i Style<'i>> { let mut result = HashMap::new(); let map = |s: &'i (u32, Style<'i>)| &s.1; let mut insert = |s: &'i Style<'i>| { diff --git a/crates/oxvg_ast/src/visitor.rs b/crates/oxvg_ast/src/visitor.rs index d163786..87085b9 100644 --- a/crates/oxvg_ast/src/visitor.rs +++ b/crates/oxvg_ast/src/visitor.rs @@ -9,6 +9,12 @@ use crate::{ style::{self, ComputedStyles, ElementData}, }; +#[derive(Debug, Default, Clone)] +pub struct Info { + pub path: Option, + pub multipass_count: usize, +} + #[derive(Debug)] pub struct Context<'i, 'o, E: Element> { pub computed_styles: crate::style::ComputedStyles<'i>, @@ -16,6 +22,7 @@ pub struct Context<'i, 'o, E: Element> { pub element_styles: &'i HashMap>, pub root: E, pub flags: ContextFlags, + pub info: &'i Info, } impl<'i, 'o, E: Element> Context<'i, 'o, E> { @@ -23,6 +30,7 @@ impl<'i, 'o, E: Element> Context<'i, 'o, E> { root: E, flags: ContextFlags, element_styles: &'i HashMap>, + info: &'i Info, ) -> Self { Self { computed_styles: crate::style::ComputedStyles::default(), @@ -30,6 +38,7 @@ impl<'i, 'o, E: Element> Context<'i, 'o, E> { element_styles, root, flags, + info, } } } @@ -88,7 +97,7 @@ pub trait Visitor { /// /// # Errors /// Whether the visitor fails - fn document(&mut self, document: &mut E) -> Result<(), Self::Error> { + fn document(&mut self, document: &mut E, context: &Context) -> Result<(), Self::Error> { Ok(()) } @@ -164,7 +173,7 @@ pub trait Visitor { /// /// # Errors /// If any of the visitor's methods fail - fn start(&mut self, root: &mut E) -> Result { + fn start(&mut self, root: &mut E, info: &Info) -> Result { let element_styles = &mut HashMap::new(); let mut flags = ContextFlags::empty(); let prepare_outcome = self.prepare(root, &mut flags); @@ -181,11 +190,14 @@ pub trait Visitor { ) .ok(); *element_styles = ElementData::new(root); - let mut context = Context::new(root.clone(), flags, element_styles); + let mut context = Context::new(root.clone(), flags, element_styles, info); context.stylesheet = stylesheet; self.visit(root, &mut context)?; } else { - self.visit(root, &mut Context::new(root.clone(), flags, element_styles))?; + self.visit( + root, + &mut Context::new(root.clone(), flags, element_styles, info), + )?; }; Ok(prepare_outcome) } @@ -201,7 +213,7 @@ pub trait Visitor { ) -> Result<(), Self::Error> { match element.node_type() { node::Type::Document => { - self.document(element)?; + self.document(element, context)?; self.visit_children(element, context)?; self.exit_document(element, context) } diff --git a/crates/oxvg_optimiser/benches/default_jobs.rs b/crates/oxvg_optimiser/benches/default_jobs.rs index 4ae6832..d3e448e 100644 --- a/crates/oxvg_optimiser/benches/default_jobs.rs +++ b/crates/oxvg_optimiser/benches/default_jobs.rs @@ -4,6 +4,7 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criteri use oxvg_ast::{ implementations::markup5ever::{Element5Ever, Node5Ever}, parse::Node, + visitor::Info, }; use oxvg_optimiser::Jobs; @@ -33,7 +34,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let dom = Node5Ever::parse(svg).unwrap(); let jobs = Jobs::::default(); let start = Instant::now(); - let _ = black_box(jobs.run(&dom)); + let _ = black_box(jobs.run(&dom, &Info::default())); result += start.elapsed(); } result diff --git a/crates/oxvg_optimiser/benches/path.rs b/crates/oxvg_optimiser/benches/path.rs index 341af90..3b93e9b 100644 --- a/crates/oxvg_optimiser/benches/path.rs +++ b/crates/oxvg_optimiser/benches/path.rs @@ -5,7 +5,7 @@ use oxvg_ast::{ element::Element, implementations::markup5ever::{Element5Ever, Node5Ever}, parse::Node, - visitor::Visitor, + visitor::{Info, Visitor}, }; use oxvg_optimiser::ConvertPathData; @@ -36,7 +36,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let mut dom = Element5Ever::from_parent(dom).unwrap(); let mut job = ConvertPathData::default(); let start = Instant::now(); - let _ = black_box(job.start(&mut dom)); + let _ = black_box(job.start(&mut dom, &Info::default())); result += start.elapsed(); } result diff --git a/crates/oxvg_optimiser/src/jobs/mod.rs b/crates/oxvg_optimiser/src/jobs/mod.rs index 14bbf12..90ea45a 100644 --- a/crates/oxvg_optimiser/src/jobs/mod.rs +++ b/crates/oxvg_optimiser/src/jobs/mod.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use oxvg_ast::{ element::Element, - visitor::{ContextFlags, PrepareOutcome, Visitor}, + visitor::{ContextFlags, Info, PrepareOutcome, Visitor}, }; use serde::Deserialize; @@ -36,10 +36,10 @@ macro_rules! jobs { impl Jobs { /// Runs each job in the config, returning the number of non-skipped jobs - fn run_jobs(&mut self, element: &mut E) -> Result { + fn run_jobs(&mut self, element: &mut E, info: &Info) -> Result { let mut count = 0; $(if let Some(job) = self.$name.as_mut() { - if !job.start(element)?.contains(PrepareOutcome::skip) { + if !job.start(element, info)?.contains(PrepareOutcome::skip) { count += 1; } })+ @@ -54,6 +54,7 @@ jobs! { add_attributes_to_svg_element: AddAttributesToSVGElement, add_classes_to_svg: AddClassesToSVG, cleanup_list_of_values: CleanupListOfValues, + prefix_ids: PrefixIds, // Default plugins remove_doctype: RemoveDoctype (is_default: true), @@ -111,14 +112,16 @@ impl std::error::Error for Error {} impl Jobs { /// # Errors /// When any job fails for the first time - pub fn run(self, root: &E::ParentChild) -> Result<(), Error> { + pub fn run(self, root: &E::ParentChild, info: &Info) -> Result<(), Error> { let Some(mut root_element) = ::from_parent(root.clone()) else { log::warn!("No elements found in the document, skipping"); return Ok(()); }; let mut jobs = self.clone(); - let count = jobs.run_jobs(&mut root_element).map_err(Error::Generic)?; + let count = jobs + .run_jobs(&mut root_element, info) + .map_err(Error::Generic)?; log::debug!("completed {count} jobs"); Ok(()) } @@ -154,7 +157,7 @@ pub(crate) fn test_config(config_json: &str, svg: Option<&str>) -> anyhow::Resul test "#, ))?; - jobs.run(&dom)?; + jobs.run(&dom, &Info::default())?; serialize::Node::serialize_with_options(&dom, serialize::Options::new().pretty()) } diff --git a/crates/oxvg_optimiser/src/jobs/prefix_ids.rs b/crates/oxvg_optimiser/src/jobs/prefix_ids.rs new file mode 100644 index 0000000..de225a6 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/prefix_ids.rs @@ -0,0 +1,615 @@ +use std::collections::HashMap; + +use derive_where::derive_where; +use itertools::Itertools; +use lightningcss::{ + printer::PrinterOptions, + selector::Component, + stylesheet::{ParserOptions, StyleSheet}, + traits::ToCss, + visit_types, +}; +use oxvg_ast::{ + attribute::{Attr, Attributes}, + element::Element, + node::{self, Node}, + visitor::{Context, ContextFlags, Info, PrepareOutcome, Visitor}, +}; +use oxvg_collections::{collections::REFERENCES_PROPS, regex::REFERENCES_URL}; +use regex::{Captures, Match}; +use serde::Deserialize; + +#[derive(Default, Clone)] +pub enum PrefixGenerator { + /// A function to create a dynamic prefix + Generator(Box &str>), + /// A string to use as a prefix + Prefix(String), + /// No prefix + None, + /// Use "prefix" as prefix + #[default] + Default, +} + +fn default_delim() -> String { + "__".to_string() +} + +const fn default_prefix_ids() -> bool { + true +} + +const fn default_prefix_class_names() -> bool { + true +} + +#[derive(Deserialize, Clone)] +#[derive_where(Default)] +#[serde(rename_all = "camelCase")] +#[serde(bound = "E: Element")] +pub struct PrefixIds { + #[serde(default = "default_delim")] + pub delim: String, + #[serde(default)] + pub prefix: PrefixGenerator, + #[serde(default = "default_prefix_ids")] + pub prefix_ids: bool, + #[serde(default = "default_prefix_class_names")] + pub prefix_class_names: bool, +} + +struct CssVisitor<'a, 'b, E: Element> { + generator: &'a mut GeneratePrefix<'b, E>, + ids: bool, + class_names: bool, +} + +impl Visitor for PrefixIds { + type Error = String; + + fn prepare(&mut self, _document: &E, _context_flags: &mut ContextFlags) -> PrepareOutcome { + if !self.prefix_ids && !self.prefix_class_names { + PrepareOutcome::skip + } else { + PrepareOutcome::none + } + } + + fn element(&mut self, element: &mut E, context: &mut Context) -> Result<(), String> { + let mut prefix_generator = GeneratePrefix { + node: element.clone(), + info: context.info, + prefix_generator: &self.prefix, + delim: &self.delim, + history: HashMap::new(), + }; + if element.prefix().is_none() + && element.local_name().as_ref() == "style" + && self + .prefix_selectors(element, &mut prefix_generator) + .is_none() + { + return Ok(()); + } + + for mut attr in element.attributes().into_iter_mut() { + let value: &str = attr.value().as_ref(); + if value.is_empty() { + continue; + } + + let prefix = attr.prefix().as_ref().map(AsRef::as_ref); + let local_name = attr.local_name().as_ref(); + + if prefix.is_none() && local_name == "id" { + if self.prefix_ids { + log::debug!("prefixing id"); + if let Some(new_id) = Self::prefix_id(value, &mut prefix_generator) { + attr.set_value(new_id.into()); + } + } + } else if prefix.is_none() && local_name == "class" { + if self.prefix_class_names { + log::debug!("prefixing class"); + let value = value + .split_whitespace() + .filter_map(|s| Self::prefix_id(s, &mut prefix_generator)) + .join(" "); + attr.set_value(value.into()); + } + } else if prefix.is_none_or(|p| p == "xlink") && local_name == "href" { + log::debug!("prefixing reference"); + if let Some(new_ref) = Self::prefix_reference(value, &mut prefix_generator) { + attr.set_value(new_ref.into()); + } + } else if prefix.is_none() && matches!(local_name, "begin" | "end") { + log::debug!("prefixing animation"); + #[allow(clippy::case_sensitive_file_extension_comparisons)] + let mut parts = value.split(';').map(str::trim).map(|s| { + if s.ends_with(".end") || s.ends_with(".start") { + let (id, postfix) = + s.split_once('.').expect("should end with `.(end|start)`"); + if let Some(id) = Self::prefix_id(id, &mut prefix_generator) { + format!("{id}.{postfix}") + } else { + s.to_string() + } + } else { + s.to_string() + } + }); + let new_animation_timing = parts.join("; ").into(); + attr.set_value(new_animation_timing); + } else if prefix.is_none() && REFERENCES_PROPS.contains(local_name) { + log::debug!("prefixing url"); + let new_value = REFERENCES_URL + .replace_all(value, |caps: &Captures| { + if let Some(prefix) = + Self::prefix_reference(&caps[1], &mut prefix_generator) + { + let start = if caps[0].starts_with(':') { ":" } else { "" }; + format!("{start}url({prefix})") + } else { + caps[0].to_string() + } + }) + .as_ref() + .into(); + attr.set_value(new_value); + } + } + + Ok(()) + } +} + +impl<'i, E: Element> lightningcss::visitor::Visitor<'i> for CssVisitor<'_, '_, E> { + type Error = (); + + fn visit_types(&self) -> lightningcss::visitor::VisitTypes { + if self.ids { + visit_types!(SELECTORS | URLS) + } else { + visit_types!(SELECTORS) + } + } + + fn visit_selector( + &mut self, + selector: &mut lightningcss::selector::Selector<'i>, + ) -> Result<(), Self::Error> { + selector.iter_mut_raw_match_order().for_each(|c| { + if matches!(c, Component::Class(_) if !self.class_names) + || matches!(c, Component::ID(_) if !self.ids) + { + return; + } + if let Component::ID(ident) | Component::Class(ident) = c { + if let Some(new_ident) = PrefixIds::prefix_id(ident, self.generator) { + *ident = new_ident.into(); + } + } + }); + Ok(()) + } + + fn visit_url( + &mut self, + url: &mut lightningcss::values::url::Url<'i>, + ) -> Result<(), Self::Error> { + if let Some(new_url) = PrefixIds::prefix_reference(&url.url, self.generator) { + url.url = new_url.into(); + } + Ok(()) + } +} + +impl PrefixIds { + fn prefix_selectors( + &self, + element: &mut E, + prefix_generator: &mut GeneratePrefix<'_, E>, + ) -> Option<()> { + if element.is_empty() { + return None; + } + + log::debug!("prefixing selectors for style element"); + element.clone().for_each_child(|mut child| { + if !matches!( + child.node_type(), + node::Type::Text | node::Type::CDataSection + ) { + return; + } + + let Some(css) = child.text_content() else { + return; + }; + let Ok(mut css_ast) = StyleSheet::parse(&css, ParserOptions::default()) else { + return; + }; + self.prefix_styles(&mut css_ast, prefix_generator); + + let options = PrinterOptions { + minify: true, + ..PrinterOptions::default() + }; + let Ok(css) = css_ast.rules.to_css_string(options) else { + return; + }; + child.set_text_content(css.into()); + }); + Some(()) + } + + fn prefix_styles(&self, css: &mut StyleSheet, prefix_generator: &mut GeneratePrefix<'_, E>) { + use lightningcss::visitor::Visitor; + + let mut visitor = CssVisitor { + generator: prefix_generator, + ids: self.prefix_ids, + class_names: self.prefix_class_names, + }; + let _ = visitor.visit_stylesheet(css); + } + + fn prefix_id(ident: &str, prefix_generator: &mut GeneratePrefix<'_, E>) -> Option { + let prefix = prefix_generator.generate(ident); + if ident.starts_with(&prefix) { + return None; + } + Some(format!("{prefix}{ident}")) + } + + fn prefix_reference(url: &str, prefix_generator: &mut GeneratePrefix<'_, E>) -> Option { + let reference = url.strip_prefix('#').unwrap_or(url); + let prefix = prefix_generator.generate(reference); + if reference.starts_with(&prefix) { + return None; + } + Some(format!("#{prefix}{reference}")) + } +} + +impl<'de, E: Element> Deserialize<'de> for PrefixGenerator { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + + match value { + serde_json::Value::String(string) => Ok(PrefixGenerator::Prefix(string)), + serde_json::Value::Bool(bool) => { + if bool { + Ok(PrefixGenerator::Default) + } else { + Ok(PrefixGenerator::None) + } + } + serde_json::Value::Null => Ok(PrefixGenerator::Default), + _ => Err(serde::de::Error::custom( + "expected a string, boolean, or null", + )), + } + } +} + +struct GeneratePrefix<'a, E: Element> { + node: E, + info: &'a Info, + prefix_generator: &'a PrefixGenerator, + delim: &'a str, + history: HashMap, +} + +impl GeneratePrefix<'_, E> { + fn generate(&mut self, body: &str) -> String { + match self.prefix_generator { + PrefixGenerator::Generator(f) => { + if let Some(prefix) = self.history.get(body) { + return (*prefix).to_string(); + }; + let prefix = f(self.node.clone(), self.info); + self.history.insert(body.to_string(), prefix); + prefix.to_string() + } + PrefixGenerator::Prefix(s) => format!("{s}{}", self.delim), + PrefixGenerator::None => String::new(), + PrefixGenerator::Default => match &self.info.path { + Some(path) => match get_basename(path) { + Some(name) => format!( + "{}{}", + ESCAPE_IDENTIFIER_NAME.replace(name.as_str(), "_"), + self.delim + ), + None => self.delim.to_string(), + }, + None => format!("prefix{}", self.delim), + }, + } + } +} + +fn get_basename(path: &std::path::Path) -> Option { + let path = path.as_os_str().to_str()?; + BASENAME_CAPTURE + .captures_iter(path) + .next() + .and_then(|m| m.get(0)) +} + +lazy_static! { + static ref ESCAPE_IDENTIFIER_NAME: regex::Regex = regex::Regex::new("[. ]").unwrap(); + static ref BASENAME_CAPTURE: regex::Regex = regex::Regex::new(r"[/\\]?([^/\\]+)").unwrap(); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn prefix_ids() -> anyhow::Result<()> { + use crate::test_config; + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds" + } }"#, + Some( + r#" + + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_02_svg_txt" + } }"#, + Some( + r#" + + + + + + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_03_svg_txt" + } }"#, + Some( + r##" + + +"## + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_04_svg_txt" + } }"#, + Some( + r##" + + + + +"## + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_05_svg_txt" + } }"#, + Some( + r#" + + + + + + + + + + + + + + + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_06_svg_txt" + } }"#, + Some( + r#" + + + + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_07_svg_txt", + "prefixIds": false + } }"#, + Some( + r#" + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_08_svg_txt", + "prefixClassNames": false + } }"#, + Some( + r#" + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_09_svg_txt", + "prefixIds": false, + "prefixClassNames": false + } }"#, + Some( + r#" + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_10_svg_txt" + } }"#, + Some( + r#" + + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_11_svg_txt" + } }"#, + Some( + r#" + + + + + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_12_svg_txt" + } }"#, + Some( + r#" + +"# + ) + )?); + + insta::assert_snapshot!(test_config( + r#"{ "prefixIds": { + "prefix": "prefixIds_13_svg_txt" + } }"#, + Some( + r#" + +"# + ) + )?); + + Ok(()) +} diff --git a/crates/oxvg_optimiser/src/jobs/remove_hidden_elems.rs b/crates/oxvg_optimiser/src/jobs/remove_hidden_elems.rs index 1d7f639..4aa19e9 100644 --- a/crates/oxvg_optimiser/src/jobs/remove_hidden_elems.rs +++ b/crates/oxvg_optimiser/src/jobs/remove_hidden_elems.rs @@ -107,11 +107,15 @@ impl Visitor for Data { impl Data { fn remove_element(&mut self, element: &E) { - if let Some(id) = element.get_attribute_local(&"id".into()) { - if let Some(parent) = Element::parent_element(element) { - if parent.prefix().is_none() && parent.local_name().as_ref() == "defs" { + if let Some(parent) = Element::parent_element(element) { + if parent.prefix().is_none() && parent.local_name().as_ref() == "defs" { + if let Some(id) = element.get_attribute_local(&"id".into()) { self.removed_def_ids.insert(id.to_string()); } + if parent.child_element_count() == 1 { + parent.remove(); + return; + } } } element.remove(); @@ -127,8 +131,8 @@ impl Visitor for RemoveHiddenElems { PrepareOutcome::use_style } - fn document(&mut self, document: &mut E) -> Result<(), Self::Error> { - self.data.start(document).map(|_| ()) + fn document(&mut self, document: &mut E, context: &Context) -> Result<(), Self::Error> { + self.data.start(document, context.info).map(|_| ()) } fn use_style(&self, _element: &E) -> bool { diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-10.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-10.snap new file mode 100644 index 0000000..3fc8cb5 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-10.snap @@ -0,0 +1,10 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_10_svg_txt\"\n } }\"#,\nSome(r#\"\n \n \n \n \n\"#))?" +--- + + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-11.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-11.snap new file mode 100644 index 0000000..75fa43d --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-11.snap @@ -0,0 +1,11 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_11_svg_txt\"\n } }\"#,\nSome(r#\"\n \n \n \n \n \n\"#))?" +--- + + + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-12.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-12.snap new file mode 100644 index 0000000..c7292a4 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-12.snap @@ -0,0 +1,10 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_12_svg_txt\"\n } }\"#,\nSome(r#\"\n \n\"#))?" +--- + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-13.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-13.snap new file mode 100644 index 0000000..1dc1c71 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-13.snap @@ -0,0 +1,12 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_13_svg_txt\"\n } }\"#,\nSome(r#\"\n \n\"#))?" +--- + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-2.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-2.snap new file mode 100644 index 0000000..6ec57ee --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-2.snap @@ -0,0 +1,14 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_02_svg_txt\"\n } }\"#,\nSome(r#\"\n \n \n \n \n \n \n \n \n\"#))?" +--- + + + + + + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-3.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-3.snap new file mode 100644 index 0000000..ba01c65 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-3.snap @@ -0,0 +1,8 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_03_svg_txt\"\n } }\"#,\nSome(r##\"\n \n \n\"##))?" +--- + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-4.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-4.snap new file mode 100644 index 0000000..0c1344a --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-4.snap @@ -0,0 +1,12 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_04_svg_txt\"\n } }\"#,\nSome(r##\"\n \n \n \n \n\"##))?" +--- + + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-5.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-5.snap new file mode 100644 index 0000000..8848848 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-5.snap @@ -0,0 +1,23 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_05_svg_txt\"\n } }\"#,\nSome(r#\"\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\"#))?" +--- + + + + + + + + + + + + + + + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-6.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-6.snap new file mode 100644 index 0000000..b0d0de9 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-6.snap @@ -0,0 +1,14 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_06_svg_txt\"\n } }\"#,\nSome(r#\"\n \n \n \n \n \n \n\"#))?" +--- + + + + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-7.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-7.snap new file mode 100644 index 0000000..cf932cb --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-7.snap @@ -0,0 +1,11 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_07_svg_txt\",\n \"prefixIds\": false\n } }\"#,\nSome(r#\"\n \n \n \n\"#))?" +--- + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-8.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-8.snap new file mode 100644 index 0000000..2c442e3 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-8.snap @@ -0,0 +1,11 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_08_svg_txt\",\n \"prefixClassNames\": false\n } }\"#,\nSome(r#\"\n \n \n \n\"#))?" +--- + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-9.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-9.snap new file mode 100644 index 0000000..3fbb622 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids-9.snap @@ -0,0 +1,16 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds_08_svg_txt\",\n \"prefixIds\": false,\n \"prefixClassNames\": false\n } }\"#,\nSome(r#\"\n \n \n \n\"#))?" +--- + + + + + diff --git a/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids.snap b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids.snap new file mode 100644 index 0000000..e5099d7 --- /dev/null +++ b/crates/oxvg_optimiser/src/jobs/snapshots/oxvg_optimiser__jobs__prefix_ids__prefix_ids.snap @@ -0,0 +1,12 @@ +--- +source: crates/oxvg_optimiser/src/jobs/prefix_ids.rs +expression: "test_config(r#\"{ \"prefixIds\": {\n \"prefix\": \"prefixIds\"\n } }\"#,\nSome(r#\"\n \n \n \n \n\"#))?" +--- + + + + + +