///|
fn uri_source_valid(source_url : String) -> Bool {
  match uri_parts(source_url) {
    Some(parts) =>
      match (parts.scheme, parts.authority, parts.fragment) {
        (Some(scheme), Some(authority), None) =>
          (scheme.to_lower() == "http" || scheme.to_lower() == "https") &&
          uri_http_authority_valid(authority)
        _ => false
      }
    None => false
  }
}

///|
fn uri_absolute(reference : String) -> String? {
  match uri_parts(reference) {
    Some(parts) if uri_valid_absolute(parts) =>
      Some(
        uri_build({
          scheme: parts.scheme,
          authority: parts.authority,
          path: uri_remove_dot_segments(parts.path),
          query: parts.query,
          fragment: parts.fragment,
        }),
      )
    _ => None
  }
}

///|
fn uri_at(
  base : String?,
  raw : String,
  field : String,
  line : Int32,
  column : Int32,
  diagnostics : Array[Diagnostic],
) -> String {
  let resolved = match base {
    Some(base) => uri_resolve(base, raw)
    None => uri_absolute(raw)
  }
  match resolved {
    Some(value) => value
    None => {
      diagnostics.push({ kind: UnresolvedUri(field), line, column, })
      raw
    }
  }
}

///|
fn uri_with_attachment_url(attachment : Attachment, url : String) -> Attachment {
  {
    source: attachment.source,
    url,
    media_type: attachment.media_type,
    size_in_bytes: attachment.size_in_bytes,
    title: attachment.title,
    duration_in_seconds: attachment.duration_in_seconds,
  }
}

///|
fn uri_with_entry_urls(
  entry : Entry,
  link : String?,
  external_url : String?,
  attachments : Array[Attachment],
  images : Array[EntryImage],
  content_source : String?,
  atom_body : AtomBody?,
  atom_source : AtomSource?,
  links : Array[AtomLink],
  comments_url : String?,
  source_url : String?,
  authors : Array[Author],
  contributors : Array[Author],
) -> Entry {
  {
    identifier: entry.identifier,
    title: entry.title,
    title_kind: entry.title_kind,
    title_xhtml: entry.title_xhtml,
    link,
    external_url,
    language: entry.language,
    description: entry.description,
    description_kind: entry.description_kind,
    description_xhtml: entry.description_xhtml,
    content: entry.content,
    content_text: entry.content_text,
    content_html: entry.content_html,
    content_kind: entry.content_kind,
    content_xhtml: entry.content_xhtml,
    content_source,
    content_media_type: entry.content_media_type,
    atom_body,
    atom_source,
    links,
    comments_url,
    source_url,
    source_title: entry.source_title,
    guid_is_permalink: entry.guid_is_permalink,
    rights: entry.rights,
    rights_kind: entry.rights_kind,
    rights_xhtml: entry.rights_xhtml,
    authors,
    contributors,
    categories: entry.categories,
    attachments,
    images,
    published_at: entry.published_at,
    updated_at: entry.updated_at,
  }
}

///|
fn uri_with_feed_urls(
  feed : Feed,
  link : String?,
  entries : Array[Entry],
  next_url : String?,
  hubs : Array[Hub],
  authors : Array[Author],
  contributors : Array[Author],
  links : Array[AtomLink],
  generator_uri : String?,
) -> Feed {
  {
    format: feed.format,
    identifier: feed.identifier,
    title: feed.title,
    title_kind: feed.title_kind,
    title_xhtml: feed.title_xhtml,
    link,
    language: feed.language,
    description: feed.description,
    description_kind: feed.description_kind,
    description_xhtml: feed.description_xhtml,
    updated_at: feed.updated_at,
    published_at: feed.published_at,
    ttl_minutes: feed.ttl_minutes,
    docs_url: feed.docs_url,
    rating: feed.rating,
    cloud: feed.cloud,
    text_input: feed.text_input,
    skip_hours: feed.skip_hours,
    skip_days: feed.skip_days,
    rights: feed.rights,
    rights_kind: feed.rights_kind,
    rights_xhtml: feed.rights_xhtml,
    generator: feed.generator,
    generator_uri,
    generator_version: feed.generator_version,
    managing_editor: feed.managing_editor,
    webmaster: feed.webmaster,
    next_url,
    expired: feed.expired,
    hubs,
    links,
    authors,
    contributors,
    categories: feed.categories,
    images: feed.images,
    entries,
    diagnostics: feed.diagnostics,
  }
}

///|
fn xml_base_attribute(element : @xml.NamespaceElement) -> String? {
  for attribute in element.attributes {
    if attribute.name.local_name == "base" &&
      attribute.name.namespace_uri ==
      Some("http://www.w3.org/XML/1998/namespace") {
      return Some(attribute.value)
    }
  }
  None
}

