///|
fn parse_gff3_attributes(raw : String) -> Array[Attribute] {
  let attrs : Array[Attribute] = []
  for part_view in raw.split(";") {
    let part = part_view.trim().to_owned()
    if !part.is_empty() {
      let pieces = part.split("=").to_array()
      if pieces.length() >= 2 {
        attrs.push({
          key: pieces[0].trim().to_owned(),
          value: percent_unescape(pieces[1].trim().to_owned()),
        })
      } else {
        attrs.push({ key: part, value: "" })
      }
    }
  }
  attrs
}

///|
fn parse_gtf_attributes(raw : String) -> Array[Attribute] {
  let attrs : Array[Attribute] = []
  for part_view in raw.split(";") {
    let part = part_view.trim().to_owned()
    if !part.is_empty() {
      let pieces = part.split(" ").to_array()
      if pieces.length() >= 2 {
        let key = pieces[0].trim().to_owned()
        let value = trim_quotes(part[key.length():].trim().to_owned())
        attrs.push({ key, value })
      }
    }
  }
  attrs
}

///|
fn trim_quotes(s : String) -> String {
  if s.length() >= 2 && s[0] == '"' && s[s.length() - 1] == '"' {
    s[1:s.length() - 1].to_owned()
  } else {
    s
  }
}

///|
fn percent_unescape(s : String) -> String {
  s
  .replace(old="%20", new=" ")
  .replace(old="%3B", new=";")
  .replace(old="%3D", new="=")
}