///|
/// Node in the lightweight XML tree.
pub(all) enum XmlNode {
  XmlElement(XmlElement)
  XmlText(String)
} derive(Debug, Eq)

///|
/// XML element with a name, attributes, and child nodes.
pub(all) struct XmlElement {
  name : String
  attributes : @sorted_map.SortedMap[String, String]
  children : Array[XmlNode]
} derive(Debug, Eq)

///|
/// Builds an XML element.
pub fn xml_element(
  name : String,
  attributes? : Map[String, String] = Map([]),
  children? : Array[XmlNode] = [],
) -> XmlElement {
  { name, attributes: SortedMap(attributes.to_array()), children }
}

///|
/// Builds an XML text node.
pub fn xml_text(value : String) -> XmlNode {
  XmlText(value)
}

///|
/// Returns the first child element with the requested tag.
pub fn XmlElement::first(self : XmlElement, name : String) -> XmlElement? {
  for child in self.children {
    match child {
      XmlElement(element) => if element.name == name { return Some(element) }
      _ => ()
    }
  }
  None
}

///|
/// Returns the first matching child text, or an empty string.
pub fn XmlElement::first_or_empty(
  self : XmlElement,
  name : String,
) -> XmlElement {
  match self.first(name) {
    Some(element) => element
    None => xml_element("")
  }
}

///|
/// Returns direct child elements with the requested tag.
pub fn XmlElement::elements_by_tag_name(
  self : XmlElement,
  name : String,
) -> Array[XmlElement] {
  let elements : Array[XmlElement] = []
  for child in self.children {
    match child {
      XmlElement(element) => if element.name == name { elements.push(element) }
      _ => ()
    }
  }
  elements
}

///|
/// Returns descendant elements with the requested tag.
pub fn XmlElement::descendants_by_tag_name(
  self : XmlElement,
  name : String,
) -> Array[XmlElement] {
  let elements : Array[XmlElement] = []
  for child in self.children {
    match child {
      XmlElement(element) => {
        if element.name == name {
          elements.push(element)
        }
        elements.append(element.descendants_by_tag_name(name))
      }
      _ => ()
    }
  }
  elements
}

///|
/// Returns concatenated text content for the XML element.
pub fn XmlElement::text(self : XmlElement) -> String raise DocxError {
  if self.children.length() == 0 {
    ""
  } else if self.children.length() == 1 {
    match self.children[0] {
      XmlText(value) => value
      _ => raise InvalidXml(message="element does not contain simple text")
    }
  } else {
    raise InvalidXml(message="element does not contain simple text")
  }
}

///|
/// Parses XML text into an element.
pub fn read_xml_string(
  source : String,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  read_xml_string_with_mode(
    source,
    namespace_map,
    false,
    None,
    false,
    None,
    false,
  )
}

///|
/// Parses tolerant XML and also returns the namespace URI that expands the
/// document element's QName. An unprefixed root without a default namespace,
/// or a root whose prefix is unbound, reports `None` even when its literal name
/// happens to match a caller's canonical prefix.
pub fn read_xml_string_with_root_namespace_uri(
  source : String,
  namespace_map? : Map[String, String] = Map([]),
) -> (String?, XmlElement) raise DocxError {
  read_xml_string_with_mode_and_root_namespace_uri(
    source,
    namespace_map,
    false,
    None,
    false,
    None,
    false,
  )
}

///|
/// Parses DTD-free XML 1.0 with Namespaces well-formedness checks enabled.
///
/// The regular parser intentionally preserves historical xmldom-compatible
/// recovery behavior for document conversion. Package metadata and mutation
/// gates should use this entry point so malformed XML fails closed.
pub fn read_xml_string_strict(
  source : String,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  read_xml_string_with_mode(
    source,
    namespace_map,
    true,
    None,
    false,
    None,
    false,
  )
}

///|
/// The byte encoding used to decode an XML source string. Passing it to
/// `read_xml_string_strict_encoded` makes the XML declaration part of the
/// strict contract instead of treating it as unaudited metadata. BOMless
/// UTF-16LE and UTF-16BE sources require a matching declaration.
pub(all) enum XmlSourceEncoding {
  Utf8
  Utf16LittleEndianWithBom
  Utf16BigEndianWithBom
  Utf16LittleEndian
  Utf16BigEndian
}

///|
/// Strictly parses an already decoded XML string and rejects an encoding
/// declaration that contradicts the supplied byte encoding.
pub fn read_xml_string_strict_encoded(
  source : String,
  source_encoding : XmlSourceEncoding,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  read_xml_string_with_mode(
    source,
    namespace_map,
    true,
    None,
    false,
    Some(source_encoding),
    false,
  )
}

///|
/// Strictly parses an already-decoded XML string under a caller-owned
/// cumulative budget while enforcing the declared byte encoding.
pub fn read_xml_string_strict_encoded_limited(
  source : String,
  source_encoding : XmlSourceEncoding,
  budget : XmlReadBudget,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  read_xml_string_with_mode(
    source,
    namespace_map,
    true,
    Some(budget),
    false,
    Some(source_encoding),
    false,
  )
}

///|
/// A cumulative allocation budget shared by one or more XML parses.
///
/// Source storage is charged before decoding, and every materialized XML name,
/// attribute value, text/CDATA segment, and element is charged before the
/// parser copies or appends it. Reusing one budget across package parts bounds
/// the aggregate DOM rather than granting every part an independent ceiling.
pub struct XmlReadBudget {
  priv mut remaining_source_units : Int
  priv mut remaining_tokens : Int
  priv mut remaining_materialized_chars : Int
  priv max_token_chars : Int
  priv cancelled : () -> Bool
}

///|
/// Builds a cumulative XML read budget. Negative ceilings clamp to zero; a
/// zero ceiling is valid and makes the corresponding first charge fail.
pub fn xml_read_budget(
  max_source_units~ : Int,
  max_tokens~ : Int,
  max_materialized_chars~ : Int,
  max_token_chars~ : Int,
  cancelled? : () -> Bool = () => false,
) -> XmlReadBudget {
  {
    remaining_source_units: if max_source_units > 0 {
      max_source_units
    } else {
      0
    },
    remaining_tokens: if max_tokens > 0 {
      max_tokens
    } else {
      0
    },
    remaining_materialized_chars: if max_materialized_chars > 0 {
      max_materialized_chars
    } else {
      0
    },
    max_token_chars: if max_token_chars > 0 {
      max_token_chars
    } else {
      0
    },
    cancelled,
  }
}

///|
fn XmlReadBudget::checkpoint(self : XmlReadBudget) -> Unit raise DocxError {
  if (self.cancelled)() {
    // Higher layers that supply a cancellation callback translate this
    // internal parser abort back to their own typed cancellation error.
    raise InvalidXml(message="XML read cancelled")
  }
}

///|
fn XmlReadBudget::charge_source(
  self : XmlReadBudget,
  units : Int,
) -> Unit raise DocxError {
  self.checkpoint()
  if units < 0 || units > self.remaining_source_units {
    raise @core.docx_xml_resource_limit_error(DocxXmlSourceUnits)
  }
  self.remaining_source_units -= units
}

///|
fn XmlReadBudget::charge_token(
  self : XmlReadBudget,
  chars : Int,
) -> Unit raise DocxError {
  self.checkpoint()
  if chars < 0 || chars > self.max_token_chars {
    raise @core.docx_xml_resource_limit_error(DocxXmlTokenLength)
  }
  if self.remaining_tokens <= 0 {
    raise @core.docx_xml_resource_limit_error(DocxXmlTokens)
  }
  if chars > self.remaining_materialized_chars {
    raise @core.docx_xml_resource_limit_error(DocxXmlMaterializedCharacters)
  }
  self.remaining_tokens -= 1
  self.remaining_materialized_chars -= chars
}

///|
/// Charges a derived string retained by a higher-level XML consumer. Parsers
/// already charge token materialization directly; projections such as indexed
/// structural paths must use this hook before allocating their own copies so
/// they remain inside the same cumulative envelope.
pub fn XmlReadBudget::charge_derived_chars(
  self : XmlReadBudget,
  chars : Int,
) -> Unit raise DocxError {
  self.checkpoint()
  if chars < 0 || chars > self.remaining_materialized_chars {
    raise @core.docx_xml_resource_limit_error(DocxXmlMaterializedCharacters)
  }
  self.remaining_materialized_chars -= chars
}

///|
fn XmlReadBudget::charge_namespace_bindings(
  self : XmlReadBudget,
  operations : Int,
) -> Unit raise DocxError {
  self.checkpoint()
  // Namespace declarations allocate one scope-delta record and later restore
  // one map binding. Charge both operations before recording the declaration.
  if operations < 0 || operations > self.remaining_tokens {
    raise @core.docx_xml_resource_limit_error(DocxXmlTokens)
  }
  self.remaining_tokens -= operations
}