///|
fn xml_effective_base(
  element : @xml.NamespaceElement,
  parent_base : String?,
  format : FeedFormat,
  line : Int32,
  column : Int32,
  diagnostics : Array[Diagnostic],
) -> String? {
  if format != Atom1 {
    return parent_base
  }
  match xml_base_attribute(element) {
    None => parent_base
    Some(reference) => {
      let resolved = match parent_base {
        Some(base) => uri_resolve(base, reference)
        None => uri_absolute(reference)
      }
      if resolved is None {
        diagnostics.push({ kind: UnresolvedUri("atom xml:base"), line, column, })
      }
      resolved
    }
  }
}

///|
fn rss_valid_enclosure(element : @xml.NamespaceElement) -> Bool {
  let url = attribute_value(element, "url")
  let media_type = attribute_value(element, "type")
  let length = attribute_value(element, "length")
  match (url, media_type, length) {
    (Some(url), Some(media_type), Some(length)) =>
      !url.trim().is_empty() &&
      !media_type.trim().is_empty() &&
      attachment_size(length) is Some(_)
    _ => false
  }
}

///|
priv struct UriXmlState {
  format : FeedFormat
  standalone : Bool
  source_url : String
  diagnostics : Array[Diagnostic]
  mut feed_link : String?
  mut feed_generator_uri : String?
  links : Array[String?]
  attachments : Array[Array[Attachment]]
  feed_links : Array[AtomLink]
  entry_links : Array[Array[AtomLink]]
  source_links : Array[Array[AtomLink]]
  source_images : Array[Array[FeedImage]]
  source_links_alt : Array[String?]
  source_generators : Array[String?]
  person_lists : Array[Array[Author]]
  person_indexes : Array[Int32]
  entry_own_authors : Array[Bool]
  mut person_group : Int32
  mut person_depth : Int32
  mut person_name : String?
  mut person_uri : String?
  mut person_uri_base : String?
  mut person_uri_line : Int32
  mut person_uri_column : Int32
  mut person_field : String?
  mut person_text : String
  mut feed_link_index : Int32
  mut entry_link_index : Int32
  mut source_link_index : Int32
  mut source_image_index : Int32
  mut source_alternate_seen : Bool
  mut source_open : Bool
  mut source_depth : Int32
  mut image_field : String?
  mut image_text : String
  mut image_base : String?
  mut image_line : Int32
  mut image_column : Int32
  content_sources : Array[String?]
  bodies : Array[AtomBody?]
  entry_link_locations : Array[(Int32, Int32)?]
  mut feed_link_location : (Int32, Int32)?
  mut channel_depth : Int32
  mut entry_index : Int32
  mut entry_depth : Int32
  mut entry_open : Bool
  mut feed_alternate_seen : Bool
  mut entry_alternate_seen : Bool
  mut attachment_index : Int32
}

///|
fn UriXmlState::new(
  feed : Feed,
  source_url : String,
  standalone : Bool,
) -> UriXmlState {
  let links : Array[String?] = []
  let attachments : Array[Array[Attachment]] = []
  let entry_links : Array[Array[AtomLink]] = []
  let source_links : Array[Array[AtomLink]] = []
  let source_images : Array[Array[FeedImage]] = []
  let source_links_alt : Array[String?] = []
  let source_generators : Array[String?] = []
  let feed_links : Array[AtomLink] = []
  let person_lists : Array[Array[Author]] = [
    copy_authors(feed.authors),
    copy_authors(feed.contributors),
  ]
  let person_indexes : Array[Int32] = [0, 0]
  let entry_own_authors : Array[Bool] = []
  for link in feed.links {
    feed_links.push(link)
  }
  let content_sources : Array[String?] = []
  let bodies : Array[AtomBody?] = []
  let locations : Array[(Int32, Int32)?] = []
  for entry in feed.entries {
    links.push(entry.link)
    let copied : Array[Attachment] = []
    for attachment in entry.attachments {
      copied.push(attachment)
    }
    attachments.push(copied)
    let copied_links : Array[AtomLink] = []
    for link in entry.links {
      copied_links.push(link)
    }
    entry_links.push(copied_links)
    let copied_source_links : Array[AtomLink] = []
    let copied_source_images : Array[FeedImage] = []
    match entry.atom_source {
      Some(source) => {
        for link in source.links {
          copied_source_links.push(link)
        }
        for image in source.images {
          copied_source_images.push(image)
        }
        source_links_alt.push(source.link)
        source_generators.push(source.generator_uri)
      }
      None => {
        source_links_alt.push(None)
        source_generators.push(None)
      }
    }
    source_links.push(copied_source_links)
    source_images.push(copied_source_images)
    person_lists.push(copy_authors(entry.authors))
    person_lists.push(copy_authors(entry.contributors))
    match entry.atom_source {
      Some(source) => {
        person_lists.push(copy_authors(source.authors))
        person_lists.push(copy_authors(source.contributors))
      }
      None => {
        person_lists.push([])
        person_lists.push([])
      }
    }
    for person_slot = 0; person_slot < 4; person_slot = person_slot + 1 {
      person_indexes.push(0)
    }
    entry_own_authors.push(false)
    content_sources.push(entry.content_source)
    bodies.push(entry.atom_body)
    locations.push(None)
  }
  {
    format: feed.format,
    standalone,
    source_url,
    diagnostics: feed.diagnostics,
    feed_link: feed.link,
    feed_generator_uri: feed.generator_uri,
    links,
    attachments,
    feed_links,
    entry_links,
    source_links,
    source_images,
    source_links_alt,
    source_generators,
    person_lists,
    person_indexes,
    entry_own_authors,
    person_group: -1,
    person_depth: 0,
    person_name: None,
    person_uri: None,
    person_uri_base: None,
    person_uri_line: 1,
    person_uri_column: 1,
    person_field: None,
    person_text: "",
    feed_link_index: 0,
    entry_link_index: 0,
    source_link_index: 0,
    source_image_index: 0,
    source_alternate_seen: false,
    source_open: false,
    source_depth: 0,
    image_field: None,
    image_text: "",
    image_base: None,
    image_line: 1,
    image_column: 1,
    content_sources,
    bodies,
    entry_link_locations: locations,
    feed_link_location: None,
    channel_depth: 0,
    entry_index: -1,
    entry_depth: 0,
    entry_open: false,
    feed_alternate_seen: false,
    entry_alternate_seen: false,
    attachment_index: 0,
  }
}

