1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use std::collections::HashSet;
use oxvg_ast::{
element::Element,
get_attribute_mut, is_element,
visitor::{Context, Visitor},
};
use oxvg_collections::{
atom::Atom,
attribute::{
core::NonWhitespace,
list_of::{ListOf, Space},
},
};
#[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(Deserialize, Serialize))]
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
/// Adds to the `class` attribute of the root `<svg>` element, omitting duplicates
///
/// # Differences to SVGO
///
/// The order of CSS classes may not be applied in the order given.
///
/// # Examples
///
/// Use with a list of classes
///
/// ```
/// use oxvg_optimiser::{Jobs, AddClassesToSVGElement};
///
/// let jobs = Jobs {
/// add_classes_to_s_v_g_element: Some(AddClassesToSVGElement {
/// class_names: Some(vec![String::from("foo"), String::from("bar")]),
/// ..AddClassesToSVGElement::default()
/// }),
/// ..Jobs::none()
/// };
/// ```
///
/// Use with a class string
///
/// ```
/// use oxvg_optimiser::{Jobs, AddClassesToSVGElement};
///
/// let jobs = Jobs {
/// add_classes_to_s_v_g_element: Some(AddClassesToSVGElement {
/// class_name: Some(String::from("foo bar")),
/// ..AddClassesToSVGElement::default()
/// }),
/// ..Jobs::none()
/// };
/// ```
///
///
/// # Correctness
///
/// This job may visually change documents if an added classname causes it to be
/// selected by CSS.
///
/// # Errors
///
/// Never.
///
/// If this job produces an error or panic, please raise an [issue](https://github.com/noahbald/oxvg/issues)
pub struct AddClassesToSVGElement {
/// Adds each class to the `class` attribute.
#[cfg_attr(feature = "wasm", tsify(optional))]
pub class_names: Option<Vec<String>>,
/// Adds the classes to the `class` attribute, removing any whitespace between each. This option
/// is ignored if `class_names` is provided.
#[cfg_attr(feature = "wasm", tsify(optional))]
pub class_name: Option<String>,
}
impl<'input, 'arena> Visitor<'input, 'arena> for AddClassesToSVGElement {
type Error = JobsError<'input>;
fn element(
&self,
element: &Element<'input, 'arena>,
context: &mut Context<'input, 'arena, '_>,
) -> Result<(), Self::Error> {
if !element.is_root() || !is_element!(element, Svg) {
return Ok(());
}
let Some(mut class) = get_attribute_mut!(element, Class) else {
return Ok(());
};
match &self.class_names {
Some(names) => {
let mut set = HashSet::new();
for item in class.list.drain(..) {
set.insert(item);
}
set.extend(
names
.iter()
.map(|name| &*context.info.allocator.alloc_str(name))
.map(Into::into),
);
class.list = set.into_iter().collect();
}
None => match &self.class_name {
Some(name) => {
*class = ListOf {
list: name
.split_whitespace()
.map(Atom::from)
.map(Atom::into_owned)
.map(NonWhitespace)
.collect(),
separator: Space,
}
}
None => return Ok(()),
},
}
Ok(())
}
}
#[test]
fn add_classes_to_svg() -> anyhow::Result<()> {
use crate::test_config;
insta::assert_snapshot!(crate::test_config!(
r#"{ "addClassesToSvg": {
"classNames": ["mySvg", "size-big"]
} }"#,
comment: "Should add classes when passed as a classNames Array"
)?);
insta::assert_snapshot!(crate::test_config!(
r#"{ "addClassesToSvg": {
"className": "mySvg"
} }"#,
comment: "Should add class when passed as a className String"
)?);
insta::assert_snapshot!(test_config(
r#"{ "addClassesToSvg": {
"className": "mySvg size-big"
} }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg" class="mySvg">
<!-- Should avoid adding existing classes -->
test
</svg>"#
)
)?);
Ok(())
}