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
use crate::plugins::Plugin;
use crate::tree::{Document, Node};
use std::collections::HashMap;
pub struct ConvertOneStopGradients;
impl Plugin for ConvertOneStopGradients {
fn apply(&self, doc: &mut Document) {
// Phase 1: Find 1-stop gradients
let gradients = find_one_stop_gradients(&doc.root);
if !gradients.is_empty() {
// Phase 2: Replace usages
replace_usages(&mut doc.root, &gradients);
}
}
}
fn find_one_stop_gradients(nodes: &[Node]) -> HashMap<String, String> {
let mut map = HashMap::new();
for node in nodes {
if let Node::Element(elem) = node {
if elem.name == "linearGradient" || elem.name == "radialGradient" {
if let Some(id) = elem.attributes.get("id") {
// Count stops
let mut stop_color = None;
let mut stop_count = 0;
for child in &elem.children {
if let Node::Element(child_elem) = child {
if child_elem.name == "stop" {
stop_count += 1;
if stop_count == 1 {
// Get color
// stop-color attr or style?
// Simplifying: check stop-color attr first
if let Some(c) = child_elem.attributes.get("stop-color") {
stop_color = Some(c.clone());
}
}
}
}
}
// SVGO logic: if 0 stops, it's transparent/none?
// Let's handle exactly 1 stop for now.
// SVGO: "if 0 stops, ???"
// Actually if we replace with 1 stop color it works.
if stop_count == 1 {
if let Some(c) = stop_color {
map.insert(id.clone(), c);
}
} else if stop_count == 0 {
// Maybe replace with "none"?
map.insert(id.clone(), "none".to_string());
}
}
}
// Recurse (gradients nicely nested in defs usually, but could be anywhere)
let sub = find_one_stop_gradients(&elem.children);
map.extend(sub);
}
}
map
}
fn replace_usages(nodes: &mut Vec<Node>, map: &HashMap<String, String>) {
for node in nodes {
if let Node::Element(elem) = node {
// Check fill and stroke
for attr in ["fill", "stroke"] {
if let Some(val) = elem.attributes.get_mut(attr) {
if val.starts_with("url(#") && val.ends_with(")") {
let id = &val[5..val.len() - 1];
if let Some(color) = map.get(id) {
*val = color.clone();
}
}
}
}
replace_usages(&mut elem.children, map);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser;
use crate::printer;
#[test]
fn test_convert_gradient() {
let input = r#"<svg>
<defs>
<linearGradient id="g1"><stop stop-color="red"/></linearGradient>
</defs>
<rect fill="url(#g1)"/>
</svg>"#;
let _expected = r#"<svg>
<defs>
<linearGradient id="g1"><stop stop-color="red"/></linearGradient>
</defs>
<rect fill="red"/>
</svg>"#;
// Note: removeUselessDefs or cleanupIds would later remove the unused gradient def.
let mut doc = parser::parse(input).unwrap();
ConvertOneStopGradients.apply(&mut doc);
// We use whitespace insensitive comparison or printer normalization
let out = printer::print(&doc);
// Simple check
assert!(out.contains("fill=\"red\""));
assert!(!out.contains("fill=\"url(#g1)\""));
}
}