///|
/// Parses tolerant XML while charging a caller-owned cumulative budget.
/// String input is charged in UTF-16 storage units before parsing.
pub fn read_xml_string_limited(
  source : String,
  budget : XmlReadBudget,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  read_xml_string_with_mode(
    source,
    namespace_map,
    false,
    Some(budget),
    false,
    None,
    false,
  )
}

///|
/// Parses strict DTD-free XML while charging a caller-owned cumulative budget.
/// String input is charged in UTF-16 storage units before parsing.
pub fn read_xml_string_strict_limited(
  source : String,
  budget : XmlReadBudget,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  read_xml_string_with_mode(
    source,
    namespace_map,
    true,
    Some(budget),
    false,
    None,
    false,
  )
}

///|
/// Decodes and parses tolerant UTF-8 XML. Source bytes are charged before a
/// String is allocated, so oversized parts fail without a second full payload.
pub fn read_xml_bytes_limited(
  source : BytesView,
  budget : XmlReadBudget,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  budget.charge_source(source.length())
  let text = @utf8.decode(source, ignore_bom=true) catch {
    _ => raise InvalidXml(message="XML source is not valid UTF-8")
  }
  read_xml_string_with_mode(
    text,
    namespace_map,
    false,
    Some(budget),
    true,
    Some(Utf8),
    false,
  )
}

///|
/// Decodes and parses tolerant UTF-8 XML under a cumulative budget, returning
/// both the document element's namespace URI and the canonicalized tree. This
/// preserves the distinction between a namespace-bound QName and an identical
/// literal QName whose prefix is unbound.
pub fn read_xml_bytes_limited_with_root_namespace_uri(
  source : BytesView,
  budget : XmlReadBudget,
  namespace_map? : Map[String, String] = Map([]),
) -> (String?, XmlElement) raise DocxError {
  budget.charge_source(source.length())
  let text = @utf8.decode(source, ignore_bom=true) catch {
    _ => raise InvalidXml(message="XML source is not valid UTF-8")
  }
  read_xml_string_with_mode_and_root_namespace_uri(
    text,
    namespace_map,
    false,
    Some(budget),
    true,
    Some(Utf8),
    false,
  )
}

///|
/// Decodes and parses strict DTD-free UTF-8 XML under a cumulative budget.
pub fn read_xml_bytes_strict_limited(
  source : BytesView,
  budget : XmlReadBudget,
  namespace_map? : Map[String, String] = Map([]),
) -> XmlElement raise DocxError {
  budget.charge_source(source.length())
  let text = @utf8.decode(source, ignore_bom=true) catch {
    _ => raise InvalidXml(message="XML source is not valid UTF-8")
  }
  read_xml_string_with_mode(
    text,
    namespace_map,
    true,
    Some(budget),
    true,
    Some(Utf8),
    false,
  )
}

///|
fn read_xml_string_with_mode(
  source : String,
  namespace_map : Map[String, String],
  strict : Bool,
  budget : XmlReadBudget?,
  source_already_charged : Bool,
  source_encoding : XmlSourceEncoding?,
  retain_namespace_declarations : Bool,
) -> XmlElement raise DocxError {
  read_xml_string_with_mode_and_root_namespace_uri(
    source, namespace_map, strict, budget, source_already_charged, source_encoding,
    retain_namespace_declarations,
  ).1
}

///|
fn read_xml_string_with_mode_and_root_namespace_uri(
  source : String,
  namespace_map : Map[String, String],
  strict : Bool,
  budget : XmlReadBudget?,
  source_already_charged : Bool,
  source_encoding : XmlSourceEncoding?,
  retain_namespace_declarations : Bool,
) -> (String?, XmlElement) raise DocxError {
  match budget {
    Some(budget) =>
      if !source_already_charged {
        budget.charge_source(source.length())
      }
    None => ()
  }
  if strict {
    validate_xml_characters(source)
  }
  let parser = XmlParser::{
    source,
    pos: 0,
    depth: 0,
    strict,
    budget,
    source_encoding,
    retain_namespace_declarations,
    seen_xml_declaration: false,
    root_namespace_uri: None,
    namespace_map: build_direct_namespace_lookup(namespace_map),
    namespaces: if strict {
      SortedMap([("xml", XML_NAMESPACE_URI)])
    } else {
      SortedMap([])
    },
  }
  parser.skip_misc(document_start=true)
  match source_encoding {
    Some(encoding) if encoding.requires_declaration() &&
      !parser.seen_xml_declaration =>
      raise InvalidXml(
        message="XML encoding declaration is required for decoded source",
      )
    _ => ()
  }
  let root = parser.parse_element()
  parser.skip_misc(document_start=false)
  if parser.pos < parser.source.length() {
    raise InvalidXml(message="unexpected content after document element")
  }
  (parser.root_namespace_uri, root)
}

///|
/// Serializes an XML element to text.
pub fn write_xml_string(
  root : XmlElement,
  namespaces? : Map[String, String] = Map([]),
) -> String {
  let writer = XmlWriter::{
    namespaces,
    namespace_lookup: build_namespace_lookup(namespaces),
  }
  let builder = StringBuilder()
  builder.write_string(
    "",
  )
  writer.write_element(builder, root, is_root=true)
  builder.to_string()
}

///|
/// Serializes one element as a FRAGMENT: no XML declaration, with the
/// given namespace declarations placed on the element itself — the
/// self-contained form the L0 splice contract requires (a fragment's
/// correctness must not depend on the destination document's own
/// bindings).
pub fn write_xml_fragment(
  element : XmlElement,
  namespaces? : Map[String, String] = Map([]),
) -> String {
  let writer = XmlWriter::{
    namespaces,
    namespace_lookup: build_namespace_lookup(namespaces),
  }
  let builder = StringBuilder()
  writer.write_element(builder, element, is_root=true)
  builder.to_string()
}

///|
/// Serializes one XML fragment only after an allocation-free sizing pass has
/// proved that its UTF-8 representation fits `max_output_bytes`. The sizing
/// pass accounts for escaping and namespace declarations and rejects excessive
/// nesting before the recursive writer runs.
pub fn write_xml_fragment_limited(
  element : XmlElement,
  max_output_bytes~ : Int,
  namespaces? : Map[String, String] = Map([]),
) -> String raise DocxError {
  let limit = if max_output_bytes > 0 { max_output_bytes } else { 0 }
  let work_scaled = limit.to_int64() * xml_write_name_work_scale.to_int64()
  let name_work_limit = if work_scaled > 2147483647L {
    2147483647
  } else {
    work_scaled.to_int()
  }
  let (namespace_lookup, remaining_name_work) = build_namespace_lookup_limited(
    namespaces, name_work_limit,
  )
  let sizer = XmlWriteSizer::{
    namespaces,
    namespace_lookup,
    remaining_bytes: limit,
    remaining_name_work,
  }
  sizer.size_element(element, is_root=true, depth=0)
  let output_bytes = limit - sizer.remaining_bytes
  let writer = XmlWriter::{ namespaces, namespace_lookup }
  let builder = StringBuilder(size_hint=output_bytes)
  writer.write_element(builder, element, is_root=true)
  builder.to_string()
}

///|
priv struct XmlWriter {
  namespaces : Map[String, String]
  namespace_lookup : NamespaceLookup
}

///|
/// The strict parser and limited writer share the same conservative recursive
/// depth ceiling, keeping their stack use portable on Wasm as well as native.
let xml_write_max_depth : Int = 256

///|
/// Namespace mapping may inspect a Clark name several times (delimiter scan and
/// comparison-based lookup in both sizing and writing). Keep that work a fixed
/// multiple of the caller's output ceiling so a reused giant URI cannot turn a
/// small permitted fragment into excessive serialization work. The multiplier
/// comfortably covers the standard OOXML namespace-to-tag ratio while rejecting
/// hostile amplification.
let xml_write_name_work_scale : Int = 64

///|
/// `Array::sort_by` is worst-case O(n log n). This conservative multiplier
/// covers pivot selection, its small-array bubble-sort path, heap-sort
/// fallback, and the fixed work around every string comparison.
let xml_write_sort_work_scale : Int = 8

///|
/// Binary lookup is performed once by the sizing pass and once by the writer.
/// This multiplier also covers loop/index overhead around every comparison.
let xml_write_lookup_work_scale : Int = 4

///|
/// A collision-independent URI-to-prefix index. Entries are sorted by URI and
/// contain at most one prefix for each URI, preserving the historical rule that
/// the last namespace binding wins when aliases share a URI.
priv struct NamespaceLookup {
  entries : Array[(String, String)]
  max_uri_chars : Int
}

///|
priv struct XmlWriteSizer {
  namespaces : Map[String, String]
  namespace_lookup : NamespaceLookup
  mut remaining_bytes : Int
  mut remaining_name_work : Int
}