///|
fn UriXmlState::resolve_atom_link(
  self : UriXmlState,
  raw : String,
  base : String?,
  line : Int32,
  column : Int32,
  entry : Bool,
) -> Unit {
  let links = if entry {
    if self.entry_index < 0 || self.entry_index >= self.entry_links.length() {
      return
    }
    self.entry_links[self.entry_index]
  } else {
    self.feed_links
  }
  let index = if entry { self.entry_link_index } else { self.feed_link_index }
  if index >= links.length() || links[index].href != raw {
    return
  }
  let link = links[index]
  links[index] = {
    relation: link.relation,
    href: uri_at(base, raw, "atom link", line, column, self.diagnostics),
    media_type: link.media_type,
    hreflang: link.hreflang,
    title: link.title,
    length: link.length,
  }
  if entry {
    self.entry_link_index = index + 1
  } else {
    self.feed_link_index = index + 1
  }
}

///|
fn UriXmlState::resolve_attachment(
  self : UriXmlState,
  raw : String,
  base : String?,
  field : String,
  line : Int32,
  column : Int32,
) -> Unit {
  if self.entry_index < 0 || self.entry_index >= self.attachments.length() {
    return
  }
  let entries_attachments = self.attachments[self.entry_index]
  if self.attachment_index >= entries_attachments.length() ||
    entries_attachments[self.attachment_index].url != raw {
    return
  }
  let resolved = uri_at(base, raw, field, line, column, self.diagnostics)
  entries_attachments[self.attachment_index] = uri_with_attachment_url(
    entries_attachments[self.attachment_index],
    resolved,
  )
  self.attachment_index = self.attachment_index + 1
}

///|
fn uri_with_source_urls(
  source : AtomSource?,
  link : String?,
  links : Array[AtomLink],
  images : Array[FeedImage],
  generator_uri : String?,
  authors : Array[Author],
  contributors : Array[Author],
) -> AtomSource? {
  match source {
    None => None
    Some(source) =>
      Some({
        identifier: source.identifier,
        title: source.title,
        title_kind: source.title_kind,
        title_xhtml: source.title_xhtml,
        subtitle: source.subtitle,
        subtitle_kind: source.subtitle_kind,
        subtitle_xhtml: source.subtitle_xhtml,
        updated_at: source.updated_at,
        rights: source.rights,
        rights_kind: source.rights_kind,
        rights_xhtml: source.rights_xhtml,
        generator: source.generator,
        generator_uri,
        generator_version: source.generator_version,
        authors,
        contributors,
        categories: source.categories,
        link,
        links,
        images,
      })
  }
}

///|
fn UriXmlState::finish_image(self : UriXmlState) -> Unit {
  match self.image_field {
    Some(field) if self.entry_index >= 0 &&
      self.entry_index < self.source_images.length() => {
      let raw = self.image_text
      let images = self.source_images[self.entry_index]
      let index = self.source_image_index
      if index < images.length() &&
        images[index].url == raw &&
        images[index].role ==
        (if field == "icon" { AtomIcon } else { AtomLogo }) {
        let image = images[index]
        images[index] = {
          source: image.source,
          role: image.role,
          url: uri_at(
            self.image_base,
            raw,
            "atom source " + field,
            self.image_line,
            self.image_column,
            self.diagnostics,
          ),
          title: image.title,
          link: image.link,
          description: image.description,
          width: image.width,
          height: image.height,
        }
        self.source_image_index = index + 1
      }
    }
    _ => ()
  }
  self.image_field = None
  self.image_text = ""
}

