///|
fn ReadXml::general_entity_ahead(p : ReadXml) -> Bool {
  if p.peek() != '&' || p.starts_with("&#") {
    return false
  }
  !(p.starts_with("&") ||
  p.starts_with("<") ||
  p.starts_with(">") ||
  p.starts_with("'") ||
  p.starts_with("""))
}

///|
fn ReadXml::attribute_source(
  p : ReadXml,
  quote : Char,
) -> String raise OdsError {
  let b = StringBuilder()
  while p.peek() != quote {
    let c = p.take()
    if c == '<' {
      raise Invalid("raw less-than in XML attribute")
    }
    b.write_char(c)
    if b.to_string().length() > 32767 {
      raise Invalid("XML attribute source limit")
    }
  }
  b.to_string()
}

///|
fn ReadXml::expand_content_entity(
  p : ReadXml,
  ns : Map[String, String],
  depth : Int32,
) -> Array[ReadPart] raise OdsError {
  p.expect('&')
  let name = p.name()
  if !xml_id_ncname(name) {
    raise Invalid("entity reference must be an NCName")
  }
  p.expect(';')
  if p.expansion.active.contains(name) || p.expansion.active.length() >= 32 {
    raise Invalid("recursive or too-deep content entity")
  }
  let info = match p.dtd {
    Some(i) => i
    None => raise Invalid("undeclared general entity")
  }
  let value = match info.entities.get(name) {
    Some(Internal(v)) => v
    Some(External(public_id, system_id, notation)) => {
      // External identifiers are parsed, never fetched by this importer.
      if notation is Some(_) {
        raise Invalid("unparsed entity in XML content")
      }
      p.event(
        "EXTERNAL_ENTITY_NOT_READ",
        name,
        "Value import refused",
        public_id~,
        system_id=Some(system_id),
      )
      raise Invalid(
        "XML_DEPENDENCY_NOT_READ: external general entity not loaded; value import refused",
      )
    }
    None =>
      if !p.standalone && (info.external_subset || info.unread_parameter_entity) {
        raise Invalid(
          "XML_DEPENDENCY_NOT_READ: general entity depends on unread declarations; value import refused",
        )
      } else {
        raise Invalid("undeclared general entity")
      }
  }
  p.expansion.units += value.length()
  if p.expansion.units > 1048576 {
    raise Invalid("content entity expansion budget exceeded")
  }
  p.expansion.active.push(name)
  let chars : Array[Char] = []
  for c in value {
    chars.push(c)
  }
  let child : ReadXml = {
    chars,
    at: 0,
    nodes: p.nodes,
    normalize_lines: false,
    document_part: p.document_part,
    location: p.location,
    events: p.events,
    standalone: p.standalone,
    dtd: p.dtd,
    expansion: p.expansion,
    declared_types: p.declared_types,
    other_ids: p.other_ids,
  }
  let parts : Array[ReadPart] = []
  while child.at < chars.length() {
    if child.starts_with("