///|
/// A reader adapter that mirrors the crate's `expand_empty_elements` setting:
/// `` surfaces as a [Start] immediately followed by its [End].
priv struct RssReader {
inner : @xml.Reader
mut pending_end : String?
}
///|
fn RssReader::new(input : String) -> RssReader raise RssError {
let reader = @xml.Reader::new(input) catch {
@xml.Syntax(pos~, msg~) => raise RssError::Xml(err=@xml.Syntax(pos~, msg~))
_ => abort("rss: unexpected error")
}
{ inner: reader, pending_end: None }
}
///|
fn RssReader::next(self : RssReader) -> @xml.Event raise RssError {
if self.pending_end is Some(name) {
self.pending_end = None
return @xml.End(name)
}
let ev = self.inner.next() catch {
@xml.Syntax(pos~, msg~) => raise RssError::Xml(err=@xml.Syntax(pos~, msg~))
_ => abort("rss: unexpected error")
}
match ev {
@xml.Empty(name~, attrs~) => {
self.pending_end = Some(name)
@xml.Start(name~, attrs~)
}
other => other
}
}
///|
/// The code-unit offset of the underlying scanner (for diagnostics).
fn RssReader::position(self : RssReader) -> Int {
self.inner.position()
}
///|
/// Port of `rss::util::element_text`: concatenated character content of the
/// currently-open element, trimmed; `None` when empty. Nested elements are
/// skipped.
fn element_text(r : RssReader) -> String? raise RssError {
let sb = StringBuilder()
for ;; {
match r.next() {
@xml.Text(t) => sb.write_string(t)
@xml.CData(t) => sb.write_string(t)
@xml.Start(..) => skip_element(r)
@xml.End(_) | @xml.Eof => break
_ => ()
}
}
let content = @xml.trim(sb.to_string())
if content == "" {
None
} else {
Some(content)
}
}
///|
/// Skip the remainder of the currently-open element (depth counting).
fn skip_element(r : RssReader) -> Unit raise RssError {
let mut depth = 1
while depth > 0 {
match r.next() {
@xml.Start(..) => depth += 1
@xml.End(_) => depth -= 1
@xml.Eof => return
_ => ()
}
}
}
///|
/// Merge `xmlns:prefix` declarations of a start tag into `base`.
fn read_namespaces(
attrs : Array[@xml.Attribute],
base : Map[String, String],
) -> Map[String, String] {
let out : Map[String, String] = Map([])
let mut added = false
for attr in attrs {
if attr.key.length() > 6 && substring_eq(attr.key, 0, "xmlns:") {
if !added {
for k, v in base {
out[k] = v
}
added = true
}
out[substring(attr.key, 6, attr.key.length())] = attr.value
}
}
if added {
out
} else {
base
}
}
///|
fn substring_eq(s : String, offset : Int, lit : String) -> Bool {
let n = lit.length()
if offset + n > s.length() || offset < 0 {
return false
}
for k in 0.. String {
let sb = StringBuilder()
for k in from.. (String, String)? {
match @xml.split_qname(qname) {
(Some(prefix), name) => Some((prefix, name))
(None, _) => None
}
}
///|
fn attribute_value(attrs : Array[@xml.Attribute], key : String) -> String? {
for attr in attrs {
if attr.key == key {
return Some(attr.value)
}
}
None
}
///|
/// Parse a generic namespaced element whose start tag has been consumed.
/// Mirrors `parse_extension_element`.
fn parse_extension_element(
r : RssReader,
start_attrs : Array[@xml.Attribute],
) -> Extension raise RssError {
let attrs : Map[String, String] = Map([])
for attr in start_attrs {
attrs[attr.key] = attr.value
}
let children : Map[String, Array[Extension]] = Map([])
let text = StringBuilder()
let mut name = ""
for ;; {
match r.next() {
@xml.Start(name=name2, attrs=attrs2) => {
let child = parse_extension_element(r, attrs2)
let lname = @xml.local_name(name2)
let list = match children.get(lname) {
Some(list) => list
None => {
let list : Array[Extension] = []
children[lname] = list
list
}
}
list.push(child)
}
@xml.Text(t) => text.write_string(t)
@xml.CData(t) => text.write_string(t)
@xml.End(name2) => {
name = name2
break
}
@xml.Eof => raise unexpected_eof(r)
_ => ()
}
}
let trimmed = @xml.trim(text.to_string())
{
name,
value: if trimmed == "" {
None
} else {
Some(trimmed)
},
attrs,
children,
}
}
///|
fn unexpected_eof(r : RssReader) -> RssError {
RssError::Xml(
err=@xml.Syntax(pos=r.position(), msg="unexpected end of input"),
)
}
///|
/// Route one namespaced element into the right bucket, mirroring the
/// namespace resolution of the crate's `channel.rs` / `item.rs`.
fn collect_extension(
exts : ExtensionMap,
atom_links : Array[@atom.Link],
scope_ns : Map[String, String],
prefix : String,
name : String,
ext : Extension,
) -> Unit {
let resolved = scope_ns.get(prefix)
if resolved is Some(ns) {
if ns == ATOM_NAMESPACE {
atom_links.push(atom_link_from(ext))
return
}
if is_itunes_namespace(ns) {
extension_entry(exts, ITUNES_NAMESPACE, name).push(ext)
return
}
if ns == DUBLIN_CORE_NAMESPACE || ns == SYNDICATION_NAMESPACE {
extension_entry(exts, ns, name).push(ext)
return
}
}
extension_entry(exts, prefix, name).push(ext)
}
///|
/// The Atom namespace used when bucketing extensions.
const ATOM_NAMESPACE : String = "http://www.w3.org/2005/Atom"
///|
/// Convert a generic `atom:link` extension element into an Atom link,
/// mirroring `AtomExtension::from_map`.
fn atom_link_from(ext : Extension) -> @atom.Link {
{
href: ext.attrs.get("href").unwrap_or(""),
rel: Some(ext.attrs.get("rel").unwrap_or("alternate")),
media_type: ext.attrs.get("type"),
hreflang: ext.attrs.get("hreflang"),
title: ext.attrs.get("title"),
length: ext.attrs.get("length"),
}
}
///|
/// Split extracted generic extensions into typed extension structs,
/// mirroring the tail of `Channel::from_xml` / `Item::from_xml`.
fn extract_extensions(
exts : ExtensionMap,
) -> (ITunesChannelExtension?, DublinCoreExtension?, SyndicationExtension?) {
let itunes = match exts.get(ITUNES_NAMESPACE) {
Some(map) => Some(ITunesChannelExtension::from_map(map))
None => None
}
exts.remove(ITUNES_NAMESPACE)
let dc = match exts.get(DUBLIN_CORE_NAMESPACE) {
Some(map) => Some(DublinCoreExtension::from_map(map))
None => None
}
exts.remove(DUBLIN_CORE_NAMESPACE)
let syn = match exts.get(SYNDICATION_NAMESPACE) {
Some(map) => Some(SyndicationExtension::from_map(map))
None => None
}
exts.remove(SYNDICATION_NAMESPACE)
(itunes, dc, syn)
}
///|
/// Extract item-level extensions (no syndication on items).
fn extract_item_extensions(
exts : ExtensionMap,
) -> (ITunesItemExtension?, DublinCoreExtension?) {
let itunes = match exts.get(ITUNES_NAMESPACE) {
Some(map) => Some(ITunesItemExtension::from_map(map))
None => None
}
exts.remove(ITUNES_NAMESPACE)
let dc = match exts.get(DUBLIN_CORE_NAMESPACE) {
Some(map) => Some(DublinCoreExtension::from_map(map))
None => None
}
exts.remove(DUBLIN_CORE_NAMESPACE)
(itunes, dc)
}