///|
fn XmlWriteSizer::charge_bytes(
  self : XmlWriteSizer,
  bytes : Int,
) -> Unit raise DocxError {
  if bytes < 0 || bytes > self.remaining_bytes {
    raise InvalidXml(message="XML serialization byte limit exceeded")
  }
  self.remaining_bytes -= bytes
}

///|
fn XmlWriteSizer::charge_name_work(
  self : XmlWriteSizer,
  units : Int,
) -> Unit raise DocxError {
  if units < 0 || units > self.remaining_name_work {
    raise InvalidXml(message="XML serialization name work limit exceeded")
  }
  self.remaining_name_work -= units
}

///|
fn checked_xml_sort_name_work(
  item_count : Int,
  max_name_chars : Int,
  remaining : Int,
) -> Int raise DocxError {
  if item_count <= 1 {
    return 0
  }
  let mut levels = 0
  let mut value = item_count - 1
  while value > 0 {
    levels += 1
    value /= 2
  }
  let mut work = xml_write_sort_work_scale
  if item_count > remaining / work {
    raise InvalidXml(message="XML serialization name work limit exceeded")
  }
  work *= item_count
  if levels > remaining / work {
    raise InvalidXml(message="XML serialization name work limit exceeded")
  }
  work *= levels
  // Adding one charges the terminating comparison even when all names are
  // empty. Check before adding so hostile maximum-length names cannot wrap.
  if max_name_chars >= remaining / work {
    raise InvalidXml(message="XML serialization name work limit exceeded")
  }
  work *= max_name_chars + 1
  work
}

///|
fn checked_xml_lookup_name_work(
  item_count : Int,
  max_name_chars : Int,
  remaining : Int,
) -> Int raise DocxError {
  if item_count <= 0 {
    return 0
  }
  let mut levels = 0
  let mut value = item_count
  while value > 0 {
    levels += 1
    value /= 2
  }
  let mut work = xml_write_lookup_work_scale
  if levels > remaining / work {
    raise InvalidXml(message="XML serialization name work limit exceeded")
  }
  work *= levels
  if max_name_chars >= remaining / work {
    raise InvalidXml(message="XML serialization name work limit exceeded")
  }
  work *= max_name_chars + 1
  work
}

///|
fn XmlWriteSizer::charge_attribute_sort_work(
  self : XmlWriteSizer,
  attributes : @sorted_map.SortedMap[String, String],
) -> Unit raise DocxError {
  let mut item_count = 0
  let mut max_name_chars = 0
  for name, _ in attributes {
    // Charge the array materialization and one unit of container traversal before
    // accumulating cardinality for the later sort.
    self.charge_name_work(1)
    item_count += 1
    max_name_chars = max_name_chars.max(name.length())
  }
  self.charge_name_work(
    checked_xml_sort_name_work(
      item_count,
      max_name_chars,
      self.remaining_name_work,
    ),
  )
}

///|
fn XmlWriteSizer::charge_scalar(
  self : XmlWriteSizer,
  character : Char,
) -> Unit raise DocxError {
  let code = character.to_int()
  let bytes = if code <= 0x7F {
    1
  } else if code <= 0x7FF {
    2
  } else if code >= 0xD800 && code <= 0xDFFF {
    raise InvalidXml(message="XML serialization contains invalid Unicode")
  } else if code <= 0xFFFF {
    3
  } else if code <= 0x10FFFF {
    4
  } else {
    raise InvalidXml(message="XML serialization contains invalid Unicode")
  }
  self.charge_bytes(bytes)
}

///|
fn XmlWriteSizer::charge_raw(
  self : XmlWriteSizer,
  value : StringView,
) -> Unit raise DocxError {
  for character in value {
    self.charge_scalar(character)
  }
}

///|
fn XmlWriteSizer::charge_escaped_text(
  self : XmlWriteSizer,
  value : StringView,
) -> Unit raise DocxError {
  for character in value {
    match character {
      '&' => self.charge_bytes(5)
      '<' | '>' => self.charge_bytes(4)
      _ => self.charge_scalar(character)
    }
  }
}

///|
fn XmlWriteSizer::charge_escaped_attribute(
  self : XmlWriteSizer,
  value : StringView,
) -> Unit raise DocxError {
  for character in value {
    match character {
      '&' => self.charge_bytes(5)
      '<' | '>' => self.charge_bytes(4)
      '"' => self.charge_bytes(6)
      _ => self.charge_scalar(character)
    }
  }
}

///|
fn XmlWriteSizer::charge_mapped_name(
  self : XmlWriteSizer,
  name : String,
) -> Unit raise DocxError {
  if !name.has_prefix("{") {
    self.charge_raw(name)
    return
  }
  // Charge before scanning the attacker-controlled Clark name.
  self.charge_name_work(name.length())
  guard name.find("}") is Some(close) else {
    self.charge_raw(name)
    return
  }
  let uri = name[1:close]
  let local_name = name[close + 1:]
  // The comparison-based lookup has a deterministic logarithmic ceiling. The
  // charge covers this lookup and the identical unmetered writer pass below.
  self.charge_name_work(
    checked_xml_lookup_name_work(
      self.namespace_lookup.entries.length(),
      self.namespace_lookup.max_uri_chars.max(uri.length()),
      self.remaining_name_work,
    ),
  )
  match self.namespace_lookup.lookup(uri) {
    Some("") => self.charge_raw(local_name)
    Some(prefix) => {
      self.charge_raw(prefix)
      self.charge_bytes(1)
      self.charge_raw(local_name)
    }
    None => self.charge_raw(name)
  }
}

///|
fn XmlWriteSizer::size_attribute(
  self : XmlWriteSizer,
  name : StringView,
  value : StringView,
) -> Unit raise DocxError {
  self.charge_bytes(1)
  self.charge_raw(name)
  self.charge_bytes(2)
  self.charge_escaped_attribute(value)
  self.charge_bytes(1)
}

///|
fn XmlWriteSizer::size_node(
  self : XmlWriteSizer,
  node : XmlNode,
  depth~ : Int,
) -> Unit raise DocxError {
  match node {
    XmlElement(element) => self.size_element(element, depth~)
    XmlText(value) => self.charge_escaped_text(value)
  }
}

///|
fn XmlWriteSizer::size_element(
  self : XmlWriteSizer,
  element : XmlElement,
  is_root? : Bool = false,
  depth~ : Int,
) -> Unit raise DocxError {
  if depth >= xml_write_max_depth {
    raise InvalidXml(message="XML serialization nesting limit exceeded")
  }
  self.charge_bytes(1)
  self.charge_mapped_name(element.name)
  if is_root {
    for prefix, uri in self.namespaces {
      if prefix == "" {
        self.size_attribute("xmlns", uri)
      } else {
        self.charge_bytes(1)
        self.charge_bytes(6)
        self.charge_raw(prefix)
        self.charge_bytes(2)
        self.charge_escaped_attribute(uri)
        self.charge_bytes(1)
      }
    }
  }
  self.charge_attribute_sort_work(element.attributes)
  for name, value in element.attributes {
    self.size_attribute(name, value)
  }
  if element.children.is_empty() {
    self.charge_bytes(2)
  } else {
    self.charge_bytes(1)
    for child in element.children {
      self.size_node(child, depth=depth + 1)
    }
    self.charge_bytes(2)
    self.charge_mapped_name(element.name)
    self.charge_bytes(1)
  }
}

///|
fn NamespaceLookup::lookup(self : NamespaceLookup, uri : StringView) -> String? {
  let mut low = 0
  let mut high = self.entries.length()
  while low < high {
    let middle = low + (high - low) / 2
    let (candidate, prefix) = self.entries[middle]
    let ordering = uri.compare(candidate)
    if ordering == 0 {
      return Some(prefix)
    } else if ordering < 0 {
      high = middle
    } else {
      low = middle + 1
    }
  }
  None
}

///|
fn build_namespace_lookup(map : Map[String, String]) -> NamespaceLookup {
  let indexed : Array[(String, String, Int)] = []
  let mut ordinal = 0
  let mut max_uri_chars = 0
  for prefix, uri in map {
    indexed.push((uri, prefix, ordinal))
    ordinal += 1
    max_uri_chars = max_uri_chars.max(uri.length())
  }
  finish_namespace_lookup(indexed, max_uri_chars)
}

///|
/// Parser namespace maps are already URI-to-prefix mappings, unlike writer
/// namespace declarations, which are prefix-to-URI and require inversion.
fn build_direct_namespace_lookup(map : Map[String, String]) -> NamespaceLookup {
  let indexed : Array[(String, String, Int)] = []
  let mut ordinal = 0
  let mut max_uri_chars = 0
  for uri, prefix in map {
    indexed.push((uri, prefix, ordinal))
    ordinal += 1
    max_uri_chars = max_uri_chars.max(uri.length())
  }
  finish_namespace_lookup(indexed, max_uri_chars)
}

