Skip to main content

feed_parser/parsers/atom/
mod.rs

1use crate::parsers::{
2    Feed,
3    errors::{ParseError, ParseResult},
4    internal::{TextRun, compile_tag_patterns, deserialize_feed, escape_inline_html},
5};
6use core::str;
7use quick_xml::Reader;
8use quick_xml::Writer;
9use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
10use regex::Regex;
11use std::io::Cursor;
12use std::sync::LazyLock;
13
14#[cfg(test)]
15mod tests;
16
17/// Elements whose content may contain unescaped HTML.
18static HTML_BEARING_TAGS: LazyLock<Vec<(&'static str, Regex)>> =
19    LazyLock::new(|| compile_tag_patterns(&["title", "description", "summary", "content"]));
20
21/// Parses an Atom document into one [`Feed`] per `<entry>`.
22///
23/// The `href` of each entry's alternate `text/html` `<link>` becomes the feed
24/// link; other link relations are ignored.
25///
26/// # Errors
27///
28/// Returns [`ParseError::XmlParseError`] if the document is not well-formed XML,
29/// [`ParseError::MissingField`] if an entry lacks a required field, and
30/// [`ParseError::InvalidFeedFormat`] if an entry is never closed.
31pub fn parse(text: &str) -> ParseResult<Vec<Feed>> {
32    let text = escape_inline_html(text, &HTML_BEARING_TAGS);
33
34    let mut reader = Reader::from_str(&text);
35
36    let mut feeds = Vec::new();
37    let mut writer = Writer::new(Cursor::new(Vec::new()));
38    // Character data is buffered rather than written straight through; see
39    // `TextRun` for why the reader's own `trim_text` cannot be used here.
40    let mut run = TextRun::default();
41    let mut parsing = false;
42    loop {
43        match reader.read_event()? {
44            Event::Start(e) => {
45                run.flush(&mut writer)?;
46                if parsing {
47                    if e.name().as_ref() == b"dc:creator" {
48                        writer.write_event(Event::Start(BytesStart::new("creator")))?;
49                    } else if e.name().as_ref() == b"dc:date" {
50                        writer.write_event(Event::Start(BytesStart::new("date")))?;
51                    } else if e.name().as_ref() == b"pubDate" || e.name().as_ref() == b"published" {
52                        writer.write_event(Event::Start(BytesStart::new("publish_date")))?;
53                    } else if e.name().as_ref() == b"description" {
54                        writer.write_event(Event::Start(BytesStart::new("description")))?;
55                    } else if e.name().as_ref() == b"link" {
56                        continue;
57                    } else {
58                        writer.write_event(Event::Start(e.clone()))?;
59                    }
60                }
61                if e.name().as_ref() == b"entry" {
62                    writer.write_event(Event::Start(BytesStart::new("entry")))?;
63                    parsing = true;
64                }
65            }
66            Event::Empty(e) => {
67                run.flush(&mut writer)?;
68                if parsing {
69                    if e.name().as_ref() == b"link" {
70                        let mut is_link = true;
71                        for attr in e.attributes() {
72                            let attr = attr.map_err(quick_xml::Error::InvalidAttr)?;
73                            if attr.key.0 == b"type" {
74                                if str::from_utf8(attr.value.as_ref())? != "text/html" {
75                                    is_link = false;
76                                }
77                            } else if attr.key.0 == b"rel"
78                                && str::from_utf8(attr.value.as_ref())? != "alternate"
79                            {
80                                is_link = false;
81                            }
82                        }
83                        if !is_link {
84                            continue;
85                        }
86                        for attr in e.attributes() {
87                            let attr = attr.map_err(quick_xml::Error::InvalidAttr)?;
88                            if attr.key.0 == b"href" {
89                                let href = str::from_utf8(attr.value.as_ref())?;
90                                writer.write_event(Event::Start(BytesStart::new("link")))?;
91                                writer.write_event(Event::Text(BytesText::new(href)))?;
92                                writer.write_event(Event::End(BytesEnd::new("link")))?;
93                            }
94                        }
95                    } else {
96                        writer.write_event(Event::Empty(e))?;
97                    }
98                }
99            }
100            Event::End(e) => {
101                run.flush(&mut writer)?;
102                if parsing && e.name().as_ref() == b"entry" {
103                    writer.write_event(Event::End(BytesEnd::new("entry")))?;
104                    let feed_text = writer.into_inner().into_inner();
105                    feeds.push(deserialize_feed(str::from_utf8(&feed_text)?)?);
106
107                    writer = Writer::new(Cursor::new(Vec::new()));
108                    parsing = false;
109                } else if parsing {
110                    if e.name().as_ref() == b"dc:creator" {
111                        writer.write_event(Event::End(BytesEnd::new("creator")))?;
112                    } else if e.name().as_ref() == b"dc:date" {
113                        writer.write_event(Event::End(BytesEnd::new("date")))?;
114                    } else if e.name().as_ref() == b"pubDate" || e.name().as_ref() == b"published" {
115                        writer.write_event(Event::End(BytesEnd::new("publish_date")))?;
116                    } else if e.name().as_ref() == b"link" {
117                        continue;
118                    } else {
119                        writer.write_event(Event::End(e))?;
120                    }
121                }
122            }
123            Event::Text(e) => {
124                if parsing {
125                    // Atom sources commonly double-escape their content, so
126                    // decode once before handing the text on.
127                    let text = html_escape::decode_html_entities(str::from_utf8(&e)?);
128                    run.push(Event::Text(BytesText::new(&text).into_owned()));
129                }
130            }
131            Event::CData(e) => {
132                if parsing {
133                    run.push(Event::CData(e));
134                }
135            }
136            // quick-xml reports entity references as their own event. Dropping
137            // them would silently delete every escaped character in the entry.
138            Event::GeneralRef(e) => {
139                if parsing {
140                    run.push(Event::GeneralRef(e));
141                }
142            }
143            Event::Eof => break,
144            _ => {}
145        }
146    }
147
148    if parsing {
149        return Err(ParseError::InvalidFeedFormat(
150            "reached end of document inside an unclosed <entry> element".to_string(),
151        ));
152    }
153
154    Ok(feeds)
155}