1use base64::Engine as _;
6
7fn format_signed_hex(v: i64) -> String {
12 if v < 0 {
13 format!("-{:x}", v.unsigned_abs())
14 } else {
15 format!("{v:x}")
16 }
17}
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum AttributeValue {
22 Null,
24 String(String),
26 BytesHex(Vec<u8>),
28 BytesBase64(Vec<u8>),
30 Int(i32),
32 IntHex(u32),
35 Long(i64),
37 LongHex(u64),
40 Float(f32),
42 Double(f64),
44 Boolean(bool),
46}
47
48impl AttributeValue {
49 pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
52 use std::borrow::Cow;
53 match self {
54 AttributeValue::Null => Cow::Borrowed(""),
55 AttributeValue::String(s) => Cow::Borrowed(s.as_str()),
56 AttributeValue::BytesHex(b) => Cow::Owned(faster_hex::hex_string(b)),
57 AttributeValue::BytesBase64(b) => {
58 Cow::Owned(base64::engine::general_purpose::STANDARD.encode(b))
59 }
60 AttributeValue::Int(v) => Cow::Owned(v.to_string()),
61 AttributeValue::IntHex(v) => Cow::Owned(format_signed_hex(*v as i32 as i64)),
62 AttributeValue::Long(v) => Cow::Owned(v.to_string()),
63 AttributeValue::LongHex(v) => Cow::Owned(format_signed_hex(*v as i64)),
64 AttributeValue::Float(v) => {
65 if v.fract() == 0.0 && v.is_finite() {
66 Cow::Owned(format!("{:.1}", v))
67 } else {
68 Cow::Owned(v.to_string())
69 }
70 }
71 AttributeValue::Double(v) => {
72 if v.fract() == 0.0 && v.is_finite() {
73 Cow::Owned(format!("{:.1}", v))
74 } else {
75 Cow::Owned(v.to_string())
76 }
77 }
78 AttributeValue::Boolean(b) => {
79 if *b {
80 Cow::Borrowed("true")
81 } else {
82 Cow::Borrowed("false")
83 }
84 }
85 }
86 }
87
88 pub fn as_string(&self) -> Option<&str> {
90 if let AttributeValue::String(s) = self {
91 Some(s)
92 } else {
93 None
94 }
95 }
96 pub fn as_int(&self) -> Option<i32> {
98 if let AttributeValue::Int(v) = self {
99 Some(*v)
100 } else {
101 None
102 }
103 }
104 pub fn as_int_hex(&self) -> Option<u32> {
106 if let AttributeValue::IntHex(v) = self {
107 Some(*v)
108 } else {
109 None
110 }
111 }
112 pub fn as_long(&self) -> Option<i64> {
114 if let AttributeValue::Long(v) = self {
115 Some(*v)
116 } else {
117 None
118 }
119 }
120 pub fn as_long_hex(&self) -> Option<u64> {
122 if let AttributeValue::LongHex(v) = self {
123 Some(*v)
124 } else {
125 None
126 }
127 }
128 pub fn as_float(&self) -> Option<f32> {
130 if let AttributeValue::Float(v) = self {
131 Some(*v)
132 } else {
133 None
134 }
135 }
136 pub fn as_double(&self) -> Option<f64> {
138 if let AttributeValue::Double(v) = self {
139 Some(*v)
140 } else {
141 None
142 }
143 }
144 pub fn as_bool(&self) -> Option<bool> {
146 if let AttributeValue::Boolean(b) = self {
147 Some(*b)
148 } else {
149 None
150 }
151 }
152 pub fn as_bytes(&self) -> Option<&[u8]> {
155 match self {
156 AttributeValue::BytesHex(b) | AttributeValue::BytesBase64(b) => Some(b),
157 _ => None,
158 }
159 }
160}
161
162pub type InternedStr = smol_str::SmolStr;
172
173#[derive(Debug, Clone, PartialEq)]
175pub struct Attribute {
176 pub name: InternedStr,
178 pub value: AttributeValue,
180}
181
182impl Attribute {
183 pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
186 self.value.as_str()
187 }
188}
189
190#[derive(Debug, Clone, PartialEq)]
193pub enum Event {
194 StartDocument,
196 EndDocument,
198 StartTag {
200 name: InternedStr,
202 attributes: Vec<Attribute>,
204 },
205 EndTag {
207 name: InternedStr,
209 },
210 Text(String),
212 CdataSection(String),
214 Comment(String),
216 ProcessingInstruction(String),
218 EntityReference(String),
221 IgnorableWhitespace(String),
224 DocDecl(String),
226}
227
228pub(crate) fn xml_escape(s: &str) -> std::borrow::Cow<'_, str> {
229 match s.find(['<', '>', '&', '"', '\'']) {
230 None => std::borrow::Cow::Borrowed(s),
231 Some(first) => {
232 let mut out = String::with_capacity(s.len() + 8);
233 out.push_str(&s[..first]);
234 for c in s[first..].chars() {
235 match c {
236 '<' => out.push_str("<"),
237 '>' => out.push_str(">"),
238 '&' => out.push_str("&"),
239 '"' => out.push_str("""),
240 '\'' => out.push_str("'"),
241 other => out.push(other),
242 }
243 }
244 std::borrow::Cow::Owned(out)
245 }
246 }
247}
248
249fn push_attr_value(buf: &mut String, value: &AttributeValue) {
254 use std::fmt::Write as _;
255 match value {
256 AttributeValue::Null => {}
257 AttributeValue::String(s) => buf.push_str(&xml_escape(s)),
258 AttributeValue::BytesHex(b) => buf.push_str(&faster_hex::hex_string(b)),
259 AttributeValue::BytesBase64(b) => {
260 base64::engine::general_purpose::STANDARD.encode_string(b, buf);
261 }
262 AttributeValue::Int(v) => {
263 let _ = write!(buf, "{v}");
264 }
265 AttributeValue::IntHex(v) => {
266 let _ = write!(buf, "{}", format_signed_hex(*v as i32 as i64));
267 }
268 AttributeValue::Long(v) => {
269 let _ = write!(buf, "{v}");
270 }
271 AttributeValue::LongHex(v) => {
272 let _ = write!(buf, "{}", format_signed_hex(*v as i64));
273 }
274 AttributeValue::Float(v) => {
275 if v.fract() == 0.0 && v.is_finite() {
276 let _ = write!(buf, "{v:.1}");
277 } else {
278 let _ = write!(buf, "{v}");
279 }
280 }
281 AttributeValue::Double(v) => {
282 if v.fract() == 0.0 && v.is_finite() {
283 let _ = write!(buf, "{v:.1}");
284 } else {
285 let _ = write!(buf, "{v}");
286 }
287 }
288 AttributeValue::Boolean(b) => buf.push_str(if *b { "true" } else { "false" }),
289 }
290}
291
292pub(crate) fn render_event(ev: &Event, buf: &mut String) {
294 match ev {
295 Event::StartDocument | Event::EndDocument => {}
296 Event::StartTag { name, attributes } => {
297 buf.push('<');
298 buf.push_str(name);
299 for attr in attributes {
300 buf.push(' ');
301 buf.push_str(&attr.name);
302 buf.push_str("=\"");
303 push_attr_value(buf, &attr.value);
304 buf.push('"');
305 }
306 buf.push('>');
307 }
308 Event::EndTag { name } => {
309 buf.push_str("</");
310 buf.push_str(name);
311 buf.push('>');
312 }
313 Event::Text(t) if !t.is_empty() => buf.push_str(&xml_escape(t)),
314 Event::CdataSection(t) => {
315 buf.push_str("<![CDATA[");
316 buf.push_str(t);
317 buf.push_str("]]>");
318 }
319 Event::Comment(t) => {
320 buf.push_str("<!--");
321 buf.push_str(t);
322 buf.push_str("-->");
323 }
324 Event::ProcessingInstruction(t) => {
325 buf.push_str("<?");
326 buf.push_str(t);
327 buf.push_str("?>");
328 }
329 Event::EntityReference(t) => {
330 buf.push('&');
331 buf.push_str(t);
332 buf.push(';');
333 }
334 Event::IgnorableWhitespace(t) => buf.push_str(t),
335 Event::DocDecl(t) => {
336 buf.push_str("<!DOCTYPE ");
337 buf.push_str(t);
338 buf.push('>');
339 }
340 _ => {}
341 }
342}