///|
fn UriXmlState::finish_person(self : UriXmlState) -> Unit {
  let group = self.person_group
  if group >= 0 && group < self.person_lists.length() {
    let people = self.person_lists[group]
    let mut index = self.person_indexes[group]
    while index < people.length() {
      let person = people[index]
      if person.name == self.person_name && person.uri == self.person_uri {
        match self.person_uri {
          Some(raw) =>
            people[index] = {
              source: person.source,
              name: person.name,
              uri: Some(
                uri_at(
                  self.person_uri_base,
                  raw,
                  "atom person uri",
                  self.person_uri_line,
                  self.person_uri_column,
                  self.diagnostics,
                ),
              ),
              email: person.email,
              avatar: person.avatar,
            }
          None => ()
        }
        self.person_indexes[group] = index + 1
        if group >= 2 && (group - 2) % 4 == 0 {
          self.entry_own_authors[(group - 2) / 4] = true
        }
        break
      }
      index = index + 1
    }
  }
  self.person_group = -1
  self.person_field = None
  self.person_text = ""
}

///|
fn UriXmlState::on_element(
  self : UriXmlState,
  element : @xml.NamespaceElement,
  depth : Int32,
  base : String?,
  line : Int32,
  column : Int32,
  is_empty : Bool,
) -> Unit {
  if self.format == Rss2 {
    if depth == 1 && is_rss_name(element.name, "channel") {
      self.channel_depth = depth + 1
    } else if self.channel_depth > 0 &&
      depth == self.channel_depth &&
      is_atom_name(element.name, "link") &&
      atom_element_relation_is(element, "self") {
      match atom_iri_attribute(element, "href") {
        Some(href) => self.resolve_atom_link(href, base, line, column, false)
        None => ()
      }
    } else if self.channel_depth > 0 &&
      depth == self.channel_depth &&
      is_rss_name(element.name, "item") {
      self.entry_index = self.entry_index + 1
      self.entry_depth = depth + 1
      self.entry_open = !is_empty
      self.attachment_index = 0
    } else if self.entry_open &&
      depth == self.entry_depth &&
      is_rss_name(element.name, "link") {
      self.entry_link_locations[self.entry_index] = Some((line, column))
    } else if !self.entry_open &&
      depth == self.channel_depth &&
      is_rss_name(element.name, "link") {
      self.feed_link_location = Some((line, column))
    } else if self.entry_open &&
      depth == self.entry_depth &&
      is_rss_name(element.name, "enclosure") &&
      rss_valid_enclosure(element) {
      match attribute_value(element, "url") {
        Some(url) =>
          self.resolve_attachment(
            url,
            base,
            "rss item " +
            self.entry_index.to_string() +
            " enclosure " +
            self.attachment_index.to_string(),
            line,
            column,
          )
        None => ()
      }
    }
    return
  }
  if self.person_group >= 0 &&
    depth == self.person_depth &&
    is_atom_name(element.name, "name") &&
    is_empty {
    self.person_name = Some("")
  } else if self.person_group >= 0 &&
    depth == self.person_depth &&
    is_atom_name(element.name, "uri") &&
    is_empty {
    self.person_uri = Some("")
    self.person_uri_base = base
    self.person_uri_line = line
    self.person_uri_column = column
  } else if self.person_group >= 0 &&
    depth == self.person_depth &&
    (is_atom_name(element.name, "name") || is_atom_name(element.name, "uri")) &&
    !is_empty {
    self.person_field = Some(element.name.local_name)
    self.person_text = ""
    if is_atom_name(element.name, "uri") {
      self.person_uri_base = base
      self.person_uri_line = line
      self.person_uri_column = column
    }
  } else if self.person_group < 0 &&
    !is_empty &&
    (
      is_atom_name(element.name, "author") ||
      is_atom_name(element.name, "contributor")
    ) {
    let offset = if is_atom_name(element.name, "author") { 0 } else { 1 }
    let group = if self.source_open && depth == self.source_depth {
      2 + self.entry_index * 4 + 2 + offset
    } else if self.entry_open && depth == self.entry_depth {
      2 + self.entry_index * 4 + offset
    } else if !self.entry_open && depth == 1 {
      offset
    } else {
      -1
    }
    if group >= 0 {
      self.person_group = group
      self.person_depth = depth + 1
      self.person_name = None
      self.person_uri = None
      self.person_field = None
      self.person_text = ""
    }
  }
  if (depth == 1 || (self.standalone && depth == 0)) &&
    is_atom_name(element.name, "entry") {
    self.entry_index = self.entry_index + 1
    self.entry_depth = depth + 1
    self.entry_open = !is_empty
    self.entry_alternate_seen = false
    self.attachment_index = 0
    self.entry_link_index = 0
    self.source_open = false
  } else if self.entry_open &&
    depth == self.entry_depth &&
    is_atom_name(element.name, "source") {
    self.source_open = !is_empty
    self.source_depth = depth + 1
    self.source_link_index = 0
    self.source_image_index = 0
    self.source_alternate_seen = false
  } else if self.source_open &&
    depth == self.source_depth &&
    is_atom_name(element.name, "link") {
    match atom_iri_attribute(element, "href") {
      Some(raw) => {
        let links = self.source_links[self.entry_index]
        let index = self.source_link_index
        if index < links.length() && links[index].href == raw {
          let link = links[index]
          links[index] = {
            relation: link.relation,
            href: uri_at(
              base,
              raw,
              "atom source link",
              line,
              column,
              self.diagnostics,
            ),
            media_type: link.media_type,
            hreflang: link.hreflang,
            title: link.title,
            length: link.length,
          }
          self.source_link_index = index + 1
        }
      }
      None => ()
    }
    if !self.source_alternate_seen {
      match atom_alternate_link(element) {
        Some(raw) => {
          self.source_alternate_seen = true
          self.source_links_alt[self.entry_index] = Some(
            uri_at(
              base,
              raw,
              "atom source alternate link",
              line,
              column,
              self.diagnostics,
            ),
          )
        }
        None => ()
      }
    }
  } else if self.source_open &&
    depth == self.source_depth &&
    is_atom_name(element.name, "generator") {
    match atom_attribute_value(element, "uri") {
      Some(raw) if self.source_generators[self.entry_index] == Some(raw) =>
        self.source_generators[self.entry_index] = Some(
          uri_at(
            base,
            raw,
            "atom source generator uri",
            line,
            column,
            self.diagnostics,
          ),
        )
      _ => ()
    }
  } else if self.source_open &&
    depth == self.source_depth &&
    (is_atom_name(element.name, "icon") || is_atom_name(element.name, "logo")) {
    self.image_field = Some(element.name.local_name)
    self.image_text = ""
    self.image_base = base
    self.image_line = line
    self.image_column = column
    if is_empty {
      self.finish_image()
    }
  } else if !self.entry_open &&
    depth == 1 &&
    is_atom_name(element.name, "generator") {
    match atom_attribute_value(element, "uri") {
      Some(raw) if self.feed_generator_uri == Some(raw) =>
        self.feed_generator_uri = Some(
          uri_at(
            base,
            raw,
            "atom feed generator uri",
            line,
            column,
            self.diagnostics,
          ),
        )
      _ => ()
    }
  } else if self.entry_open &&
    depth == self.entry_depth &&
    is_atom_name(element.name, "content") {
    match atom_attribute_value(element, "src") {
      Some(raw) if self.content_sources[self.entry_index] == Some(raw) => {
        let resolved = uri_at(
          base,
          raw,
          "atom entry " + self.entry_index.to_string() + " content src",
          line,
          column,
          self.diagnostics,
        )
        self.content_sources[self.entry_index] = Some(resolved)
        match self.bodies[self.entry_index] {
          Some(External(reference)) if reference.src == raw =>
            self.bodies[self.entry_index] = Some(
              External({
                src: reference.src,
                url: resolved,
                media_type: reference.media_type,
                base_uri: base,
              }),
            )
          _ => ()
        }
      }
      _ => ()
    }
  } else if !self.entry_open && depth == 1 && is_atom_name(element.name, "link") {
    match atom_iri_attribute(element, "href") {
      Some(raw) => self.resolve_atom_link(raw, base, line, column, false)
      None => ()
    }
    if !self.feed_alternate_seen {
      match atom_alternate_link(element) {
        Some(href) => {
          self.feed_alternate_seen = true
          self.feed_link = Some(
            uri_at(
              base,
              href,
              "atom feed alternate link",
              line,
              column,
              self.diagnostics,
            ),
          )
        }
        None => ()
      }
    }
  } else if self.entry_open &&
    depth == self.entry_depth &&
    is_atom_name(element.name, "link") {
    match atom_iri_attribute(element, "href") {
      Some(raw) => self.resolve_atom_link(raw, base, line, column, true)
      None => ()
    }
    if !self.entry_alternate_seen {
      match atom_alternate_link(element) {
        Some(href) => {
          self.entry_alternate_seen = true
          self.links[self.entry_index] = Some(
            uri_at(
              base,
              href,
              "atom entry " + self.entry_index.to_string() + " alternate link",
              line,
              column,
              self.diagnostics,
            ),
          )
        }
        None => ()
      }
    }
    if atom_element_relation_is(element, "enclosure") {
      match atom_iri_attribute(element, "href") {
        Some(href) =>
          self.resolve_attachment(
            href,
            base,
            "atom entry " +
            self.entry_index.to_string() +
            " enclosure " +
            self.attachment_index.to_string(),
            line,
            column,
          )
        None => ()
      }
    }
  }
}

