Skip to main content

usvg/parser/svgtree/
parse.rs

1// Copyright 2021 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::collections::HashMap;
5
6use roxmltree::Error;
7use simplecss::Declaration;
8use svgtypes::FontShorthand;
9
10use super::{AId, Attribute, Document, EId, NodeData, NodeId, NodeKind, ShortRange};
11
12const SVG_NS: &str = "http://www.w3.org/2000/svg";
13const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
14const XML_NAMESPACE_NS: &str = "http://www.w3.org/XML/1998/namespace";
15
16impl<'input> Document<'input> {
17    /// Parses a [`Document`] from a [`roxmltree::Document`].
18    pub fn parse_tree(
19        xml: &roxmltree::Document<'input>,
20        injected_stylesheet: Option<&'input str>,
21    ) -> Result<Document<'input>, Error> {
22        parse(xml, injected_stylesheet)
23    }
24
25    pub(crate) fn append(&mut self, parent_id: NodeId, kind: NodeKind) -> NodeId {
26        let new_child_id = NodeId::from(self.nodes.len());
27        self.nodes.push(NodeData {
28            parent: Some(parent_id),
29            next_sibling: None,
30            children: None,
31            kind,
32        });
33
34        let last_child_id = self.nodes[parent_id.get_usize()].children.map(|(_, id)| id);
35
36        if let Some(id) = last_child_id {
37            self.nodes[id.get_usize()].next_sibling = Some(new_child_id);
38        }
39
40        self.nodes[parent_id.get_usize()].children = Some(
41            if let Some((first_child_id, _)) = self.nodes[parent_id.get_usize()].children {
42                (first_child_id, new_child_id)
43            } else {
44                (new_child_id, new_child_id)
45            },
46        );
47
48        new_child_id
49    }
50
51    fn append_attribute(
52        &mut self,
53        name: AId,
54        value: roxmltree::StringStorage<'input>,
55        important: bool,
56    ) {
57        self.attrs.push(Attribute {
58            name,
59            value,
60            important,
61        });
62    }
63}
64
65fn parse<'input>(
66    xml: &roxmltree::Document<'input>,
67    injected_stylesheet: Option<&'input str>,
68) -> Result<Document<'input>, Error> {
69    let mut doc = Document {
70        nodes: Vec::new(),
71        attrs: Vec::new(),
72        links: HashMap::new(),
73    };
74
75    // build a map of id -> node for resolve_href
76    let mut id_map = HashMap::new();
77    for node in xml.descendants() {
78        if let Some(id) = node.attribute("id") {
79            if !id_map.contains_key(id) {
80                id_map.insert(id, node);
81            }
82        }
83    }
84
85    // Add a root node.
86    doc.nodes.push(NodeData {
87        parent: None,
88        next_sibling: None,
89        children: None,
90        kind: NodeKind::Root,
91    });
92
93    let style_sheet = resolve_css(xml, injected_stylesheet);
94
95    parse_xml_node_children(
96        xml.root(),
97        xml.root(),
98        doc.root().id,
99        &style_sheet,
100        false,
101        0,
102        &mut doc,
103        &id_map,
104    )?;
105
106    // Check that the root element is `svg`.
107    match doc.root().first_element_child() {
108        Some(child) => {
109            if child.tag_name() != Some(EId::Svg) {
110                return Err(roxmltree::Error::NoRootNode);
111            }
112        }
113        None => return Err(roxmltree::Error::NoRootNode),
114    }
115
116    // Collect all elements with `id` attribute.
117    let mut links = HashMap::new();
118    for node in doc.descendants() {
119        if let Some(id) = node.attribute::<&str>(AId::Id) {
120            links.insert(id.to_string(), node.id);
121        }
122    }
123    doc.links = links;
124
125    fix_recursive_patterns(&mut doc);
126    fix_recursive_links(EId::ClipPath, AId::ClipPath, &mut doc);
127    fix_recursive_links(EId::Mask, AId::Mask, &mut doc);
128    fix_recursive_links(EId::Filter, AId::Filter, &mut doc);
129    fix_recursive_fe_image(&mut doc);
130
131    Ok(doc)
132}
133
134pub(crate) fn parse_tag_name(node: roxmltree::Node) -> Option<EId> {
135    if !node.is_element() {
136        return None;
137    }
138
139    if !matches!(node.tag_name().namespace(), None | Some(SVG_NS)) {
140        return None;
141    }
142
143    EId::from_str(node.tag_name().name())
144}
145
146fn parse_xml_node_children<'input>(
147    parent: roxmltree::Node<'_, 'input>,
148    origin: roxmltree::Node,
149    parent_id: NodeId,
150    style_sheet: &simplecss::StyleSheet,
151    ignore_ids: bool,
152    depth: u32,
153    doc: &mut Document<'input>,
154    id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>,
155) -> Result<(), Error> {
156    for node in parent.children() {
157        parse_xml_node(
158            node,
159            origin,
160            parent_id,
161            style_sheet,
162            ignore_ids,
163            depth,
164            doc,
165            id_map,
166        )?;
167    }
168
169    Ok(())
170}
171
172fn parse_xml_node<'input>(
173    node: roxmltree::Node<'_, 'input>,
174    origin: roxmltree::Node,
175    parent_id: NodeId,
176    style_sheet: &simplecss::StyleSheet,
177    ignore_ids: bool,
178    depth: u32,
179    doc: &mut Document<'input>,
180    id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>,
181) -> Result<(), Error> {
182    if depth > 1024 {
183        return Err(Error::NodesLimitReached);
184    }
185
186    let mut tag_name = match parse_tag_name(node) {
187        Some(id) => id,
188        None => return Ok(()),
189    };
190
191    if tag_name == EId::Style {
192        return Ok(());
193    }
194
195    // TODO: remove?
196    // Treat links as groups.
197    if tag_name == EId::A {
198        tag_name = EId::G;
199    }
200
201    let node_id = parse_svg_element(node, parent_id, tag_name, style_sheet, ignore_ids, doc)?;
202    if tag_name == EId::Text {
203        super::text::parse_svg_text_element(node, node_id, style_sheet, doc)?;
204    } else if tag_name == EId::Use {
205        parse_svg_use_element(node, origin, node_id, style_sheet, depth + 1, doc, id_map)?;
206    } else {
207        parse_xml_node_children(
208            node,
209            origin,
210            node_id,
211            style_sheet,
212            ignore_ids,
213            depth + 1,
214            doc,
215            id_map,
216        )?;
217    }
218
219    Ok(())
220}
221
222pub(crate) fn parse_svg_element<'input>(
223    xml_node: roxmltree::Node<'_, 'input>,
224    parent_id: NodeId,
225    tag_name: EId,
226    style_sheet: &simplecss::StyleSheet,
227    ignore_ids: bool,
228    doc: &mut Document<'input>,
229) -> Result<NodeId, Error> {
230    let attrs_start_idx = doc.attrs.len();
231    let mut href_idx: Option<usize> = None;
232
233    // Copy presentational attributes first.
234    for attr in xml_node.attributes() {
235        match attr.namespace() {
236            None | Some(SVG_NS) | Some(XLINK_NS) | Some(XML_NAMESPACE_NS) => {}
237            _ => continue,
238        }
239
240        let aid = match AId::from_str(attr.name()) {
241            Some(v) => v,
242            None => continue,
243        };
244
245        // During a `use` resolving, all `id` attributes must be ignored.
246        // Otherwise we will get elements with duplicated id's.
247        if ignore_ids && aid == AId::Id {
248            continue;
249        }
250
251        // For some reason those properties are allowed only inside a `style` attribute and CSS.
252        if matches!(aid, AId::MixBlendMode | AId::Isolation | AId::FontKerning) {
253            continue;
254        } else if aid == AId::ImageRendering
255            && matches!(
256                attr.value(),
257                "smooth" | "high-quality" | "crisp-edges" | "pixelated"
258            )
259        {
260            continue;
261        }
262
263        // SVG 2: when both `href` and `xlink:href` are present, the unprefixed
264        // `href` takes precedence and the XLink one must be ignored, regardless
265        // of their order in the source document.
266        // https://www.w3.org/TR/SVG2/linking.html#XLinkRefAttrs
267        if aid == AId::Href {
268            let is_unprefixed = attr.namespace().is_none();
269            let is_xlink = attr.namespace() == Some(XLINK_NS);
270            if !is_unprefixed && !is_xlink {
271                continue;
272            }
273
274            if let Some(idx) = href_idx {
275                // An `href` was already stored. Only an unprefixed `href` is
276                // allowed to override it; an `xlink:href` is ignored.
277                if is_unprefixed {
278                    doc.attrs[idx].value = attr.value_storage().clone();
279                }
280                continue;
281            }
282        }
283
284        let added = append_attribute(
285            parent_id,
286            tag_name,
287            aid,
288            attr.value_storage().clone(),
289            false,
290            doc,
291        );
292        if added && aid == AId::Href {
293            href_idx = Some(doc.attrs.len() - 1);
294        }
295    }
296
297    let mut insert_attribute = |aid, value: &str, important: bool| {
298        // Check that attribute already exists.
299        let idx = doc.attrs[attrs_start_idx..]
300            .iter_mut()
301            .position(|a| a.name == aid);
302
303        // Append an attribute as usual.
304        let added = append_attribute(
305            parent_id,
306            tag_name,
307            aid,
308            roxmltree::StringStorage::new_owned(value),
309            important,
310            doc,
311        );
312
313        // Check that attribute was actually added, because it could be skipped.
314        if added {
315            if let Some(idx) = idx {
316                let last_idx = doc.attrs.len() - 1;
317                let existing_idx = attrs_start_idx + idx;
318
319                // See https://developer.mozilla.org/en-US/docs/Web/CSS/important
320                // When a declaration is important, the order of precedence is reversed.
321                // Declarations marked as important in the user-agent style sheets override
322                // all important declarations in the user style sheets. Similarly, all important
323                // declarations in the user style sheets override all important declarations in the
324                // author's style sheets. Finally, all important declarations take precedence over
325                // all animations.
326                //
327                // Which means:
328                // 1) Existing is not important, new is not important -> swap
329                // 2) Existing is important, new is not important -> don't swap
330                // 3) Existing is not important, new is important -> swap
331                // 4) Existing is important, new is important -> don't swap (since the order
332                // is reversed, so existing important attributes take precedence over new
333                // important attributes)
334                let has_precedence = !doc.attrs[existing_idx].important;
335
336                if has_precedence {
337                    doc.attrs.swap(existing_idx, last_idx);
338                }
339
340                // Remove last.
341                doc.attrs.pop();
342            }
343        }
344    };
345
346    let mut write_declaration = |declaration: &Declaration| {
347        // TODO: perform XML attribute normalization
348        let imp = declaration.important;
349        let val = declaration.value;
350
351        if declaration.name == "marker" {
352            insert_attribute(AId::MarkerStart, val, imp);
353            insert_attribute(AId::MarkerMid, val, imp);
354            insert_attribute(AId::MarkerEnd, val, imp);
355        } else if declaration.name == "font" {
356            if let Ok(shorthand) = FontShorthand::from_str(val) {
357                // First we need to reset all values to their default.
358                insert_attribute(AId::FontStyle, "normal", imp);
359                insert_attribute(AId::FontVariant, "normal", imp);
360                insert_attribute(AId::FontWeight, "normal", imp);
361                insert_attribute(AId::FontStretch, "normal", imp);
362                insert_attribute(AId::LineHeight, "normal", imp);
363                insert_attribute(AId::FontSizeAdjust, "none", imp);
364                insert_attribute(AId::FontKerning, "auto", imp);
365                insert_attribute(AId::FontVariantCaps, "normal", imp);
366                insert_attribute(AId::FontVariantLigatures, "normal", imp);
367                insert_attribute(AId::FontVariantNumeric, "normal", imp);
368                insert_attribute(AId::FontVariantEastAsian, "normal", imp);
369                insert_attribute(AId::FontVariantPosition, "normal", imp);
370
371                // Then, we set the properties that have been declared.
372                shorthand
373                    .font_stretch
374                    .map(|s| insert_attribute(AId::FontStretch, s, imp));
375                shorthand
376                    .font_weight
377                    .map(|s| insert_attribute(AId::FontWeight, s, imp));
378                shorthand
379                    .font_variant
380                    .map(|s| insert_attribute(AId::FontVariant, s, imp));
381                shorthand
382                    .font_style
383                    .map(|s| insert_attribute(AId::FontStyle, s, imp));
384                insert_attribute(AId::FontSize, shorthand.font_size, imp);
385                insert_attribute(AId::FontFamily, shorthand.font_family, imp);
386            } else {
387                log::warn!(
388                    "Failed to parse {} value: '{}'",
389                    AId::Font,
390                    declaration.value
391                );
392            }
393        } else if let Some(aid) = AId::from_str(declaration.name) {
394            // Parse only the presentation attributes.
395            if aid.is_presentation() {
396                insert_attribute(aid, val, imp);
397            }
398        }
399    };
400
401    // Apply CSS.
402    for rule in &style_sheet.rules {
403        if rule.selector.matches(&XmlNode(xml_node)) {
404            for declaration in &rule.declarations {
405                write_declaration(declaration);
406            }
407        }
408    }
409
410    // Split a `style` attribute.
411    if let Some(value) = xml_node.attribute("style") {
412        for declaration in simplecss::DeclarationTokenizer::from(value) {
413            write_declaration(&declaration);
414        }
415    }
416
417    if doc.nodes.len() > 1_000_000 {
418        return Err(Error::NodesLimitReached);
419    }
420
421    let node_id = doc.append(
422        parent_id,
423        NodeKind::Element {
424            tag_name,
425            attributes: ShortRange::new(attrs_start_idx as u32, doc.attrs.len() as u32),
426        },
427    );
428
429    Ok(node_id)
430}
431
432fn append_attribute<'input>(
433    parent_id: NodeId,
434    tag_name: EId,
435    aid: AId,
436    value: roxmltree::StringStorage<'input>,
437    important: bool,
438    doc: &mut Document<'input>,
439) -> bool {
440    match aid {
441        // The `style` attribute will be split into attributes, so we don't need it.
442        AId::Style |
443        // No need to copy a `class` attribute since CSS were already resolved.
444        AId::Class => return false,
445        _ => {}
446    }
447
448    // Ignore `xlink:href` on `tspan` (which was originally `tref` or `a`),
449    // because we will convert `tref` into `tspan` anyway.
450    if tag_name == EId::Tspan && aid == AId::Href {
451        return false;
452    }
453
454    if aid.allows_inherit_value() && &*value == "inherit" {
455        return resolve_inherit(parent_id, aid, doc);
456    }
457
458    doc.append_attribute(aid, value, important);
459    true
460}
461
462fn resolve_inherit(parent_id: NodeId, aid: AId, doc: &mut Document) -> bool {
463    if aid.is_inheritable() {
464        // Inheritable attributes can inherit a value from an any ancestor.
465        let node_id = doc
466            .get(parent_id)
467            .ancestors()
468            .find(|n| n.has_attribute(aid))
469            .map(|n| n.id);
470        if let Some(node_id) = node_id {
471            if let Some(attr) = doc
472                .get(node_id)
473                .attributes()
474                .iter()
475                .find(|a| a.name == aid)
476                .cloned()
477            {
478                doc.attrs.push(Attribute {
479                    name: aid,
480                    value: attr.value,
481                    important: attr.important,
482                });
483
484                return true;
485            }
486        }
487    } else {
488        // Non-inheritable attributes can inherit a value only from a direct parent.
489        if let Some(attr) = doc
490            .get(parent_id)
491            .attributes()
492            .iter()
493            .find(|a| a.name == aid)
494            .cloned()
495        {
496            doc.attrs.push(Attribute {
497                name: aid,
498                value: attr.value,
499                important: attr.important,
500            });
501
502            return true;
503        }
504    }
505
506    // Fallback to a default value if possible.
507    let value = match aid {
508        AId::ImageRendering | AId::ShapeRendering | AId::TextRendering => "auto",
509
510        AId::ClipPath
511        | AId::Filter
512        | AId::MarkerEnd
513        | AId::MarkerMid
514        | AId::MarkerStart
515        | AId::Mask
516        | AId::Stroke
517        | AId::StrokeDasharray
518        | AId::TextDecoration => "none",
519
520        AId::FontStretch
521        | AId::FontStyle
522        | AId::FontVariant
523        | AId::FontWeight
524        | AId::LetterSpacing
525        | AId::WordSpacing => "normal",
526
527        AId::Fill | AId::FloodColor | AId::StopColor => "black",
528
529        AId::FillOpacity
530        | AId::FloodOpacity
531        | AId::Opacity
532        | AId::StopOpacity
533        | AId::StrokeOpacity => "1",
534
535        AId::ClipRule | AId::FillRule => "nonzero",
536
537        AId::BaselineShift => "baseline",
538        AId::ColorInterpolationFilters => "linearRGB",
539        AId::Direction => "ltr",
540        AId::Display => "inline",
541        AId::FontSize => "medium",
542        AId::Overflow => "visible",
543        AId::StrokeDashoffset => "0",
544        AId::StrokeLinecap => "butt",
545        AId::StrokeLinejoin => "miter",
546        AId::StrokeMiterlimit => "4",
547        AId::StrokeWidth => "1",
548        AId::TextAnchor => "start",
549        AId::Visibility => "visible",
550        AId::WritingMode => "lr-tb",
551        _ => return false,
552    };
553
554    doc.append_attribute(aid, roxmltree::StringStorage::Borrowed(value), false);
555    true
556}
557
558fn resolve_href<'a, 'input: 'a>(
559    node: roxmltree::Node<'a, 'input>,
560    id_map: &HashMap<&str, roxmltree::Node<'a, 'input>>,
561) -> Option<roxmltree::Node<'a, 'input>> {
562    // See the comment in `parse_svg_element` about `href` precedence.
563    //
564    // Note: `roxmltree::Node::attribute("href")` matches by local name only and
565    // would return whichever `href`/`xlink:href` comes first, so we have to
566    // filter by namespace explicitly.
567    let link_value = node
568        .attributes()
569        .find(|a| a.name() == "href" && a.namespace().is_none())
570        .or_else(|| {
571            node.attributes()
572                .find(|a| a.name() == "href" && a.namespace() == Some(XLINK_NS))
573        })
574        .map(|a| a.value())?;
575
576    let link_id = svgtypes::IRI::from_str(link_value).ok()?.0;
577
578    id_map.get(link_id).copied()
579}
580
581fn parse_svg_use_element<'input>(
582    node: roxmltree::Node<'_, 'input>,
583    origin: roxmltree::Node,
584    parent_id: NodeId,
585    style_sheet: &simplecss::StyleSheet,
586    depth: u32,
587    doc: &mut Document<'input>,
588    id_map: &HashMap<&str, roxmltree::Node<'_, 'input>>,
589) -> Result<(), Error> {
590    let link = match resolve_href(node, id_map) {
591        Some(v) => v,
592        None => return Ok(()),
593    };
594
595    if link == node || link == origin {
596        log::warn!(
597            "Recursive 'use' detected. '{}' will be skipped.",
598            node.attribute((SVG_NS, "id")).unwrap_or_default()
599        );
600        return Ok(());
601    }
602
603    // Make sure we're linked to an SVG element.
604    if parse_tag_name(link).is_none() {
605        return Ok(());
606    }
607
608    // Check that none of the linked node's children reference current `use` node
609    // via other `use` node.
610    //
611    // Example:
612    // <g id="g1">
613    //     <use xlink:href="#use1" id="use2"/>
614    // </g>
615    // <use xlink:href="#g1" id="use1"/>
616    //
617    // `use2` should be removed.
618    //
619    // Also, child should not reference its parent:
620    // <g id="g1">
621    //     <use xlink:href="#g1" id="use1"/>
622    // </g>
623    //
624    // `use1` should be removed.
625    let mut is_recursive = false;
626    for link_child in link
627        .descendants()
628        .skip(1)
629        .filter(|n| n.has_tag_name((SVG_NS, "use")))
630    {
631        if let Some(link2) = resolve_href(link_child, id_map) {
632            if link2 == node || link2 == link {
633                is_recursive = true;
634                break;
635            }
636        }
637    }
638
639    if is_recursive {
640        log::warn!(
641            "Recursive 'use' detected. '{}' will be skipped.",
642            node.attribute((SVG_NS, "id")).unwrap_or_default()
643        );
644        return Ok(());
645    }
646
647    parse_xml_node(
648        link,
649        node,
650        parent_id,
651        style_sheet,
652        true,
653        depth + 1,
654        doc,
655        id_map,
656    )
657}
658
659fn resolve_css<'a>(
660    xml: &'a roxmltree::Document<'a>,
661    style_sheet: Option<&'a str>,
662) -> simplecss::StyleSheet<'a> {
663    let mut sheet = simplecss::StyleSheet::new();
664
665    // Injected style sheets do not override internal ones (we mimic the logic of rsvg-convert),
666    // so we need to parse it first.
667    if let Some(style_sheet) = style_sheet {
668        sheet.parse_more(style_sheet);
669    }
670
671    for node in xml.descendants().filter(|n| n.has_tag_name("style")) {
672        match node.attribute("type") {
673            Some("text/css") => {}
674            Some(_) => continue,
675            None => {}
676        }
677
678        let text = match node.text() {
679            Some(v) => v,
680            None => continue,
681        };
682
683        sheet.parse_more(text);
684    }
685
686    sheet
687}
688
689struct XmlNode<'a, 'input: 'a>(roxmltree::Node<'a, 'input>);
690
691impl simplecss::Element for XmlNode<'_, '_> {
692    fn parent_element(&self) -> Option<Self> {
693        self.0.parent_element().map(XmlNode)
694    }
695
696    fn prev_sibling_element(&self) -> Option<Self> {
697        self.0.prev_sibling_element().map(XmlNode)
698    }
699
700    fn has_local_name(&self, local_name: &str) -> bool {
701        self.0.tag_name().name() == local_name
702    }
703
704    fn attribute_matches(&self, local_name: &str, operator: simplecss::AttributeOperator) -> bool {
705        match self.0.attribute(local_name) {
706            Some(value) => operator.matches(value),
707            None => false,
708        }
709    }
710
711    fn pseudo_class_matches(&self, class: simplecss::PseudoClass) -> bool {
712        match class {
713            simplecss::PseudoClass::FirstChild => self.prev_sibling_element().is_none(),
714            // TODO: lang
715            _ => false, // Since we are querying a static SVG we can ignore other pseudo-classes.
716        }
717    }
718}
719
720fn fix_recursive_patterns(doc: &mut Document) {
721    while let Some(node_id) = find_recursive_pattern(AId::Fill, doc) {
722        let idx = doc.get(node_id).attribute_id(AId::Fill).unwrap();
723        doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
724    }
725
726    while let Some(node_id) = find_recursive_pattern(AId::Stroke, doc) {
727        let idx = doc.get(node_id).attribute_id(AId::Stroke).unwrap();
728        doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
729    }
730}
731
732fn find_recursive_pattern(aid: AId, doc: &mut Document) -> Option<NodeId> {
733    for pattern_node in doc
734        .root()
735        .descendants()
736        .filter(|n| n.tag_name() == Some(EId::Pattern))
737    {
738        for node in pattern_node.descendants() {
739            let value = match node.attribute(aid) {
740                Some(v) => v,
741                None => continue,
742            };
743
744            if let Ok(svgtypes::Paint::FuncIRI(link_id, _)) = svgtypes::Paint::from_str(value) {
745                if link_id == pattern_node.element_id() {
746                    // If a pattern child has a link to the pattern itself
747                    // then we have to replace it with `none`.
748                    // Otherwise we will get endless loop/recursion and stack overflow.
749                    return Some(node.id);
750                } else {
751                    // Check that linked node children doesn't link this pattern.
752                    if let Some(linked_node) = doc.element_by_id(link_id) {
753                        for node2 in linked_node.descendants() {
754                            let value2 = match node2.attribute(aid) {
755                                Some(v) => v,
756                                None => continue,
757                            };
758
759                            if let Ok(svgtypes::Paint::FuncIRI(link_id2, _)) =
760                                svgtypes::Paint::from_str(value2)
761                            {
762                                if link_id2 == pattern_node.element_id() {
763                                    return Some(node2.id);
764                                }
765                            }
766                        }
767                    }
768                }
769            }
770        }
771    }
772
773    None
774}
775
776fn fix_recursive_links(eid: EId, aid: AId, doc: &mut Document) {
777    while let Some(node_id) = find_recursive_link(eid, aid, doc) {
778        let idx = doc.get(node_id).attribute_id(aid).unwrap();
779        doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
780    }
781}
782
783fn find_recursive_link(eid: EId, aid: AId, doc: &Document) -> Option<NodeId> {
784    for node in doc
785        .root()
786        .descendants()
787        .filter(|n| n.tag_name() == Some(eid))
788    {
789        for child in node.descendants() {
790            if let Some(link) = child.node_attribute(aid) {
791                if link == node {
792                    // If an element child has a link to the element itself
793                    // then we have to replace it with `none`.
794                    // Otherwise we will get endless loop/recursion and stack overflow.
795                    return Some(child.id);
796                } else {
797                    // Check that linked node children doesn't link this element.
798                    for node2 in link.descendants() {
799                        if let Some(link2) = node2.node_attribute(aid) {
800                            if link2 == node {
801                                return Some(node2.id);
802                            }
803                        }
804                    }
805                }
806            }
807        }
808    }
809
810    None
811}
812
813/// Detects cases like:
814///
815/// ```xml
816/// <filter id="filter1">
817///   <feImage xlink:href="#rect1"/>
818/// </filter>
819/// <rect id="rect1" x="36" y="36" width="120" height="120" fill="green" filter="url(#filter1)"/>
820/// ```
821fn fix_recursive_fe_image(doc: &mut Document) {
822    let mut ids = Vec::new();
823    for fe_node in doc
824        .root()
825        .descendants()
826        .filter(|n| n.tag_name() == Some(EId::FeImage))
827    {
828        if let Some(link) = fe_node.node_attribute(AId::Href) {
829            if let Some(filter_uri) = link.attribute::<&str>(AId::Filter) {
830                let filter_id = fe_node.parent().unwrap().element_id();
831                for func in svgtypes::FilterValueListParser::from(filter_uri).flatten() {
832                    if let svgtypes::FilterValue::Url(url) = func {
833                        if url == filter_id {
834                            ids.push(link.id);
835                        }
836                    }
837                }
838            }
839        }
840    }
841
842    for id in ids {
843        let idx = doc.get(id).attribute_id(AId::Filter).unwrap();
844        doc.attrs[idx].value = roxmltree::StringStorage::Borrowed("none");
845    }
846}