///|
fn feed_image_xml(w : XmlWriter, image : FeedImage) -> Unit {
w.open("image", [])
w.leaf("url", image.url)
w.leaf("title", image.title)
w.leaf("link", image.link)
w.opt_int_leaf("width", image.width)
w.opt_int_leaf("height", image.height)
w.opt_leaf("description", image.description)
w.close("image")
}
///|
fn is_reserved_rss_namespace_prefix(prefix : String) -> Bool {
prefix == "dc" || prefix == "content" || prefix == "atom" || prefix == "geo"
}
///|
fn validate_rss_fields(config : FeedConfig, errors : Array[FeedError]) -> Unit {
match config.ttl {
Some(ttl) =>
if ttl < 0 {
errors.push(InvalidValue("ttl is negative: \{ttl}"))
}
None => ()
}
match config.image {
Some(img) => {
match img.width {
Some(width) =>
if width <= 0 || width > 144 {
errors.push(InvalidValue("image width out of range: \{width}"))
}
None => ()
}
match img.height {
Some(height) =>
if height <= 0 || height > 400 {
errors.push(InvalidValue("image height out of range: \{height}"))
}
None => ()
}
}
None => ()
}
}
///|
/// Render the feed as an XML string. Returns `Err` if `title` or
/// `description` is empty, or if any custom element has an invalid tag
/// name; all accumulated errors are reported without halting rendering of
/// the remaining elements.
///
/// When `indent` is `true`, produces indented output; otherwise minified.
pub fn FeedConfig::to_xml(
self : FeedConfig,
indent? : Bool = false,
) -> Result[String, Array[FeedError]] {
let errors : Array[FeedError] = []
validate_common_fields(self, errors, is_reserved_rss_namespace_prefix)
validate_rss_fields(self, errors)
// RSS uses 4 spaces per level: at depth 1, its children at depth 2.
let w = XmlWriter::new_with_size(indent, 4)
let geo_rss = self.items
.iter()
.any(fn(item) { item.lat is Some(_) || item.long is Some(_) })
// Write the RSS header manually; and are not managed by
// open()/close() so that we can emit the multi-attribute opening tag.
w.write_raw("")
w.newline()
w.write_raw("")
w.newline()
// depth is 0 here; manually indent to depth 1 for
w.indent()
w.pad()
w.write_raw("")
w.newline()
// depth is now 1; children will be at depth 2
w.indent()
w.leaf_cdata("title", self.title)
w.leaf_cdata("description", self.description)
w.leaf("link", self.site_url)
match self.image {
Some(img) => feed_image_xml(w, img)
None => ()
}
w.leaf("generator", self.generator)
w.opt_leaf("lastBuildDate", optional_date_to_rfc822(self.last_build_date))
match self.feed_url {
Some(url) =>
w.self_closing("atom:link", [
("href", url),
("rel", "self"),
("type", "application/rss+xml"),
])
None => ()
}
match self.author {
Some(a) => w.leaf_cdata("dc:creator", a.name)
None => ()
}
w.opt_leaf("pubDate", optional_date_to_rfc822(self.pub_date))
w.opt_leaf_cdata("copyright", self.copyright)
w.opt_leaf_cdata("language", self.language)
w.opt_leaf_cdata("managingEditor", self.managing_editor)
w.opt_leaf_cdata("webMaster", self.web_master)
w.opt_leaf("docs", self.docs)
w.opt_int_leaf("ttl", self.ttl)
for cat in self.categories {
feed_category_xml(w, cat)
}
match self.hub {
Some(h) => w.self_closing("atom:link", [("href", h), ("rel", "hub")])
None => ()
}
for elem in self.custom_elements {
custom_elem_xml(errors, w, elem)
}
for item in self.items {
feed_item_xml(errors, w, item, self.author)
}
if !errors.is_empty() {
return Err(errors)
}
// Close at depth 1, then at depth 0.
w.dedent()
w.pad()
w.write_raw("")
w.newline()
w.dedent()
w.write_raw("")
w.newline()
Ok(w.to_string())
}