///|
fn finish_namespace_lookup(
  indexed : Array[(String, String, Int)],
  max_uri_chars : Int,
) -> NamespaceLookup {
  indexed.sort_by(fn(a, b) {
    let (uri_a, _, ordinal_a) = a
    let (uri_b, _, ordinal_b) = b
    let ordering = uri_a.compare(uri_b)
    if ordering != 0 {
      ordering
    } else {
      ordinal_a.compare(ordinal_b)
    }
  })
  let entries : Array[(String, String)] = []
  let mut previous_uri : String? = None
  for item in indexed {
    let (uri, prefix, _) = item
    match previous_uri {
      Some(previous) if previous == uri =>
        entries[entries.length() - 1] = (uri, prefix)
      _ => entries.push((uri, prefix))
    }
    previous_uri = Some(uri)
  }
  { entries, max_uri_chars }
}

///|
/// Builds the namespace writer's collision-independent URI index without doing
/// work ahead of the limited writer's contract. Every binding has a positive
/// structural cost; prefix/URI handling, array materialization, and both URI
/// and output-prefix sorts are charged before serialization.
fn build_namespace_lookup_limited(
  map : Map[String, String],
  name_work_limit : Int,
) -> (NamespaceLookup, Int) raise DocxError {
  let indexed : Array[(String, String, Int)] = []
  let mut remaining = if name_work_limit > 0 { name_work_limit } else { 0 }
  let mut item_count = 0
  let mut max_prefix_chars = 0
  let mut max_uri_chars = 0
  for prefix, uri in map {
    // Every binding has positive cost, including an empty prefix and URI.
    // Prefix/URI materialization and the later namespace array copies are all
    // charged before sorting or indexing the bindings.
    if remaining < 4 {
      raise InvalidXml(message="XML serialization name work limit exceeded")
    }
    remaining -= 4
    if prefix.length() > remaining {
      raise InvalidXml(message="XML serialization name work limit exceeded")
    }
    remaining -= prefix.length()
    if uri.length() > remaining {
      raise InvalidXml(message="XML serialization name work limit exceeded")
    }
    remaining -= uri.length()
    if uri.length() > remaining {
      raise InvalidXml(message="XML serialization name work limit exceeded")
    }
    remaining -= uri.length()
    item_count += 1
    max_prefix_chars = max_prefix_chars.max(prefix.length())
    max_uri_chars = max_uri_chars.max(uri.length())
    indexed.push((uri, prefix, item_count - 1))
  }
  remaining -= checked_xml_sort_name_work(item_count, max_uri_chars, remaining)
  remaining -= checked_xml_sort_name_work(
    item_count, max_prefix_chars, remaining,
  )
  (finish_namespace_lookup(indexed, max_uri_chars), remaining)
}

///|
fn XmlWriter::write_node(
  self : XmlWriter,
  builder : StringBuilder,
  node : XmlNode,
) -> Unit {
  match node {
    XmlElement(element) => self.write_element(builder, element)
    XmlText(value) => builder.write_string(escape_xml_text(value))
  }
}

///|
fn XmlWriter::write_element(
  self : XmlWriter,
  builder : StringBuilder,
  element : XmlElement,
  is_root? : Bool = false,
) -> Unit {
  builder.write_string("<")
  builder.write_string(self.map_element_name(element.name))
  if is_root {
    self.write_namespace_attributes(builder)
  }
  // The root's OWN attributes too — namespace declarations do not
  // replace them (mc:Ignorable on annotation parts lives here).
  write_serialized_xml_attributes(builder, element.attributes)
  if element.children.is_empty() {
    builder.write_string("/>")
  } else {
    builder.write_string(">")
    for child in element.children {
      self.write_node(builder, child)
    }
    builder.write_string("")
  }
}

///|
fn XmlWriter::map_element_name(self : XmlWriter, name : String) -> String {
  if name.has_prefix("{") {
    match name.find("}") {
      Some(close) => {
        let uri = name[1:close]
        let local_name = name[close + 1:].to_owned()
        match self.namespace_lookup.lookup(uri) {
          Some(prefix) =>
            if prefix == "" {
              local_name
            } else {
              prefix + ":" + local_name
            }
          None => name
        }
      }
      None => name
    }
  } else {
    name
  }
}

///|
fn XmlWriter::write_namespace_attributes(
  self : XmlWriter,
  builder : StringBuilder,
) -> Unit {
  let namespaces = self.namespaces.to_array()
  namespaces.sort_by(fn(a, b) {
    let (prefix_a, _) = a
    let (prefix_b, _) = b
    // `xmlns` sorts before `xmlns:`, and every non-empty name shares
    // the `xmlns:` stem, so comparing prefixes produces the same order
    // without allocating two derived attribute names per comparison.
    prefix_a.compare(prefix_b)
  })
  for item in namespaces {
    let (prefix, uri) = item
    write_serialized_xml_namespace_attribute(builder, prefix, uri)
  }
}

///|
fn write_serialized_xml_namespace_attribute(
  builder : StringBuilder,
  prefix : String,
  uri : String,
) -> Unit {
  builder.write_string(" xmlns")
  if prefix != "" {
    builder.write_string(":")
    builder.write_string(prefix)
  }
  builder.write_string("=\"")
  builder.write_string(escape_xml_attribute(uri))
  builder.write_string("\"")
}

///|
fn write_serialized_xml_attributes(
  builder : StringBuilder,
  attributes : @sorted_map.SortedMap[String, String],
) -> Unit {
  let items = attributes.to_array()
  items.sort_by(fn(a, b) {
    let (key_a, _) = a
    let (key_b, _) = b
    let rank_diff = xml_attribute_rank(key_a) - xml_attribute_rank(key_b)
    if rank_diff != 0 {
      rank_diff
    } else {
      key_a.compare(key_b)
    }
  })
  for item in items {
    let (key, value) = item
    write_serialized_xml_attribute(builder, key, value)
  }
}

///|
fn xml_attribute_rank(key : String) -> Int {
  match key {
    "Id" => 0
    "Extension" => 0
    "PartName" => 0
    "Type" => 1
    "ContentType" => 1
    "Target" => 2
    "TargetMode" => 3
    _ => 10
  }
}

///|
fn write_serialized_xml_attribute(
  builder : StringBuilder,
  name : String,
  value : String,
) -> Unit {
  builder.write_string(" ")
  builder.write_string(name)
  builder.write_string("=\"")
  builder.write_string(escape_xml_attribute(value))
  builder.write_string("\"")
}

///|
/// Maximum element nesting depth accepted by the tolerant XML parser. Keep it
/// aligned with strict parsing and serialization so every recursive XML path
/// reaches its typed guard before exhausting the Wasm stack.
let xml_max_depth : Int = 256

///|
let strict_xml_max_depth : Int = 256

///|
let xml_diagnostic_token_chars : Int = 160

///|
/// Bounds attacker-controlled text before it participates in an error-message
/// concatenation. Short diagnostics remain byte-for-byte unchanged.
fn xml_diagnostic_token(value : String) -> String {
  if value.length() <= xml_diagnostic_token_chars {
    return value
  }
  let output = StringBuilder::new()
  let mut count = 0
  for character in value {
    if count == xml_diagnostic_token_chars - 1 {
      output.write_string("…") |> ignore
      break
    }
    output.write_char(character) |> ignore
    count += 1
  }
  output.to_string()
}

///|
/// Pre-sizes the native/Wasm UTF-16 builder exactly when the byte-sized hint
/// fits in `Int`. Exact sizing lets `to_string()` transfer the completed buffer
/// without retaining a second token-sized allocation.
fn xml_string_builder_for_units(units : Int) -> StringBuilder {
  let bytes = units.to_int64() * 2L
  if units > 0 && bytes <= 2147483647L {
    StringBuilder(size_hint=bytes.to_int())
  } else {
    StringBuilder()
  }
}

///|
const XML_NAMESPACE_URI : String = "http://www.w3.org/XML/1998/namespace"

///|
const XMLNS_NAMESPACE_URI : String = "http://www.w3.org/2000/xmlns/"

///|
fn is_xml_character(char : Char) -> Bool {
  let code = char.to_int()
  code == 0x9 ||
  code == 0xa ||
  code == 0xd ||
  (code >= 0x20 && code <= 0xd7ff) ||
  (code >= 0xe000 && code <= 0xfffd) ||
  (code >= 0x10000 && code <= 0x10ffff)
}

///|
fn validate_xml_characters(source : String) -> Unit raise DocxError {
  let mut index = 0
  while index < source.length() {
    let char = match source.get_char(index) {
      Some(char) => char
      None => raise InvalidXml(message="invalid UTF-16 in XML content")
    }
    if !is_xml_character(char) {
      raise InvalidXml(message="illegal XML character")
    }
    index = index + char.utf16_len()
  }
}

