use base64::Engine as _;
fn format_signed_hex(v: i64) -> String {
if v < 0 {
format!("-{:x}", v.unsigned_abs())
} else {
format!("{v:x}")
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AttributeValue {
Null,
String(String),
BytesHex(Vec<u8>),
BytesBase64(Vec<u8>),
Int(i32),
IntHex(u32),
Long(i64),
LongHex(u64),
Float(f32),
Double(f64),
Boolean(bool),
}
impl AttributeValue {
pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
use std::borrow::Cow;
match self {
AttributeValue::Null => Cow::Borrowed(""),
AttributeValue::String(s) => Cow::Borrowed(s.as_str()),
AttributeValue::BytesHex(b) => Cow::Owned(faster_hex::hex_string(b)),
AttributeValue::BytesBase64(b) => {
Cow::Owned(base64::engine::general_purpose::STANDARD.encode(b))
}
AttributeValue::Int(v) => Cow::Owned(v.to_string()),
AttributeValue::IntHex(v) => Cow::Owned(format_signed_hex(*v as i32 as i64)),
AttributeValue::Long(v) => Cow::Owned(v.to_string()),
AttributeValue::LongHex(v) => Cow::Owned(format_signed_hex(*v as i64)),
AttributeValue::Float(v) => {
if v.fract() == 0.0 && v.is_finite() {
Cow::Owned(format!("{:.1}", v))
} else {
Cow::Owned(v.to_string())
}
}
AttributeValue::Double(v) => {
if v.fract() == 0.0 && v.is_finite() {
Cow::Owned(format!("{:.1}", v))
} else {
Cow::Owned(v.to_string())
}
}
AttributeValue::Boolean(b) => {
if *b {
Cow::Borrowed("true")
} else {
Cow::Borrowed("false")
}
}
}
}
pub fn as_string(&self) -> Option<&str> {
if let AttributeValue::String(s) = self {
Some(s)
} else {
None
}
}
pub fn as_int(&self) -> Option<i32> {
if let AttributeValue::Int(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_int_hex(&self) -> Option<u32> {
if let AttributeValue::IntHex(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_long(&self) -> Option<i64> {
if let AttributeValue::Long(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_long_hex(&self) -> Option<u64> {
if let AttributeValue::LongHex(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_float(&self) -> Option<f32> {
if let AttributeValue::Float(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_double(&self) -> Option<f64> {
if let AttributeValue::Double(v) = self {
Some(*v)
} else {
None
}
}
pub fn as_bool(&self) -> Option<bool> {
if let AttributeValue::Boolean(b) = self {
Some(*b)
} else {
None
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
AttributeValue::BytesHex(b) | AttributeValue::BytesBase64(b) => Some(b),
_ => None,
}
}
}
pub type InternedStr = smol_str::SmolStr;
#[derive(Debug, Clone, PartialEq)]
pub struct Attribute {
pub name: InternedStr,
pub value: AttributeValue,
}
impl Attribute {
pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
self.value.as_str()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Event {
StartDocument,
EndDocument,
StartTag {
name: InternedStr,
attributes: Vec<Attribute>,
},
EndTag {
name: InternedStr,
},
Text(String),
CdataSection(String),
Comment(String),
ProcessingInstruction(String),
EntityReference(String),
IgnorableWhitespace(String),
DocDecl(String),
}
pub(crate) fn xml_escape(s: &str) -> std::borrow::Cow<'_, str> {
if s.bytes()
.any(|c| matches!(c, b'<' | b'>' | b'&' | b'"' | b'\''))
{
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
std::borrow::Cow::Owned(out)
} else {
std::borrow::Cow::Borrowed(s)
}
}
fn push_attr_value(buf: &mut String, value: &AttributeValue) {
use std::fmt::Write as _;
match value {
AttributeValue::Null => {}
AttributeValue::String(s) => buf.push_str(&xml_escape(s)),
AttributeValue::BytesHex(b) => buf.push_str(&faster_hex::hex_string(b)),
AttributeValue::BytesBase64(b) => {
base64::engine::general_purpose::STANDARD.encode_string(b, buf);
}
AttributeValue::Int(v) => {
let _ = write!(buf, "{v}");
}
AttributeValue::IntHex(v) => {
let _ = write!(buf, "{}", format_signed_hex(*v as i32 as i64));
}
AttributeValue::Long(v) => {
let _ = write!(buf, "{v}");
}
AttributeValue::LongHex(v) => {
let _ = write!(buf, "{}", format_signed_hex(*v as i64));
}
AttributeValue::Float(v) => {
if v.fract() == 0.0 && v.is_finite() {
let _ = write!(buf, "{v:.1}");
} else {
let _ = write!(buf, "{v}");
}
}
AttributeValue::Double(v) => {
if v.fract() == 0.0 && v.is_finite() {
let _ = write!(buf, "{v:.1}");
} else {
let _ = write!(buf, "{v}");
}
}
AttributeValue::Boolean(b) => buf.push_str(if *b { "true" } else { "false" }),
}
}
pub(crate) fn render_event(ev: &Event, buf: &mut String) {
match ev {
Event::StartDocument | Event::EndDocument => {}
Event::StartTag { name, attributes } => {
buf.push('<');
buf.push_str(name);
for attr in attributes {
buf.push(' ');
buf.push_str(&attr.name);
buf.push_str("=\"");
push_attr_value(buf, &attr.value);
buf.push('"');
}
buf.push('>');
}
Event::EndTag { name } => {
buf.push_str("</");
buf.push_str(name);
buf.push('>');
}
Event::Text(t) if !t.is_empty() => buf.push_str(&xml_escape(t)),
Event::CdataSection(t) => {
buf.push_str("<![CDATA[");
buf.push_str(t);
buf.push_str("]]>");
}
Event::Comment(t) => {
buf.push_str("<!--");
buf.push_str(t);
buf.push_str("-->");
}
Event::ProcessingInstruction(t) => {
buf.push_str("<?");
buf.push_str(t);
buf.push_str("?>");
}
Event::EntityReference(t) => {
buf.push('&');
buf.push_str(t);
buf.push(';');
}
Event::IgnorableWhitespace(t) => buf.push_str(t),
Event::DocDecl(t) => {
buf.push_str("<!DOCTYPE ");
buf.push_str(t);
buf.push('>');
}
_ => {}
}
}