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
use std::collections::BTreeMap;
use oxvg_ast::{
element::Element,
is_element,
visitor::{Context, Visitor},
};
use oxvg_collections::attribute::Attr;
#[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(Default, Clone, Debug)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
/// Adds attributes to SVG elements in the document. This is not an optimisation
/// and will increase the size of SVG documents.
///
/// # Differences to SVGO
///
/// It's not possible to set a *none* value to an attribute. Elements like
/// `<svg data-icon />` are valid in HTML but not XML, so it's only possible to create
/// an attribute like `<svg data-icon="" />`.
///
/// It's also not possible to create React-like syntax. In SVGO it's possible to define
/// an attribute as `{ "key={value}": undefined }` to produce an attribute like
/// `<svg key={value} />`, however in OXVG you have to provide a string value, so it's
/// output would look like `<svg key={value}="" />`.
///
/// # Examples
///
/// Add an attribute with a prefix
///
/// ```
/// use std::collections::BTreeMap;
/// use oxvg_optimiser::{Jobs, AddAttributesToSVGElement};
///
/// let jobs = Jobs {
/// add_attributes_to_s_v_g_element: Some(AddAttributesToSVGElement {
/// attributes: BTreeMap::from([(String::from("prefix:local"), String::from("value"))]),
/// }),
/// ..Jobs::none()
/// };
/// ```
///
/// # Correctness
///
/// This job may visually change documents if the attribute is a presentation attribute
/// or selected via CSS.
///
/// No validation is applied to provided attribute and may produce incorrect or invalid documents.
///
/// # Errors
///
/// Never.
///
/// If this job produces an error or panic, please raise an [issue](https://github.com/noahbald/oxvg/issues)
pub struct AddAttributesToSVGElement {
/// Pairs of qualified names and attribute values that are assigned to the `svg`
#[cfg_attr(feature = "wasm", tsify(type = "Record<string, string>"))]
pub attributes: BTreeMap<String, String>,
}
impl<'input, 'arena> Visitor<'input, 'arena> for AddAttributesToSVGElement {
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(());
}
for (name, value) in &self.attributes {
let name = element.parse_attr_id(name);
if element.has_attribute(&name) {
continue;
}
let value = context.info.allocator.alloc_str(value);
let attr = Attr::new(name, value);
element.set_attribute(attr);
}
Ok(())
}
}
#[test]
fn add_attributes_to_s_v_g_element() -> anyhow::Result<()> {
use crate::test_config;
// WARN: This output is different to SVGO, and may break SVGs for use in React
// SVGO: `<svg data-icon className={classes} />`
// OXVG: `<svg data-icon="" className={classes}="" />`
//
// TODO: Maybe we can add a post-processor to remove trailing `=""`
insta::assert_snapshot!(test_config!(
r#"{ "addAttributesToSvgElement": {
"attributes": { "data-icon": "", "className={classes}": "" }
} }"#,
comment: "Add multiple attributes without value"
)?);
// WARN: This output is different to SVGO
// SVGO: `<svg data-icon />`
// OXVG: `<svg data-icon="" />`
insta::assert_snapshot!(test_config!(
r#"{ "addAttributesToSvgElement": {
"attributes": { "data-icon": "" }
} }"#,
comment: "Add single attribute without value"
)?);
insta::assert_snapshot!(test_config!(
r#"{ "addAttributesToSvgElement": {
"attributes": { "focusable": "false", "data-image": "icon" }
} }"#,
comment: "Add multiple attributes with values"
)?);
insta::assert_snapshot!(test_config(
r#"{ "addAttributesToSvgElement": {
"attributes": { "data-icon": "" }
} }"#,
Some(
r#"<svg xmlns="http://www.w3.org/2000/svg">
<!-- Ignore nested <svg> elements -->
test
<svg />
</svg>"#
),
)?);
Ok(())
}