///|
fn is_xml_name_start_char(char : Char) -> Bool {
  let code = char.to_int()
  char.is_ascii_alphabetic() ||
  char == ':' ||
  char == '_' ||
  (code >= 0xc0 && code <= 0xd6) ||
  (code >= 0xd8 && code <= 0xf6) ||
  (code >= 0xf8 && code <= 0x2ff) ||
  (code >= 0x370 && code <= 0x37d) ||
  (code >= 0x37f && code <= 0x1fff) ||
  (code >= 0x200c && code <= 0x200d) ||
  (code >= 0x2070 && code <= 0x218f) ||
  (code >= 0x2c00 && code <= 0x2fef) ||
  (code >= 0x3001 && code <= 0xd7ff) ||
  (code >= 0xf900 && code <= 0xfdcf) ||
  (code >= 0xfdf0 && code <= 0xfffd) ||
  (code >= 0x10000 && code <= 0xeffff)
}

///|
fn is_xml_name_char(char : Char) -> Bool {
  let code = char.to_int()
  is_xml_name_start_char(char) ||
  char == '-' ||
  char == '.' ||
  char.is_ascii_digit() ||
  code == 0xb7 ||
  (code >= 0x300 && code <= 0x36f) ||
  (code >= 0x203f && code <= 0x2040)
}

///|
fn is_xml_ncname(value : StringView) -> Bool {
  let chars = value.to_array()
  if chars.length() == 0 || chars[0] == ':' || !is_xml_name_start_char(chars[0]) {
    return false
  }
  for index in 1.. Bool {
  match value.find(":") {
    Some(colon) => {
      let prefix = value[:colon]
      let local_name = value[colon + 1:]
      is_xml_ncname(prefix) &&
      is_xml_ncname(local_name) &&
      !local_name.contains(":")
    }
    None => is_xml_ncname(value)
  }
}

///|
priv struct XmlParser {
  source : String
  mut pos : Int
  mut depth : Int
  strict : Bool
  budget : XmlReadBudget?
  source_encoding : XmlSourceEncoding?
  retain_namespace_declarations : Bool
  mut seen_xml_declaration : Bool
  mut root_namespace_uri : String?
  namespace_map : NamespaceLookup
  namespaces : @sorted_map.SortedMap[String, String]
}

///|
/// One reversible namespace declaration. Scope restoration applies these
/// deltas in reverse order, avoiding full-map snapshots and retained-capacity
/// clear/copy cycles for sibling elements.
priv struct NamespaceBindingChange {
  prefix : String
  previous : String?
}

///|
fn XmlParser::parse_element(self : XmlParser) -> XmlElement raise DocxError {
  let max_depth = if self.strict { strict_xml_max_depth } else { xml_max_depth }
  if self.depth >= max_depth {
    raise @core.docx_xml_resource_limit_error(DocxXmlNestingDepth)
  }
  self.charge_token(0)
  self.depth = self.depth + 1
  let element = if self.strict {
    self.parse_element_inner_strict()
  } else {
    self.parse_element_inner_tolerant()
  }
  self.depth = self.depth - 1
  element
}

///|
fn XmlParser::charge_token(
  self : XmlParser,
  chars : Int,
) -> Unit raise DocxError {
  match self.budget {
    Some(budget) => budget.charge_token(chars)
    None => ()
  }
}

///|
/// Charges an owned name derived from already-tokenized source (for example,
/// `{namespace-uri}local`). The charge happens before any builder allocation,
/// so repeatedly reusing a long namespace URI cannot amplify a small source
/// into unbounded derived strings.
fn XmlParser::charge_derived_name(
  self : XmlParser,
  chars : Int,
) -> Unit raise DocxError {
  self.charge_token(chars)
}

///|
fn xml_derived_name_length(
  first : Int,
  second : Int,
  punctuation : Int,
) -> Int raise DocxError {
  let total = first.to_int64() + second.to_int64() + punctuation.to_int64()
  if first < 0 || second < 0 || punctuation < 0 || total > 2147483647L {
    raise @core.docx_xml_resource_limit_error(DocxXmlMaterializedCharacters)
  }
  total.to_int()
}

///|
fn XmlParser::record_namespace_binding(
  self : XmlParser,
  prefix : String,
  namespace_uri : String,
  changes : Array[NamespaceBindingChange],
) -> Unit raise DocxError {
  match self.budget {
    // One delta allocation and one eventual restore/remove operation.
    Some(budget) => budget.charge_namespace_bindings(2)
    None => ()
  }
  changes.push({ prefix, previous: self.namespaces.get(prefix) })
  self.namespaces[prefix] = namespace_uri
}

///|
fn XmlParser::restore_namespace_bindings(
  self : XmlParser,
  changes : Array[NamespaceBindingChange],
) -> Unit {
  for offset in 0.. self.namespaces[change.prefix] = previous
      None => self.namespaces.remove(change.prefix)
    }
  }
}

///|
fn XmlParser::parse_element_inner_tolerant(
  self : XmlParser,
) -> XmlElement raise DocxError {
  self.expect("<")
  let raw_name = self.read_name()
  let raw_attributes : Array[(String, String)] = []
  let namespace_changes : Array[NamespaceBindingChange] = []
  while true {
    self.skip_whitespace()
    if self.starts_with("/>") {
      self.pos = self.pos + 2
      let element = self.build_element(raw_name, raw_attributes, [], [])
      self.restore_namespace_bindings(namespace_changes)
      return element
    } else if self.starts_with(">") {
      self.pos = self.pos + 1
      let children = self.parse_children(raw_name)
      let element = self.build_element(raw_name, raw_attributes, children, [])
      self.restore_namespace_bindings(namespace_changes)
      return element
    } else {
      let attr_name = self.read_name()
      self.skip_whitespace()
      self.expect("=")
      self.skip_whitespace()
      let attr_value = self.read_quoted_value_tolerant_decoded()
      if attr_name == "xmlns" {
        self.record_namespace_binding("", attr_value, namespace_changes)
      } else if attr_name.has_prefix("xmlns:") {
        self.charge_derived_name(attr_name.length() - 6)
        let prefix = attr_name[6:].to_owned()
        self.record_namespace_binding(prefix, attr_value, namespace_changes)
      } else {
        raw_attributes.push((attr_name, attr_value))
      }
    }
  }
  raise InvalidXml(message="unterminated element")
}

///|
fn XmlParser::parse_element_inner_strict(
  self : XmlParser,
) -> XmlElement raise DocxError {
  self.expect("<")
  let raw_name = self.read_name()
  if !is_xml_qname(raw_name) {
    raise InvalidXml(
      message="invalid XML qualified name: " + xml_diagnostic_token(raw_name),
    )
  }
  let raw_attributes : Array[(String, String)] = []
  let raw_attribute_names : @sorted_map.SortedMap[String, Bool] = SortedMap([])
  let namespace_changes : Array[NamespaceBindingChange] = []
  let namespace_declarations : Array[(String, String)] = []
  while true {
    let separated = self.skip_whitespace_and_report()
    if self.starts_with("/>") {
      self.pos = self.pos + 2
      let element = self.build_element(
        raw_name,
        raw_attributes,
        [],
        namespace_declarations,
      )
      self.restore_namespace_bindings(namespace_changes)
      return element
    } else if self.starts_with(">") {
      self.pos = self.pos + 1
      let children = self.parse_children(raw_name)
      let element = self.build_element(
        raw_name, raw_attributes, children, namespace_declarations,
      )
      self.restore_namespace_bindings(namespace_changes)
      return element
    } else {
      if !separated {
        raise InvalidXml(message="attributes must be separated by whitespace")
      }
      let attr_name = self.read_name()
      if !is_xml_qname(attr_name) {
        raise InvalidXml(
          message="invalid XML qualified name: " +
            xml_diagnostic_token(attr_name),
        )
      }
      if raw_attribute_names.contains(attr_name) {
        raise InvalidXml(
          message="duplicate XML attribute: " + xml_diagnostic_token(attr_name),
        )
      }
      raw_attribute_names[attr_name] = true
      self.skip_whitespace()
      self.expect("=")
      self.skip_whitespace()
      let attr_value = self.read_quoted_value_strict_decoded()
      if attr_name == "xmlns" {
        validate_namespace_declaration("", attr_value)
        self.record_namespace_binding("", attr_value, namespace_changes)
        if self.retain_namespace_declarations {
          namespace_declarations.push(("", attr_value))
        }
      } else if attr_name.has_prefix("xmlns:") {
        self.charge_derived_name(attr_name.length() - 6)
        let prefix = attr_name[6:].to_owned()
        validate_namespace_declaration(prefix, attr_value)
        self.record_namespace_binding(prefix, attr_value, namespace_changes)
        if self.retain_namespace_declarations {
          namespace_declarations.push((prefix, attr_value))
        }
      } else {
        raw_attributes.push((attr_name, attr_value))
      }
    }
  }
  // Defensive: the loop above always returns or raises.
  raise InvalidXml(message="unterminated element")
}

