#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
#![warn(missing_copy_implementations)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::uninlined_format_args)]
use std::collections::HashMap;
use std::num::NonZeroU32;
#[rustfmt::skip] mod names;
mod parse;
mod text;
pub use names::{AttributeId, ElementId};
pub use roxmltree::{self, Error};
pub struct Document<'input> {
nodes: Vec<NodeData>,
attrs: Vec<Attribute<'input>>,
links: HashMap<String, NodeId>,
}
impl<'input> Document<'input> {
#[inline]
pub fn root<'a>(&'a self) -> Node<'a, 'input> {
Node {
id: NodeId::new(0),
d: &self.nodes[0],
doc: self,
}
}
#[inline]
pub fn root_element<'a>(&'a self) -> Node<'a, 'input> {
self.root().first_element_child().unwrap()
}
#[inline]
pub fn descendants<'a>(&'a self) -> Descendants<'a, 'input> {
self.root().descendants()
}
#[inline]
pub fn element_by_id<'a>(&'a self, id: &str) -> Option<Node<'a, 'input>> {
let node_id = self.links.get(id)?;
Some(self.get(*node_id))
}
#[inline]
fn get<'a>(&'a self, id: NodeId) -> Node<'a, 'input> {
Node {
id,
d: &self.nodes[id.get_usize()],
doc: self,
}
}
}
impl std::fmt::Debug for Document<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
if !self.root().has_children() {
return write!(f, "Document []");
}
macro_rules! writeln_indented {
($depth:expr, $f:expr, $fmt:expr) => {
for _ in 0..$depth { write!($f, " ")?; }
writeln!($f, $fmt)?;
};
($depth:expr, $f:expr, $fmt:expr, $($arg:tt)*) => {
for _ in 0..$depth { write!($f, " ")?; }
writeln!($f, $fmt, $($arg)*)?;
};
}
fn print_children(
parent: Node,
depth: usize,
f: &mut std::fmt::Formatter,
) -> Result<(), std::fmt::Error> {
for child in parent.children() {
if child.is_element() {
writeln_indented!(depth, f, "Element {{");
writeln_indented!(depth, f, " tag_name: {:?}", child.tag_name());
if !child.attributes().is_empty() {
writeln_indented!(depth + 1, f, "attributes: [");
for attr in child.attributes() {
writeln_indented!(depth + 2, f, "{:?}", attr);
}
writeln_indented!(depth + 1, f, "]");
}
if child.has_children() {
writeln_indented!(depth, f, " children: [");
print_children(child, depth + 2, f)?;
writeln_indented!(depth, f, " ]");
}
writeln_indented!(depth, f, "}}");
} else {
writeln_indented!(depth, f, "{:?}", child);
}
}
Ok(())
}
writeln!(f, "Document [")?;
print_children(self.root(), 1, f)?;
writeln!(f, "]")?;
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
struct ShortRange {
start: u32,
end: u32,
}
impl ShortRange {
#[inline]
fn new(start: u32, end: u32) -> Self {
ShortRange { start, end }
}
#[inline]
fn to_urange(self) -> std::ops::Range<usize> {
self.start as usize..self.end as usize
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
struct NodeId(NonZeroU32);
impl NodeId {
#[inline]
fn new(id: u32) -> Self {
debug_assert!(id < core::u32::MAX);
NodeId(NonZeroU32::new(id + 1).unwrap())
}
#[inline]
fn get(self) -> u32 {
self.0.get() - 1
}
#[inline]
fn get_usize(self) -> usize {
self.get() as usize
}
}
impl From<usize> for NodeId {
#[inline]
fn from(id: usize) -> Self {
debug_assert!(id <= core::u32::MAX as usize);
NodeId::new(id as u32)
}
}
enum NodeKind {
Root,
Element {
tag_name: ElementId,
attributes: ShortRange,
},
Text(String),
}
struct NodeData {
parent: Option<NodeId>,
next_sibling: Option<NodeId>,
children: Option<(NodeId, NodeId)>,
kind: NodeKind,
}
#[derive(Clone)]
pub struct Attribute<'input> {
pub name: AttributeId,
pub value: roxmltree::StringStorage<'input>,
}
impl std::fmt::Debug for Attribute<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(
f,
"Attribute {{ name: {:?}, value: {} }}",
self.name, self.value
)
}
}
#[derive(Clone, Copy)]
pub struct Node<'a, 'input: 'a> {
id: NodeId,
doc: &'a Document<'input>,
d: &'a NodeData,
}
impl Eq for Node<'_, '_> {}
impl PartialEq for Node<'_, '_> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.id == other.id && std::ptr::eq(self.doc, other.doc) && std::ptr::eq(self.d, other.d)
}
}
impl<'a, 'input: 'a> Node<'a, 'input> {
#[inline]
fn id(&self) -> NodeId {
self.id
}
#[inline]
pub fn is_element(&self) -> bool {
matches!(self.d.kind, NodeKind::Element { .. })
}
#[inline]
pub fn is_text(&self) -> bool {
matches!(self.d.kind, NodeKind::Text(_))
}
#[inline]
pub fn document(&self) -> &'a Document<'input> {
self.doc
}
#[inline]
pub fn tag_name(&self) -> Option<ElementId> {
match self.d.kind {
NodeKind::Element { tag_name, .. } => Some(tag_name),
_ => None,
}
}
#[inline]
pub fn element_id(&self) -> &'a str {
self.attribute(AttributeId::Id).unwrap_or("")
}
#[inline]
pub fn attribute(&self, aid: AttributeId) -> Option<&'a str> {
self.attributes()
.iter()
.find(|a| a.name == aid)
.map(|a| a.value.as_str())
}
#[inline]
fn node_attribute(&self, aid: AttributeId) -> Option<Node<'a, 'input>> {
let value = self.attribute(aid)?;
let id = if aid == AttributeId::Href {
svgtypes::IRI::from_str(value).ok().map(|v| v.0)
} else {
svgtypes::FuncIRI::from_str(value).ok().map(|v| v.0)
}?;
self.document().element_by_id(id)
}
#[inline]
pub fn has_attribute(&self, aid: AttributeId) -> bool {
self.attributes().iter().any(|a| a.name == aid)
}
#[inline]
pub fn attributes(&self) -> &'a [Attribute<'input>] {
match self.d.kind {
NodeKind::Element { ref attributes, .. } => &self.doc.attrs[attributes.to_urange()],
_ => &[],
}
}
#[inline]
fn attribute_id(&self, aid: AttributeId) -> Option<usize> {
match self.d.kind {
NodeKind::Element { ref attributes, .. } => {
let idx = self.attributes().iter().position(|attr| attr.name == aid)?;
Some(attributes.start as usize + idx)
}
_ => None,
}
}
#[inline]
pub fn find_attribute(&self, aid: AttributeId) -> Option<Node<'a, 'input>> {
if aid.is_inheritable() {
for n in self.ancestors() {
if n.has_attribute(aid) {
return Some(n);
}
}
None
} else {
if self.has_attribute(aid) {
Some(*self)
} else {
let n = self.parent_element()?;
if n.has_attribute(aid) {
Some(n)
} else {
None
}
}
}
}
#[inline]
pub fn text(&self) -> &'a str {
match self.d.kind {
NodeKind::Element { .. } => match self.first_child() {
Some(child) if child.is_text() => match self.doc.nodes[child.id.get_usize()].kind {
NodeKind::Text(ref text) => text,
_ => "",
},
_ => "",
},
NodeKind::Text(ref text) => text,
_ => "",
}
}
#[inline]
pub fn parent(&self) -> Option<Self> {
self.d.parent.map(|id| self.doc.get(id))
}
#[inline]
pub fn parent_element(&self) -> Option<Self> {
self.ancestors().skip(1).find(|n| n.is_element())
}
#[inline]
pub fn next_sibling(&self) -> Option<Self> {
self.d.next_sibling.map(|id| self.doc.get(id))
}
#[inline]
pub fn first_child(&self) -> Option<Self> {
self.d.children.map(|(id, _)| self.doc.get(id))
}
#[inline]
pub fn first_element_child(&self) -> Option<Self> {
self.children().find(|n| n.is_element())
}
#[inline]
pub fn last_child(&self) -> Option<Self> {
self.d.children.map(|(_, id)| self.doc.get(id))
}
#[inline]
pub fn has_children(&self) -> bool {
self.d.children.is_some()
}
#[inline]
pub fn ancestors(&self) -> Ancestors<'a, 'input> {
Ancestors(Some(*self))
}
#[inline]
pub fn children(&self) -> Children<'a, 'input> {
Children {
front: self.first_child(),
back: self.last_child(),
}
}
#[inline]
fn traverse(&self) -> Traverse<'a, 'input> {
Traverse {
root: *self,
edge: None,
}
}
#[inline]
pub fn descendants(&self) -> Descendants<'a, 'input> {
Descendants(self.traverse())
}
#[inline]
pub fn href_iter(&self) -> HrefIter<'a, 'input> {
HrefIter {
doc: self.document(),
origin: self.id(),
curr: self.id(),
is_first: true,
is_finished: false,
}
}
}
impl std::fmt::Debug for Node<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
match self.d.kind {
NodeKind::Root => write!(f, "Root"),
NodeKind::Element { .. } => {
write!(
f,
"Element {{ tag_name: {:?}, attributes: {:?} }}",
self.tag_name(),
self.attributes()
)
}
NodeKind::Text(ref text) => write!(f, "Text({:?})", text),
}
}
}
#[derive(Clone, Debug)]
pub struct Ancestors<'a, 'input: 'a>(Option<Node<'a, 'input>>);
impl<'a, 'input: 'a> Iterator for Ancestors<'a, 'input> {
type Item = Node<'a, 'input>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let node = self.0.take();
self.0 = node.as_ref().and_then(Node::parent);
node
}
}
#[derive(Clone, Debug)]
pub struct Children<'a, 'input: 'a> {
front: Option<Node<'a, 'input>>,
back: Option<Node<'a, 'input>>,
}
impl<'a, 'input: 'a> Iterator for Children<'a, 'input> {
type Item = Node<'a, 'input>;
fn next(&mut self) -> Option<Self::Item> {
let node = self.front.take();
if self.front == self.back {
self.back = None;
} else {
self.front = node.as_ref().and_then(Node::next_sibling);
}
node
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Edge<'a, 'input: 'a> {
Open(Node<'a, 'input>),
Close(Node<'a, 'input>),
}
#[derive(Clone, Debug)]
struct Traverse<'a, 'input: 'a> {
root: Node<'a, 'input>,
edge: Option<Edge<'a, 'input>>,
}
impl<'a, 'input: 'a> Iterator for Traverse<'a, 'input> {
type Item = Edge<'a, 'input>;
fn next(&mut self) -> Option<Self::Item> {
match self.edge {
Some(Edge::Open(node)) => {
self.edge = Some(match node.first_child() {
Some(first_child) => Edge::Open(first_child),
None => Edge::Close(node),
});
}
Some(Edge::Close(node)) => {
if node == self.root {
self.edge = None;
} else if let Some(next_sibling) = node.next_sibling() {
self.edge = Some(Edge::Open(next_sibling));
} else {
self.edge = node.parent().map(Edge::Close);
}
}
None => {
self.edge = Some(Edge::Open(self.root));
}
}
self.edge
}
}
#[derive(Clone, Debug)]
pub struct Descendants<'a, 'input: 'a>(Traverse<'a, 'input>);
impl<'a, 'input: 'a> Iterator for Descendants<'a, 'input> {
type Item = Node<'a, 'input>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
for edge in &mut self.0 {
if let Edge::Open(node) = edge {
return Some(node);
}
}
None
}
}
#[derive(Clone, Debug)]
pub struct HrefIter<'a, 'input: 'a> {
doc: &'a Document<'input>,
origin: NodeId,
curr: NodeId,
is_first: bool,
is_finished: bool,
}
impl<'a, 'input: 'a> Iterator for HrefIter<'a, 'input> {
type Item = Node<'a, 'input>;
fn next(&mut self) -> Option<Self::Item> {
if self.is_finished {
return None;
}
if self.is_first {
self.is_first = false;
return Some(self.doc.get(self.curr));
}
if let Some(link) = self.doc.get(self.curr).node_attribute(AttributeId::Href) {
if link.id() == self.curr || link.id() == self.origin {
log::warn!(
"Element '#{}' cannot reference itself via 'xlink:href'.",
self.doc.get(self.origin).element_id()
);
self.is_finished = true;
return None;
}
self.curr = link.id();
Some(self.doc.get(self.curr))
} else {
None
}
}
}
impl ElementId {
pub fn is_graphic(&self) -> bool {
matches!(
self,
ElementId::Circle
| ElementId::Ellipse
| ElementId::Image
| ElementId::Line
| ElementId::Path
| ElementId::Polygon
| ElementId::Polyline
| ElementId::Rect
| ElementId::Text
| ElementId::Use
)
}
pub fn is_gradient(&self) -> bool {
matches!(self, ElementId::LinearGradient | ElementId::RadialGradient)
}
pub fn is_paint_server(&self) -> bool {
matches!(
self,
ElementId::LinearGradient | ElementId::RadialGradient | ElementId::Pattern
)
}
}
impl AttributeId {
fn is_presentation(&self) -> bool {
matches!(
self,
AttributeId::AlignmentBaseline
| AttributeId::BaselineShift
| AttributeId::ClipPath
| AttributeId::ClipRule
| AttributeId::Color
| AttributeId::ColorInterpolation
| AttributeId::ColorInterpolationFilters
| AttributeId::ColorRendering
| AttributeId::Direction
| AttributeId::Display
| AttributeId::DominantBaseline
| AttributeId::Fill
| AttributeId::FillOpacity
| AttributeId::FillRule
| AttributeId::Filter
| AttributeId::FloodColor
| AttributeId::FloodOpacity
| AttributeId::FontFamily
| AttributeId::FontKerning | AttributeId::FontSize
| AttributeId::FontSizeAdjust
| AttributeId::FontStretch
| AttributeId::FontStyle
| AttributeId::FontVariant
| AttributeId::FontWeight
| AttributeId::GlyphOrientationHorizontal
| AttributeId::GlyphOrientationVertical
| AttributeId::ImageRendering
| AttributeId::Isolation | AttributeId::LetterSpacing
| AttributeId::LightingColor
| AttributeId::MarkerEnd
| AttributeId::MarkerMid
| AttributeId::MarkerStart
| AttributeId::Mask
| AttributeId::MixBlendMode | AttributeId::Opacity
| AttributeId::Overflow
| AttributeId::PaintOrder
| AttributeId::ShapeRendering
| AttributeId::StopColor
| AttributeId::StopOpacity
| AttributeId::Stroke
| AttributeId::StrokeDasharray
| AttributeId::StrokeDashoffset
| AttributeId::StrokeLinecap
| AttributeId::StrokeLinejoin
| AttributeId::StrokeMiterlimit
| AttributeId::StrokeOpacity
| AttributeId::StrokeWidth
| AttributeId::TextAnchor
| AttributeId::TextDecoration
| AttributeId::TextOverflow
| AttributeId::TextRendering
| AttributeId::Transform
| AttributeId::UnicodeBidi
| AttributeId::VectorEffect
| AttributeId::Visibility
| AttributeId::WhiteSpace
| AttributeId::WordSpacing
| AttributeId::WritingMode
)
}
fn is_inheritable(&self) -> bool {
if self.is_presentation() {
!is_non_inheritable(*self)
} else {
false
}
}
fn allows_inherit_value(&self) -> bool {
matches!(
self,
AttributeId::AlignmentBaseline
| AttributeId::BaselineShift
| AttributeId::ClipPath
| AttributeId::ClipRule
| AttributeId::Color
| AttributeId::ColorInterpolationFilters
| AttributeId::Direction
| AttributeId::Display
| AttributeId::DominantBaseline
| AttributeId::Fill
| AttributeId::FillOpacity
| AttributeId::FillRule
| AttributeId::Filter
| AttributeId::FloodColor
| AttributeId::FloodOpacity
| AttributeId::FontFamily
| AttributeId::FontKerning
| AttributeId::FontSize
| AttributeId::FontStretch
| AttributeId::FontStyle
| AttributeId::FontVariant
| AttributeId::FontWeight
| AttributeId::ImageRendering
| AttributeId::Kerning
| AttributeId::LetterSpacing
| AttributeId::MarkerEnd
| AttributeId::MarkerMid
| AttributeId::MarkerStart
| AttributeId::Mask
| AttributeId::Opacity
| AttributeId::Overflow
| AttributeId::ShapeRendering
| AttributeId::StopColor
| AttributeId::StopOpacity
| AttributeId::Stroke
| AttributeId::StrokeDasharray
| AttributeId::StrokeDashoffset
| AttributeId::StrokeLinecap
| AttributeId::StrokeLinejoin
| AttributeId::StrokeMiterlimit
| AttributeId::StrokeOpacity
| AttributeId::StrokeWidth
| AttributeId::TextAnchor
| AttributeId::TextDecoration
| AttributeId::TextRendering
| AttributeId::Visibility
| AttributeId::WordSpacing
| AttributeId::WritingMode
)
}
}
fn is_non_inheritable(id: AttributeId) -> bool {
matches!(
id,
AttributeId::AlignmentBaseline
| AttributeId::BaselineShift
| AttributeId::ClipPath
| AttributeId::Display
| AttributeId::DominantBaseline
| AttributeId::Filter
| AttributeId::FloodColor
| AttributeId::FloodOpacity
| AttributeId::Mask
| AttributeId::Opacity
| AttributeId::Overflow
| AttributeId::LightingColor
| AttributeId::StopColor
| AttributeId::StopOpacity
| AttributeId::TextDecoration
| AttributeId::Transform
)
}