///|
fn UriXmlState::finish(self : UriXmlState, feed : Feed) -> Feed {
  if self.format == Rss2 {
    match self.feed_link {
      Some(link) => {
        let (line, column) = self.feed_link_location.unwrap_or((1, 1))
        self.feed_link = Some(
          uri_at(
            Some(self.source_url),
            link,
            "rss channel link",
            line,
            column,
            self.diagnostics,
          ),
        )
      }
      None => ()
    }
    for index = 0; index < self.links.length(); index = index + 1 {
      match self.links[index] {
        Some(link) => {
          let (line, column) = self.entry_link_locations[index].unwrap_or(
            (1, 1),
          )
          self.links[index] = Some(
            uri_at(
              Some(self.source_url),
              link,
              "rss item " + index.to_string() + " link",
              line,
              column,
              self.diagnostics,
            ),
          )
        }
        None => ()
      }
    }
  }
  let entries : Array[Entry] = []
  for index = 0; index < feed.entries.length(); index = index + 1 {
    let person_group = 2 + index * 4
    let effective_authors = if self.format != Atom1 {
      feed.entries[index].authors
    } else if self.entry_own_authors[index] {
      self.person_lists[person_group]
    } else if !self.person_lists[person_group + 2].is_empty() {
      self.person_lists[person_group + 2]
    } else if !self.person_lists[0].is_empty() {
      self.person_lists[0]
    } else {
      feed.entries[index].authors
    }
    entries.push(
      uri_with_entry_urls(
        feed.entries[index],
        self.links[index],
        feed.entries[index].external_url,
        self.attachments[index],
        feed.entries[index].images,
        self.content_sources[index],
        self.bodies[index],
        if self.format == Atom1 {
          uri_with_source_urls(
            feed.entries[index].atom_source,
            self.source_links_alt[index],
            self.source_links[index],
            self.source_images[index],
            self.source_generators[index],
            self.person_lists[person_group + 2],
            self.person_lists[person_group + 3],
          )
        } else {
          feed.entries[index].atom_source
        },
        self.entry_links[index],
        if self.format == Rss2 {
          match feed.entries[index].comments_url {
            Some(raw) =>
              Some(
                uri_at(
                  Some(self.source_url),
                  raw,
                  "rss item " + index.to_string() + " comments",
                  1,
                  1,
                  self.diagnostics,
                ),
              )
            None => None
          }
        } else {
          feed.entries[index].comments_url
        },
        if self.format == Rss2 {
          match feed.entries[index].source_url {
            Some(raw) =>
              Some(
                uri_at(
                  Some(self.source_url),
                  raw,
                  "rss item " + index.to_string() + " source",
                  1,
                  1,
                  self.diagnostics,
                ),
              )
            None => None
          }
        } else {
          feed.entries[index].source_url
        },
        effective_authors,
        if self.format == Atom1 {
          self.person_lists[person_group + 1]
        } else {
          feed.entries[index].contributors
        },
      ),
    )
  }
  uri_with_feed_urls(
    feed,
    self.feed_link,
    entries,
    feed.next_url,
    feed.hubs,
    if self.format == Atom1 {
      self.person_lists[0]
    } else {
      feed.authors
    },
    if self.format == Atom1 {
      self.person_lists[1]
    } else {
      feed.contributors
    },
    self.feed_links,
    self.feed_generator_uri,
  )
}

