///|
/// 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("")
      }
      @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 }