///|
fn XmlParser::parse_children(
  self : XmlParser,
  raw_name : String,
) -> Array[XmlNode] raise DocxError {
  let children : Array[XmlNode] = []
  while self.pos < self.source.length() {
    if self.starts_with("")
      return children
    } else if self.starts_with("")
    return
  }
  self.pos = self.pos + "") {
      self.pos = self.pos + "-->".length()
      return
    }
    if self.starts_with("--") {
      raise InvalidXml(message="XML comments cannot contain '--'")
    }
    let char = match self.source.get_char(self.pos) {
      Some(char) => char
      None => raise InvalidXml(message="invalid UTF-16 in XML content")
    }
    self.pos = self.pos + char.utf16_len()
  }
  raise InvalidXml(message="unterminated XML comment")
}

///|
fn is_valid_xml_encoding_name(value : StringView) -> Bool {
  let chars = value.to_array()
  if chars.length() == 0 || !chars[0].is_ascii_alphabetic() {
    return false
  }
  for index in 1.. Bool {
  let normalized = value.to_lower()
  match self {
    Utf8 => normalized == "utf-8"
    Utf16LittleEndianWithBom | Utf16BigEndianWithBom => normalized == "utf-16"
    Utf16LittleEndian => normalized == "utf-16le"
    Utf16BigEndian => normalized == "utf-16be"
  }
}

///|
fn XmlSourceEncoding::requires_declaration(self : XmlSourceEncoding) -> Bool {
  match self {
    Utf16LittleEndian | Utf16BigEndian => true
    Utf8 | Utf16LittleEndianWithBom | Utf16BigEndianWithBom => false
  }
}

///|
fn XmlParser::parse_xml_declaration(self : XmlParser) -> Unit raise DocxError {
  let names : @sorted_map.SortedMap[String, Bool] = SortedMap([])
  let mut position = 0
  let mut has_version = false
  while true {
    let separated = self.skip_whitespace_and_report()
    if self.starts_with("?>") {
      if !has_version {
        raise InvalidXml(message="XML declaration has no version")
      }
      self.pos = self.pos + "?>".length()
      self.seen_xml_declaration = true
      return
    }
    if !separated {
      raise InvalidXml(message="malformed XML declaration")
    }
    let name = self.read_name()
    if !is_xml_ncname(name) || names.contains(name) {
      raise InvalidXml(
        message="invalid XML declaration attribute: " +
          xml_diagnostic_token(name),
      )
    }
    names[name] = true
    self.skip_whitespace()
    self.expect("=")
    self.skip_whitespace()
    let value = self.read_quoted_value()
    if value.contains("&") {
      raise InvalidXml(message="entities are not allowed in XML declarations")
    }
    match (position, name) {
      (0, "version") => {
        if value != "1.0" {
          raise InvalidXml(
            message="unsupported XML version: " + xml_diagnostic_token(value),
          )
        }
        has_version = true
      }
      (1, "encoding") =>
        if !is_valid_xml_encoding_name(value) {
          raise InvalidXml(
            message="invalid XML encoding name: " + xml_diagnostic_token(value),
          )
        } else {
          match self.source_encoding {
            Some(encoding) if !encoding.matches_declaration(value) =>
              raise InvalidXml(
                message="XML encoding declaration does not match decoded source",
              )
            _ => ()
          }
        }
      (1, "standalone") | (2, "standalone") =>
        if value != "yes" && value != "no" {
          raise InvalidXml(
            message="invalid XML standalone value: " +
              xml_diagnostic_token(value),
          )
        }
      _ =>
        raise InvalidXml(
          message="misordered XML declaration attribute: " +
            xml_diagnostic_token(name),
        )
    }
    position = position + 1
  }
}

///|
fn XmlParser::skip_processing_instruction(
  self : XmlParser,
  allow_xml_declaration~ : Bool,
) -> Unit raise DocxError {
  self.pos = self.pos + "") {
    self.pos = self.pos + "?>".length()
    return
  }
  if !self.skip_whitespace_and_report() {
    raise InvalidXml(message="malformed processing instruction")
  }
  self.skip_until("?>")
}

///|
fn XmlParser::skip_whitespace(self : XmlParser) -> Unit {
  ignore(self.skip_whitespace_and_report())
}

///|
fn XmlParser::skip_whitespace_and_report(self : XmlParser) -> Bool {
  let start = self.pos
  while self.pos < self.source.length() &&
        (
          self.source[self.pos] == ' ' ||
          self.source[self.pos] == '\n' ||
          self.source[self.pos] == '\r' ||
          self.source[self.pos] == '\t'
        ) {
    self.pos = self.pos + 1
  }
  self.pos > start
}

///|
fn xml_boundary_splits_pair(source : String, boundary : Int) -> Bool {
  if boundary <= 0 || boundary >= source.length() {
    return false
  }
  let unit = source[boundary].to_int()
  let previous = source[boundary - 1].to_int()
  unit >= 0xDC00 && unit <= 0xDFFF && previous >= 0xD800 && previous <= 0xDBFF
}

