use crate::parsers::{
Feed,
errors::{ParseError, ParseResult},
internal::{TextRun, compile_tag_patterns, deserialize_feed, escape_inline_html},
};
use core::str;
use quick_xml::Reader;
use quick_xml::Writer;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use regex::Regex;
use std::io::Cursor;
use std::sync::LazyLock;
#[cfg(test)]
mod tests;
static HTML_BEARING_TAGS: LazyLock<Vec<(&'static str, Regex)>> =
LazyLock::new(|| compile_tag_patterns(&["title", "description", "summary", "content"]));
pub fn parse(text: &str) -> ParseResult<Vec<Feed>> {
let text = escape_inline_html(text, &HTML_BEARING_TAGS);
let mut reader = Reader::from_str(&text);
let mut feeds = Vec::new();
let mut writer = Writer::new(Cursor::new(Vec::new()));
let mut run = TextRun::default();
let mut parsing = false;
loop {
match reader.read_event()? {
Event::Start(e) => {
run.flush(&mut writer)?;
if parsing {
if e.name().as_ref() == b"dc:creator" {
writer.write_event(Event::Start(BytesStart::new("creator")))?;
} else if e.name().as_ref() == b"dc:date" {
writer.write_event(Event::Start(BytesStart::new("date")))?;
} else if e.name().as_ref() == b"pubDate" || e.name().as_ref() == b"published" {
writer.write_event(Event::Start(BytesStart::new("publish_date")))?;
} else if e.name().as_ref() == b"description" {
writer.write_event(Event::Start(BytesStart::new("description")))?;
} else if e.name().as_ref() == b"link" {
continue;
} else {
writer.write_event(Event::Start(e.clone()))?;
}
}
if e.name().as_ref() == b"entry" {
writer.write_event(Event::Start(BytesStart::new("entry")))?;
parsing = true;
}
}
Event::Empty(e) => {
run.flush(&mut writer)?;
if parsing {
if e.name().as_ref() == b"link" {
let mut is_link = true;
for attr in e.attributes() {
let attr = attr.map_err(quick_xml::Error::InvalidAttr)?;
if attr.key.0 == b"type" {
if str::from_utf8(attr.value.as_ref())? != "text/html" {
is_link = false;
}
} else if attr.key.0 == b"rel"
&& str::from_utf8(attr.value.as_ref())? != "alternate"
{
is_link = false;
}
}
if !is_link {
continue;
}
for attr in e.attributes() {
let attr = attr.map_err(quick_xml::Error::InvalidAttr)?;
if attr.key.0 == b"href" {
let href = str::from_utf8(attr.value.as_ref())?;
writer.write_event(Event::Start(BytesStart::new("link")))?;
writer.write_event(Event::Text(BytesText::new(href)))?;
writer.write_event(Event::End(BytesEnd::new("link")))?;
}
}
} else {
writer.write_event(Event::Empty(e))?;
}
}
}
Event::End(e) => {
run.flush(&mut writer)?;
if parsing && e.name().as_ref() == b"entry" {
writer.write_event(Event::End(BytesEnd::new("entry")))?;
let feed_text = writer.into_inner().into_inner();
feeds.push(deserialize_feed(str::from_utf8(&feed_text)?)?);
writer = Writer::new(Cursor::new(Vec::new()));
parsing = false;
} else if parsing {
if e.name().as_ref() == b"dc:creator" {
writer.write_event(Event::End(BytesEnd::new("creator")))?;
} else if e.name().as_ref() == b"dc:date" {
writer.write_event(Event::End(BytesEnd::new("date")))?;
} else if e.name().as_ref() == b"pubDate" || e.name().as_ref() == b"published" {
writer.write_event(Event::End(BytesEnd::new("publish_date")))?;
} else if e.name().as_ref() == b"link" {
continue;
} else {
writer.write_event(Event::End(e))?;
}
}
}
Event::Text(e) => {
if parsing {
let text = html_escape::decode_html_entities(str::from_utf8(&e)?);
run.push(Event::Text(BytesText::new(&text).into_owned()));
}
}
Event::CData(e) => {
if parsing {
run.push(Event::CData(e));
}
}
Event::GeneralRef(e) => {
if parsing {
run.push(Event::GeneralRef(e));
}
}
Event::Eof => break,
_ => {}
}
}
if parsing {
return Err(ParseError::InvalidFeedFormat(
"reached end of document inside an unclosed <entry> element".to_string(),
));
}
Ok(feeds)
}