///|
/// Lossless URDF XML parsing.

///|
/// An XML element together with its source location, kept internally so that
/// conversion errors can point at the offending line and column.
priv struct XNode {
  tag : String
  attrs : Array[(String, String)]
  attr_pos : Array[(Int32, Int32)]
  text : String
  children : Array[XNode]
  line : Int32
  col : Int32
}

///|
priv struct XmlParser {
  text : String
  pos : Array[Int32]
  line : Array[Int32]
  col : Array[Int32]
}

///|
fn xp_new(text : String) -> XmlParser {
  { text, pos: [0], line: [1], col: [1], }
}

///|
fn xp_len(p : XmlParser) -> Int32 {
  p.text.length()
}

///|
fn xp_at(p : XmlParser, i : Int32) -> Int32 {
  if i < 0 || i >= xp_len(p) {
    -1
  } else {
    UInt16::to_int(String::code_unit_at(p.text, i))
  }
}

///|
fn xp_peek(p : XmlParser) -> Int32 {
  xp_at(p, p.pos[0])
}

///|
fn xp_advance(p : XmlParser) -> Unit {
  let c = xp_peek(p)
  if c < 0 {
    return
  }
  p.pos[0] = p.pos[0] + 1
  if c == Char::to_int('\n') {
    p.line[0] = p.line[0] + 1
    p.col[0] = 1
  } else {
    p.col[0] = p.col[0] + 1
  }
}

///|
fn xp_starts(p : XmlParser, s : String) -> Bool {
  let n = s.length()
  if p.pos[0] + n > xp_len(p) {
    return false
  }
  for i in 0.. String {
  String::unsafe_substring(p.text, start~, end~)
}

///|
fn xp_snippet(p : XmlParser) -> String {
  let n = xp_len(p)
  let i = p.pos[0]
  let mut start = i
  while start > 0 && xp_at(p, start - 1) != Char::to_int('\n') {
    start = start - 1
  }
  let mut end = i
  while end < n && xp_at(p, end) != Char::to_int('\n') {
    end = end + 1
  }
  xp_substr(p, start, end)
}

///|
fn xp_error(p : XmlParser, message : String) -> Unit raise UrdfParseError {
  raise UrdfParseError({
    message,
    line: Some(p.line[0]),
    col: Some(p.col[0]),
    snippet: Some(xp_snippet(p)),
  })
}

///|
fn is_space_code(c : Int32) -> Bool {
  c == Char::to_int(' ') ||
  c == Char::to_int('\t') ||
  c == Char::to_int('\r') ||
  c == Char::to_int('\n')
}

///|
fn xp_skip_ws(p : XmlParser) -> Unit {
  while is_space_code(xp_peek(p)) {
    xp_advance(p)
  }
}

///|
fn is_name_char(c : Int32) -> Bool {
  c >= 0 &&
  c != Char::to_int(' ') &&
  c != Char::to_int('\t') &&
  c != Char::to_int('\r') &&
  c != Char::to_int('\n') &&
  c != Char::to_int('<') &&
  c != Char::to_int('>') &&
  c != Char::to_int('/') &&
  c != Char::to_int('=')
}

///|
fn xp_skip_pi(p : XmlParser) -> Unit raise UrdfParseError {
  // Consume "".
  xp_advance(p)
  xp_advance(p)
  while true {
    if p.pos[0] >= xp_len(p) {
      xp_error(p, "unterminated processing instruction")
    }
    if xp_starts(p, "?>") {
      xp_advance(p)
      xp_advance(p)
      return
    }
    xp_advance(p)
  }
}

///|
fn xp_skip_comment(p : XmlParser) -> String raise UrdfParseError {
  // Consume "") {
      let content = xp_substr(p, start, p.pos[0])
      xp_advance(p)
      xp_advance(p)
      xp_advance(p)
      return content
    }
    xp_advance(p)
  }
  ""
}

///|
fn xp_skip_misc(p : XmlParser) -> Unit raise UrdfParseError {
  while true {
    xp_skip_ws(p)
    if xp_starts(p, "