///|
/// Parse an RSS feed from any of the supported versions
/// (RSS 0.90, 0.91, 0.92, 1.0 and 2.0).
///
/// Mirrors `Channel::read_from`. As a best effort for invalid feeds,
/// elements the RSS 2.0 spec declares required default to an empty string.
pub fn Channel::read_from(input : String) -> Channel raise RssError {
Channel::read_from_impl(input)
}
///|
fn Channel::read_from_impl(input : String) -> Channel raise RssError {
let r = RssReader::new(input)
// Find the opening element.
let mut root_attrs : Array[@xml.Attribute] = []
for ;; {
match r.next() {
@xml.Start(name~, attrs~) =>
if name is "rss" || name is "rdf:RDF" {
root_attrs = attrs
break
} else {
raise RssError::InvalidStartTag
}
@xml.Eof | @xml.End(_) => raise RssError::InvalidStartTag
_ => continue
}
}
let namespaces = read_namespaces(root_attrs, {})
let mut channel : Channel? = None
let extra_items : Array[Item] = []
let mut has_extra_items = false
let mut top_image : Image? = None
let mut top_text_input : TextInput? = None
for ;; {
match r.next() {
@xml.Start(name~, attrs~) =>
match @xml.local_name(name) {
"channel" => channel = Some(read_channel(r, attrs, namespaces))
"item" => {
extra_items.push(read_item(r, attrs, namespaces))
has_extra_items = true
}
"image" => top_image = Some(read_image(r))
"textinput" | "textInput" => top_text_input = Some(read_text_input(r))
_ => skip_element(r)
}
@xml.End(_) | @xml.Eof => break
_ => ()
}
}
match channel {
Some(ch) => {
// RSS 0.9 / 1.0 keep items (and sometimes image/textinput) as siblings
// of ; merge them in.
if has_extra_items {
for item in extra_items {
ch.items.push(item)
}
}
let image = if top_image is Some(_) { top_image } else { ch.image }
let text_input = if top_text_input is Some(_) {
top_text_input
} else {
ch.text_input
}
{ ..ch, image, text_input, namespaces }
}
None => raise RssError::Eof
}
}
///|
fn read_channel(
r : RssReader,
attrs : Array[@xml.Attribute],
parent_ns : Map[String, String],
) -> Channel raise RssError {
let scope_ns = read_namespaces(attrs, parent_ns)
let mut title = ""
let mut link = ""
let mut description = ""
let mut language : String? = None
let mut copyright : String? = None
let mut managing_editor : String? = None
let mut webmaster : String? = None
let mut pub_date : String? = None
let mut last_build_date : String? = None
let categories : Array[Category] = []
let mut generator : String? = None
let mut docs : String? = None
let mut cloud : Cloud? = None
let mut rating : String? = None
let mut ttl : String? = None
let mut image : Image? = None
let mut text_input : TextInput? = None
let skip_hours : Array[String] = []
let skip_days : Array[String] = []
let items : Array[Item] = []
let extensions : ExtensionMap = Map([])
let atom_links : Array[@atom.Link] = []
for ;; {
match r.next() {
@xml.Start(name~, attrs=el_attrs) =>
match name {
"category" => categories.push(read_category(r, el_attrs))
"cloud" => cloud = Some(read_cloud(r, el_attrs))
"image" => image = Some(read_image(r))
"textInput" => text_input = Some(read_text_input(r))
"item" => items.push(read_item(r, el_attrs, scope_ns))
"title" => if element_text(r) is Some(content) { title = content }
"link" => if element_text(r) is Some(content) { link = content }
"description" =>
if element_text(r) is Some(content) {
description = content
}
"language" => language = element_text(r)
"copyright" => copyright = element_text(r)
"managingEditor" => managing_editor = element_text(r)
"webMaster" => webmaster = element_text(r)
"pubDate" => pub_date = element_text(r)
"lastBuildDate" => last_build_date = element_text(r)
"generator" => generator = element_text(r)
"rating" => rating = element_text(r)
"docs" => docs = element_text(r)
"ttl" => ttl = element_text(r)
"skipHours" => read_skip_list(r, skip_hours, "hour")
"skipDays" => read_skip_list(r, skip_days, "day")
_ =>
match extension_name(name) {
Some((prefix, ename)) => {
let inner_ns = read_namespaces(el_attrs, scope_ns)
let ext = parse_extension_element(r, el_attrs)
collect_extension(
extensions, atom_links, inner_ns, prefix, ename, ext,
)
}
None => skip_element(r)
}
}
@xml.End(_) => break
@xml.Eof => raise unexpected_eof(r)
_ => ()
}
}
let (itunes, dc, syn) = extract_extensions(extensions)
{
title,
link,
description,
language,
copyright,
managing_editor,
webmaster,
pub_date,
last_build_date,
categories,
generator,
docs,
cloud,
rating,
ttl,
image,
text_input,
skip_hours,
skip_days,
items,
extensions,
atom_links,
itunes_ext: itunes,
dublin_core_ext: dc,
syndication_ext: syn,
namespaces: scope_ns,
}
}
///|
fn read_skip_list(
r : RssReader,
out : Array[String],
entry : String,
) -> Unit raise RssError {
for ;; {
match r.next() {
@xml.Start(name=tag, ..) =>
if tag == entry && element_text(r) is Some(content) {
out.push(content)
} else {
skip_element(r)
}
@xml.End(_) | @xml.Eof => break
_ => ()
}
}
}
///|
fn read_item(
r : RssReader,
attrs : Array[@xml.Attribute],
parent_ns : Map[String, String],
) -> Item raise RssError {
let scope_ns = read_namespaces(attrs, parent_ns)
let mut title : String? = None
let mut link : String? = None
let mut description : String? = None
let mut author : String? = None
let categories : Array[Category] = []
let mut comments : String? = None
let mut enclosure : Enclosure? = None
let mut guid : Guid? = None
let mut pub_date : String? = None
let mut source : Source? = None
let mut content : String? = None
let extensions : ExtensionMap = Map([])
let atom_links : Array[@atom.Link] = []
for ;; {
match r.next() {
@xml.Start(name~, attrs=el_attrs) =>
match name {
"category" => categories.push(read_category(r, el_attrs))
"guid" => guid = Some(read_guid(r, el_attrs))
"enclosure" => enclosure = Some(read_enclosure(r, el_attrs))
"source" => source = Some(read_source(r, el_attrs))
"title" => title = element_text(r)
"link" =>
// Mirrors the crate: empty links are dropped on items.
if element_text(r) is Some(value) {
link = Some(value)
}
"description" => description = element_text(r)
"author" => author = element_text(r)
"comments" => comments = element_text(r)
"pubDate" => pub_date = element_text(r)
"content:encoded" => content = element_text(r)
_ =>
match extension_name(name) {
Some((prefix, ename)) => {
let inner_ns = read_namespaces(el_attrs, scope_ns)
let ext = parse_extension_element(r, el_attrs)
collect_extension(
extensions, atom_links, inner_ns, prefix, ename, ext,
)
}
None => skip_element(r)
}
}
@xml.End(_) => break
@xml.Eof => raise unexpected_eof(r)
_ => ()
}
}
let (itunes, dc) = extract_item_extensions(extensions)
{
title,
link,
description,
author,
categories,
comments,
enclosure,
guid,
pub_date,
source,
content,
extensions,
atom_links,
itunes_ext: itunes,
dublin_core_ext: dc,
}
}
///|
fn read_category(
r : RssReader,
attrs : Array[@xml.Attribute],
) -> Category raise RssError {
{
domain: attribute_value(attrs, "domain"),
name: element_text(r).unwrap_or(""),
}
}
///|
fn read_guid(
r : RssReader,
attrs : Array[@xml.Attribute],
) -> Guid raise RssError {
let permalink = match attribute_value(attrs, "isPermaLink") {
// Mirrors the crate: any value other than `false` counts as a permalink.
Some(v) => !(v is "false")
None => true
}
{ is_permalink: permalink, value: element_text(r).unwrap_or("") }
}
///|
fn read_cloud(
r : RssReader,
attrs : Array[@xml.Attribute],
) -> Cloud raise RssError {
// Consume the synthetic end tag of a self-closing element plus children.
skip_element(r)
{
domain: attribute_value(attrs, "domain").unwrap_or(""),
port: attribute_value(attrs, "port").unwrap_or(""),
path: attribute_value(attrs, "path").unwrap_or(""),
register_procedure: attribute_value(attrs, "registerProcedure").unwrap_or(
"",
),
protocol: attribute_value(attrs, "protocol").unwrap_or(""),
}
}
///|
fn read_enclosure(
r : RssReader,
attrs : Array[@xml.Attribute],
) -> Enclosure raise RssError {
skip_element(r)
{
url: attribute_value(attrs, "url").unwrap_or(""),
length: attribute_value(attrs, "length").unwrap_or(""),
mime_type: attribute_value(attrs, "type").unwrap_or(""),
}
}
///|
fn read_source(
r : RssReader,
attrs : Array[@xml.Attribute],
) -> Source raise RssError {
{ url: attribute_value(attrs, "url").unwrap_or(""), title: element_text(r) }
}
///|
/// Read an `` element whose start tag has been consumed.
fn read_image(r : RssReader) -> Image raise RssError {
let mut url = ""
let mut title = ""
let mut link = ""
let mut width : String? = None
let mut height : String? = None
let mut img_description : String? = None
for ;; {
match r.next() {
@xml.Start(name=tag, ..) =>
match tag {
"url" => if element_text(r) is Some(content) { url = content }
"title" => if element_text(r) is Some(content) { title = content }
"link" => if element_text(r) is Some(content) { link = content }
"width" => width = element_text(r)
"height" => height = element_text(r)
"description" => img_description = element_text(r)
_ => skip_element(r)
}
@xml.End(_) | @xml.Eof => break
_ => ()
}
}
{ url, title, link, width, height, description: img_description }
}
///|
/// Read a `` (or RSS 1.0 ``) element whose start tag
/// has been consumed.
fn read_text_input(r : RssReader) -> TextInput raise RssError {
let mut title = ""
let mut description = ""
let mut input_name = ""
let mut link = ""
for ;; {
match r.next() {
@xml.Start(name=tag, ..) =>
match tag {
"title" => if element_text(r) is Some(content) { title = content }
"description" =>
if element_text(r) is Some(content) {
description = content
}
"name" => if element_text(r) is Some(content) { input_name = content }
"link" => if element_text(r) is Some(content) { link = content }
_ => skip_element(r)
}
@xml.End(_) | @xml.Eof => break
_ => ()
}
}
{ title, description, name: input_name, link }
}