///|
fn resolve_xml_feed(
  input : String,
  feed : Feed,
  source_url : String,
  standalone : Bool,
) -> Result[Feed, ParseError] {
  let reader = @xml.NamespaceReader::from_string(input)
  let state = UriXmlState::new(feed, source_url, standalone)
  let bases : Array[String?] = []
  let mut depth = 0
  try {
    for ;; {
      let event = reader.read_event()
      match event.kind {
        @xml.Start(element) => {
          let parent_base = if bases.is_empty() {
            Some(source_url)
          } else {
            bases[bases.length() - 1]
          }
          let base = xml_effective_base(
            element,
            parent_base,
            feed.format,
            event.span.start.line,
            event.span.start.column,
            state.diagnostics,
          )
          state.on_element(
            element,
            depth,
            base,
            event.span.start.line,
            event.span.start.column,
            false,
          )
          bases.push(base)
          depth = depth + 1
        }
        @xml.Empty(element) => {
          let parent_base = if bases.is_empty() {
            Some(source_url)
          } else {
            bases[bases.length() - 1]
          }
          let base = xml_effective_base(
            element,
            parent_base,
            feed.format,
            event.span.start.line,
            event.span.start.column,
            state.diagnostics,
          )
          state.on_element(
            element,
            depth,
            base,
            event.span.start.line,
            event.span.start.column,
            true,
          )
        }
        @xml.End(name) => {
          if state.person_group >= 0 &&
            state.person_field is Some(field) &&
            depth == state.person_depth + 1 &&
            is_atom_name(name, field) {
            if field == "name" {
              state.person_name = Some(state.person_text)
            } else {
              state.person_uri = Some(state.person_text)
            }
            state.person_field = None
            state.person_text = ""
          }
          if state.person_group >= 0 &&
            depth == state.person_depth &&
            (is_atom_name(name, "author") || is_atom_name(name, "contributor")) {
            state.finish_person()
          }
          if state.image_field is Some(field) &&
            state.source_open &&
            depth == state.source_depth + 1 &&
            is_atom_name(name, field) {
            state.finish_image()
          }
          if state.source_open &&
            depth == state.source_depth &&
            is_atom_name(name, "source") {
            state.source_open = false
          }
          if state.entry_open &&
            depth == state.entry_depth &&
            (
              (state.format == Rss2 && is_rss_name(name, "item")) ||
              (state.format == Atom1 && is_atom_name(name, "entry"))
            ) {
            state.entry_open = false
          }
          depth = depth - 1
          ignore(bases.pop())
        }
        @xml.Text(value) | @xml.CData(value) => {
          if state.image_field is Some(_) {
            state.image_text = state.image_text + value
          }
          if state.person_field is Some(_) {
            state.person_text = state.person_text + value
          }
        }
        @xml.Eof => return Ok(state.finish(feed))
        _ => ()
      }
    }
    abort("unreachable")
  } catch {
    @xml.At(error~, span~) =>
      Err(InvalidXml(error.to_string(), span.start.line, span.start.column))
  }
}

