///|
///|
/// Sorted map keys, matching the crate's BTreeMap iteration order.
///|
/// Lexicographic (code-unit wise) string comparison, matching the byte-wise
/// `Ord` of Rust strings used by the oracle's BTreeMap ordering.
pub fn str_lexicographic(a : String, b : String) -> Int {
let n = if a.length() < b.length() { a.length() } else { b.length() }
for i in 0.. Array[String] {
let keys : Array[String] = []
for k, _ in map {
keys.push(k)
}
keys.sort_by(str_lexicographic)
keys
}
///|
fn sorted_extension_names(map : Map[String, Array[Extension]]) -> Array[String] {
sorted_keys(map)
}
///|
/// Namespaces required by this item's content.
fn Item::used_namespaces(self : Item) -> Map[String, String] {
let namespaces : Map[String, String] = Map([])
if self.content is Some(_) {
namespaces["content"] = "http://purl.org/rss/1.0/modules/content/"
}
if self.atom_links.length() > 0 {
namespaces["atom"] = ATOM_NAMESPACE
}
if self.itunes_ext is Some(_) {
namespaces["itunes"] = ITUNES_NAMESPACE
}
if self.dublin_core_ext is Some(_) {
namespaces["dc"] = DUBLIN_CORE_NAMESPACE
}
namespaces
}
///|
/// Namespaces required to write this channel, computed bottom-up like the
/// crate's `used_namespaces`.
fn Channel::used_namespaces(self : Channel) -> Map[String, String] {
let namespaces : Map[String, String] = Map([])
for item in self.items {
for k, v in item.used_namespaces() {
namespaces[k] = v
}
}
if self.itunes_ext is Some(_) {
namespaces["itunes"] = ITUNES_NAMESPACE
}
if self.dublin_core_ext is Some(_) {
namespaces["dc"] = DUBLIN_CORE_NAMESPACE
}
if self.atom_links.length() > 0 {
namespaces["atom"] = ATOM_NAMESPACE
}
namespaces
}
///|
fn @xml.Writer::write_text_element(
self : @xml.Writer,
name : String,
content : String,
) -> Unit {
self.open(name)
self.text(content)
self.close(name)
}
///|
/// Count non-overlapping occurrences of `needle` in `s`.
fn count_occurrences(s : String, needle : String) -> Int {
let n = s.length()
let m = needle.length()
if m == 0 {
return 0
}
let mut count = 0
let mut i = 0
while i + m <= n {
if s[i] == needle[0] {
let mut ok = true
for k in 1.. Unit {
// Content containing more than one `]]>` cannot be represented losslessly
// with CDATA section splitting (the Rust crate emits broken XML there);
// fall back to escaped text, which always roundtrips.
if count_occurrences(content, "]]>") > 1 {
self.write_text_element(name, content)
} else {
self.open(name)
self.cdata(content)
self.close(name)
}
}
///|
fn Extension::write(self : Extension, w : @xml.Writer) -> Unit {
let attrs : Array[(String, String)] = []
for key in sorted_keys(self.attrs) {
attrs.push((key, self.attrs[key]))
}
w.open_with(self.name, attrs~)
if self.value is Some(value) {
w.text(value)
}
for name in sorted_extension_names(self.children) {
for child in self.children[name] {
child.write(w)
}
}
w.close(self.name)
}
///|
fn Category::write(self : Category, w : @xml.Writer) -> Unit {
let attrs : Array[(String, String)] = []
if self.domain is Some(domain) {
attrs.push(("domain", domain))
}
w.open_with("category", attrs~)
w.text(self.name)
w.close("category")
}
///|
fn Cloud::write(self : Cloud, w : @xml.Writer) -> Unit {
w.empty("cloud", attrs=[
("domain", self.domain),
("port", self.port),
("path", self.path),
("registerProcedure", self.register_procedure),
("protocol", self.protocol),
])
}
///|
fn Enclosure::write(self : Enclosure, w : @xml.Writer) -> Unit {
w.empty("enclosure", attrs=[
("url", self.url),
("length", self.length),
("type", self.mime_type),
])
}
///|
fn Guid::write(self : Guid, w : @xml.Writer) -> Unit {
if self.is_permalink {
w.open("guid")
} else {
w.open_with("guid", attrs=[("isPermaLink", "false")])
}
w.text(self.value)
w.close("guid")
}
///|
fn Image::write(self : Image, w : @xml.Writer) -> Unit {
w.open("image")
w.write_text_element("url", self.url)
w.write_text_element("title", self.title)
w.write_text_element("link", self.link)
if self.width is Some(width) {
w.write_text_element("width", width)
}
if self.height is Some(height) {
w.write_text_element("height", height)
}
if self.description is Some(description) {
w.write_text_element("description", description)
}
w.close("image")
}
///|
fn Source::write(self : Source, w : @xml.Writer) -> Unit {
w.open_with("source", attrs=[("url", self.url)])
if self.title is Some(title) {
w.text(title)
}
w.close("source")
}
///|
fn TextInput::write(self : TextInput, w : @xml.Writer) -> Unit {
w.open("textInput")
w.write_text_element("title", self.title)
w.write_text_element("description", self.description)
w.write_text_element("name", self.name)
w.write_text_element("link", self.link)
w.close("textInput")
}
///|
fn ITunesCategory::write(self : ITunesCategory, w : @xml.Writer) -> Unit {
w.open_with("itunes:category", attrs=[("text", self.text)])
if self.subcategory is Some(sub) {
sub.write(w)
}
w.close("itunes:category")
}
///|
fn ITunesOwner::write(self : ITunesOwner, w : @xml.Writer) -> Unit {
w.open("itunes:owner")
if self.name is Some(name) {
w.write_text_element("itunes:name", name)
}
if self.email is Some(email) {
w.write_text_element("itunes:email", email)
}
w.close("itunes:owner")
}
///|
fn ITunesChannelExtension::write(
self : ITunesChannelExtension,
w : @xml.Writer,
) -> Unit {
if self.author is Some(v) {
w.write_text_element("itunes:author", v)
}
if self.block is Some(v) {
w.write_text_element("itunes:block", v)
}
for category in self.categories {
category.write(w)
}
if self.image is Some(image) {
w.empty("itunes:image", attrs=[("href", image)])
}
if self.explicit is Some(v) {
w.write_text_element("itunes:explicit", v)
}
if self.complete is Some(v) {
w.write_text_element("itunes:complete", v)
}
if self.new_feed_url is Some(v) {
w.write_text_element("itunes:new-feed-url", v)
}
if self.owner is Some(owner) {
owner.write(w)
}
if self.subtitle is Some(v) {
w.write_text_element("itunes:subtitle", v)
}
if self.summary is Some(v) {
w.write_text_element("itunes:summary", v)
}
if self.keywords is Some(v) {
w.write_text_element("itunes:keywords", v)
}
if self.itunes_type is Some(v) {
w.write_text_element("itunes:type", v)
}
}
///|
fn ITunesItemExtension::write(
self : ITunesItemExtension,
w : @xml.Writer,
) -> Unit {
if self.author is Some(v) {
w.write_text_element("itunes:author", v)
}
if self.block is Some(v) {
w.write_text_element("itunes:block", v)
}
if self.image is Some(image) {
w.open_with("itunes:image", attrs=[("href", image)])
w.close("itunes:image")
}
if self.duration is Some(v) {
w.write_text_element("itunes:duration", v)
}
if self.explicit is Some(v) {
w.write_text_element("itunes:explicit", v)
}
if self.closed_captioned is Some(v) {
w.write_text_element("itunes:isClosedCaptioned", v)
}
if self.order is Some(v) {
w.write_text_element("itunes:order", v)
}
if self.subtitle is Some(v) {
w.write_text_element("itunes:subtitle", v)
}
if self.summary is Some(v) {
w.write_text_element("itunes:summary", v)
}
if self.keywords is Some(v) {
w.write_text_element("itunes:keywords", v)
}
if self.episode is Some(v) {
w.write_text_element("itunes:episode", v)
}
if self.season is Some(v) {
w.write_text_element("itunes:season", v)
}
if self.episode_type is Some(v) {
w.write_text_element("itunes:episodeType", v)
}
}
///|
fn DublinCoreExtension::write(
self : DublinCoreExtension,
w : @xml.Writer,
) -> Unit {
fn elements(name : String, values : Array[String]) -> Unit {
for value in values {
w.write_text_element(name, value)
}
}
elements("dc:contributor", self.contributors)
elements("dc:coverage", self.coverages)
elements("dc:creator", self.creators)
elements("dc:date", self.dates)
elements("dc:description", self.descriptions)
elements("dc:format", self.formats)
elements("dc:identifier", self.identifiers)
elements("dc:language", self.languages)
elements("dc:publisher", self.publishers)
elements("dc:relation", self.relations)
elements("dc:rights", self.rights)
elements("dc:source", self.sources)
elements("dc:subject", self.subjects)
elements("dc:title", self.titles)
elements("dc:type", self.types)
}
///|
fn SyndicationExtension::write(
self : SyndicationExtension,
namespaces : Map[String, String],
w : @xml.Writer,
) -> Unit {
for prefix in sorted_keys(namespaces) {
if namespaces[prefix] == SYNDICATION_NAMESPACE {
w.write_text_element("\{prefix}:updatePeriod", self.period.to_string())
w.write_text_element(
"\{prefix}:updateFrequency",
self.frequency.to_string(),
)
w.write_text_element("\{prefix}:updateBase", self.base)
}
}
}
///|
fn Item::write(self : Item, w : @xml.Writer) -> Unit {
w.open("item")
if self.title is Some(v) {
w.write_text_element("title", v)
}
if self.link is Some(v) {
w.write_text_element("link", v)
}
if self.description is Some(v) {
w.write_cdata_element("description", v)
}
if self.author is Some(v) {
w.write_text_element("author", v)
}
for category in self.categories {
category.write(w)
}
if self.comments is Some(v) {
w.write_text_element("comments", v)
}
if self.enclosure is Some(enclosure) {
enclosure.write(w)
}
if self.guid is Some(guid) {
guid.write(w)
}
if self.pub_date is Some(v) {
w.write_text_element("pubDate", v)
}
if self.source is Some(source) {
source.write(w)
}
if self.content is Some(v) {
w.write_cdata_element("content:encoded", v)
}
for _ns in sorted_keys(self.extensions) {
let inner = self.extensions[_ns]
for name in sorted_extension_names(inner) {
for ext in inner[name] {
ext.write(w)
}
}
}
for link in self.atom_links {
atom_link_write(link, w)
}
if self.itunes_ext is Some(ext) {
ext.write(w)
}
if self.dublin_core_ext is Some(ext) {
ext.write(w)
}
w.close("item")
}
///|
/// Write an `atom:link` extension element (href and rel are always emitted).
fn atom_link_write(link : @atom.Link, w : @xml.Writer) -> Unit {
let attrs : Array[(String, String)] = [
("href", link.href),
("rel", link.rel.unwrap_or("alternate")),
]
if link.hreflang is Some(v) {
attrs.push(("hreflang", v))
}
if link.media_type is Some(v) {
attrs.push(("type", v))
}
if link.title is Some(v) {
attrs.push(("title", v))
}
if link.length is Some(v) {
attrs.push(("length", v))
}
w.empty("atom:link", attrs~)
}
///|
///|
/// Serialise the channel as RSS 2.0 XML.
pub fn Channel::to_xml(self : Channel) -> String {
channel_to_xml(self)
}
///|
pub impl Show for Channel with fn output(self, logger) -> Unit {
logger.write_string(channel_to_xml(self))
}
///|
/// Serialise the channel as RSS 2.0 XML. Writing follows the crate:
/// only RSS 2.0 output is produced regardless of the version parsed.
fn channel_to_xml(channel : Channel) -> String {
let w = @xml.Writer::new()
w.write_decl()
// Merge computed namespaces with explicitly-declared ones; declared wins.
let merged : Map[String, String] = Map([])
for k, v in channel.used_namespaces() {
merged[k] = v
}
for k, v in channel.namespaces {
merged[k] = v
}
let root_attrs : Array[(String, String)] = [("version", "2.0")]
for prefix in sorted_keys(merged) {
root_attrs.push(("xmlns:\{prefix}", merged[prefix]))
}
w.open_with("rss", attrs=root_attrs)
w.open("channel")
w.write_text_element("title", channel.title)
w.write_text_element("link", channel.link)
w.write_text_element("description", channel.description)
if channel.language is Some(v) {
w.write_text_element("language", v)
}
if channel.copyright is Some(v) {
w.write_text_element("copyright", v)
}
if channel.managing_editor is Some(v) {
w.write_text_element("managingEditor", v)
}
if channel.webmaster is Some(v) {
w.write_text_element("webMaster", v)
}
if channel.pub_date is Some(v) {
w.write_text_element("pubDate", v)
}
if channel.last_build_date is Some(v) {
w.write_text_element("lastBuildDate", v)
}
for category in channel.categories {
category.write(w)
}
if channel.generator is Some(v) {
w.write_text_element("generator", v)
}
if channel.rating is Some(v) {
w.write_text_element("rating", v)
}
if channel.docs is Some(v) {
w.write_text_element("docs", v)
}
if channel.cloud is Some(cloud) {
cloud.write(w)
}
if channel.ttl is Some(v) {
w.write_text_element("ttl", v)
}
if channel.image is Some(image) {
image.write(w)
}
if channel.text_input is Some(text_input) {
text_input.write(w)
}
if channel.skip_hours.length() > 0 {
w.open("skipHours")
for hour in channel.skip_hours {
w.write_text_element("hour", hour)
}
w.close("skipHours")
}
if channel.skip_days.length() > 0 {
w.open("skipDays")
for day in channel.skip_days {
w.write_text_element("day", day)
}
w.close("skipDays")
}
for _ns in sorted_keys(channel.extensions) {
let inner = channel.extensions[_ns]
for name in sorted_extension_names(inner) {
for ext in inner[name] {
ext.write(w)
}
}
}
for link in channel.atom_links {
atom_link_write(link, w)
}
if channel.itunes_ext is Some(ext) {
ext.write(w)
}
if channel.dublin_core_ext is Some(ext) {
ext.write(w)
}
if channel.syndication_ext is Some(ext) {
ext.write(channel.namespaces, w)
}
for item in channel.items {
item.write(w)
}
w.close("channel")
w.close("rss")
w.to_string()
}