use std::collections::HashMap;
use roxmltree::Error;
use simplecss::Declaration;
use svgtypes::FontShorthand;
use super::{AId, Attribute, Document, EId, NodeData, NodeId, NodeKind, ShortRange};
const SVG_NS: &str = "http://www.w3.org/2000/svg";
const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
const XML_NAMESPACE_NS: &str = "http://www.w3.org/XML/1998/namespace";
impl<'input> Document<'input> {
pub fn parse_tree(xml: &roxmltree::Document<'input>) -> Result<Document<'input>, Error> {
parse(xml)
}
pub(crate) fn append(&mut self, parent_id: NodeId, kind: NodeKind) -> NodeId {
let new_child_id = NodeId::from(self.nodes.len());
self.nodes.push(NodeData {
parent: Some(parent_id),
next_sibling: None,
children: None,
kind,
});
let last_child_id = self.nodes[parent_id.get_usize()].children.map(|(_, id)| id);
if let Some(id) = last_child_id {
self.nodes[id.get_usize()].next_sibling = Some(new_child_id);
}
self.nodes[parent_id.get_usize()].children = Some(
if let Some((first_child_id, _)) = self.nodes[parent_id.get_usize()].children {
(first_child_id, new_child_id)
} else {
(new_child_id, new_child_id)
},
);
new_child_id
}
fn append_attribute(&mut self, name: AId, value: roxmltree::StringStorage<'input>) {
self.attrs.push(Attribute { name, value });
}
}
fn parse<'input>(xml: &roxmltree::Document<'input>) -> Result<Document<'input>, Error> {
let mut doc = Document {
nodes: Vec::new(),
attrs: Vec::new(),
links: HashMap::new(),
};
let mut id_map = HashMap::new();
for node in xml.descendants() {
if let Some(id) = node.attribute("id") {
if !id_map.contains_key(id) {
id_map.insert(id, node);
}
}
}
doc.nodes.push(NodeData {
parent: None,
next_sibling: None,
children: None,
kind: NodeKind::Root,
});
let style_sheet = resolve_css(xml);
parse_xml_node_children(
xml.root(),
xml.root(),
doc.root().id,
&style_sheet,
false,
0,
&mut doc,
&id_map,
)?;
match doc.root().first_element_child() {
Some(child) => {
if child.tag_name() != Some(EId::Svg) {
return Err(roxmltree::Error::NoRootNode);
}
}
None => return Err(roxmltree::Error::NoRootNode),
}
let mut links = HashMap::new();
for node in doc.descendants() {
if let Some(id) = node.attribute::<&str>(AId::Id) {
links.insert(id.to_string(), node.id);
}
}
doc.links = links;
fix_recursive_patterns(&mut doc);
fix_recursive_links(EId::ClipPath, AId::ClipPath, &mut doc);
fix_recursive_links(EId::Mask, AId::Mask, &mut doc);
fix_recursive_links(EId::Filter, AId::Filter, &mut doc);
fix_recursive_fe_image(&mut doc);
Ok(doc)
}
pub(crate) fn parse_tag_name(node: roxmltree::Node) -> Option<EId> {
if !node.is_element() {
return None;
}
if node.tag_name().namespace() != Some(SVG_NS) {
return None;
}
EId::from_str(node.tag_name().name())
}
fn parse_xml_node_children<'input>(
parent: roxmltree::Node<'_, 'input>,
origin: roxmltree::Node,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
ignore_ids: bool,
depth: u32,
doc: &mut Document<'input>,
id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>,
) -> Result<(), Error> {
for node in parent.children() {
parse_xml_node(
node,
origin,
parent_id,
style_sheet,
ignore_ids,
depth,
doc,
id_map,
)?;
}
Ok(())
}
fn parse_xml_node<'input>(
node: roxmltree::Node<'_, 'input>,
origin: roxmltree::Node,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
ignore_ids: bool,
depth: u32,
doc: &mut Document<'input>,
id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>,
) -> Result<(), Error> {
if depth > 1024 {
return Err(Error::NodesLimitReached);
}
let mut tag_name = match parse_tag_name(node) {
Some(id) => id,
None => return Ok(()),
};
if tag_name == EId::Style {
return Ok(());
}
if tag_name == EId::A {
tag_name = EId::G;
}
let node_id = parse_svg_element(node, parent_id, tag_name, style_sheet, ignore_ids, doc)?;
if tag_name == EId::Text {
super::text::parse_svg_text_element(node, node_id, style_sheet, doc)?;
} else if tag_name == EId::Use {
parse_svg_use_element(node, origin, node_id, style_sheet, depth + 1, doc, id_map)?;
} else {
parse_xml_node_children(
node,
origin,
node_id,
style_sheet,
ignore_ids,
depth + 1,
doc,
id_map,
)?;
}
Ok(())
}
pub(crate) fn parse_svg_element<'input>(
xml_node: roxmltree::Node<'_, 'input>,
parent_id: NodeId,
tag_name: EId,
style_sheet: &simplecss::StyleSheet,
ignore_ids: bool,
doc: &mut Document<'input>,
) -> Result<NodeId, Error> {
let attrs_start_idx = doc.attrs.len();
for attr in xml_node.attributes() {
match attr.namespace() {
None | Some(SVG_NS) | Some(XLINK_NS) | Some(XML_NAMESPACE_NS) => {}
_ => continue,
}
let aid = match AId::from_str(attr.name()) {
Some(v) => v,
None => continue,
};
if ignore_ids && aid == AId::Id {
continue;
}
if matches!(aid, AId::MixBlendMode | AId::Isolation | AId::FontKerning) {
continue;
}
append_attribute(parent_id, tag_name, aid, attr.value_storage().clone(), doc);
}
let mut insert_attribute = |aid, value: &str| {
let idx = doc.attrs[attrs_start_idx..]
.iter_mut()
.position(|a| a.name == aid);
let added = append_attribute(
parent_id,
tag_name,
aid,
roxmltree::StringStorage::new_owned(value),
doc,
);
if added {
if let Some(idx) = idx {
let last_idx = doc.attrs.len() - 1;
doc.attrs.swap(attrs_start_idx + idx, last_idx);
doc.attrs.pop();
}
}
};
let mut write_declaration = |declaration: &Declaration| {
if declaration.name == "marker" {
insert_attribute(AId::MarkerStart, declaration.value);
insert_attribute(AId::MarkerMid, declaration.value);
insert_attribute(AId::MarkerEnd, declaration.value);
} else if declaration.name == "font" {
if let Ok(shorthand) = FontShorthand::from_str(declaration.value) {
insert_attribute(AId::FontStyle, "normal");
insert_attribute(AId::FontVariant, "normal");
insert_attribute(AId::FontWeight, "normal");
insert_attribute(AId::FontStretch, "normal");
insert_attribute(AId::LineHeight, "normal");
insert_attribute(AId::FontSizeAdjust, "none");
insert_attribute(AId::FontKerning, "auto");
insert_attribute(AId::FontVariantCaps, "normal");
insert_attribute(AId::FontVariantLigatures, "normal");
insert_attribute(AId::FontVariantNumeric, "normal");
insert_attribute(AId::FontVariantEastAsian, "normal");
insert_attribute(AId::FontVariantPosition, "normal");
shorthand
.font_stretch
.map(|s| insert_attribute(AId::FontStretch, s));
shorthand
.font_weight
.map(|s| insert_attribute(AId::FontWeight, s));
shorthand
.font_variant
.map(|s| insert_attribute(AId::FontVariant, s));
shorthand
.font_style
.map(|s| insert_attribute(AId::FontStyle, s));
insert_attribute(AId::FontSize, shorthand.font_size);
insert_attribute(AId::FontFamily, shorthand.font_family);
} else {
log::warn!(
"Failed to parse {} value: '{}'",
AId::Font,
declaration.value
);
}
} else if let Some(aid) = AId::from_str(declaration.name) {
if aid.is_presentation() {
insert_attribute(aid, declaration.value);
}
}
};
for rule in &style_sheet.rules {
if rule.selector.matches(&XmlNode(xml_node)) {
for declaration in &rule.declarations {
write_declaration(declaration);
}
}
}
if let Some(value) = xml_node.attribute("style") {
for declaration in simplecss::DeclarationTokenizer::from(value) {
write_declaration(&declaration);
}
}
if doc.nodes.len() > 1_000_000 {
return Err(Error::NodesLimitReached);
}
let node_id = doc.append(
parent_id,
NodeKind::Element {
tag_name,
attributes: ShortRange::new(attrs_start_idx as u32, doc.attrs.len() as u32),
},
);
Ok(node_id)
}
fn append_attribute<'input>(
parent_id: NodeId,
tag_name: EId,
aid: AId,
value: roxmltree::StringStorage<'input>,
doc: &mut Document<'input>,
) -> bool {
match aid {
AId::Style |
AId::Class => return false,
_ => {}
}
if tag_name == EId::Tspan && aid == AId::Href {
return false;
}
if aid.allows_inherit_value() && &*value == "inherit" {
return resolve_inherit(parent_id, aid, doc);
}
doc.append_attribute(aid, value);
true
}
fn resolve_inherit(parent_id: NodeId, aid: AId, doc: &mut Document) -> bool {
if aid.is_inheritable() {
let node_id = doc
.get(parent_id)
.ancestors()
.find(|n| n.has_attribute(aid))
.map(|n| n.id);
if let Some(node_id) = node_id {
if let Some(attr) = doc
.get(node_id)
.attributes()
.iter()
.find(|a| a.name == aid)
.cloned()
{
doc.attrs.push(Attribute {
name: aid,
value: attr.value,
});
return true;
}
}
} else {
if let Some(attr) = doc
.get(parent_id)
.attributes()
.iter()
.find(|a| a.name == aid)
.cloned()
{
doc.attrs.push(Attribute {
name: aid,
value: attr.value,
});
return true;
}
}
let value = match aid {
AId::ImageRendering | AId::ShapeRendering | AId::TextRendering => "auto",
AId::ClipPath
| AId::Filter
| AId::MarkerEnd
| AId::MarkerMid
| AId::MarkerStart
| AId::Mask
| AId::Stroke
| AId::StrokeDasharray
| AId::TextDecoration => "none",
AId::FontStretch
| AId::FontStyle
| AId::FontVariant
| AId::FontWeight
| AId::LetterSpacing
| AId::WordSpacing => "normal",
AId::Fill | AId::FloodColor | AId::StopColor => "black",
AId::FillOpacity
| AId::FloodOpacity
| AId::Opacity
| AId::StopOpacity
| AId::StrokeOpacity => "1",
AId::ClipRule | AId::FillRule => "nonzero",
AId::BaselineShift => "baseline",
AId::ColorInterpolationFilters => "linearRGB",
AId::Direction => "ltr",
AId::Display => "inline",
AId::FontSize => "medium",
AId::Overflow => "visible",
AId::StrokeDashoffset => "0",
AId::StrokeLinecap => "butt",
AId::StrokeLinejoin => "miter",
AId::StrokeMiterlimit => "4",
AId::StrokeWidth => "1",
AId::TextAnchor => "start",
AId::Visibility => "visible",
AId::WritingMode => "lr-tb",
_ => return false,
};
doc.append_attribute(aid, roxmltree::StringStorage::Borrowed(value));
true
}
fn resolve_href<'a, 'input: 'a>(
node: roxmltree::Node<'a, 'input>,
id_map: &HashMap<&str, roxmltree::Node<'a, 'input>>,
) -> Option<roxmltree::Node<'a, 'input>> {
let link_value = node
.attribute((XLINK_NS, "href"))
.or_else(|| node.attribute("href"))?;
let link_id = svgtypes::IRI::from_str(link_value).ok()?.0;
id_map.get(link_id).copied()
}
fn parse_svg_use_element<'input>(
node: roxmltree::Node<'_, 'input>,
origin: roxmltree::Node,
parent_id: NodeId,
style_sheet: &simplecss::StyleSheet,
depth: u32,
doc: &mut Document<'input>,
id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>,
) -> Result<(), Error> {
let link = match resolve_href(node, id_map) {
Some(v) => v,
None => return Ok(()),
};
if link == node || link == origin {
log::warn!(
"Recursive 'use' detected. '{}' will be skipped.",
node.attribute((SVG_NS, "id")).unwrap_or_default()
);
return Ok(());
}
if parse_tag_name(link).is_none() {
return Ok(());
}
let mut is_recursive = false;
for link_child in link
.descendants()
.skip(1)
.filter(|n| n.has_tag_name((SVG_NS, "use")))
{
if let Some(link2) = resolve_href(link_child, id_map) {
if link2 == node || link2 == link {
is_recursive = true;
break;
}
}
}
if is_recursive {
log::warn!(
"Recursive 'use' detected. '{}' will be skipped.",
node.attribute((SVG_NS, "id")).unwrap_or_default()
);
return Ok(());
}
parse_xml_node(
link,
node,
parent_id,
style_sheet,
true,
depth + 1,
doc,
id_map,
)
}
fn resolve_css<'a>(xml: &'a roxmltree::Document<'a>) -> simplecss::StyleSheet<'a> {
let mut sheet = simplecss::StyleSheet::new();
for node in xml.descendants().filter(|n| n.has_tag_name("style")) {
match node.attribute("type") {
Some("text/css") => {}
Some(_) => continue,
None => {}
}
let text = match node.text() {
Some(v) => v,
None => continue,
};
sheet.parse_more(text);
}
sheet
}
struct XmlNode<'a, 'input: 'a>(roxmltree::Node<'a, 'input>);
impl simplecss::Element for XmlNode<'_, '_> {
fn parent_element(&self) -> Option<Self> {
self.0.parent_element().map(XmlNode)
}
fn prev_sibling_element(&self) -> Option<Self> {
self.0.prev_sibling_element().map(XmlNode)
}
fn has_local_name(&self, local_name: &str) -> bool {
self.0.tag_name().name() == local_name
}
fn attribute_matches(&self, local_name: &str, operator: simplecss::AttributeOperator) -> bool {
match self.0.attribute(local_name) {
Some(value) => operator.matches(value),
None => false,
}
}
fn pseudo_class_matches(&self, class: simplecss::PseudoClass) -> bool {
match class {
simplecss::PseudoClass::FirstChild => self.prev_sibling_element().is_none(),
_ => false, }
}
}
fn fix_recursive_patterns(doc: &mut Document) {
while let Some(node_id) = find_recursive_pattern(AId::Fill, doc) {
let idx = doc.get(node_id).attribute_id(AId::Fill).unwrap();
doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
}
while let Some(node_id) = find_recursive_pattern(AId::Stroke, doc) {
let idx = doc.get(node_id).attribute_id(AId::Stroke).unwrap();
doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
}
}
fn find_recursive_pattern(aid: AId, doc: &mut Document) -> Option<NodeId> {
for pattern_node in doc
.root()
.descendants()
.filter(|n| n.tag_name() == Some(EId::Pattern))
{
for node in pattern_node.descendants() {
let value = match node.attribute(aid) {
Some(v) => v,
None => continue,
};
if let Ok(svgtypes::Paint::FuncIRI(link_id, _)) = svgtypes::Paint::from_str(value) {
if link_id == pattern_node.element_id() {
return Some(node.id);
} else {
if let Some(linked_node) = doc.element_by_id(link_id) {
for node2 in linked_node.descendants() {
let value2 = match node2.attribute(aid) {
Some(v) => v,
None => continue,
};
if let Ok(svgtypes::Paint::FuncIRI(link_id2, _)) =
svgtypes::Paint::from_str(value2)
{
if link_id2 == pattern_node.element_id() {
return Some(node2.id);
}
}
}
}
}
}
}
}
None
}
fn fix_recursive_links(eid: EId, aid: AId, doc: &mut Document) {
while let Some(node_id) = find_recursive_link(eid, aid, doc) {
let idx = doc.get(node_id).attribute_id(aid).unwrap();
doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
}
}
fn find_recursive_link(eid: EId, aid: AId, doc: &Document) -> Option<NodeId> {
for node in doc
.root()
.descendants()
.filter(|n| n.tag_name() == Some(eid))
{
for child in node.descendants() {
if let Some(link) = child.node_attribute(aid) {
if link == node {
return Some(child.id);
} else {
for node2 in link.descendants() {
if let Some(link2) = node2.node_attribute(aid) {
if link2 == node {
return Some(node2.id);
}
}
}
}
}
}
}
None
}
fn fix_recursive_fe_image(doc: &mut Document) {
let mut ids = Vec::new();
for fe_node in doc
.root()
.descendants()
.filter(|n| n.tag_name() == Some(EId::FeImage))
{
if let Some(link) = fe_node.node_attribute(AId::Href) {
if let Some(filter_uri) = link.attribute::<&str>(AId::Filter) {
let filter_id = fe_node.parent().unwrap().element_id();
for func in svgtypes::FilterValueListParser::from(filter_uri).flatten() {
if let svgtypes::FilterValue::Url(url) = func {
if url == filter_id {
ids.push(link.id);
}
}
}
}
}
}
for id in ids {
let idx = doc.get(id).attribute_id(AId::Filter).unwrap();
doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
}
}