///|
fn resolve_json_feed(feed : Feed, source_url : String) -> Feed {
  let diagnostics = feed.diagnostics
  let images : Array[FeedImage] = []
  for image in feed.images {
    images.push({
      source: image.source,
      role: image.role,
      url: uri_at(
        Some(source_url),
        image.url,
        "json feed image",
        1,
        1,
        diagnostics,
      ),
      title: image.title,
      link: image.link,
      description: image.description,
      width: image.width,
      height: image.height,
    })
  }
  let link = match feed.link {
    Some(raw) =>
      Some(
        uri_at(
          Some(source_url),
          raw,
          "json feed home_page_url",
          1,
          1,
          diagnostics,
        ),
      )
    None => None
  }
  let entries : Array[Entry] = []
  for index = 0; index < feed.entries.length(); index = index + 1 {
    let entry = feed.entries[index]
    let entry_link = match entry.link {
      Some(raw) =>
        Some(
          uri_at(
            Some(source_url),
            raw,
            "json feed item " + index.to_string() + " url",
            1,
            1,
            diagnostics,
          ),
        )
      None => None
    }
    let external_url = match entry.external_url {
      Some(raw) =>
        Some(
          uri_at(
            Some(source_url),
            raw,
            "json feed item " + index.to_string() + " external_url",
            1,
            1,
            diagnostics,
          ),
        )
      None => None
    }
    let attachments : Array[Attachment] = []
    for attachment_index = 0
        attachment_index < entry.attachments.length()
        attachment_index = attachment_index + 1 {
      let attachment = entry.attachments[attachment_index]
      let url = uri_at(
        Some(source_url),
        attachment.url,
        "json feed item " +
        index.to_string() +
        " attachment " +
        attachment_index.to_string(),
        1,
        1,
        diagnostics,
      )
      attachments.push(uri_with_attachment_url(attachment, url))
    }
    let images : Array[EntryImage] = []
    for image in entry.images {
      images.push({
        source: image.source,
        role: image.role,
        url: uri_at(
          Some(source_url),
          image.url,
          "json feed item " + index.to_string() + " image",
          1,
          1,
          diagnostics,
        ),
      })
    }
    entries.push(
      uri_with_entry_urls(
        entry,
        entry_link,
        external_url,
        attachments,
        images,
        entry.content_source,
        entry.atom_body,
        entry.atom_source,
        entry.links,
        entry.comments_url,
        entry.source_url,
        resolve_json_authors(
          entry.authors,
          source_url,
          diagnostics,
          "json feed item " + index.to_string() + " author",
        ),
        entry.contributors,
      ),
    )
  }
  let next_url = match feed.next_url {
    Some(raw) => {
      let url = uri_at(
        Some(source_url),
        raw,
        "json feed next_url",
        1,
        1,
        diagnostics,
      )
      // Compare resolved addresses without changing the public, opaque
      // feed_url identifier. A malformed next_url keeps its own URI diagnostic.
      match uri_resolve(source_url, raw) {
        Some(target) => {
          let declared = match feed.identifier {
            Some(feed_url) => uri_resolve(source_url, feed_url)
            None => None
          }
          if Some(target) == declared {
            add_json_diagnostic(diagnostics, None, "next_url equals feed_url")
            None
          } else if Some(target) == uri_absolute(source_url) {
            add_json_diagnostic(diagnostics, None, "next_url equals source_url")
            None
          } else {
            Some(url)
          }
        }
        None => Some(url)
      }
    }
    None => None
  }
  let hubs : Array[Hub] = []
  for hub in feed.hubs {
    hubs.push({
      transport_type: hub.transport_type,
      url: uri_at(Some(source_url), hub.url, "json feed hub", 1, 1, diagnostics),
    })
  }
  uri_with_feed_urls(
    with_feed_images(feed, images),
    link,
    entries,
    next_url,
    hubs,
    resolve_json_authors(
      feed.authors,
      source_url,
      diagnostics,
      "json feed author",
    ),
    feed.contributors,
    feed.links,
    feed.generator_uri,
  )
}