///|
/// Extract a substring by UTF-16 code-unit range. Core string slicing aborts
/// when a boundary splits a surrogate pair, so malicious input could crash
/// the parser; raise the parser's typed error instead of aborting or of
/// silently emitting an invalid-UTF-16 slice. Slices whose boundaries fall on
/// valid positions are copied verbatim (exactly what core slicing produced
/// historically), so well-formed documents are unaffected.
fn xml_substring(
  source : String,
  start : Int,
  end : Int,
) -> String raise DocxError {
  if xml_boundary_splits_pair(source, start) ||
    xml_boundary_splits_pair(source, end) {
    raise InvalidXml(message="invalid UTF-16 in XML content")
  }
  let builder = xml_string_builder_for_units(end - start)
  for index in start.. String raise DocxError {
  let start = self.pos
  if self.strict {
    let first = match self.source.get_char(self.pos) {
      Some(char) => char
      None => raise InvalidXml(message="expected XML name")
    }
    if !is_xml_name_start_char(first) {
      raise InvalidXml(message="expected XML name")
    }
    self.pos = self.pos + first.utf16_len()
    while self.pos < self.source.length() {
      let char = match self.source.get_char(self.pos) {
        Some(char) => char
        None => raise InvalidXml(message="invalid UTF-16 in XML content")
      }
      if !is_xml_name_char(char) {
        break
      }
      self.pos = self.pos + char.utf16_len()
    }
    self.charge_token(self.pos - start)
    return xml_substring(self.source, start, self.pos)
  }
  while self.pos < self.source.length() {
    let char = match self.source.get_char(self.pos) {
      Some(char) => char
      None => raise InvalidXml(message="invalid UTF-16 in XML content")
    }
    // The compatibility parser remains permissive about the first character
    // (for example, it historically accepted digit-start names), but valid
    // Unicode XML names must not be truncated to their ASCII prefix.
    if !is_xml_name_char(char) {
      break
    }
    self.pos += char.utf16_len()
  }
  if self.pos == start {
    raise InvalidXml(message="expected XML name")
  }
  self.charge_token(self.pos - start)
  xml_substring(self.source, start, self.pos)
}

///|
fn XmlParser::read_quoted_value(self : XmlParser) -> String raise DocxError {
  if self.pos >= self.source.length() {
    raise InvalidXml(message="expected quoted attribute value")
  }
  let quote = self.source[self.pos]
  if quote != '"' && quote != '\'' {
    raise InvalidXml(message="expected quoted attribute value")
  }
  self.pos = self.pos + 1
  let start = self.pos
  while self.pos < self.source.length() && self.source[self.pos] != quote {
    if self.strict && self.source[self.pos] == '<' {
      raise InvalidXml(message="raw '<' is not allowed in an attribute value")
    }
    self.pos = self.pos + 1
  }
  if self.pos >= self.source.length() {
    raise InvalidXml(message="unterminated attribute value")
  }
  self.charge_token(self.pos - start)
  let value = xml_substring(self.source, start, self.pos)
  self.pos = self.pos + 1
  value
}

///|
/// Reads one tolerant element attribute directly into its retained,
/// entity-decoded value. Keeping the parser source borrowed avoids retaining a
/// raw token, an entity substring, and growable decoder buffers concurrently.
fn XmlParser::read_quoted_value_tolerant_decoded(
  self : XmlParser,
) -> String raise DocxError {
  if self.pos >= self.source.length() {
    raise InvalidXml(message="expected quoted attribute value")
  }
  let quote = self.source[self.pos]
  if quote != '"' && quote != '\'' {
    raise InvalidXml(message="expected quoted attribute value")
  }
  self.pos += 1
  let start = self.pos
  while self.pos < self.source.length() && self.source[self.pos] != quote {
    self.pos += 1
  }
  if self.pos >= self.source.length() {
    raise InvalidXml(message="unterminated attribute value")
  }
  self.charge_token(self.pos - start)
  let value = decode_tolerant_xml_range(self.source, start, self.pos)
  self.pos += 1
  value
}

///|
/// Reads one strict element attribute directly into its retained normalized,
/// entity-decoded value. The parser source remains borrowed throughout; no raw
/// or separately normalized token-sized strings are materialized.
fn XmlParser::read_quoted_value_strict_decoded(
  self : XmlParser,
) -> String raise DocxError {
  if self.pos >= self.source.length() {
    raise InvalidXml(message="expected quoted attribute value")
  }
  let quote = self.source[self.pos]
  if quote != '"' && quote != '\'' {
    raise InvalidXml(message="expected quoted attribute value")
  }
  self.pos += 1
  let start = self.pos
  while self.pos < self.source.length() && self.source[self.pos] != quote {
    if self.source[self.pos] == '<' {
      raise InvalidXml(message="raw '<' is not allowed in an attribute value")
    }
    self.pos += 1
  }
  if self.pos >= self.source.length() {
    raise InvalidXml(message="unterminated attribute value")
  }
  self.charge_token(self.pos - start)
  let value = decode_strict_xml_range(
    self.source,
    start,
    self.pos,
    attribute=true,
    entities=true,
  )
  self.pos += 1
  value
}

///|
/// Reads tolerant character data into one retained decoded string. Entity
/// decoding is fused over the borrowed parser source just as it is for
/// tolerant attributes.
fn XmlParser::read_text_tolerant_decoded(
  self : XmlParser,
) -> String raise DocxError {
  let start = self.pos
  while self.pos < self.source.length() && self.source[self.pos] != '<' {
    self.pos += 1
  }
  self.charge_token(self.pos - start)
  decode_tolerant_xml_range(self.source, start, self.pos)
}

///|
/// Reads strict character data into one retained decoded string, fusing XML
/// line-ending normalization and entity replacement over the borrowed source.
fn XmlParser::read_text_strict_decoded(
  self : XmlParser,
) -> String raise DocxError {
  let start = self.pos
  while self.pos < self.source.length() && self.source[self.pos] != '<' {
    if self.starts_with("]]>") {
      raise InvalidXml(message="']]>' is not allowed in XML character data")
    }
    self.pos += 1
  }
  self.charge_token(self.pos - start)
  decode_strict_xml_range(
    self.source,
    start,
    self.pos,
    attribute=false,
    entities=true,
  )
}

///|
fn XmlParser::expect(self : XmlParser, value : String) -> Unit raise DocxError {
  if self.starts_with(value) {
    self.pos = self.pos + value.length()
  } else {
    raise InvalidXml(message="expected `" + value + "`")
  }
}

///|
fn XmlParser::starts_with(self : XmlParser, value : String) -> Bool {
  let value_length = value.length()
  if self.pos + value_length > self.source.length() {
    return false
  }
  for index in 0.. String raise DocxError {
  let start = self.pos
  while self.pos + needle.length() <= self.source.length() {
    if self.starts_with(needle) {
      self.charge_token(self.pos - start)
      return xml_substring(self.source, start, self.pos)
    }
    self.pos = self.pos + 1
  }
  raise InvalidXml(message="expected `" + needle + "`")
}

///|
/// Reads strict CDATA content while normalizing literal line endings directly
/// from the borrowed source. Entity-looking text remains literal in CDATA.
fn XmlParser::read_until_strict_normalized(
  self : XmlParser,
  needle : String,
) -> String raise DocxError {
  let start = self.pos
  while self.pos + needle.length() <= self.source.length() {
    if self.starts_with(needle) {
      self.charge_token(self.pos - start)
      return decode_strict_xml_range(
        self.source,
        start,
        self.pos,
        attribute=false,
        entities=false,
      )
    }
    self.pos += 1
  }
  raise InvalidXml(message="expected `" + needle + "`")
}

///|
fn XmlParser::skip_until(
  self : XmlParser,
  needle : String,
) -> Unit raise DocxError {
  while self.pos + needle.length() <= self.source.length() {
    if self.starts_with(needle) {
      self.pos += needle.length()
      return
    }
    self.pos += 1
  }
  raise InvalidXml(message="expected `" + needle + "`")
}

///|
fn xml_source_range_equals(
  source : String,
  start : Int,
  end : Int,
  expected : String,
) -> Bool {
  if end - start != expected.length() {
    return false
  }
  for offset in 0.. Char? {
  if xml_source_range_equals(source, start, end, "amp") {
    return Some('&')
  }
  if xml_source_range_equals(source, start, end, "lt") {
    return Some('<')
  }
  if xml_source_range_equals(source, start, end, "gt") {
    return Some('>')
  }
  if xml_source_range_equals(source, start, end, "quot") {
    return Some('"')
  }
  if xml_source_range_equals(source, start, end, "apos") {
    return Some('\'')
  }
  if end - start < 2 || source[start] != '#' {
    return None
  }
  let text_start = start + 1
  let mut first_lower_x = -1
  for index in text_start..= '0' && char <= '9') ||
      (char >= 'A' && char <= 'Z') ||
      (char >= 'a' && char <= 'z') ||
      char == '_') {
      return None
    }
    if first_lower_x < 0 && char == 'x' {
      first_lower_x = index
    }
  }
  match parse_xmldom_numeric_xml_range(source, text_start, end, first_lower_x) {
    None => Some('\u{0}')
    Some(codepoint) => codepoint.to_char()
  }
}

///|
/// Returns one code unit from xmldom's historical transformed numeric token:
/// the first lowercase `x` is replaced with `0x`. The virtual view preserves
/// those compatibility semantics without allocating the transformed suffix.
fn xmldom_numeric_virtual_unit(
  source : String,
  text_start : Int,
  first_lower_x : Int,
  virtual_index : Int,
) -> UInt16 {
  if first_lower_x < 0 {
    return source[text_start + virtual_index]
  }
  let prefix_length = first_lower_x - text_start
  if virtual_index < prefix_length {
    source[text_start + virtual_index]
  } else if virtual_index == prefix_length {
    '0'
  } else if virtual_index == prefix_length + 1 {
    'x'
  } else {
    source[text_start + virtual_index - 1]
  }
}

///|
/// Allocation-free equivalent of xmldom's `parseInt(text.replace('x','0x'))`.
/// `None` means no leading digit (decoded historically as NUL); the sentinel
/// U+110000 distinguishes overflow so the caller preserves the entity raw.
fn parse_xmldom_numeric_xml_range(
  source : String,
  text_start : Int,
  text_end : Int,
  first_lower_x : Int,
) -> Int? {
  let virtual_length = text_end -
    text_start +
    (if first_lower_x >= 0 { 1 } else { 0 })
  let mut index = 0
  let mut radix = 10
  if virtual_length >= 2 &&
    xmldom_numeric_virtual_unit(source, text_start, first_lower_x, 0) == '0' &&
    (
      xmldom_numeric_virtual_unit(source, text_start, first_lower_x, 1) == 'x' ||
      xmldom_numeric_virtual_unit(source, text_start, first_lower_x, 1) == 'X'
    ) {
    radix = 16
    index = 2
  }
  let mut value = 0
  let mut saw_digit = false
  while index < virtual_length {
    let char = xmldom_numeric_virtual_unit(
      source, text_start, first_lower_x, index,
    )
    match xml_entity_digit_value(char, radix) {
      Some(digit) => {
        saw_digit = true
        if value > (0x10FFFF - digit) / radix {
          return Some(0x110000)
        }
        value = value * radix + digit
        index += 1
      }
      None => break
    }
  }
  if saw_digit {
    Some(value)
  } else {
    None
  }
}

///|
/// Computes the exact retained size of a tolerant token without allocating an
/// entity substring. A failed semicolon search is cached so unterminated runs
/// remain linear.
fn tolerant_xml_decoded_shape(
  source : String,
  start : Int,
  end : Int,
) -> (Int, Bool) raise DocxError {
  let mut index = start
  let mut output_units = 0
  let mut changed = false
  let mut no_remaining_semicolon = false
  while index < end {
    if source[index] == '&' && !no_remaining_semicolon {
      let mut entity_end = index + 1
      while entity_end < end && source[entity_end] != ';' {
        entity_end += 1
      }
      if entity_end < end {
        match tolerant_xml_entity_char(source, index + 1, entity_end) {
          Some(char) => {
            output_units += char.utf16_len()
            changed = true
          }
          None => output_units += entity_end - index + 1
        }
        index = entity_end + 1
        continue
      }
      no_remaining_semicolon = true
    }
    let char = match source.get_char(index) {
      Some(char) => char
      None => raise InvalidXml(message="invalid UTF-16 in XML content")
    }
    output_units += char.utf16_len()
    index += char.utf16_len()
  }
  (output_units, changed)
}

///|
fn write_xml_source_range(
  output : StringBuilder,
  source : String,
  start : Int,
  end : Int,
) -> Unit raise DocxError {
  let mut index = start
  while index < end {
    let char = match source.get_char(index) {
      Some(char) => char
      None => raise InvalidXml(message="invalid UTF-16 in XML content")
    }
    output.write_char(char)
    index += char.utf16_len()
  }
}

