///|
/// Parse an Atom 1.0 document into a [Feed].
///
/// The root element must be `feed`. Children are matched by local name;
/// unprefixed children resolve against the feed's default namespace, and the
/// conventional `atom:` prefix is always accepted. Unknown elements are
/// skipped, which keeps the parser forward-compatible with extensions.
pub fn Feed::read_from(input : String) -> Feed raise AtomError {
Feed::read_from_impl(input) catch {
err => raise AtomError::Xml(err~)
}
}
///|
fn Feed::read_from_impl(input : String) -> Feed raise @xml.XmlError {
let r = @xml.Reader::new(input)
// Scan to the root start tag.
for ;; {
match r.next() {
@xml.Start(name~, attrs~) => {
if @xml.local_name(name) != "feed" {
raise @xml.Syntax(pos=r.position(), msg="root element is not ")
}
let default_ns = attr_value(attrs, "xmlns")
return parse_feed_children(r, attrs, default_ns)
}
@xml.Empty(name~) =>
raise @xml.Syntax(pos=0, msg="unexpected self-closing root <\{name}>")
@xml.Eof => raise @xml.Syntax(pos=0, msg="empty document")
_ => continue
}
}
}
///|
fn parse_feed_children(
r : @xml.Reader,
ns_attrs : Array[@xml.Attribute],
default_ns : String?,
) -> Feed raise @xml.XmlError {
let feed : Feed = {
id: "",
title: Text::plain(""),
updated: "",
authors: [],
contributors: [],
categories: [],
links: [],
generator: None,
icon: None,
logo: None,
rights: None,
subtitle: None,
entries: [],
}
let mut id = ""
let mut title : Text? = None
let mut updated = ""
let mut icon : String? = None
let mut logo : String? = None
let mut generator : Generator? = None
let mut rights : Text? = None
let mut subtitle : Text? = None
for ;; {
match r.next() {
@xml.Start(name~, attrs~) => {
let lname = @xml.local_name(name)
if !atom_ns_ok(name, ns_attrs, default_ns) {
skip_element(r)
continue
}
match lname {
"id" => id = read_text_element(r)
"title" => title = Some(read_text_construct(r, attrs))
"subtitle" => subtitle = Some(read_text_construct(r, attrs))
"rights" => rights = Some(read_text_construct(r, attrs))
"updated" => updated = read_text_element(r)
"icon" => icon = Some(read_text_element(r))
"logo" => logo = Some(read_text_element(r))
"generator" =>
generator = Some({
value: Some(read_text_element(r)),
uri: attr_value(attrs, "uri"),
version: attr_value(attrs, "version"),
})
"author" => feed.authors.push(read_person(r))
"contributor" => feed.contributors.push(read_person(r))
"category" => {
feed.categories.push(category_from(attrs))
skip_element(r)
}
"link" => {
feed.links.push(link_from(attrs))
skip_element(r)
}
"entry" => feed.entries.push(read_entry(r, ns_attrs, default_ns))
_ => skip_element(r)
}
}
@xml.Empty(name~, attrs~) =>
// Attribute-style elements carry all their data in attributes.
if atom_ns_ok(name, ns_attrs, default_ns) {
match @xml.local_name(name) {
"category" => feed.categories.push(category_from(attrs))
"link" => feed.links.push(link_from(attrs))
_ => ()
}
}
@xml.End(_) | @xml.Eof => break
_ => continue
}
}
{
..feed,
id,
title: title.unwrap_or(Text::plain("")),
updated,
icon,
logo,
generator,
rights,
subtitle,
}
}
///|
fn read_entry(
r : @xml.Reader,
ns_attrs : Array[@xml.Attribute],
default_ns : String?,
) -> Entry raise @xml.XmlError {
let entry = Entry::new(id="", title="", updated="")
let mut id = ""
let mut title : Text? = None
let mut updated = ""
let mut published : String? = None
let mut rights : Text? = None
let mut summary : Text? = None
let mut content : Content? = None
let mut source : Source? = None
for ;; {
match r.next() {
@xml.Start(name~, attrs~) => {
let lname = @xml.local_name(name)
if !atom_ns_ok(name, ns_attrs, default_ns) {
skip_element(r)
continue
}
match lname {
"id" => id = read_text_element(r)
"title" => title = Some(read_text_construct(r, attrs))
"updated" => updated = read_text_element(r)
"published" => published = Some(read_text_element(r))
"rights" => rights = Some(read_text_construct(r, attrs))
"summary" => summary = Some(read_text_construct(r, attrs))
"content" => content = Some(read_content(r, attrs))
"author" => entry.authors.push(read_person(r))
"contributor" => entry.contributors.push(read_person(r))
"category" => {
entry.categories.push(category_from(attrs))
skip_element(r)
}
"link" => {
entry.links.push(link_from(attrs))
skip_element(r)
}
"source" => source = Some(read_source(r, ns_attrs, default_ns))
_ => skip_element(r)
}
}
@xml.Empty(name~, attrs~) =>
if atom_ns_ok(name, ns_attrs, default_ns) {
match @xml.local_name(name) {
"content" =>
content = Some(
Content::Src(
src=attr_value(attrs, "src").unwrap_or(""),
media_type=attr_value(attrs, "type"),
),
)
"category" => entry.categories.push(category_from(attrs))
"link" => entry.links.push(link_from(attrs))
_ => ()
}
}
@xml.End(_) | @xml.Eof => break
_ => continue
}
}
{
..entry,
id,
title: title.unwrap_or(Text::plain("")),
updated,
published,
rights,
summary,
content,
source,
}
}
///|
fn read_source(
r : @xml.Reader,
ns_attrs : Array[@xml.Attribute],
default_ns : String?,
) -> Source raise @xml.XmlError {
let mut id : String? = None
let mut title : Text? = None
let mut updated : String? = None
for ;; {
match r.next() {
@xml.Start(name~, attrs~) => {
let lname = @xml.local_name(name)
if !atom_ns_ok(name, ns_attrs, default_ns) {
skip_element(r)
continue
}
match lname {
"id" => id = Some(read_text_element(r))
"title" => title = Some(read_text_construct(r, attrs))
"updated" => updated = Some(read_text_element(r))
_ => skip_element(r)
}
}
@xml.Empty(_) => ()
@xml.End(_) | @xml.Eof => break
_ => ()
}
}
{ id, title, updated }
}
///|
fn read_person(r : @xml.Reader) -> Person raise @xml.XmlError {
let mut pname = ""
let mut email : String? = None
let mut uri : String? = None
for ;; {
match r.next() {
@xml.Start(name=qname, ..) =>
match @xml.local_name(qname) {
"name" => pname = read_text_element(r)
"email" => email = Some(read_text_element(r))
"uri" => uri = Some(read_text_element(r))
_ => skip_element(r)
}
@xml.Empty(_) => ()
@xml.End(_) | @xml.Eof => break
_ => ()
}
}
{ name: pname, email, uri }
}
///|
/// Parse a text construct whose start tag has been consumed.
/// XHTML payloads are captured verbatim, minus the mandatory `div` wrapper.
fn read_text_construct(
r : @xml.Reader,
attrs : Array[@xml.Attribute],
) -> Text raise @xml.XmlError {
let typ : TextType = match attr_value(attrs, "type") {
Some("html") => TextType::Html
Some("xhtml") => TextType::Xhtml
_ => TextType::Text
}
let sb = StringBuilder()
let open_stack : Array[String] = []
let mut result : String? = None
for ;; {
match r.next() {
@xml.Text(t) => sb.write_string(t)
@xml.CData(t) => sb.write_string(t)
@xml.Comment(_) => ()
@xml.Start(name~, attrs=attrs2) => {
open_stack.push(name)
sb.write_string("<\{name}")
for attr in attrs2 {
sb.write_string(" \{attr.key}=\"\{@xml.escape_attr(attr.value)}\"")
}
sb.write_string(">")
}
@xml.Empty(name~, attrs=attrs2) => {
sb.write_string("<\{name}")
for attr in attrs2 {
sb.write_string(" \{attr.key}=\"\{@xml.escape_attr(attr.value)}\"")
}
sb.write_string("/>")
}
@xml.End(_) => {
if open_stack.length() == 0 {
result = Some(sb.to_string())
break
}
let name = open_stack[open_stack.length() - 1]
ignore(open_stack.pop())
sb.write_string("\{name}>")
}
@xml.Eof => {
result = Some(sb.to_string())
break
}
}
}
let raw = match result {
Some(raw) => raw
None => ""
}
let value = if typ is TextType::Xhtml { strip_div_wrapper(raw) } else { raw }
{ typ, value }
}
///|
/// Remove the mandatory `...
` wrapper from an XHTML
/// payload, tolerating attributes and nested markup.
fn strip_div_wrapper(raw : String) -> String {
let len = raw.length()
// Find the end of the opening tag.
let mut open_end = -1
if raw.length() >= 4 && substring_eq(raw, 0, "
' : UInt16) {
i += 1
}
open_end = i + 1
}
guard open_end >= 0 && open_end <= len else { return raw }
// Find the last
.
let close_lit = "
"
let close_start = len - close_lit.length()
if close_start < open_end || !substring_eq(raw, close_start, close_lit) {
return raw
}
substring(raw, open_end, close_start)
}
///|
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.. Content raise @xml.XmlError {
match attr_value(attrs, "src") {
Some(src) => {
skip_element(r)
Content::Src(src~, media_type=attr_value(attrs, "type"))
}
None => {
let text = read_text_construct(r, attrs)
match text.typ {
TextType::Html => Content::Html(text.value)
TextType::Xhtml => Content::Xhtml(text.value)
TextType::Text => Content::Text(text.value)
}
}
}
}
///|
/// Read the decoded character data of a simple element whose opening tag has
/// already been consumed.
fn read_text_element(r : @xml.Reader) -> String raise @xml.XmlError {
let sb = StringBuilder()
for ;; {
match r.next() {
@xml.Text(t) => sb.write_string(t)
@xml.CData(t) => sb.write_string(t)
@xml.End(_) | @xml.Eof => return sb.to_string()
_ => ()
}
}
}
///|
/// Consume events until the currently-open element closes (depth counting).
fn skip_element(r : @xml.Reader) -> Unit raise @xml.XmlError {
let mut depth = 1
while depth > 0 {
match r.next() {
@xml.Start(..) => depth += 1
@xml.End(_) => depth -= 1
@xml.Eof => return
_ => ()
}
}
}
///|
/// Copy a code-unit range into a new string without unicode-boundary checks.
fn substring(input : String, from : Int, to : Int) -> String {
let sb = StringBuilder()
for k in from.. Category {
{
term: attr_value(attrs, "term").unwrap_or(""),
scheme: attr_value(attrs, "scheme"),
label: attr_value(attrs, "label"),
}
}
///|
fn link_from(attrs : Array[@xml.Attribute]) -> Link {
{
href: attr_value(attrs, "href").unwrap_or(""),
rel: attr_value(attrs, "rel"),
media_type: attr_value(attrs, "type"),
hreflang: attr_value(attrs, "hreflang"),
title: attr_value(attrs, "title"),
length: attr_value(attrs, "length"),
}
}
///|
fn attr_value(attrs : Array[@xml.Attribute], key : String) -> String? {
for attr in attrs {
if attr.key == key {
return Some(attr.value)
}
}
None
}
///|
/// True when `qname` belongs to the Atom namespace given the in-scope
/// declarations. Unprefixed names require the document's default namespace
/// to be absent or the Atom namespace.
fn atom_ns_ok(
qname : String,
ns_attrs : Array[@xml.Attribute],
default_ns : String?,
) -> Bool {
match @xml.split_qname(qname) {
(Some(p), _) =>
if p == "atom" {
true
} else {
prefix_bound_to_atom(p, ns_attrs)
}
(None, _) => default_ns is None || default_ns == Some(ATOM_NAMESPACE)
}
}
///|
fn prefix_bound_to_atom(
prefix : String,
ns_attrs : Array[@xml.Attribute],
) -> Bool {
let decl = "xmlns:\{prefix}"
for attr in ns_attrs {
if attr.key == decl {
return attr.value == ATOM_NAMESPACE
}
}
false
}