///|
fn resolve_json_authors(
  authors : Array[Author],
  source_url : String,
  diagnostics : Array[Diagnostic],
  field : String,
) -> Array[Author] {
  let resolved : Array[Author] = []
  for author in authors {
    let uri = match author.uri {
      Some(raw) =>
        Some(uri_at(Some(source_url), raw, field + " url", 1, 1, diagnostics))
      None => None
    }
    let avatar = match author.avatar {
      Some(raw) =>
        Some(
          uri_at(Some(source_url), raw, field + " avatar", 1, 1, diagnostics),
        )
      None => None
    }
    resolved.push({
      source: author.source,
      name: author.name,
      uri,
      email: author.email,
      avatar,
    })
  }
  resolved
}

///|
/// Parse a document and resolve documented feed/entry links, attachment URLs,
/// feed-image URLs, and JSON Feed item external/image URLs against an explicit
/// HTTP(S) source URL. Atom `xml:base` is inherited by nested elements.
/// Identifiers and HTML content are never rewritten.
/// `parse(String)` remains the raw-reference entry point.
pub fn parse_with_source(
  input : String,
  source_url : String,
) -> Result[Feed, ParseError] {
  if !uri_source_valid(source_url) {
    return Err(InvalidSourceUrl(source_url))
  }
  let input = feed_input_without_bom(input)
  match parse(input) {
    Err(error) => Err(error)
    Ok(feed) =>
      match feed.format {
        JsonFeed1 => Ok(resolve_json_feed(feed, source_url))
        Rss2 | Atom1 =>
          match resolve_xml_feed(input, feed, source_url, false) {
            Ok(resolved) =>
              match
                add_xml_feed_images(input, resolved, Some(source_url), false) {
                Ok(imaged) =>
                  if feed.format == Rss2 {
                    add_rss_optional_channel(
                      input,
                      imaged,
                      Some(source_url),
                      false,
                    )
                  } else {
                    Ok(imaged)
                  }
                Err(error) => Err(error)
              }
            Err(error) => Err(error)
          }
      }
  }
}

///|
/// Resolve links and external content in either document shape using the
/// supplied HTTP(S) document URL and Atom xml:base scopes.
pub fn parse_document_with_source(
  input : String,
  source_url : String,
) -> Result[ParsedDocument, ParseError] {
  if !uri_source_valid(source_url) {
    return Err(InvalidSourceUrl(source_url))
  }
  let input = feed_input_without_bom(input)
  match parse_document(input) {
    Ok(FeedDocument(_)) =>
      match parse_with_source(input, source_url) {
        Ok(feed) => Ok(FeedDocument(feed))
        Err(error) => Err(error)
      }
    Ok(AtomEntryDocument(entry, diagnostics)) => {
      let feed : Feed = {
        format: Atom1,
        identifier: None,
        title: None,
        title_kind: Text,
        title_xhtml: None,
        link: None,
        language: None,
        description: None,
        description_kind: Text,
        description_xhtml: None,
        updated_at: None,
        published_at: None,
        ttl_minutes: None,
        docs_url: None,
        rating: None,
        cloud: None,
        text_input: None,
        skip_hours: [],
        skip_days: [],
        rights: None,
        rights_kind: Text,
        rights_xhtml: None,
        generator: None,
        generator_uri: None,
        generator_version: None,
        managing_editor: None,
        webmaster: None,
        next_url: None,
        expired: None,
        hubs: [],
        authors: [],
        contributors: [],
        categories: [],
        images: [],
        entries: [entry],
        links: [],
        diagnostics,
      }
      match resolve_xml_feed(input, feed, source_url, true) {
        Ok(resolved) =>
          Ok(AtomEntryDocument(resolved.entries[0], resolved.diagnostics))
        Err(error) => Err(error)
      }
    }
    Err(error) => Err(error)
  }
}