feed_parser/parsers/atom/
mod.rs1use 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
17static HTML_BEARING_TAGS: LazyLock<Vec<(&'static str, Regex)>> =
19 LazyLock::new(|| compile_tag_patterns(&["title", "description", "summary", "content"]));
20
21pub 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 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 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 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}