use crate::AttributeSort;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedTag {
pub name: String,
pub attributes: Vec<ParsedAttribute>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAttribute {
pub name: String,
pub value: Option<ParsedAttributeValue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAttributeValue {
pub raw: String,
pub original_quote: Option<char>,
}
pub fn reorder_attributes(attributes: &mut [ParsedAttribute], mode: AttributeSort) {
match mode {
AttributeSort::None => {}
AttributeSort::Alphabetical => {
attributes.sort_by_key(|attribute| attribute.name.to_ascii_lowercase());
}
AttributeSort::Canonical => {
attributes.sort_by_key(|attribute| canonical_attribute_sort_key(&attribute.name));
}
}
}
#[repr(u8)]
enum CanonicalGroup {
Identity = 0,
Geometry = 1,
Drawing = 2,
Reference = 3,
Presentation = 4,
Other = 5,
Namespace = 6,
Version = 7,
}
pub fn canonical_group_key(name: &str) -> u8 {
let lowered = name.to_ascii_lowercase();
canonical_attribute_sort_key(&lowered).0
}
fn canonical_attribute_sort_key(name: &str) -> (u8, u16, String) {
let lowered = name.to_ascii_lowercase();
if lowered == "id" {
return (CanonicalGroup::Identity as u8, 0, lowered);
}
if lowered == "class" {
return (CanonicalGroup::Identity as u8, 1, lowered);
}
if let Some(order) = canonical_geometry_order(&lowered) {
return (CanonicalGroup::Geometry as u8, order, lowered);
}
if let Some(order) = canonical_drawing_order(&lowered) {
return (CanonicalGroup::Drawing as u8, order, lowered);
}
if let Some(order) = canonical_reference_order(&lowered) {
return (CanonicalGroup::Reference as u8, order, lowered);
}
if let Some(order) = canonical_presentation_order(&lowered) {
return (CanonicalGroup::Presentation as u8, order, lowered);
}
if lowered == "version" {
return (CanonicalGroup::Version as u8, 0, lowered);
}
if lowered == "xmlns" {
return (CanonicalGroup::Namespace as u8, 0, lowered);
}
if lowered.starts_with("xmlns:") {
return (CanonicalGroup::Namespace as u8, 1, lowered);
}
(CanonicalGroup::Other as u8, u16::MAX, lowered)
}
fn canonical_geometry_order(name: &str) -> Option<u16> {
let order = [
"x",
"y",
"x1",
"y1",
"x2",
"y2",
"cx",
"cy",
"r",
"rx",
"ry",
"width",
"height",
"viewbox",
"preserveaspectratio",
];
order
.iter()
.position(|candidate| *candidate == name)
.and_then(|i| u16::try_from(i).ok())
}
fn canonical_drawing_order(name: &str) -> Option<u16> {
let order = ["d", "points", "transform"];
order
.iter()
.position(|candidate| *candidate == name)
.and_then(|i| u16::try_from(i).ok())
}
fn canonical_reference_order(name: &str) -> Option<u16> {
let order = ["href", "xlink:href"];
order
.iter()
.position(|candidate| *candidate == name)
.and_then(|i| u16::try_from(i).ok())
}
fn canonical_presentation_order(name: &str) -> Option<u16> {
let anchors = ["fill", "stroke", "stroke-width", "opacity", "style"];
if let Some(i) = anchors.iter().position(|candidate| *candidate == name) {
return u16::try_from(i).ok();
}
if name.starts_with("stroke-")
|| matches!(
name,
"fill-opacity"
| "fill-rule"
| "stroke-opacity"
| "color"
| "visibility"
| "display"
| "paint-order"
| "vector-effect"
| "shape-rendering"
| "image-rendering"
| "text-rendering"
| "color-interpolation"
| "color-interpolation-filters"
)
{
return Some(u16::MAX - 1);
}
None
}
pub fn parse_tag(raw: &str, self_closing: bool) -> Option<ParsedTag> {
let trimmed = raw.trim();
if !trimmed.starts_with('<') {
return None;
}
let inner = if self_closing {
if let Some(stripped) = trimmed.strip_suffix("/>") {
stripped
} else {
trimmed.strip_suffix(" />")?
}
} else {
trimmed.strip_suffix('>')?
};
let inner = inner.strip_prefix('<')?.trim();
if inner.is_empty() {
return None;
}
let mut i = 0usize;
let bytes = inner.as_bytes();
while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
i += 1;
}
if i == 0 {
return None;
}
let name = inner[..i].to_string();
let mut attrs = Vec::new();
let mut j = i;
while j < bytes.len() {
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j >= bytes.len() {
break;
}
let start = j;
while j < bytes.len() && !bytes[j].is_ascii_whitespace() && bytes[j] != b'=' {
j += 1;
}
if start == j {
break;
}
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j < bytes.len() && bytes[j] == b'=' {
j += 1;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j < bytes.len() && (bytes[j] == b'"' || bytes[j] == b'\'') {
let quote = bytes[j];
j += 1;
let found = loop {
if j >= bytes.len() {
break false;
}
if bytes[j] == quote {
j += 1;
break true;
}
j += 1;
};
if !found {
return None;
}
} else {
while j < bytes.len() && !bytes[j].is_ascii_whitespace() {
j += 1;
}
}
}
let attribute = inner[start..j].trim();
if !attribute.is_empty() {
attrs.push(parse_attribute(attribute));
}
}
Some(ParsedTag {
name,
attributes: attrs,
})
}
pub fn parse_attribute(attribute: &str) -> ParsedAttribute {
let trimmed = attribute.trim();
if let Some((name, raw_value)) = trimmed.split_once('=') {
let name = name.trim().to_string();
let raw_value = raw_value.trim();
let value = raw_value
.strip_prefix('"')
.and_then(|v| v.strip_suffix('"'))
.map(|inner| (inner, '"'))
.or_else(|| {
raw_value
.strip_prefix('\'')
.and_then(|v| v.strip_suffix('\''))
.map(|inner| (inner, '\''))
})
.map_or_else(
|| ParsedAttributeValue {
raw: raw_value.to_string(),
original_quote: None,
},
|(inner, quote)| ParsedAttributeValue {
raw: inner.to_string(),
original_quote: Some(quote),
},
);
ParsedAttribute {
name,
value: Some(value),
}
} else {
ParsedAttribute {
name: trimmed.to_string(),
value: None,
}
}
}