Skip to main content

use_svg/
attribute.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub struct SvgAttribute {
3    pub name: String,
4    pub value: String,
5}
6
7impl SvgAttribute {
8    #[must_use]
9    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
10        Self {
11            name: name.into(),
12            value: value.into(),
13        }
14    }
15}
16
17#[must_use]
18pub fn extract_attributes(element: &str) -> Vec<SvgAttribute> {
19    let tag = match opening_tag_slice(element) {
20        Some(tag) => tag,
21        None => return Vec::new(),
22    };
23    let bytes = tag.as_bytes();
24    let mut index = 1;
25
26    if bytes.get(index) == Some(&b'/') {
27        return Vec::new();
28    }
29
30    while index < bytes.len()
31        && !bytes[index].is_ascii_whitespace()
32        && bytes[index] != b'>'
33        && bytes[index] != b'/'
34    {
35        index += 1;
36    }
37
38    let mut attributes = Vec::new();
39
40    while index < bytes.len() {
41        while index < bytes.len() && (bytes[index].is_ascii_whitespace() || bytes[index] == b'/') {
42            index += 1;
43        }
44
45        if index >= bytes.len() || bytes[index] == b'>' {
46            break;
47        }
48
49        let name_start = index;
50        while index < bytes.len()
51            && !bytes[index].is_ascii_whitespace()
52            && bytes[index] != b'='
53            && bytes[index] != b'>'
54            && bytes[index] != b'/'
55        {
56            index += 1;
57        }
58
59        let name = tag[name_start..index].trim();
60        if name.is_empty() {
61            if index < bytes.len() {
62                index += 1;
63            }
64            continue;
65        }
66
67        while index < bytes.len() && bytes[index].is_ascii_whitespace() {
68            index += 1;
69        }
70
71        let value = if index < bytes.len() && bytes[index] == b'=' {
72            index += 1;
73            while index < bytes.len() && bytes[index].is_ascii_whitespace() {
74                index += 1;
75            }
76            parse_attribute_value(tag, &mut index)
77        } else {
78            String::new()
79        };
80
81        attributes.push(SvgAttribute::new(name, value));
82    }
83
84    attributes
85}
86
87#[must_use]
88pub fn get_attribute(element: &str, name: &str) -> Option<String> {
89    extract_attributes(element)
90        .into_iter()
91        .find(|attribute| attribute.name == name)
92        .map(|attribute| attribute.value)
93}
94
95#[must_use]
96pub fn has_attribute(element: &str, name: &str) -> bool {
97    get_attribute(element, name).is_some()
98}
99
100pub(crate) fn extract_attribute_values(input: &str, name: &str) -> Vec<String> {
101    let cleaned = crate::normalize::strip_comments(input);
102    let mut values = Vec::new();
103    let mut index = 0;
104
105    while let Some(relative) = cleaned[index..].find('<') {
106        let start = index + relative;
107        let Some(end) = find_tag_end(cleaned.as_str(), start) else {
108            break;
109        };
110        let next = cleaned[start + 1..].chars().next().unwrap_or('\0');
111
112        if !matches!(next, '/' | '!' | '?') {
113            let tag = &cleaned[start..end];
114            if let Some(value) = get_attribute(tag, name) {
115                values.push(value);
116            }
117        }
118
119        index = end;
120    }
121
122    values
123}
124
125pub(crate) fn find_opening_tag(input: &str, tag_name: &str, from: usize) -> Option<(usize, usize)> {
126    let mut index = from;
127
128    while let Some(relative) = input[index..].find('<') {
129        let start = index + relative;
130        let next = input[start + 1..].chars().next().unwrap_or('\0');
131
132        if matches!(next, '/' | '!' | '?') {
133            index = start + 1;
134            continue;
135        }
136
137        if tag_name_matches(input, start, tag_name) {
138            let end = find_tag_end(input, start)?;
139            return Some((start, end));
140        }
141
142        index = start + 1;
143    }
144
145    None
146}
147
148pub(crate) fn find_tag_end(input: &str, start: usize) -> Option<usize> {
149    if !input[start..].starts_with('<') {
150        return None;
151    }
152
153    let mut active_quote = None;
154
155    for (offset, ch) in input[start + 1..].char_indices() {
156        if let Some(quote) = active_quote {
157            if ch == quote {
158                active_quote = None;
159            }
160            continue;
161        }
162
163        match ch {
164            '"' | '\'' => active_quote = Some(ch),
165            '>' => return Some(start + offset + 2),
166            _ => {}
167        }
168    }
169
170    None
171}
172
173pub(crate) fn opening_tag_name(element: &str) -> Option<String> {
174    let tag = opening_tag_slice(element)?;
175    let bytes = tag.as_bytes();
176    let mut index = 1;
177
178    if bytes.get(index) == Some(&b'/') {
179        return None;
180    }
181
182    let start = index;
183    while index < bytes.len()
184        && !bytes[index].is_ascii_whitespace()
185        && bytes[index] != b'>'
186        && bytes[index] != b'/'
187    {
188        index += 1;
189    }
190
191    (start < index).then(|| tag[start..index].to_string())
192}
193
194pub(crate) fn tag_name_matches(input: &str, start: usize, tag_name: &str) -> bool {
195    if !input[start..].starts_with('<') {
196        return false;
197    }
198
199    let name_start = start + 1;
200    let Some(candidate) = input[name_start..].get(..tag_name.len()) else {
201        return false;
202    };
203
204    if !candidate.eq_ignore_ascii_case(tag_name) {
205        return false;
206    }
207
208    match input[name_start + tag_name.len()..].chars().next() {
209        Some(ch) => ch.is_ascii_whitespace() || matches!(ch, '>' | '/'),
210        None => true,
211    }
212}
213
214fn opening_tag_slice(element: &str) -> Option<&str> {
215    let start = element.find('<')?;
216    let end = find_tag_end(element, start)?;
217    Some(&element[start..end])
218}
219
220fn parse_attribute_value(tag: &str, index: &mut usize) -> String {
221    let bytes = tag.as_bytes();
222
223    if *index >= bytes.len() {
224        return String::new();
225    }
226
227    match bytes[*index] {
228        b'"' | b'\'' => {
229            let quote = bytes[*index];
230            *index += 1;
231            let start = *index;
232
233            while *index < bytes.len() && bytes[*index] != quote {
234                *index += 1;
235            }
236
237            let value = tag[start..*index].to_string();
238            if *index < bytes.len() {
239                *index += 1;
240            }
241            value
242        }
243        _ => {
244            let start = *index;
245            while *index < bytes.len()
246                && !bytes[*index].is_ascii_whitespace()
247                && bytes[*index] != b'>'
248                && bytes[*index] != b'/'
249            {
250                *index += 1;
251            }
252
253            tag[start..*index].to_string()
254        }
255    }
256}