///|
fn write_tolerant_xml_decoded_range(
  output : StringBuilder,
  source : String,
  start : Int,
  end : Int,
) -> Unit raise DocxError {
  let mut index = start
  let mut no_remaining_semicolon = false
  while index < end {
    if source[index] == '&' && !no_remaining_semicolon {
      let mut entity_end = index + 1
      while entity_end < end && source[entity_end] != ';' {
        entity_end += 1
      }
      if entity_end < end {
        match tolerant_xml_entity_char(source, index + 1, entity_end) {
          Some(char) => output.write_char(char)
          None => write_xml_source_range(output, source, index, entity_end + 1)
        }
        index = entity_end + 1
        continue
      }
      no_remaining_semicolon = true
    }
    let char = match source.get_char(index) {
      Some(char) => char
      None => raise InvalidXml(message="invalid UTF-16 in XML content")
    }
    output.write_char(char)
    index += char.utf16_len()
  }
}

///|
/// Fuses tolerant entity decoding over a borrowed parser range. The unchanged
/// fast path makes one exact copy; transformed tokens use a size pass and one
/// exactly-sized output allocation.
fn decode_tolerant_xml_range(
  source : String,
  start : Int,
  end : Int,
) -> String raise DocxError {
  let (output_units, changed) = tolerant_xml_decoded_shape(source, start, end)
  if output_units == 0 {
    return ""
  }
  if !changed {
    return xml_substring(source, start, end)
  }
  let output = xml_string_builder_for_units(output_units)
  write_tolerant_xml_decoded_range(output, source, start, end)
  output.to_string()
}

///|
/// Bounds a source range before it participates in an error message without
/// first copying an attacker-sized entity token.
fn xml_diagnostic_range(
  source : String,
  start : Int,
  end : Int,
) -> String raise DocxError {
  if end - start <= xml_diagnostic_token_chars {
    return xml_substring(source, start, end)
  }
  let output = xml_string_builder_for_units(xml_diagnostic_token_chars)
  let mut index = start
  let mut count = 0
  while index < end && count < xml_diagnostic_token_chars - 1 {
    let char = match source.get_char(index) {
      Some(char) => char
      None => raise InvalidXml(message="invalid UTF-16 in XML content")
    }
    output.write_char(char)
    index += char.utf16_len()
    count += 1
  }
  if index < end {
    output.write_string("…")
  }
  output.to_string()
}

///|
/// Decodes one strict entity directly from the parser's borrowed source range.
/// No entity-sized substring is allocated, including on error paths.
fn strict_xml_entity_char(
  source : String,
  start : Int,
  end : Int,
) -> Char raise DocxError {
  if xml_source_range_equals(source, start, end, "amp") {
    return '&'
  }
  if xml_source_range_equals(source, start, end, "lt") {
    return '<'
  }
  if xml_source_range_equals(source, start, end, "gt") {
    return '>'
  }
  if xml_source_range_equals(source, start, end, "quot") {
    return '"'
  }
  if xml_source_range_equals(source, start, end, "apos") {
    return '\''
  }
  if start >= end || source[start] != '#' {
    raise InvalidXml(
      message="undeclared XML entity: &" +
        xml_diagnostic_range(source, start, end) +
        ";",
    )
  }
  let (base, digits_start) = if start + 1 < end && source[start + 1] == 'x' {
    (16, start + 2)
  } else {
    (10, start + 1)
  }
  if digits_start >= end {
    raise InvalidXml(message="malformed numeric XML entity")
  }
  let mut codepoint = 0
  for index in digits_start..= '0' && char <= '9' {
      char.to_int() - '0'.to_int()
    } else if base == 16 && char >= 'A' && char <= 'F' {
      char.to_int() - 'A'.to_int() + 10
    } else if base == 16 && char >= 'a' && char <= 'f' {
      char.to_int() - 'a'.to_int() + 10
    } else {
      raise InvalidXml(message="malformed numeric XML entity")
    }
    if codepoint > (0x10ffff - digit) / base {
      raise InvalidXml(message="numeric XML entity is out of range")
    }
    codepoint = codepoint * base + digit
  }
  match Int::to_char(codepoint) {
    Some(char) if is_xml_character(char) => char
    _ => raise InvalidXml(message="numeric XML entity is not an XML character")
  }
}

///|
/// First pass over a borrowed strict token: validates every entity and computes
/// the exact UTF-16 output size without allocating raw or normalized copies.
fn strict_xml_decoded_shape(
  source : String,
  start : Int,
  end : Int,
  attribute~ : Bool,
  entities~ : Bool,
) -> (Int, Bool) raise DocxError {
  let mut index = start
  let mut output_units = 0
  let mut changed = false
  while index < end {
    if entities && source[index] == '&' {
      let mut entity_end = index + 1
      while entity_end < end && source[entity_end] != ';' {
        entity_end += 1
      }
      if entity_end >= end {
        raise InvalidXml(message="unterminated XML entity")
      }
      output_units += strict_xml_entity_char(source, index + 1, entity_end).utf16_len()
      changed = true
      index = entity_end + 1
    } else {
      let char = match source.get_char(index) {
        Some(char) => char
        None => raise InvalidXml(message="invalid UTF-16 in XML content")
      }
      let next = index + char.utf16_len()
      if char == '\r' {
        changed = true
        output_units += 1
        index = if next < end && source[next] == '\n' { next + 1 } else { next }
      } else if attribute && (char == '\t' || char == '\n') {
        changed = true
        output_units += 1
        index = next
      } else {
        output_units += char.utf16_len()
        index = next
      }
    }
  }
  (output_units, changed)
}

///|
/// Second pass writes directly from the borrowed source into one exactly-sized
/// result buffer. Character references are decoded before literal line-ending
/// normalization, preserving values such as `
` as required by XML 1.0.
fn write_strict_xml_decoded_range(
  output : StringBuilder,
  source : String,
  start : Int,
  end : Int,
  attribute~ : Bool,
  entities~ : Bool,
) -> Unit raise DocxError {
  let mut index = start
  while index < end {
    if entities && source[index] == '&' {
      let mut entity_end = index + 1
      while entity_end < end && source[entity_end] != ';' {
        entity_end += 1
      }
      output.write_char(strict_xml_entity_char(source, index + 1, entity_end))
      index = entity_end + 1
    } else {
      let char = match source.get_char(index) {
        Some(char) => char
        None => raise InvalidXml(message="invalid UTF-16 in XML content")
      }
      let next = index + char.utf16_len()
      if char == '\r' {
        output.write_char(if attribute { ' ' } else { '\n' })
        index = if next < end && source[next] == '\n' { next + 1 } else { next }
      } else if attribute && (char == '\t' || char == '\n') {
        output.write_char(' ')
        index = next
      } else {
        output.write_char(char)
        index = next
      }
    }
  }
}

///|
/// Fuses strict source normalization and entity decoding over a borrowed range.
/// The unchanged fast path makes one exact copy; transformed tokens use a
/// validation/size pass followed by one exactly-sized output allocation.
fn decode_strict_xml_range(
  source : String,
  start : Int,
  end : Int,
  attribute~ : Bool,
  entities~ : Bool,
) -> String raise DocxError {
  let (output_units, changed) = strict_xml_decoded_shape(
    source,
    start,
    end,
    attribute~,
    entities~,
  )
  if output_units == 0 {
    return ""
  }
  if !changed {
    return xml_substring(source, start, end)
  }
  let output = xml_string_builder_for_units(output_units)
  write_strict_xml_decoded_range(
    output,
    source,
    start,
    end,
    attribute~,
    entities~,
  )
  output.to_string()
}

///|
fn xml_entity_digit_value(char : UInt16, radix : Int) -> Int? {
  let value = if char >= '0' && char <= '9' {
    Some(char.to_int() - '0'.to_int())
  } else if char >= 'A' && char <= 'F' {
    Some(char.to_int() - 'A'.to_int() + 10)
  } else if char >= 'a' && char <= 'f' {
    Some(char.to_int() - 'a'.to_int() + 10)
  } else {
    None
  }
  match value {
    Some(digit) => if digit < radix { Some(digit) } else { None }
    None => None
  }
}

///|
fn escape_xml_text(value : String) -> String {
  let builder = StringBuilder()
  for char in value {
    match char {
      '&' => builder.write_string("&")
      '<' => builder.write_string("<")
      '>' => builder.write_string(">")
      _ => builder.write_char(char)
    }
  }
  builder.to_string()
}

///|
fn escape_xml_attribute(value : String) -> String {
  let builder = StringBuilder()
  for char in value {
    match char {
      '&' => builder.write_string("&")
      '<' => builder.write_string("<")
      '>' => builder.write_string(">")
      '"' => builder.write_string(""")
      _ => builder.write_char(char)
    }
  }
  builder.to_string()
}