///|
fn is_utf8_encoding(value : StringView) -> Bool {
  let normalized = value.to_owned().to_lower()
  normalized == "utf-8" || normalized == "utf8"
}

///|
fn is_encoding_char(value : Byte) -> Bool {
  (value >= b'a' && value <= b'z') ||
  (value >= b'A' && value <= b'Z') ||
  (value >= b'0' && value <= b'9') ||
  value == b'-' ||
  value == b'_' ||
  value == b'.'
}

///|
fn xml_encoding(bytes : BytesView) -> String? {
  let len = bytes.length()
  if len < 5 {
    return None
  }
  let prefix_len = if len < 256 { len } else { 256 }
  let prefix = bytes[:prefix_len]
  let start = if prefix_len >= 3 && prefix[:3].equal(b"\xEF\xBB\xBF") {
    3
  } else {
    0
  }
  if prefix_len < start + 5 || !prefix[start:start + 5].equal(b" (pos + b"encoding=\"".length(), b'"')
    None =>
      match prefix.find(b"encoding='") {
        Some(pos) => (pos + b"encoding='".length(), b'\'')
        None => return None
      }
  }
  let mut i = needle
  while i < prefix_len && prefix[i] != quote {
    if !is_encoding_char(prefix[i]) {
      return None
    }
    i = i + 1
  }
  if i <= needle || i >= prefix_len {
    return None
  }
  let slice = prefix[needle:i]
  let value = @encoding/utf8.decode(slice) catch { _ => return None }
  Some(value)
}

///|
fn decode_utf8(
  bytes : BytesView,
  transcoder : ((String, Bytes) -> String raise XlsxError)?,
) -> String raise XlsxError {
  // A UTF-16 byte-order mark is authoritative: a UTF-16 document's
  //  declaration is itself UTF-16 bytes, so the ASCII scan below
  // cannot detect it. Fall back to the declared encoding otherwise.
  let declared = match utf16_bom_label(bytes) {
    Some(label) => Some(label)
    None =>
      match xml_encoding(bytes) {
        Some(encoding) =>
          if is_utf8_encoding(encoding) {
            None
          } else {
            Some(encoding)
          }
        None => None
      }
  }
  match declared {
    Some(encoding) =>
      match transcoder {
        // A caller-supplied transcoder replaces the default entirely,
        // like Go's SetCharsetReader.
        Some(value) => return value(encoding, bytes.to_owned())
        None =>
          match builtin_charset_decode(encoding, bytes) {
            Some(text) => return text
            None =>
              raise InvalidXml(
                msg="xml encoding \{encoding} requires charset transcoder",
              )
          }
      }
    None => ()
  }
  @encoding/utf8.decode(bytes, ignore_bom=true) catch {
    _ => raise InvalidXml(msg="invalid utf8")
  }
}

///|
test "XLSX XML decoding strips a legal UTF-8 BOM" {
  inspect(decode_utf8(b"\xEF\xBB\xBF", None), content="")
}

///|
/// OOXML numeric attributes must be finite before they enter the workbook
/// model. MoonBit's parser accepts IEEE NaN and infinities, but those values
/// cannot be represented by JSON and make comparisons in layout/style logic
/// non-deterministic.
fn parse_finite_double_for_read(
  value : StringView,
  message : String,
) -> Double raise XlsxError {
  let parsed = @string.parse_double(value) catch {
    _ => raise InvalidXml(msg=message)
  }
  if parsed.is_nan() || parsed.is_inf() {
    raise InvalidXml(msg=message)
  }
  parsed
}

///|
fn parse_default_font(xml : StringView) -> String {
  let xml_str = xml.to_owned()
  let marker = " "Calibri"
    Some(start) => {
      let rest = xml_str[start + marker.length():]
      match rest.find(" "Calibri"
        Some(name_start) => {
          let name_rest = rest[name_start + 5:]
          match name_rest.find(">") {
            None => "Calibri"
            Some(end) => {
              let tag = name_rest[:end]
              let value_result : Result[String?, Error] = Ok(
                attr_value(tag, "val"),
              ) catch {
                e => Err(e)
              }
              match value_result {
                Ok(value) =>
                  match value {
                    Some(text) => unescape_xml_text(text)
                    None => "Calibri"
                  }
                Err(_) => "Calibri"
              }
            }
          }
        }
      }
    }
  }
}

///|
fn parse_font_entry(xml : StringView) -> Font raise XlsxError {
  let font = Font::new()
  match tag_attributes_in(xml, "b") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => if parse_bool_attr(value) { font.bold = Some(true) }
        None => font.bold = Some(true)
      }
    None => ()
  }
  match tag_attributes_in(xml, "i") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => if parse_bool_attr(value) { font.italic = Some(true) }
        None => font.italic = Some(true)
      }
    None => ()
  }
  match tag_attributes_in(xml, "strike") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => if parse_bool_attr(value) { font.strike = Some(true) }
        None => font.strike = Some(true)
      }
    None => ()
  }
  match tag_attributes_in(xml, "outline") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => if parse_bool_attr(value) { font.outline = Some(true) }
        None => font.outline = Some(true)
      }
    None => ()
  }
  match tag_attributes_in(xml, "shadow") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => if parse_bool_attr(value) { font.shadow = Some(true) }
        None => font.shadow = Some(true)
      }
    None => ()
  }
  match tag_attributes_in(xml, "condense") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => if parse_bool_attr(value) { font.condense = Some(true) }
        None => font.condense = Some(true)
      }
    None => ()
  }
  match tag_attributes_in(xml, "extend") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => if parse_bool_attr(value) { font.extended = Some(true) }
        None => font.extended = Some(true)
      }
    None => ()
  }
  match tag_attributes_in(xml, "u") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => font.underline = Some(unescape_xml_text(value))
        None => font.underline = Some("single")
      }
    None => ()
  }
  match tag_attributes_in(xml, "sz") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) =>
          font.size = Some(
            parse_finite_double_for_read(value, "font sz invalid"),
          )
        None => ()
      }
    None => ()
  }
  match tag_attributes_in(xml, "charset") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) =>
          try @string.parse_int(value, base=10) catch {
            _ => raise InvalidXml(msg="font charset invalid")
          } noraise {
            val => font.charset = Some(val)
          }
        None => ()
      }
    None => ()
  }
  match tag_attributes_in(xml, "name") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => font.family = Some(unescape_xml_text(value))
        None => ()
      }
    None => ()
  }
  match tag_attributes_in(xml, "family") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) =>
          try @string.parse_int(value, base=10) catch {
            _ => raise InvalidXml(msg="font family invalid")
          } noraise {
            val => if val != 2 { font.family_number = Some(val) }
          }
        None => ()
      }
    None => ()
  }
  match tag_attributes_in(xml, "vertAlign") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => font.vert_align = Some(unescape_xml_text(value))
        None => ()
      }
    None => ()
  }
  match tag_attributes_in(xml, "scheme") {
    Some(tag) =>
      match attr_value(tag, "val") {
        Some(value) => font.scheme = Some(unescape_xml_text(value))
        None => ()
      }
    None => ()
  }
  match tag_attributes_in(xml, "color") {
    Some(tag) => {
      match attr_value(tag, "rgb") {
        Some(value) => {
          let rgb = unescape_xml_text(value)
          font.color = Some(normalize_rgb_hex(rgb))
        }
        None => ()
      }
      match attr_value(tag, "theme") {
        Some(value) =>
          try @string.parse_int(value, base=10) catch {
            _ => raise InvalidXml(msg="font color theme invalid")
          } noraise {
            val => font.color_theme = Some(val)
          }
        None => ()
      }
      match attr_value(tag, "indexed") {
        Some(value) =>
          try @string.parse_int(value, base=10) catch {
            _ => raise InvalidXml(msg="font color indexed invalid")
          } noraise {
            val => font.color_indexed = Some(val)
          }
        None => ()
      }
      match attr_value(tag, "tint") {
        Some(value) =>
          font.color_tint = Some(
            parse_finite_double_for_read(value, "font color tint invalid"),
          )
        None => ()
      }
    }
    None => ()
  }
  font
}

///|
fn parse_fonts(xml : StringView) -> Array[Font] raise XlsxError {
  let fonts : Array[Font] = []
  let body = match extract_tag_body(xml, "fonts") {
    Some(value) => value
    None => {
      fonts.push(Font::new())
      return fonts
    }
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => continue
    }
    let rest = text[open_end + 1:]
    let end = match rest.find("") {
      Some(pos) => pos
      None => continue
    }
    let content = rest[:end]
    fonts.push(parse_font_entry(content))
  }
  if fonts.length() == 0 {
    fonts.push(Font::new())
  }
  fonts
}

///|
fn parse_fill_entry(xml : StringView) -> Fill raise XlsxError {
  let fill = Fill::new()
  let patterns : Array[String] = [
    "none", "solid", "mediumGray", "darkGray", "lightGray", "darkHorizontal", "darkVertical",
    "darkDown", "darkUp", "darkGrid", "darkTrellis", "lightHorizontal", "lightVertical",
    "lightDown", "lightUp", "lightGrid", "lightTrellis", "gray125", "gray0625",
  ]
  fn parse_rgb_color(rgb : StringView) -> (String, Int?) {
    // Strip an 8-char value's alpha only when the alpha really is hex —
    // a malformed value must stay malformed (GG112233 is not #112233).
    // Char-level, since parse_int would accept a signed "+1"/"-1".
    fn is_hex(c : Int) -> Bool {
      (c >= '0'.to_int() && c <= '9'.to_int()) ||
      (c >= 'a'.to_int() && c <= 'f'.to_int()) ||
      (c >= 'A'.to_int() && c <= 'F'.to_int())
    }

    let alpha_is_hex = rgb.length() == 8 &&
      is_hex(rgb[0].to_int()) &&
      is_hex(rgb[1].to_int())
    if alpha_is_hex {
      let alpha = parse_hex_byte(rgb, 0)
      let opacity = (alpha * 100 + 127) / 255
      let t = 100 - opacity
      let transparency = if t <= 0 { None } else { Some(t) }
      let color = rgb[2:].to_owned()
      (color, transparency)
    } else {
      (rgb.to_owned(), None)
    }
  }

  match tag_attributes_in(xml, "gradientFill") {
    Some(tag) => {
      fill.typ = Some("gradient")
      let bottom = match attr_value(tag, "bottom") {
        Some(value) =>
          parse_finite_double_for_read(value, "gradientFill bottom invalid")
        None => 0.0
      }
      let degree = match attr_value(tag, "degree") {
        Some(value) =>
          parse_finite_double_for_read(value, "gradientFill degree invalid")
        None => 0.0
      }
      let left = match attr_value(tag, "left") {
        Some(value) =>
          parse_finite_double_for_read(value, "gradientFill left invalid")
        None => 0.0
      }
      let right = match attr_value(tag, "right") {
        Some(value) =>
          parse_finite_double_for_read(value, "gradientFill right invalid")
        None => 0.0
      }
      let top = match attr_value(tag, "top") {
        Some(value) =>
          parse_finite_double_for_read(value, "gradientFill top invalid")
        None => 0.0
      }
      let typ = match attr_value(tag, "type") {
        Some(value) => unescape_xml_text(value)
        None => ""
      }
      let mut transparency : Int? = None
      let stop_positions : Array[Double] = []
      let stop_colors : Array[String] = []
      match extract_tag_body(xml, "gradientFill") {
        Some(body) => {
          let mut first = true
          for chunk in body.split("") {
              Some(pos) => pos
              None => continue
            }
            let open_tag = text[:open_end]
            match attr_value(open_tag, "position") {
              Some(value) =>
                stop_positions.push(
                  parse_finite_double_for_read(
                    value, "gradientFill stop position invalid",
                  ),
                )
              None => stop_positions.push(0.0)
            }
            let rest = text[open_end + 1:]
            let end = match rest.find("") {
              Some(pos) => pos
              None => continue
            }
            let content = rest[:end]
            match tag_attributes_in(content, "color") {
              Some(color_tag) =>
                match attr_value(color_tag, "rgb") {
                  Some(value) => {
                    let rgb = unescape_xml_text(value)
                    let (c, t) = parse_rgb_color(rgb)
                    stop_colors.push(c)
                    match (transparency, t) {
                      (None, Some(v)) => transparency = Some(v)
                      _ => ()
                    }
                  }
                  None => ()
                }
              None => ()
            }
          }
        }
        None => ()
      }
      let mut shading = 0
      for i in 0.. fill.colors = Some([c1, c2])
        _ => ()
      }
      match transparency {
        Some(v) => fill.transparency = Some(v)
        None => ()
      }
    }
    None => ()
  }
  match tag_attributes_in(xml, "patternFill") {
    Some(tag) => {
      let pattern_type = match attr_value(tag, "patternType") {
        Some(value) => unescape_xml_text(value)
        None => "none"
      }
      let mut pattern = 0
      let mut found = false
      for i in 0.. {
          match attr_value(color_tag, "theme") {
            Some(value) =>
              try @string.parse_int(value, base=10) catch {
                _ => raise InvalidXml(msg="fill fgColor theme invalid")
              } noraise {
                val => fill.fg_theme = Some(val)
              }
            None => ()
          }
          match attr_value(color_tag, "indexed") {
            Some(value) =>
              try @string.parse_int(value, base=10) catch {
                _ => raise InvalidXml(msg="fill fgColor indexed invalid")
              } noraise {
                val => fill.fg_indexed = Some(val)
              }
            None => ()
          }
          match attr_value(color_tag, "tint") {
            Some(value) =>
              fill.fg_tint = Some(
                parse_finite_double_for_read(value, "fill fgColor tint invalid"),
              )
            None => ()
          }
          match attr_value(color_tag, "rgb") {
            Some(value) => {
              let rgb = unescape_xml_text(value)
              let (c, t) = parse_rgb_color(rgb)
              fg_color = Some(c)
              match t {
                Some(v) => transparency = Some(v)
                None => ()
              }
            }
            None => ()
          }
        }
        None => ()
      }
      match tag_attributes_in(xml, "bgColor") {
        Some(color_tag) => {
          match attr_value(color_tag, "theme") {
            Some(value) =>
              try @string.parse_int(value, base=10) catch {
                _ => raise InvalidXml(msg="fill bgColor theme invalid")
              } noraise {
                val => fill.bg_theme = Some(val)
              }
            None => ()
          }
          match attr_value(color_tag, "indexed") {
            Some(value) =>
              try @string.parse_int(value, base=10) catch {
                _ => raise InvalidXml(msg="fill bgColor indexed invalid")
              } noraise {
                val => fill.bg_indexed = Some(val)
              }
            None => ()
          }
          match attr_value(color_tag, "tint") {
            Some(value) =>
              fill.bg_tint = Some(
                parse_finite_double_for_read(value, "fill bgColor tint invalid"),
              )
            None => ()
          }
          match attr_value(color_tag, "rgb") {
            Some(value) => {
              let rgb = unescape_xml_text(value)
              let (c, t) = parse_rgb_color(rgb)
              bg_color = Some(c)
              match t {
                Some(v) => transparency = Some(v)
                None => ()
              }
            }
            None => ()
          }
        }
        None => ()
      }
      let colors : Array[String] = []
      match fg_color {
        Some(c) => colors.push(c)
        None => ()
      }
      match bg_color {
        Some(c) => colors.push(c)
        None => ()
      }
      if colors.length() > 0 {
        fill.colors = Some(colors)
      }
      match transparency {
        Some(v) => fill.transparency = Some(v)
        None => ()
      }
      fill
    }
    None => fill
  }
}

///|
fn parse_fills(xml : StringView) -> Array[Fill] raise XlsxError {
  let fills : Array[Fill] = []
  let body = match extract_tag_body(xml, "fills") {
    Some(value) => value
    None => return fills
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => continue
    }
    let open_tag = text[:open_end]
    if open_tag.has_suffix("/") {
      fills.push(Fill::new())
      continue
    }
    let rest = text[open_end + 1:]
    let end = match rest.find("") {
      Some(pos) => pos
      None => continue
    }
    let content = rest[:end]
    fills.push(parse_fill_entry(content))
  }
  fills
}

///|
fn parse_border_entry(
  border_tag : StringView,
  xml : StringView,
) -> Array[Border] raise XlsxError {
  let border_styles : Array[String] = [
    "none", "thin", "medium", "dashed", "dotted", "thick", "double", "hair", "mediumDashed",
    "dashDot", "mediumDashDot", "dashDotDot", "mediumDashDotDot", "slantDashDot",
  ]
  fn parse_side(
    xml : StringView,
    side : StringView,
  ) -> Border? raise XlsxError {
    let tag = match tag_attributes_in(xml, side) {
      Some(value) => value
      None => return None
    }
    let style_name = match attr_value(tag, "style") {
      Some(value) => unescape_xml_text(value)
      None => "none"
    }
    let mut style = 0
    for i in 0..
        match tag_attributes_in(body, "color") {
          Some(color_tag) =>
            match attr_value(color_tag, "rgb") {
              Some(value) => color = Some(unescape_xml_text(value))
              None => ()
            }
          None => ()
        }
      None => ()
    }
    match color {
      Some(rgb) =>
        Some(
          Border::with_values(
            side.to_owned(),
            color=normalize_rgb_hex(rgb),
            style~,
          ),
        )
      None =>
        if style > 0 {
          Some(Border::with_values(side.to_owned(), style~))
        } else {
          None
        }
    }
  }

  let diagonal_up = match attr_value(border_tag, "diagonalUp") {
    Some(value) => parse_bool_attr(value)
    None => false
  }
  let diagonal_down = match attr_value(border_tag, "diagonalDown") {
    Some(value) => parse_bool_attr(value)
    None => false
  }
  let borders : Array[Border] = []
  match parse_side(xml, "left") {
    Some(b) => borders.push(b)
    None => ()
  }
  match parse_side(xml, "right") {
    Some(b) => borders.push(b)
    None => ()
  }
  match parse_side(xml, "top") {
    Some(b) => borders.push(b)
    None => ()
  }
  match parse_side(xml, "bottom") {
    Some(b) => borders.push(b)
    None => ()
  }
  let diagonal = parse_side(xml, "diagonal")
  match diagonal {
    Some(b) =>
      if diagonal_up || diagonal_down {
        let diagonal_style = match b.style {
          Some(value) => value
          None => 0
        }
        if diagonal_up {
          match b.color {
            Some(color) =>
              borders.push(
                Border::with_values("diagonalUp", color~, style=diagonal_style),
              )
            None =>
              borders.push(
                Border::with_values("diagonalUp", style=diagonal_style),
              )
          }
        }
        if diagonal_down {
          match b.color {
            Some(color) =>
              borders.push(
                Border::with_values(
                  "diagonalDown",
                  color~,
                  style=diagonal_style,
                ),
              )
            None =>
              borders.push(
                Border::with_values("diagonalDown", style=diagonal_style),
              )
          }
        }
      } else {
        borders.push(b)
      }
    None => ()
  }
  match parse_side(xml, "vertical") {
    Some(b) => borders.push(b)
    None => ()
  }
  match parse_side(xml, "horizontal") {
    Some(b) => borders.push(b)
    None => ()
  }
  borders
}

///|
fn parse_borders(xml : StringView) -> Array[Array[Border]] raise XlsxError {
  let borders : Array[Array[Border]] = []
  let body = match extract_tag_body(xml, "borders") {
    Some(value) => value
    None => {
      borders.push([])
      return borders
    }
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => continue
    }
    let open_tag = text[:open_end]
    if open_tag.has_suffix("/") {
      borders.push([])
      continue
    }
    let rest = text[open_end + 1:]
    let end = match rest.find("") {
      Some(pos) => pos
      None => continue
    }
    let content = rest[:end]
    borders.push(parse_border_entry(open_tag, content))
  }
  if borders.length() == 0 {
    borders.push([])
  }
  borders
}

///|
fn parse_protection_entry(xml : StringView) -> Protection? raise XlsxError {
  let tag = match tag_attributes_in(xml, "protection") {
    Some(value) => value
    None => return None
  }
  let protection = Protection::new()
  match attr_value(tag, "hidden") {
    Some(value) => protection.hidden = Some(parse_bool_attr(value))
    None => ()
  }
  match attr_value(tag, "locked") {
    Some(value) => protection.locked = Some(parse_bool_attr(value))
    None => ()
  }
  match (protection.hidden, protection.locked) {
    (None, None) => None
    _ => Some(protection)
  }
}

///|
fn parse_alignment_entry(xml : StringView) -> Alignment? raise XlsxError {
  let tag = match tag_attributes_in(xml, "alignment") {
    Some(value) => value
    None => return None
  }
  let alignment = Alignment::new()
  match attr_value(tag, "horizontal") {
    Some(value) => alignment.horizontal = Some(unescape_xml_text(value))
    None => ()
  }
  match attr_value(tag, "vertical") {
    Some(value) => alignment.vertical = Some(unescape_xml_text(value))
    None => ()
  }
  match attr_value(tag, "wrapText") {
    Some(value) => alignment.wrap_text = Some(parse_bool_attr(value))
    None => ()
  }
  match attr_value(tag, "textRotation") {
    Some(value) =>
      alignment.text_rotation = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="textRotation invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "indent") {
    Some(value) =>
      alignment.indent = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="indent invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "shrinkToFit") {
    Some(value) => alignment.shrink_to_fit = Some(parse_bool_attr(value))
    None => ()
  }
  match attr_value(tag, "justifyLastLine") {
    Some(value) => alignment.justify_last_line = Some(parse_bool_attr(value))
    None => ()
  }
  match attr_value(tag, "readingOrder") {
    Some(value) =>
      alignment.reading_order = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="readingOrder invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "relativeIndent") {
    Some(value) =>
      alignment.relative_indent = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="relativeIndent invalid")
        },
      )
    None => ()
  }
  match
    (
      alignment.horizontal,
      alignment.vertical,
      alignment.wrap_text,
      alignment.text_rotation,
      alignment.indent,
      alignment.shrink_to_fit,
      alignment.justify_last_line,
      alignment.reading_order,
      alignment.relative_indent,
    ) {
    (None, None, None, None, None, None, None, None, None) => None
    _ => Some(alignment)
  }
}

///|
fn parse_pivot_table_name(xml : StringView) -> String raise XlsxError {
  match tag_attributes_in(xml, "pivotTableDefinition") {
    Some(tag) =>
      match attr_value(tag, "name") {
        Some(value) => unescape_xml_text(value)
        None => ""
      }
    None => ""
  }
}

///|
fn parse_defined_names(
  xml : StringView,
  sheet_names : Array[String],
) -> Array[DefinedName] raise XlsxError {
  let names : Array[DefinedName] = []
  let body = match extract_tag_body(xml, "definedNames") {
    Some(value) => value
    None => return names
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => raise InvalidXml(msg="definedName tag not closed")
    }
    let tag = text[:end]
    let name = match attr_value(tag, "name") {
      Some(value) => unescape_xml_text(value)
      None => raise InvalidXml(msg="definedName name missing")
    }
    let comment = match attr_value(tag, "comment") {
      Some(value) => unescape_xml_text(value)
      None => ""
    }
    let local_id = match attr_value(tag, "localSheetId") {
      Some(value) =>
        Some(
          @string.parse_int(value, base=10) catch {
            _ => raise InvalidXml(msg="definedName localSheetId invalid")
          },
        )
      None => None
    }
    let rest = text[end + 1:]
    let data_end = match rest.find("") {
      Some(pos) => pos
      None => raise InvalidXml(msg="definedName close missing")
    }
    let data = rest[:data_end]
    let scope = match local_id {
      Some(id) =>
        if id >= 0 && id < sheet_names.length() {
          sheet_names[id]
        } else {
          raise InvalidXml(msg="definedName localSheetId missing")
        }
      None => "Workbook"
    }
    names.push({ name, refers_to: unescape_xml_text(data), scope, comment })
  }
  names
}

///|
fn parse_merge_cells(xml : StringView) -> Array[String] raise XlsxError {
  let merged : Array[String] = []
  let body = match extract_tag_body(xml, "mergeCells") {
    Some(value) => value
    None => return merged
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None =>
        match text.find(">") {
          Some(pos) => pos
          None => raise InvalidXml(msg="mergeCell tag not closed")
        }
    }
    let tag = text[:end]
    let ref_value = match attr_value(tag, "ref") {
      Some(value) => value
      None => raise InvalidXml(msg="mergeCell ref missing")
    }
    let normalized = if ref_value.contains(":") {
      normalize_range_ref(ref_value)
    } else {
      let (row, col) = cell_ref_to_rc(ref_value)
      let cell_ref = cell_ref_from(row, col)
      "\{cell_ref}:\{cell_ref}"
    }
    merged.push(normalized)
  }
  merged
}

///|
fn parse_data_validations(
  xml : StringView,
  budget? : ReadBudget,
) -> Array[String] raise XlsxError {
  let validations : Array[String] = []
  let body = match extract_tag_body(xml, "dataValidations") {
    Some(value) => value
    None => return validations
  }
  let mut first = true
  for chunk in body.split(" {
        let end = pos + close_tag.length()
        let slice = text[:end]
        " {
        let end = match text.find("/>") {
          Some(pos) => pos + 2
          None => raise InvalidXml(msg="dataValidation tag not closed")
        }
        let slice = text[:end]
        " Array[String] raise XlsxError {
  let validations : Array[String] = []
  let xml_str = xml.to_owned()
  if !xml_str.contains(" pos + close_tag.length()
      None => raise InvalidXml(msg="x14:dataValidation tag not closed")
    }
    let slice = text[:end]
    let full = "") {
      Some(pos) => pos
      None => raise InvalidXml(msg="x14:dataValidation open tag missing")
    }
    let attrs = full[" value
      None => "0"
    }
    let typ = match attr_value(attrs, "type") {
      Some(value) => value
      None => ""
    }
    let sqref = match extract_tag_body_from(full, "xm:sqref") {
      Some(value) =>
        normalize_sqref_for_read(
          unescape_xml_text(value),
          "x14 data validation",
          budget?,
        )
      None => raise InvalidXml(msg="x14:dataValidation sqref missing")
    }
    let mut formula1 = ""
    match extract_tag_body_from(full, "x14:formula1") {
      Some(value) => {
        let body = value.to_string()
        match extract_tag_body_from(body, "xm:f") {
          Some(f) => formula1 = unescape_xml_text(f)
          None => formula1 = unescape_xml_text(value)
        }
      }
      None => ()
    }
    let mut formula2 = ""
    match extract_tag_body_from(full, "x14:formula2") {
      Some(value) => {
        let body = value.to_string()
        match extract_tag_body_from(body, "xm:f") {
          Some(f) => formula2 = unescape_xml_text(f)
          None => formula2 = unescape_xml_text(value)
        }
      }
      None => ()
    }
    let sb = StringBuilder::new()
    sb.write_view("")
    if formula1 != "" {
      sb.write_view("")
      sb.write_view(escape_xml_text(formula1))
      sb.write_view("")
    }
    if formula2 != "" {
      sb.write_view("")
      sb.write_view(escape_xml_text(formula2))
      sb.write_view("")
    }
    sb.write_view("")
    validations.push(sb.to_string())
  }
  validations
}

///|
fn parse_conditional_formats(
  xml : StringView,
  budget? : ReadBudget,
) -> Array[String] raise XlsxError {
  let formats : Array[String] = []
  let xml_str = xml.to_owned()
  if !xml_str.contains(" {
        let end = pos + close_tag.length()
        let slice = text[:end]
        " {
        let end = match text.find("/>") {
          Some(pos) => pos + 2
          None => raise InvalidXml(msg="conditionalFormatting tag not closed")
        }
        let slice = text[:end]
        " String {
  let sb = StringBuilder::new()
  sb.write_view("")
  let show_value = if opt.icons_only { "0" } else { "1" }
  sb.write_view(
    "")
  if cfvos.length() > 0 {
    for cfvo in cfvos {
      sb.write_view(cfvo)
    }
  } else {
    match icon_set_steps(opt.icon_style) {
      Some(steps) =>
        for step in steps {
          sb.write_view("")
        }
      None => ()
    }
  }
  sb.write_view("")
  sb.write_view("")
  sb.to_string()
}

///|
fn parse_x14_cfvo_values(xml : StringView) -> Array[String] raise XlsxError {
  let cfvos : Array[String] = []
  let xml_str = xml.to_owned()
  if !xml_str.contains(" {
        let end = pos + close_tag.length()
        let slice = text[:end]
        " {
        let end = match text.find("/>") {
          Some(pos) => pos + 2
          None => raise InvalidXml(msg="x14:cfvo tag not closed")
        }
        let slice = text[:end]
        "") {
      Some(pos) => pos
      None => raise InvalidXml(msg="x14:cfvo open tag missing")
    }
    let attrs = full[" value.to_string()
      None => ""
    }
    let value = match extract_tag_body_from(full, "xm:f") {
      Some(v) => unescape_xml_text(v)
      None =>
        match extract_tag_body_from(full, "f") {
          Some(v) => unescape_xml_text(v)
          None => ""
        }
    }
    if typ == "" {
      continue
    }
    let sb = StringBuilder::new()
    sb.write_view("")
    cfvos.push(sb.to_string())
  }
  cfvos
}

///|
fn color_text_from_rgb_x14(rgb : StringView) -> String {
  let text = rgb.to_owned()
  match text.strip_prefix("FF") {
    Some(rest) if text.length() == 8 => "#\{rest}"
    _ => "#\{text}"
  }
}

///|
fn parse_x14_conditional_formatting_blocks(
  xml : StringView,
) -> Array[String] raise XlsxError {
  let blocks : Array[String] = []
  if xml.contains("") {
    raise InvalidXml(msg="x14:conditionalFormattings tag not closed")
  }
  let needle = "
        raise InvalidXml(msg="x14:conditionalFormatting close before open")
      (Some(_), None) =>
        raise InvalidXml(msg="x14:conditionalFormatting close without open")
      _ => ()
    }
    let rel_start = match rel_start {
      Some(pos) => pos
      None => break
    }
    let start = search_from + rel_start
    let after_needle = start + needle.length()
    if after_needle >= xml.length() {
      raise InvalidXml(msg="x14:conditionalFormatting tag not closed")
    }
    if xml[after_needle] == ('s' : UInt16) {
      search_from = after_needle
      continue
    }
    if !is_xml_open_tag_boundary(xml[after_needle]) {
      search_from = after_needle
      continue
    }
    let open_end = match xml_open_tag_end_from(xml, after_needle) {
      Some(pos) => pos
      None => raise InvalidXml(msg="x14:conditionalFormatting tag not closed")
    }
    let self_closing = open_end > start && xml[open_end - 1] == ('/' : UInt16)
    let end = if self_closing {
      open_end + 1
    } else {
      let body_start = open_end + 1
      let relative_close = match xml[body_start:].find(close_tag) {
        Some(pos) => pos
        None => raise InvalidXml(msg="x14:conditionalFormatting tag not closed")
      }
      body_start + relative_close + close_tag.length()
    }
    blocks.push(xml[start:end].to_owned())
    search_from = end
  }
  blocks
}

///|
fn parse_x14_data_bars(
  xml : StringView,
  budget? : ReadBudget,
) -> Map[String, X14DataBarProps] raise XlsxError {
  let bars : Map[String, X14DataBarProps] = Map([])
  let blocks = parse_x14_conditional_formatting_blocks(xml)
  if blocks.length() == 0 {
    return bars
  }
  for full in blocks {
    let sqref = match extract_tag_body_from(full, "xm:sqref") {
      Some(value) =>
        normalize_sqref_for_read(
          unescape_xml_text(value),
          "x14 conditional formatting",
          budget?,
        )
      None =>
        match extract_tag_body_from(full, "sqref") {
          Some(value) =>
            normalize_sqref_for_read(
              unescape_xml_text(value),
              "x14 conditional formatting",
              budget?,
            )
          None => continue
        }
    }
    if !full.contains(" {
          let end = pos + rule_close.length()
          let slice = rule_text[:end]
          " {
          let end = match rule_text.find("/>") {
            Some(pos) => pos + 2
            None => raise InvalidXml(msg="x14:cfRule tag not closed")
          }
          let slice = rule_text[:end]
          "") {
        Some(pos) => pos
        None => continue
      }
      let attrs = rule_full[" value.to_string()
        None => ""
      }
      if typ != "dataBar" {
        continue
      }
      let id = match attr_value(attrs, "id") {
        Some(value) => value.to_string()
        None => continue
      }
      let mut bar_direction = ""
      let mut bar_solid = false
      let mut bar_border_color = ""
      match tag_attributes_in(rule_full, "x14:dataBar") {
        Some(tag) => {
          match attr_value(tag, "direction") {
            Some(value) => bar_direction = value.to_string()
            None => ()
          }
          match attr_value(tag, "gradient") {
            Some(value) => bar_solid = !parse_xml_bool(value)
            None => ()
          }
        }
        None => ()
      }
      match tag_attributes_in(rule_full, "x14:borderColor") {
        Some(tag) =>
          match attr_value(tag, "rgb") {
            Some(value) => bar_border_color = color_text_from_rgb_x14(value)
            None => ()
          }
        None => ()
      }
      bars[id] = { sqref, bar_direction, bar_solid, bar_border_color }
    }
  }
  bars
}

///|
fn worksheet_ext_uri_is_known(uri : StringView) -> Bool {
  match uri {
    "{78C0D931-6437-407d-A8EE-F0AAD7539E65}" => true
    "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}" => true
    "{A8765BA9-456A-4dab-B4F3-ACF838C121DE}" => true
    "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}" => true
    _ => false
  }
}

///|
fn parse_unknown_worksheet_ext_blocks(
  xml : StringView,
) -> Array[String] raise XlsxError {
  let unknown : Array[String] = []
  let text = xml.to_owned()
  let ext_start = match text.rev_find(" pos
    None => return unknown
  }
  let ext_tail = text[ext_start:]
  let open_end = match ext_tail.find(">") {
    Some(pos) => pos
    None => raise InvalidXml(msg="worksheet extLst open tag invalid")
  }
  let close_rel = match ext_tail.find("") {
    Some(pos) => pos
    None => raise InvalidXml(msg="worksheet extLst close tag missing")
  }
  let body = ext_tail[open_end + 1:close_rel]
  let mut first = true
  for chunk in body.to_owned().split("") {
      Some(pos) => pos + "".length()
      None =>
        match full.find("/>") {
          Some(pos) => pos + 2
          None => raise InvalidXml(msg="worksheet ext block not closed")
        }
    }
    let block = full[:end]
    let open_end = match block.find(">") {
      Some(pos) => pos
      None => raise InvalidXml(msg="worksheet ext open tag invalid")
    }
    let open_tag = block[:open_end]
    let uri = match attr_value(open_tag, "uri") {
      Some(value) => value.to_string()
      None => ""
    }
    if worksheet_ext_uri_is_known(uri) {
      continue
    }
    if block.contains(" Array[String] raise XlsxError {
  let formats : Array[String] = []
  let blocks = parse_x14_conditional_formatting_blocks(xml)
  if blocks.length() == 0 {
    return formats
  }
  for full in blocks {
    let sqref = match extract_tag_body_from(full, "xm:sqref") {
      Some(value) =>
        normalize_sqref_for_read(
          unescape_xml_text(value),
          "x14 conditional formatting",
          budget?,
        )
      None =>
        match extract_tag_body_from(full, "sqref") {
          Some(value) =>
            normalize_sqref_for_read(
              unescape_xml_text(value),
              "x14 conditional formatting",
              budget?,
            )
          None =>
            raise InvalidXml(msg="x14:conditionalFormatting sqref missing")
        }
    }
    let rules : Array[String] = []
    if full.contains(" {
            let end = pos + rule_close.length()
            let slice = rule_text[:end]
            " {
            let end = match rule_text.find("/>") {
              Some(pos) => pos + 2
              None => raise InvalidXml(msg="x14:cfRule tag not closed")
            }
            let slice = rule_text[:end]
            "") {
          Some(pos) => pos
          None => continue
        }
        let attrs = rule_full[" value.to_string()
          None => ""
        }
        if typ != "iconSet" {
          continue
        }
        let mut priority = match attr_value(attrs, "priority") {
          Some(value) =>
            @string.parse_int(value, base=10) catch {
              _ => raise InvalidXml(msg="x14:cfRule priority invalid")
            }
          None => 1
        }
        if priority <= 0 {
          priority = 1
        }
        let stop_if_true = match attr_value(attrs, "stopIfTrue") {
          Some(value) => parse_xml_bool(value)
          None => false
        }
        let opt = ConditionalFormatOptions::new("icon_set")
        opt.stop_if_true = stop_if_true
        match tag_attributes_in(rule_full, "x14:iconSet") {
          Some(tag) => {
            match attr_value(tag, "iconSet") {
              Some(value) => opt.icon_style = value.to_string()
              None => ()
            }
            match attr_value(tag, "reverse") {
              Some(value) => opt.reverse_icons = parse_xml_bool(value)
              None => ()
            }
            let show_value = match attr_value(tag, "showValue") {
              Some(value) => parse_xml_bool(value)
              None => true
            }
            opt.icons_only = !show_value
            match attr_value(tag, "percent") {
              Some(value) => opt.percent = parse_xml_bool(value)
              None =>
                match attr_value(attrs, "percent") {
                  Some(value) => opt.percent = parse_xml_bool(value)
                  None => ()
                }
            }
          }
          None => continue
        }
        let cfvos = parse_x14_cfvo_values(rule_full)
        rules.push(build_icon_set_rule_xml_from_x14(opt, priority, cfvos))
      }
    }
    if rules.length() > 0 {
      formats.push(conditional_format_xml(sqref, rules))
    }
  }
  formats
}

///|
fn parse_sheet_dimension(xml : StringView) -> String? raise XlsxError {
  let xml_str = xml.to_owned()
  let marker = " None
    Some(start) => {
      let rest = xml_str[start + marker.length():]
      let end = match rest.find("/>") {
        Some(pos) => pos
        None =>
          match rest.find(">") {
            Some(pos) => pos
            None => raise InvalidXml(msg="dimension tag not closed")
          }
      }
      let tag = rest[:end]
      match attr_value(tag, "ref") {
        Some(value) =>
          Some(
            validate_cell_or_range_ref_for_read(
              unescape_xml_text(value),
              "dimension",
            ),
          )
        None => None
      }
    }
  }
}

///|
fn parse_sheet_view_pane(body : StringView) -> SheetPane? raise XlsxError {
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None =>
        match text.find(">") {
          Some(pos) => pos
          None => raise InvalidXml(msg="pane tag not closed")
        }
    }
    let tag = text[:end]
    let pane = SheetPane::new()
    match attr_value(tag, "activePane") {
      Some(value) => pane.active_pane = unescape_xml_text(value)
      None => ()
    }
    match attr_value(tag, "state") {
      Some(value) => pane.state = unescape_xml_text(value)
      None => ()
    }
    match attr_value(tag, "topLeftCell") {
      Some(value) =>
        pane.top_left_cell = validate_cell_ref_for_read(
          unescape_xml_text(value),
          "pane topLeftCell",
        )
      None => ()
    }
    match attr_value(tag, "xSplit") {
      Some(value) =>
        pane.x_split = @string.parse_double(value) catch {
          _ => raise InvalidXml(msg="pane xSplit invalid")
        }
      None => ()
    }
    match attr_value(tag, "ySplit") {
      Some(value) =>
        pane.y_split = @string.parse_double(value) catch {
          _ => raise InvalidXml(msg="pane ySplit invalid")
        }
      None => ()
    }
    return Some(pane)
  }
  None
}

///|
fn parse_sheet_view_selections(
  body : StringView,
  budget? : ReadBudget,
) -> Array[Selection] raise XlsxError {
  let selections : Array[Selection] = []
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None =>
        match text.find(">") {
          Some(pos) => pos
          None => raise InvalidXml(msg="selection tag not closed")
        }
    }
    let tag = text[:end]
    let selection = { sqref: "", active_cell: "", pane: "" }
    match attr_value(tag, "sqref") {
      Some(value) =>
        selection.sqref = normalize_sqref_for_read(
          unescape_xml_text(value),
          "selection",
          budget?,
        )
      None => ()
    }
    match attr_value(tag, "activeCell") {
      Some(value) =>
        selection.active_cell = validate_cell_ref_for_read(
          unescape_xml_text(value),
          "selection activeCell",
        )
      None => ()
    }
    match attr_value(tag, "pane") {
      Some(value) => selection.pane = unescape_xml_text(value)
      None => ()
    }
    selections.push(selection)
  }
  selections
}

///|
fn parse_sheet_views(
  xml : StringView,
  budget? : ReadBudget,
) -> Array[SheetView] raise XlsxError {
  let views : Array[SheetView] = []
  let body = match extract_tag_body(xml, "sheetViews") {
    Some(value) => value
    None => return views
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => raise InvalidXml(msg="sheetView tag not closed")
    }
    let tag = text[:end]
    let view = SheetView::new()
    match attr_value(tag, "defaultGridColor") {
      Some(value) => view.default_grid_color = Some(parse_cell_bool(value))
      None => ()
    }
    match attr_value(tag, "rightToLeft") {
      Some(value) => view.right_to_left = Some(parse_cell_bool(value))
      None => ()
    }
    match attr_value(tag, "showFormulas") {
      Some(value) => view.show_formulas = Some(parse_cell_bool(value))
      None => ()
    }
    match attr_value(tag, "showGridLines") {
      Some(value) => view.show_grid_lines = Some(parse_cell_bool(value))
      None => ()
    }
    match attr_value(tag, "showRowColHeaders") {
      Some(value) => view.show_row_col_headers = Some(parse_cell_bool(value))
      None => ()
    }
    match attr_value(tag, "showRuler") {
      Some(value) => view.show_ruler = Some(parse_cell_bool(value))
      None => ()
    }
    match attr_value(tag, "showZeros") {
      Some(value) => view.show_zeros = Some(parse_cell_bool(value))
      None => ()
    }
    match attr_value(tag, "topLeftCell") {
      Some(value) =>
        view.top_left_cell = Some(
          validate_cell_ref_for_read(
            unescape_xml_text(value),
            "sheetView topLeftCell",
          ),
        )
      None => ()
    }
    match attr_value(tag, "view") {
      Some(value) => view.view = Some(unescape_xml_text(value))
      None => ()
    }
    match attr_value(tag, "zoomScale") {
      Some(value) =>
        view.zoom_scale = Some(
          @string.parse_double(value) catch {
            _ => raise InvalidXml(msg="sheetView zoomScale invalid")
          },
        )
      None => ()
    }
    match attr_value(tag, "tabSelected") {
      Some(value) => view.tab_selected = parse_cell_bool(value)
      None => ()
    }
    match attr_value(tag, "workbookViewId") {
      Some(value) =>
        view.workbook_view_id = @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="sheetView workbookViewId invalid")
        }
      None => ()
    }
    let self_closing = tag.to_owned().trim().has_suffix("/")
    if !self_closing {
      let close = match text.find("") {
        Some(pos) => pos
        None => raise InvalidXml(msg="sheetView close missing")
      }
      let body_start = end + 1
      if close > body_start {
        let inner = text[body_start:close]
        view.pane = parse_sheet_view_pane(inner)
        let selections = parse_sheet_view_selections(inner, budget?)
        view.selection.clear()
        view.selection.append(selections)
      }
    }
    views.push(view)
  }
  views
}

///|
fn parse_sheet_props(xml : StringView) -> SheetPropsOptions? raise XlsxError {
  let mut options : SheetPropsOptions? = None
  match tag_attributes_in(xml, "sheetPr") {
    Some(tag) => {
      let opts = SheetPropsOptions::new()
      match attr_value(tag, "codeName") {
        Some(value) => opts.code_name = Some(unescape_xml_text(value))
        None => ()
      }
      match attr_value(tag, "enableFormatConditionsCalculation") {
        Some(value) =>
          opts.enable_format_conditions_calculation = Some(
            parse_bool_attr(value),
          )
        None => ()
      }
      match attr_value(tag, "published") {
        Some(value) => opts.published = Some(parse_bool_attr(value))
        None => ()
      }
      options = Some(opts)
    }
    None => ()
  }
  match tag_attributes_in(xml, "tabColor") {
    Some(tag) => {
      let opts = match options {
        Some(value) => value
        None => SheetPropsOptions::new()
      }
      match attr_value(tag, "indexed") {
        Some(value) =>
          opts.tab_color_indexed = Some(
            @string.parse_int(value, base=10) catch {
              _ => raise InvalidXml(msg="tabColor indexed invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "rgb") {
        Some(value) => opts.tab_color_rgb = Some(value.to_string())
        None => ()
      }
      match attr_value(tag, "theme") {
        Some(value) =>
          opts.tab_color_theme = Some(
            @string.parse_int(value, base=10) catch {
              _ => raise InvalidXml(msg="tabColor theme invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "tint") {
        Some(value) =>
          opts.tab_color_tint = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="tabColor tint invalid")
            },
          )
        None => ()
      }
      options = Some(opts)
    }
    None => ()
  }
  match tag_attributes_in(xml, "outlinePr") {
    Some(tag) => {
      let opts = match options {
        Some(value) => value
        None => SheetPropsOptions::new()
      }
      match attr_value(tag, "summaryBelow") {
        Some(value) => opts.outline_summary_below = Some(parse_bool_attr(value))
        None => ()
      }
      match attr_value(tag, "summaryRight") {
        Some(value) => opts.outline_summary_right = Some(parse_bool_attr(value))
        None => ()
      }
      options = Some(opts)
    }
    None => ()
  }
  match tag_attributes_in(xml, "pageSetUpPr") {
    Some(tag) => {
      let opts = match options {
        Some(value) => value
        None => SheetPropsOptions::new()
      }
      match attr_value(tag, "autoPageBreaks") {
        Some(value) => opts.auto_page_breaks = Some(parse_bool_attr(value))
        None => ()
      }
      match attr_value(tag, "fitToPage") {
        Some(value) => opts.fit_to_page = Some(parse_bool_attr(value))
        None => ()
      }
      options = Some(opts)
    }
    None => ()
  }
  match tag_attributes_in(xml, "sheetFormatPr") {
    Some(tag) => {
      let opts = match options {
        Some(value) => value
        None => SheetPropsOptions::new()
      }
      match attr_value(tag, "baseColWidth") {
        Some(value) =>
          opts.base_col_width = Some(
            @string.parse_int(value, base=10) catch {
              _ => raise InvalidXml(msg="sheetFormatPr baseColWidth invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "defaultColWidth") {
        Some(value) =>
          opts.default_col_width = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="sheetFormatPr defaultColWidth invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "defaultRowHeight") {
        Some(value) =>
          opts.default_row_height = Some(
            @string.parse_double(value) catch {
              _ =>
                raise InvalidXml(msg="sheetFormatPr defaultRowHeight invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "customHeight") {
        Some(value) => opts.custom_height = Some(parse_bool_attr(value))
        None => ()
      }
      match attr_value(tag, "zeroHeight") {
        Some(value) => opts.zero_height = Some(parse_bool_attr(value))
        None => ()
      }
      match attr_value(tag, "thickTop") {
        Some(value) => opts.thick_top = Some(parse_bool_attr(value))
        None => ()
      }
      match attr_value(tag, "thickBottom") {
        Some(value) => opts.thick_bottom = Some(parse_bool_attr(value))
        None => ()
      }
      options = Some(opts)
    }
    None => ()
  }
  match options {
    Some(value) => if sheet_props_is_empty(value) { None } else { Some(value) }
    None => None
  }
}

///|
fn parse_page_margins(
  xml : StringView,
) -> PageLayoutMarginsOptions? raise XlsxError {
  let mut options : PageLayoutMarginsOptions? = None
  match tag_attributes_in(xml, "pageMargins") {
    Some(tag) => {
      let opts = PageLayoutMarginsOptions::new()
      match attr_value(tag, "left") {
        Some(value) =>
          opts.left = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="pageMargins left invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "right") {
        Some(value) =>
          opts.right = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="pageMargins right invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "top") {
        Some(value) =>
          opts.top = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="pageMargins top invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "bottom") {
        Some(value) =>
          opts.bottom = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="pageMargins bottom invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "header") {
        Some(value) =>
          opts.header = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="pageMargins header invalid")
            },
          )
        None => ()
      }
      match attr_value(tag, "footer") {
        Some(value) =>
          opts.footer = Some(
            @string.parse_double(value) catch {
              _ => raise InvalidXml(msg="pageMargins footer invalid")
            },
          )
        None => ()
      }
      options = Some(opts)
    }
    None => ()
  }
  match tag_attributes_in(xml, "printOptions") {
    Some(tag) => {
      let opts = match options {
        Some(value) => value
        None => PageLayoutMarginsOptions::new()
      }
      match attr_value(tag, "horizontalCentered") {
        Some(value) => opts.horizontally = Some(parse_bool_attr(value))
        None => ()
      }
      match attr_value(tag, "verticalCentered") {
        Some(value) => opts.vertically = Some(parse_bool_attr(value))
        None => ()
      }
      options = Some(opts)
    }
    None => ()
  }
  options
}

///|
fn parse_page_layout(xml : StringView) -> PageLayoutOptions? raise XlsxError {
  let tag = match tag_attributes_in(xml, "pageSetup") {
    Some(value) => value
    None => return None
  }
  let opts = PageLayoutOptions::new()
  match attr_value(tag, "paperSize") {
    Some(value) =>
      opts.size = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="pageSetup paperSize invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "orientation") {
    Some(value) => opts.orientation = Some(value.to_string())
    None => ()
  }
  match attr_value(tag, "firstPageNumber") {
    Some(value) =>
      opts.first_page_number = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="pageSetup firstPageNumber invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "scale") {
    Some(value) =>
      opts.adjust_to = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="pageSetup scale invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "fitToHeight") {
    Some(value) =>
      opts.fit_to_height = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="pageSetup fitToHeight invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "fitToWidth") {
    Some(value) =>
      opts.fit_to_width = Some(
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="pageSetup fitToWidth invalid")
        },
      )
    None => ()
  }
  match attr_value(tag, "blackAndWhite") {
    Some(value) => opts.black_and_white = Some(parse_bool_attr(value))
    None => ()
  }
  match attr_value(tag, "pageOrder") {
    Some(value) => opts.page_order = Some(value.to_string())
    None => ()
  }
  Some(opts)
}

///|
fn parse_header_footer(
  xml : StringView,
) -> HeaderFooterOptions? raise XlsxError {
  let tag = match tag_attributes_in(xml, "headerFooter") {
    Some(value) => value
    None => return None
  }
  let options = HeaderFooterOptions::new()
  match attr_value(tag, "differentOddEven") {
    Some(value) => options.different_odd_even = parse_bool_attr(value)
    None => ()
  }
  match attr_value(tag, "differentFirst") {
    Some(value) => options.different_first = parse_bool_attr(value)
    None => ()
  }
  match attr_value(tag, "scaleWithDoc") {
    Some(value) => options.scale_with_doc = Some(parse_bool_attr(value))
    None => ()
  }
  match attr_value(tag, "alignWithMargins") {
    Some(value) => options.align_with_margins = Some(parse_bool_attr(value))
    None => ()
  }
  let body = match extract_tag_body(xml, "headerFooter") {
    Some(value) => value
    None => ""
  }
  match extract_tag_body_from(body, "oddHeader") {
    Some(value) => options.odd_header = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body_from(body, "oddFooter") {
    Some(value) => options.odd_footer = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body_from(body, "evenHeader") {
    Some(value) => options.even_header = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body_from(body, "evenFooter") {
    Some(value) => options.even_footer = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body_from(body, "firstHeader") {
    Some(value) => options.first_header = unescape_xml_text(value)
    None => ()
  }
  match extract_tag_body_from(body, "firstFooter") {
    Some(value) => options.first_footer = unescape_xml_text(value)
    None => ()
  }
  Some(options)
}

///|
fn parse_sheet_protection(xml : StringView) -> SheetProtection? raise XlsxError {
  let tag = match tag_attributes_in(xml, "sheetProtection") {
    Some(value) => value
    None => return None
  }
  let mut spin_count = 0
  match attr_value(tag, "spinCount") {
    Some(value) =>
      spin_count = @string.parse_int(value, base=10) catch {
        _ => raise InvalidXml(msg="sheetProtection spinCount invalid")
      }
    None => ()
  }
  Some({
    algorithm_name: match attr_value(tag, "algorithmName") {
      Some(value) => value.to_string()
      None => ""
    },
    password: match attr_value(tag, "password") {
      Some(value) => value.to_string()
      None => ""
    },
    hash_value: match attr_value(tag, "hashValue") {
      Some(value) => value.to_string()
      None => ""
    },
    salt_value: match attr_value(tag, "saltValue") {
      Some(value) => value.to_string()
      None => ""
    },
    spin_count,
    sheet: match attr_value(tag, "sheet") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    objects: match attr_value(tag, "objects") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    scenarios: match attr_value(tag, "scenarios") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    format_cells: match attr_value(tag, "formatCells") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    format_columns: match attr_value(tag, "formatColumns") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    format_rows: match attr_value(tag, "formatRows") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    insert_columns: match attr_value(tag, "insertColumns") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    insert_rows: match attr_value(tag, "insertRows") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    insert_hyperlinks: match attr_value(tag, "insertHyperlinks") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    delete_columns: match attr_value(tag, "deleteColumns") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    delete_rows: match attr_value(tag, "deleteRows") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    select_locked_cells: match attr_value(tag, "selectLockedCells") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    sort: match attr_value(tag, "sort") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    auto_filter: match attr_value(tag, "autoFilter") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    pivot_tables: match attr_value(tag, "pivotTables") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
    select_unlocked_cells: match attr_value(tag, "selectUnlockedCells") {
      Some(value) => parse_bool_attr(value)
      None => false
    },
  })
}

///|
fn parse_page_breaks(
  xml : StringView,
  tag_name : StringView,
) -> Array[PageBreak] raise XlsxError {
  let breaks : Array[PageBreak] = []
  let body = match extract_tag_body(xml, tag_name) {
    Some(value) => value
    None => return breaks
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None =>
        match text.find(">") {
          Some(pos) => pos
          None => raise InvalidXml(msg="brk tag not closed")
        }
    }
    let tag = text[:end]
    let id = match attr_value(tag, "id") {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="brk id invalid")
        }
      None => raise InvalidXml(msg="brk id missing")
    }
    let min = match attr_value(tag, "min") {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="brk min invalid")
        }
      None => 0
    }
    let max = match attr_value(tag, "max") {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="brk max invalid")
        }
      None => 0
    }
    let (id_limit, span_limit) = match tag_name {
      "rowBreaks" => (max_page_break_rows, max_page_break_cols)
      "colBreaks" => (max_page_break_cols, max_page_break_rows)
      _ => raise InvalidXml(msg="page break axis invalid")
    }
    if id < 0 ||
      id > id_limit ||
      min < 0 ||
      min > span_limit ||
      max < 0 ||
      max > span_limit ||
      min > max {
      raise InvalidXml(msg="brk coordinate invalid")
    }
    let manual = match attr_value(tag, "man") {
      Some(value) => parse_bool_attr(value)
      None => false
    }
    breaks.push({ id, min, max, manual })
  }
  breaks
}

///|
fn parse_picture_rel_id(xml : StringView) -> String? raise XlsxError {
  let tag = match tag_attributes_in(xml, "picture") {
    Some(value) => value
    None => return None
  }
  match attr_value(tag, "r:id") {
    Some(value) => Some(value.to_string())
    None => raise InvalidXml(msg="picture relationship missing")
  }
}

///|
fn parse_auto_filter_columns(
  body : StringView,
  min_col : Int,
  max_col : Int,
) -> Array[AutoFilterColumn] raise XlsxError {
  let columns : Array[AutoFilterColumn] = []
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => raise InvalidXml(msg="filterColumn tag not closed")
    }
    let tag = text[:end]
    let col_id = match attr_value(tag, "colId") {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="filterColumn colId invalid")
        }
      None => raise InvalidXml(msg="filterColumn colId missing")
    }
    if col_id < 0 || col_id > max_col - min_col {
      raise InvalidXml(msg="filterColumn colId out of range")
    }
    let col = min_col + col_id
    let mut filters : Array[String]? = None
    let mut custom_filters : AutoFilterCustomFilters? = None
    if !tag.has_suffix("/") {
      let rest = text[end + 1:]
      let body_end = match rest.find("") {
        Some(pos) => pos
        None => 0
      }
      let column_body = if body_end > 0 { rest[:body_end] } else { "" }
      match extract_tag_body_from(column_body, "filters") {
        Some(filters_body) => {
          let values : Array[String] = []
          let mut first_filter = true
          for part in filters_body.split("") {
              Some(pos) => pos
              None =>
                match filter_text.find(">") {
                  Some(pos) => pos
                  None => raise InvalidXml(msg="filter tag not closed")
                }
            }
            let filter_tag = filter_text[:filter_end]
            match attr_value(filter_tag, "val") {
              Some(value) => values.push(unescape_xml_text(value))
              None => values.push("")
            }
          }
          filters = Some(values)
        }
        None => ()
      }
      match extract_tag_body_from(column_body, "customFilters") {
        Some(custom_body) => {
          let and_value = match
            tag_attributes_in(column_body, "customFilters") {
            Some(custom_tag) =>
              match attr_value(custom_tag, "and") {
                Some(value) => parse_bool_attr(value)
                None => false
              }
            None => false
          }
          let entries : Array[AutoFilterCustomFilter] = []
          let mut first_custom = true
          for part in custom_body.split("") {
              Some(pos) => pos
              None =>
                match custom_text.find(">") {
                  Some(pos) => pos
                  None => raise InvalidXml(msg="customFilter tag not closed")
                }
            }
            let custom_tag = custom_text[:custom_end]
            let operator = match attr_value(custom_tag, "operator") {
              Some(value) => value
              None => "equal"
            }
            let val = match attr_value(custom_tag, "val") {
              Some(value) => unescape_xml_text(value)
              None => ""
            }
            entries.push({ operator, value: val })
          }
          custom_filters = Some({ and_filter: and_value, filters: entries })
        }
        None => ()
      }
    }
    columns.push({ col, filters, custom_filters })
  }
  columns
}

///|
fn parse_auto_filter(xml : StringView) -> AutoFilter? raise XlsxError {
  let tag = match tag_attributes_in(xml, "autoFilter") {
    Some(value) => value
    None => return None
  }
  let ref_value = match attr_value(tag, "ref") {
    Some(value) => value
    None => raise InvalidXml(msg="autoFilter ref missing")
  }
  let range_ref = normalize_range_ref(ref_value)
  let (_min_row, min_col, _max_row, max_col) = parse_range_ref(range_ref)
  let self_closing = tag.trim().has_suffix("/")
  let columns = if self_closing {
    []
  } else {
    match extract_tag_body(xml, "autoFilter") {
      Some(body) => parse_auto_filter_columns(body, min_col, max_col)
      None => []
    }
  }
  Some({ range_ref, columns })
}

///|
priv struct HyperlinkElement {
  reference : String
  r_id : String?
  location : String?
  display : String?
  tooltip : String?
}

///|
fn parse_hyperlink_elements(
  xml : StringView,
) -> Array[HyperlinkElement] raise XlsxError {
  let links : Array[HyperlinkElement] = []
  let body = match extract_tag_body(xml, "hyperlinks") {
    Some(value) => value
    None => return links
  }
  let mut first = true
  for chunk in body.split(" pos
      None => raise InvalidXml(msg="hyperlink tag not closed")
    }
    let tag = text[:end]
    let raw_ref = match attr_value(tag, "ref") {
      Some(value) => value
      None => raise InvalidXml(msg="hyperlink ref missing")
    }
    let reference = normalize_cell_or_range_ref(unescape_xml_text(raw_ref)) catch {
      _ => raise InvalidXml(msg="hyperlink reference invalid")
    }
    let location = match attr_value(tag, "location") {
      Some(value) => Some(unescape_xml_text(value))
      None => None
    }
    let display = match attr_value(tag, "display") {
      Some(value) => Some(unescape_xml_text(value))
      None => None
    }
    let tooltip = match attr_value(tag, "tooltip") {
      Some(value) => Some(unescape_xml_text(value))
      None => None
    }
    let r_id = match attr_value(tag, "r:id") {
      Some(value) => Some(value.to_string())
      None => None
    }
    links.push({ reference, r_id, location, display, tooltip })
  }
  links
}

///|
fn parse_sheet_drawing_rel_ids(
  xml : StringView,
  root_local_name : StringView,
  budget? : ReadBudget,
  cancelled? : () -> Bool = () => false,
) -> (String?, String?, String?) raise XlsxError {
  match budget {
    Some(value) => {
      value.checkpoint()
      value.charge_work(xml.length())
    }
    None => check_read_cancelled(cancelled)
  }
  let scanner = @ooxml.XmlStartTagScanner::new(xml, cancelled~)
  if !workbook_scanner_next(scanner) ||
    scanner.depth() != 1 ||
    scanner.local_name() != root_local_name ||
    (
      scanner.namespace_uri() != transitional_spreadsheet_namespace &&
      scanner.namespace_uri() != strict_spreadsheet_namespace
    ) {
    raise InvalidXml(
      msg="\{root_local_name.to_owned()} document element is invalid",
    )
  }
  let spreadsheet_namespace = scanner.namespace_uri().to_owned()
  let (relationship_namespace, other_relationship_namespace) = if spreadsheet_namespace ==
    strict_spreadsheet_namespace {
    (
      strict_relationship_attribute_namespace, transitional_relationship_attribute_namespace,
    )
  } else {
    (
      transitional_relationship_attribute_namespace, strict_relationship_attribute_namespace,
    )
  }
  let mut drawing : String? = None
  let mut legacy_drawing : String? = None
  let mut legacy_drawing_hf : String? = None
  while workbook_scanner_next(scanner) {
    let local_name = scanner.local_name()
    let selected = local_name == "drawing" ||
      (
        root_local_name == "worksheet" &&
        (local_name == "legacyDrawing" || local_name == "legacyDrawingHF")
      )
    if !selected ||
      (
        scanner.namespace_uri() != transitional_spreadsheet_namespace &&
        scanner.namespace_uri() != strict_spreadsheet_namespace
      ) {
      continue
    }
    if scanner.namespace_uri() != spreadsheet_namespace ||
      scanner.depth() != 2 ||
      scanner.parent_local_name() != Some(root_local_name) ||
      scanner.parent_namespace_uri() != Some(spreadsheet_namespace) {
      raise InvalidXml(
        msg="\{local_name.to_owned()} relationship element is misplaced",
      )
    }
    if workbook_scanner_attribute(scanner, other_relationship_namespace, "id")
      is Some(_) {
      raise InvalidXml(
        msg="\{local_name.to_owned()} relationship namespace is invalid",
      )
    }
    let id = match
      workbook_scanner_attribute(scanner, relationship_namespace, "id") {
      Some(value) if value != "" => value
      _ =>
        raise InvalidXml(
          msg="\{local_name.to_owned()} relationship id is missing",
        )
    }
    if local_name == "drawing" {
      if drawing is Some(_) {
        raise InvalidXml(msg="drawing relationship element is duplicated")
      }
      drawing = Some(id)
    } else if local_name == "legacyDrawing" {
      if legacy_drawing is Some(_) {
        raise InvalidXml(msg="legacyDrawing relationship element is duplicated")
      }
      legacy_drawing = Some(id)
    } else {
      if legacy_drawing_hf is Some(_) {
        raise InvalidXml(
          msg="legacyDrawingHF relationship element is duplicated",
        )
      }
      legacy_drawing_hf = Some(id)
    }
  }
  (drawing, legacy_drawing, legacy_drawing_hf)
}

///|
test "sheet drawing relationships require direct dialect-matched roots" {
  let transitional =
    #|
  let (drawing, legacy, header_footer) = parse_sheet_drawing_rel_ids(
    transitional, "worksheet",
  )
  assert_true(drawing == Some("drawing"))
  assert_true(legacy == Some("legacy"))
  assert_true(header_footer == Some("header-footer"))
  let strict =
    #|
  let (strict_drawing, strict_legacy, strict_header_footer) = parse_sheet_drawing_rel_ids(
    strict, "chartsheet",
  )
  assert_true(strict_drawing == Some("strict-drawing"))
  assert_true(strict_legacy is None)
  assert_true(strict_header_footer is None)

  let invalid : Array[(String, String, String)] = [
    (
      (
        #|
      ),
      "worksheet",
      "drawing relationship element is misplaced",
    ),
    (
      (
        #|
      ),
      "worksheet",
      "drawing relationship element is misplaced",
    ),
    (
      (
        #|
      ),
      "chartsheet",
      "drawing relationship namespace is invalid",
    ),
    (
      (
        #|
      ),
      "chartsheet",
      "drawing relationship element is duplicated",
    ),
  ]
  for entry in invalid {
    let (xml, root, expected) = entry
    try parse_sheet_drawing_rel_ids(xml, root) catch {
      InvalidXml(msg~) => assert_eq(msg, expected)
      _ => fail("unexpected sheet drawing relationship error")
    } noraise {
      _ => fail("expected invalid sheet drawing relationship rejection")
    }
  }
}

///|
fn parse_bool_attr(value : StringView) -> Bool {
  match value {
    "1" | "true" | "TRUE" => true
    _ => false
  }
}

///|
fn parse_inline_string(rest : StringView) -> InlineStringEntry raise XlsxError {
  let text = rest.to_owned()
  let start = match text.find(" pos
    None => return { text: "", runs: None }
  }
  let after_start = text[start:]
  let open_end = match after_start.find(">") {
    Some(pos) => pos
    None => raise InvalidXml(msg="inline string not closed")
  }
  let body_start = start + open_end + 1
  let end = match find_xml_close_tag_from(text, "is", body_start) {
    Some((pos, _)) => pos
    None => raise InvalidXml(msg="inline string end missing")
  }
  let body = text[body_start:end]
  let runs = if body.contains(" [] }
    if parsed.length() > 0 {
      Some(parsed)
    } else {
      None
    }
  } else {
    None
  }
  { text: parse_text_nodes(body), runs }
}

///|
fn parse_row_dimensions(
  xml : StringView,
  style_count : Int,
  materialized_dimensions : Ref[Int],
  max_materialized_dimensions : Int,
  dimension_work : Ref[Int],
  max_dimension_work : Int,
) -> Map[Int, RowDimension] raise XlsxError {
  let dims : Map[Int, RowDimension] = Map([])
  let mut first = true
  for chunk in xml.split("") {
      Some(pos) => pos
      None => raise InvalidXml(msg="row tag not closed")
    }
    let tag = text[:end]
    let row_text = match attr_value(tag, "r") {
      Some(value) => value
      None => raise InvalidXml(msg="row index missing")
    }
    let row = @string.parse_int(row_text, base=10) catch {
      _ => raise InvalidXml(msg="row index invalid")
    }
    if row <= 0 || row > cell_ref_max_rows {
      raise InvalidXml(msg="row index invalid")
    }
    let height = match attr_value(tag, "ht") {
      Some(value) =>
        Some(
          @string.parse_double(value) catch {
            _ => raise InvalidXml(msg="row height invalid")
          },
        )
      None => None
    }
    let hidden = match attr_value(tag, "hidden") {
      Some(value) => parse_bool_attr(value)
      None => false
    }
    let outline_level = match attr_value(tag, "outlineLevel") {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="row outline level invalid")
        }
      None => 0
    }
    let style_attribute = attr_value(tag, "s")
    let style_id = match style_attribute {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="row style id invalid")
        }
      None => 0
    }
    if style_id < 0 || style_id >= style_count {
      raise InvalidStyleId(index=style_id)
    }
    // `s` is only an active row format when `customFormat` is true. Keep
    // `Some(0)` for that case: an explicit default row format is a real
    // inheritance boundary and must block a nonzero column style.
    let custom_format = match attr_value(tag, "customFormat") {
      Some(value) => parse_bool_attr(value)
      None => false
    }
    let style = if custom_format { Some(style_id) } else { None }
    if height is None && !hidden && outline_level == 0 && style is None {
      continue
    }
    if dimension_work.val >= max_dimension_work {
      raise ResourceLimitExceeded(
        kind="row_column_dimension_work",
        limit=max_dimension_work,
        actual=bounded_actual_above_limit(max_dimension_work),
      )
    }
    dimension_work.val = dimension_work.val + 1
    if !dims.contains(row) {
      if materialized_dimensions.val >= max_materialized_dimensions {
        raise ResourceLimitExceeded(
          kind="materialized_row_column_dimensions",
          limit=max_materialized_dimensions,
          actual=bounded_actual_above_limit(max_materialized_dimensions),
        )
      }
      let next = materialized_dimensions.val + 1
      materialized_dimensions.val = next
    }
    dims[row] = { height, hidden, outline_level, style_id: style }
  }
  dims
}

///|
fn parse_col_dimensions(
  xml : StringView,
  style_count : Int,
  materialized_dimensions : Ref[Int],
  max_materialized_dimensions : Int,
  dimension_work : Ref[Int],
  max_dimension_work : Int,
) -> Map[Int, ColDimension] raise XlsxError {
  let dims : Map[Int, ColDimension] = Map([])
  let body = match extract_tag_body(xml, "cols") {
    Some(value) => value
    None => return dims
  }
  let mut first = true
  for chunk in body.split("") {
      Some(pos) => pos
      None => raise InvalidXml(msg="col tag not closed")
    }
    let tag = text[:end]
    let min_text = match attr_value(tag, "min") {
      Some(value) => value
      None => raise InvalidXml(msg="col min missing")
    }
    let max_text = match attr_value(tag, "max") {
      Some(value) => value
      None => raise InvalidXml(msg="col max missing")
    }
    let min_col = @string.parse_int(min_text, base=10) catch {
      _ => raise InvalidXml(msg="col min invalid")
    }
    let max_col = @string.parse_int(max_text, base=10) catch {
      _ => raise InvalidXml(msg="col max invalid")
    }
    if min_col <= 0 ||
      max_col <= 0 ||
      max_col < min_col ||
      min_col > cell_ref_max_cols ||
      max_col > cell_ref_max_cols {
      raise InvalidXml(msg="col range invalid")
    }
    let width = match attr_value(tag, "width") {
      Some(value) =>
        Some(
          @string.parse_double(value) catch {
            _ => raise InvalidXml(msg="col width invalid")
          },
        )
      None => None
    }
    let hidden = match attr_value(tag, "hidden") {
      Some(value) => parse_bool_attr(value)
      None => false
    }
    let outline_level = match attr_value(tag, "outlineLevel") {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="col outline level invalid")
        }
      None => 0
    }
    let style_attribute = attr_value(tag, "style")
    let style_id = match style_attribute {
      Some(value) =>
        @string.parse_int(value, base=10) catch {
          _ => raise InvalidXml(msg="col style id invalid")
        }
      None => 0
    }
    if style_id < 0 || style_id >= style_count {
      raise InvalidStyleId(index=style_id)
    }
    // Unlike rows, columns have no `customFormat` gate. Attribute presence is
    // therefore the semantic distinction, including an explicit style 0.
    let style = match style_attribute {
      Some(_) => Some(style_id)
      None => None
    }
    if width is None && !hidden && outline_level == 0 && style is None {
      continue
    }
    let span = max_col - min_col + 1
    if span > max_dimension_work - dimension_work.val {
      raise ResourceLimitExceeded(
        kind="row_column_dimension_work",
        limit=max_dimension_work,
        actual=bounded_actual_above_limit(max_dimension_work),
      )
    }
    dimension_work.val = dimension_work.val + span
    let dim = { width, hidden, outline_level, style_id: style }
    let mut col = min_col
    while col <= max_col {
      if !dims.contains(col) {
        if materialized_dimensions.val >= max_materialized_dimensions {
          raise ResourceLimitExceeded(
            kind="materialized_row_column_dimensions",
            limit=max_materialized_dimensions,
            actual=bounded_actual_above_limit(max_materialized_dimensions),
          )
        }
        let next = materialized_dimensions.val + 1
        materialized_dimensions.val = next
      }
      dims[col] = dim
      col = col + 1
    }
  }
  dims
}

///|
fn is_xml_package_part(name : StringView) -> Bool {
  let lower = name.to_owned().to_lower()
  lower.has_suffix(".xml") ||
  lower.has_suffix(".rels") ||
  lower.has_suffix(".vml")
}

///|
fn enforce_archive_limits(
  archive : @zip.Archive,
  limits : ReadLimits,
  cancelled? : () -> Bool = () => false,
) -> Unit raise XlsxError {
  check_read_cancelled(cancelled)
  let mut total_size = 0
  for entry in archive.entries() {
    check_read_cancelled(cancelled)
    let size = entry.data().length()
    if size > limits.max_entry_uncompressed_bytes {
      raise ResourceLimitExceeded(
        kind="entry_uncompressed_bytes",
        limit=limits.max_entry_uncompressed_bytes,
        actual=size,
      )
    }
    total_size = total_size + size
    if total_size > limits.max_total_uncompressed_bytes {
      raise ResourceLimitExceeded(
        kind="total_uncompressed_bytes",
        limit=limits.max_total_uncompressed_bytes,
        actual=total_size,
      )
    }
    if is_xml_package_part(entry.name()) && size > limits.max_xml_part_bytes {
      raise ResourceLimitExceeded(
        kind="xml_part_bytes",
        limit=limits.max_xml_part_bytes,
        actual=size,
      )
    }
    let actual_crc = @zip.crc32_cancellable(entry.data(), cancelled~) catch {
      ReadCancelled => raise ReadCancelled
      _ => raise InvalidPackage(msg="ZIP entry CRC verification failed")
    }
    if actual_crc != entry.crc32() {
      raise InvalidPackage(msg="ZIP entry CRC mismatch")
    }
    check_read_cancelled(cancelled)
  }
}

///|
fn require_bounded_archive(
  archive : @zip.Archive,
  limits : ReadLimits,
  cancelled? : () -> Bool = () => false,
) -> Unit raise XlsxError {
  check_read_cancelled(cancelled)
  match
    archive.bounded_source_package_size(
      max_entries=limits.max_archive_entries,
      max_entry_uncompressed_bytes=limits.max_entry_uncompressed_bytes,
      max_total_uncompressed_bytes=limits.max_total_uncompressed_bytes,
      max_total_preserved_source_bytes=limits.max_total_preserved_source_bytes,
    ) {
    Some(package_bytes) =>
      if package_bytes > limits.max_package_bytes {
        raise ResourceLimitExceeded(
          kind="package_bytes",
          limit=limits.max_package_bytes,
          actual=package_bytes,
        )
      }
    None => raise UnboundedArchive
  }
  if archive.entries().length() > limits.max_archive_entries {
    raise ResourceLimitExceeded(
      kind="entry_count",
      limit=limits.max_archive_entries,
      actual=archive.entries().length(),
    )
  }
  enforce_archive_limits(archive, limits, cancelled~)
}

///|
fn read_limited_archive(
  bytes : BytesView,
  limits : ReadLimits,
  cancelled? : () -> Bool = () => false,
) -> @zip.Archive raise XlsxError {
  check_read_cancelled(cancelled)
  @zip.read_limited(
    bytes,
    max_package_bytes=limits.max_package_bytes,
    max_entries=limits.max_archive_entries,
    max_entry_uncompressed_bytes=limits.max_entry_uncompressed_bytes,
    max_total_uncompressed_bytes=limits.max_total_uncompressed_bytes,
    // The ZIP package preserves exact source records for lossless rewrites.
    // Bound that second compressed representation independently from inflation.
    max_total_preserved_source_bytes=limits.max_total_preserved_source_bytes,
    cancelled~,
  ) catch {
    ResourceLimitExceeded(kind~, limit~, actual~) =>
      raise ResourceLimitExceeded(kind~, limit~, actual~)
    ReadCancelled => raise ReadCancelled
    _ => raise InvalidPackage(msg="unreadable ZIP package")
  }
}

///|
fn check_source_package_size(
  bytes : BytesView,
  limits : ReadLimits,
) -> Unit raise XlsxError {
  if bytes.length() > limits.max_package_bytes {
    raise ResourceLimitExceeded(
      kind="package_bytes",
      limit=limits.max_package_bytes,
      actual=bytes.length(),
    )
  }
}

///|
fn first_existing_rel_target_path(
  targets : Map[String, String],
  source_part : StringView,
  part_names : Map[String, String],
  cancelled? : () -> Bool = () => false,
) -> String? raise XlsxError {
  for _, target in targets {
    let path = actual_relationship_target_path(
      source_part,
      target,
      part_names,
      cancelled~,
    )
    // `path` is the physical ZIP spelling when the target exists. The archive
    // index is keyed in logical OPC PartName space, so restore percent-encoded
    // Unicode before testing identity.
    let logical_path = logical_archive_part_path(path)
    if part_names.contains(@ooxml.package_part_name_key(logical_path)) {
      return Some(path)
    }
  }
  None
}

///|
fn first_existing_workbook_rel_target_path(
  targets : Map[String, String],
  workbook_part : StringView,
  part_names : Map[String, String],
  cancelled? : () -> Bool = () => false,
) -> String? raise XlsxError {
  for _, target in targets {
    let path = resolve_part_rel_target(
      logical_archive_part_path(workbook_part),
      target,
      cancelled~,
    )
    match part_names.get(@ooxml.package_part_name_key(path)) {
      Some(actual) => return Some(actual)
      None => ()
    }
  }
  None
}

///|
fn budgeted_package_part_name_key(
  name : StringView,
  budget : ReadBudget,
) -> String raise XlsxError {
  budget.checkpoint()
  budget.charge_work(name.length())
  @ooxml.package_part_name_key_cancellable(name, cancelled=budget.cancelled) catch {
    InvalidXml(msg~) => raise InvalidPackage(msg~)
    ReadCancelled => raise ReadCancelled
  }
}

///|
fn budgeted_logical_opc_part_name(
  name : StringView,
  budget : ReadBudget,
) -> String? raise XlsxError {
  budget.checkpoint()
  budget.charge_work(name.length())
  @ooxml.logical_opc_part_name_from_zip_item_name_cancellable(
    name,
    cancelled=budget.cancelled,
  ) catch {
    InvalidXml(msg~) => raise InvalidPackage(msg~)
    ReadCancelled => raise ReadCancelled
  }
}

///|
fn budgeted_register_opc_part_name(
  registry : @ooxml.OpcPartNameRegistry,
  name : StringView,
  display : String,
  budget : ReadBudget,
) -> @ooxml.OpcPartNameConflict? raise XlsxError {
  budget.checkpoint()
  // Count bounded identity nodes without allocating them, then reserve their
  // cumulative parser capacity before registration can materialize either its
  // token array or a detached trie suffix.
  budget.charge_work(name.length())
  let identity_nodes = @ooxml.opc_part_name_identity_node_count_cancellable(
    name,
    cancelled=budget.cancelled,
  ) catch {
    InvalidXml(msg~) => raise InvalidPackage(msg~)
    ReadCancelled => raise ReadCancelled
  }
  budget.charge_items(identity_nodes)
  // Registration performs a tokenization pass followed by a mutation-free
  // lookup/allocation pass. Charge both before either can run.
  budget.charge_work(name.length())
  budget.charge_work(name.length())
  registry.register_cancellable(name, display, cancelled=budget.cancelled) catch {
    InvalidXml(msg~) => raise InvalidPackage(msg~)
    ReadCancelled => raise ReadCancelled
  }
}

///|
fn archive_part_name_index(
  archive : @zip.Archive,
  budget? : ReadBudget,
) -> Map[String, String] raise XlsxError {
  let budget = budget.unwrap_or(ReadBudget::new(ReadLimits::new()))
  let names : Map[String, String] = Map([])
  let registry = @ooxml.OpcPartNameRegistry::new(
    maximum_identity_nodes=budget.limits.max_parser_items,
  )
  let content_types_key = @ooxml.package_part_name_key(content_types_part_path)
  for entry in archive.entries() {
    budget.checkpoint()
    let physical_name = entry.name()
    if physical_name.has_suffix("/") {
      continue
    }
    budget.charge_work(physical_name.length())
    let logical_name = if physical_name == content_types_part_path {
      physical_name
    } else if budgeted_package_part_name_key(physical_name, budget) ==
      content_types_key {
      raise InvalidPackage(
        msg="reserved content-types part name has invalid spelling",
      )
    } else {
      match budgeted_logical_opc_part_name(physical_name, budget) {
        Some(value) => value
        None => raise InvalidPackage(msg="invalid OPC ZIP item name")
      }
    }
    let key = budgeted_package_part_name_key(logical_name, budget)
    if logical_name != content_types_part_path {
      match
        budgeted_register_opc_part_name(
          registry, logical_name, physical_name, budget,
        ) {
        Some(Equivalent(_)) =>
          raise InvalidPackage(msg="duplicate or equivalent OPC part name")
        Some(Derivable(existing)) =>
          raise InvalidPackage(
            msg="OPC part names are derivable: \{existing} and \{physical_name}",
          )
        None => ()
      }
    }
    budget.charge_work(key.length())
    match names.get(key) {
      Some(_) =>
        raise InvalidPackage(msg="duplicate or equivalent OPC part name")
      None => ()
    }
    budget.charge_work(key.length())
    names[key] = physical_name
  }
  names
}

///|
test "read archive index maps physical Unicode OPC names to logical identity" {
  let archive = @zip.Archive::new()
  archive.add(content_types_part_path, b"manifest")
  archive.add("custom/%E6%96%87.xml", b"workbook")
  let names = archive_part_name_index(archive)
  debug_inspect(
    names.get(@ooxml.package_part_name_key("custom/文.xml")),
    content="Some(\"custom/%E6%96%87.xml\")",
  )
}

///|
test "relationship selection recognizes physical Unicode chart parts" {
  let archive = @zip.Archive::new()
  archive.add(content_types_part_path, b"manifest")
  archive.add("xl/charts/%E6%96%87.xml", b"unicode chart")
  archive.add("xl/charts/chart2.xml", b"other chart")
  let part_names = archive_part_name_index(archive)
  let targets : Map[String, String] = Map([
    ("unicode", "../charts/文.xml"),
    ("other", "../charts/chart2.xml"),
  ])
  debug_inspect(
    first_existing_rel_target_path(
      targets, "xl/drawings/drawing1.xml", part_names,
    ),
    content="Some(\"xl/charts/%E6%96%87.xml\")",
  )
}

///|
test "read archive index rejects ambiguous and misspelled reserved entries" {
  let duplicate = @zip.Archive::new()
  duplicate.add(content_types_part_path, b"first")
  duplicate.add(content_types_part_path, b"second")
  try archive_part_name_index(duplicate) catch {
    InvalidPackage(msg~) =>
      inspect(msg, content="duplicate or equivalent OPC part name")
    _ => fail("unexpected duplicate OPC part error")
  } noraise {
    _ => fail("expected duplicate OPC part rejection")
  }

  let misspelled = @zip.Archive::new()
  misspelled.add("[content_types].xml", b"manifest")
  try archive_part_name_index(misspelled) catch {
    InvalidPackage(msg~) =>
      inspect(
        msg,
        content="reserved content-types part name has invalid spelling",
      )
    _ => fail("unexpected reserved OPC part error")
  } noraise {
    _ => fail("expected reserved OPC part spelling rejection")
  }
}

///|
test "read archive index rejects OPC-derivable parts in either order" {
  for
    entries in [
      ["custom/a.xml", "custom/a.xml/child.xml"],
      ["custom/a.xml/child.xml", "custom/a.xml"],
    ] {
    let archive = @zip.Archive::new()
    archive.add(content_types_part_path, b"manifest")
    for name in entries {
      archive.add(name, b"part")
    }
    try archive_part_name_index(archive) catch {
      InvalidPackage(msg~) =>
        assert_true(msg.contains("OPC part names are derivable"))
      _ => fail("unexpected derivable OPC part error")
    } noraise {
      _ => fail("expected derivable OPC part rejection")
    }
  }
}

///|
test "read archive index charges physical OPC name work before decoding" {
  let archive = @zip.Archive::new()
  archive.add("custom/" + "a".repeat(64) + ".xml", b"part")
  let budget = ReadBudget::new(
    ReadLimits::with_values(max_parser_work_units=32),
  )
  try archive_part_name_index(archive, budget~) catch {
    ResourceLimitExceeded(kind~, limit~, actual~) => {
      inspect(kind, content="parser_work_units")
      assert_eq(limit, 32)
      assert_true(actual > limit)
    }
    _ => fail("unexpected archive name budget error")
  } noraise {
    _ => fail("expected archive name work limit")
  }
}

///|
test "read archive index charges OPC identity nodes as parser items" {
  let archive = @zip.Archive::new()
  archive.add("a/b/c.xml", b"part")
  let budget = ReadBudget::new(ReadLimits::with_values(max_parser_items=2))
  try archive_part_name_index(archive, budget~) catch {
    ResourceLimitExceeded(kind~, limit~, actual~) => {
      inspect(kind, content="parser_items")
      assert_eq(limit, 2)
      assert_true(actual > limit)
    }
    _ => fail("unexpected archive identity-node budget error")
  } noraise {
    _ => fail("expected archive identity-node parser limit")
  }
  assert_eq(budget.parser_items, 0)
}

///|
test "read archive index polls cancellation inside long OPC names" {
  let archive = @zip.Archive::new()
  archive.add("custom/" + "a".repeat(9000) + ".xml", b"part")
  let checks = [0]
  let budget = ReadBudget::new(ReadLimits::new(), cancelled=() => {
    checks[0] = checks[0] + 1
    checks[0] >= 4
  })
  try archive_part_name_index(archive, budget~) catch {
    ReadCancelled => assert_true(checks[0] >= 4)
    _ => fail("unexpected archive name cancellation error")
  } noraise {
    _ => fail("expected archive name cancellation")
  }
}

///|
fn actual_archive_part_path(
  part_names : Map[String, String],
  requested : String,
) -> String {
  part_names.get(@ooxml.package_part_name_key(requested)).unwrap_or(requested)
}

///|
/// Returns the logical OPC name for either a logical PartName or its physical
/// ASCII ZIP projection. Relationship resolution must never use the physical
/// `%HH` spelling because relationships and content types live in the logical
/// PartName space.
fn logical_archive_part_path(path : StringView) -> String {
  if path == content_types_part_path {
    content_types_part_path
  } else {
    @ooxml.logical_opc_part_name_from_zip_item_name(path).unwrap_or(
      path.to_owned(),
    )
  }
}

///|
fn actual_relationship_part_path(
  source_part : StringView,
  part_names : Map[String, String],
) -> String raise XlsxError {
  actual_archive_part_path(
    part_names,
    rels_path_for_part(logical_archive_part_path(source_part)),
  )
}

///|
fn actual_relationship_target_path(
  source_part : StringView,
  target : StringView,
  part_names : Map[String, String],
  cancelled? : () -> Bool = () => false,
) -> String raise XlsxError {
  let source_relative = resolve_part_rel_target(
    logical_archive_part_path(source_part),
    target,
    cancelled~,
  )
  part_names
  .get(@ooxml.package_part_name_key(source_relative))
  .unwrap_or(source_relative)
}

///|
test "relationship targets never fall back from source-relative to package root" {
  let root_media = "xl/media/image1.png"
  let part_names : Map[String, String] = Map([
    (@ooxml.package_part_name_key(root_media), root_media),
  ])
  inspect(
    actual_relationship_target_path(
      "xl/drawings/drawing1.xml", root_media, part_names,
    ),
    content="xl/drawings/xl/media/image1.png",
  )
  inspect(
    actual_relationship_target_path(
      "xl/drawings/drawing1.xml", "../media/image1.png", part_names,
    ),
    content="xl/media/image1.png",
  )
}

///|
fn workbook_related_part_path(
  workbook_rels : StringView,
  relationship_type : StringView,
  workbook_part : StringView,
  fallback : StringView,
  part_names : Map[String, String],
  budget? : ReadBudget,
  cancelled? : () -> Bool = () => false,
) -> String? raise XlsxError {
  let targets = parse_internal_relationship_targets(
    workbook_rels,
    relationship_type,
    budget?,
    cancelled~,
  )
  match
    first_existing_workbook_rel_target_path(
      targets,
      workbook_part,
      part_names,
      cancelled~,
    ) {
    Some(path) => Some(path)
    None =>
      match first_relationship_target(targets) {
        Some(target) =>
          Some(
            actual_archive_part_path(
              part_names,
              resolve_part_rel_target(
                logical_archive_part_path(workbook_part),
                target,
                cancelled~,
              ),
            ),
          )
        None => part_names.get(@ooxml.package_part_name_key(fallback))
      }
  }
}

///|
fn read_zip_archive_core_unchecked(
  archive : @zip.Archive,
  options : Options,
  limits : ReadLimits,
  cancelled : () -> Bool,
  transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
  let read_budget = ReadBudget::new(limits, cancelled~)
  let decoded_xml_bytes = Ref(0)
  let xml_markup_tokens = Ref(0)
  let decode_source = (value : BytesView) => {
    if value.length() > limits.max_xml_part_bytes {
      raise ResourceLimitExceeded(
        kind="xml_part_bytes",
        limit=limits.max_xml_part_bytes,
        actual=value.length(),
      )
    }
    if value.length() > limits.max_total_xml_bytes - decoded_xml_bytes.val {
      raise ResourceLimitExceeded(
        kind="total_xml_bytes",
        limit=limits.max_total_xml_bytes,
        actual=bounded_actual_above_limit(limits.max_total_xml_bytes),
      )
    }
    let next_xml_bytes = decoded_xml_bytes.val + value.length()
    decoded_xml_bytes.val = next_xml_bytes
    for byte in value {
      if byte == b'<' {
        if xml_markup_tokens.val >= limits.max_xml_markup_tokens {
          raise ResourceLimitExceeded(
            kind="xml_markup_tokens",
            limit=limits.max_xml_markup_tokens,
            actual=bounded_actual_above_limit(limits.max_xml_markup_tokens),
          )
        }
        let next_token_count = xml_markup_tokens.val + 1
        xml_markup_tokens.val = next_token_count
      }
    }
    let decoded = decode_utf8(value, transcoder)
    read_budget.scan_xml(decoded)
    decoded
  }
  preflight_archive_relationship_limits(
    archive,
    limits,
    decode=decode_source,
    budget=read_budget,
    cancelled~,
  )
  let part_names = archive_part_name_index(archive, budget=read_budget)
  let materialized_cells = Ref(0)
  let materialized_dimensions = Ref(0)
  let dimension_work = Ref(0)
  let drawing_media_cache : Map[String, Bytes] = Map([])
  let decode = (value : BytesView) => {
    let decoded = decode_source(value)
    read_budget.charge_work(decoded.length())
    let canonical = canonicalize_xlsx_xml(
      decoded,
      max_output_chars=limits.max_xml_part_bytes,
      cancelled~,
    )
    read_budget.checkpoint()
    read_budget.charge_work(canonical.length())
    canonical
  }
  let (workbook_logical_part_path, content_types) = resolve_workbook_xml_part_path(
    archive,
    part_names,
    decode,
    budget=read_budget,
    cancelled~,
  )
  let workbook_xml_part_path = actual_archive_part_path(
    part_names, workbook_logical_part_path,
  )
  let workbook_bytes = match archive.get(workbook_xml_part_path) {
    Some(value) => value
    None => raise MissingPart(path=workbook_xml_part_path)
  }
  let workbook_raw_xml = decode_source(workbook_bytes)
  read_budget.charge_work(workbook_raw_xml.length())
  let workbook_source_xml = project_xlsx_markup_compatibility(
    workbook_raw_xml,
    limits.max_xml_part_bytes,
    cancelled~,
  )
  if workbook_source_xml != workbook_raw_xml {
    read_budget.charge_work(workbook_source_xml.length())
  }
  let workbook_xml = canonicalize_xlsx_xml(
    workbook_source_xml,
    max_output_chars=limits.max_xml_part_bytes,
    cancelled~,
  )
  read_budget.checkpoint()
  read_budget.charge_work(workbook_xml.length())
  let workbook_core_xml = match
    xlsx_core_source_without_foreign(
      workbook_source_xml,
      "workbook",
      cancelled~,
    ) {
    Some(filtered) => {
      read_budget.charge_work(filtered.length())
      let canonical = canonicalize_xlsx_xml(
        filtered,
        max_output_chars=limits.max_xml_part_bytes,
        cancelled~,
      )
      read_budget.checkpoint()
      read_budget.charge_work(canonical.length())
      canonical
    }
    None => workbook_xml
  }
  let workbook_rels_path = actual_relationship_part_path(
    workbook_xml_part_path, part_names,
  )
  let workbook_rels = decode(
    load_relationship_part(
      archive,
      content_types,
      workbook_rels_path,
      "workbook relationships",
      cancelled~,
    ),
  )
  let sheets = parse_workbook_sheets(
    workbook_source_xml,
    limits.max_workbook_sheets,
    cancelled~,
  )
  let sheet_names : Array[String] = []
  for entry in sheets {
    sheet_names.push(entry.name)
  }
  let defined_names = parse_defined_names(workbook_core_xml, sheet_names)
  let active_index = match parse_active_sheet_index(workbook_core_xml) {
    Some(value) => value
    None => 0
  }
  let safe_active_index = if active_index < 0 || active_index >= sheets.length() {
    0
  } else {
    active_index
  }
  let shared_strings_path = workbook_related_part_path(
    workbook_rels,
    rel_shared_strings,
    workbook_xml_part_path,
    shared_strings_part_path,
    part_names,
    budget=read_budget,
    cancelled~,
  )
  let shared_strings = match shared_strings_path {
    Some(path) => {
      let value = load_typed_part(
        archive,
        content_types,
        path,
        [ct_shared_strings],
        "shared strings",
        cancelled~,
      )
      let raw_source = decode_source(value)
      read_budget.charge_work(raw_source.length())
      let source = project_xlsx_markup_compatibility(
        raw_source,
        limits.max_xml_part_bytes,
        cancelled~,
      )
      if source != raw_source {
        read_budget.charge_work(source.length())
      }
      let core_source = match
        xlsx_core_source_without_foreign(source, "sst", cancelled~) {
        Some(filtered) => filtered
        None => source
      }
      if core_source != source {
        read_budget.charge_work(core_source.length())
      }
      let canonical = canonicalize_xlsx_xml(
        core_source,
        max_output_chars=limits.max_xml_part_bytes,
        cancelled~,
      )
      read_budget.checkpoint()
      read_budget.charge_work(canonical.length())
      parse_shared_strings(canonical, budget=read_budget)
    }
    None => []
  }
  let styles_path = workbook_related_part_path(
    workbook_rels,
    rel_styles,
    workbook_xml_part_path,
    styles_part_path,
    part_names,
    budget=read_budget,
    cancelled~,
  )
  let (
    styles,
    conditional_styles,
    default_font,
    default_table_style,
    default_pivot_style,
    indexed_colors,
    mru_colors_xml,
    styles_ext_lst_xml,
  ) = match styles_path {
    Some(path) => {
      let value = load_typed_part(
        archive,
        content_types,
        path,
        [ct_styles],
        "styles",
        cancelled~,
      )
      let styles_raw_xml = decode_source(value)
      read_budget.charge_work(styles_raw_xml.length())
      let styles_source_xml = project_xlsx_markup_compatibility(
        styles_raw_xml,
        limits.max_xml_part_bytes,
        cancelled~,
      )
      if styles_source_xml != styles_raw_xml {
        read_budget.charge_work(styles_source_xml.length())
      }
      let styles_xml = canonicalize_xlsx_xml(
        styles_source_xml,
        max_output_chars=limits.max_xml_part_bytes,
        cancelled~,
      )
      read_budget.checkpoint()
      read_budget.charge_work(styles_xml.length())
      let styles_core_xml = match
        xlsx_core_source_without_foreign(
          styles_source_xml,
          "styleSheet",
          cancelled~,
        ) {
        Some(filtered) => {
          read_budget.charge_work(filtered.length())
          let canonical = canonicalize_xlsx_xml(
            filtered,
            max_output_chars=limits.max_xml_part_bytes,
            cancelled~,
          )
          read_budget.checkpoint()
          read_budget.charge_work(canonical.length())
          canonical
        }
        None => styles_xml
      }
      let (styles, conditional_styles) = parse_styles(styles_core_xml)
      let (default_table_style, default_pivot_style) = parse_table_styles_defaults(
        styles_core_xml,
      )
      let indexed_colors = parse_indexed_colors(styles_core_xml)
      let mru_colors_xml = parse_mru_colors_xml(styles_core_xml)
      let styles_ext_lst_xml = parse_styles_ext_lst_xml(styles_xml)
      (
        styles,
        conditional_styles,
        parse_default_font(styles_core_xml),
        default_table_style,
        default_pivot_style,
        indexed_colors,
        mru_colors_xml,
        styles_ext_lst_xml,
      )
    }
    None =>
      (
        [Style::new()],
        [],
        "Calibri",
        "TableStyleMedium9",
        "PivotStyleLight16",
        None,
        None,
        None,
      )
  }
  let theme_path = workbook_related_part_path(
    workbook_rels,
    rel_theme,
    workbook_xml_part_path,
    theme_part_path,
    part_names,
    budget=read_budget,
    cancelled~,
  )
  let theme_xml = match theme_path {
    Some(path) => {
      let value = load_typed_part(
        archive,
        content_types,
        path,
        [ct_theme],
        "theme",
        cancelled~,
      )
      Some(decode(value))
    }
    None => None
  }
  let theme_colors = match theme_xml {
    Some(xml) => parse_theme_colors(xml)
    None => None
  }
  let core_properties = match archive.get("docProps/core.xml") {
    Some(value) => parse_core_properties(decode(value))
    None => CoreProperties::new()
  }
  let app_properties = match archive.get("docProps/app.xml") {
    Some(value) => parse_app_properties(decode(value))
    None => AppProperties::new()
  }
  let custom_properties = match archive.get("docProps/custom.xml") {
    Some(value) => parse_custom_properties(decode(value))
    None => []
  }
  let workbook_props = parse_workbook_props(workbook_core_xml)
  let calc_props = parse_calc_props(workbook_core_xml)
  let workbook_protection = parse_workbook_protection(workbook_core_xml)
  let style_count = styles.length()
  let cell_images_part = actual_archive_part_path(
    part_names, "xl/cellimages.xml",
  )
  let cell_images = match archive.get(cell_images_part) {
    Some(value) => {
      let cell_images_rels_path = actual_relationship_part_path(
        cell_images_part, part_names,
      )
      let rels_xml = match
        load_optional_relationship_part(
          archive,
          content_types,
          cell_images_rels_path,
          "cell image relationships",
          cancelled~,
        ) {
        Some(rels) => decode(rels)
        None => ""
      }
      parse_cell_images(
        decode(value),
        rels_xml,
        cell_images_part,
        part_names,
        content_types,
        archive,
        budget=read_budget,
        cancelled~,
      )
    }
    None => []
  }
  let rich_value_images = parse_rich_value_images(
    archive,
    part_names,
    content_types,
    decode,
    budget=read_budget,
    cancelled~,
  )
  let rich_value_media : Map[String, Bytes] = Map([])
  match rich_value_images {
    Some(data) => {
      for _, target in data.rel_targets {
        match archive.get(target) {
          Some(_) => {
            let (bytes, identity) = load_image_part(
              archive,
              content_types,
              target,
              "rich value image",
              cancelled~,
            )
            rich_value_media[target] = bytes.to_owned()
            data.media_identities[target] = identity
          }
          None => ()
        }
      }
      for _, target in data.web_rel_targets {
        match archive.get(target) {
          Some(_) => {
            let (bytes, identity) = load_image_part(
              archive,
              content_types,
              target,
              "rich value web image",
              cancelled~,
            )
            rich_value_media[target] = bytes.to_owned()
            data.media_identities[target] = identity
          }
          None => ()
        }
      }
    }
    None => ()
  }
  let workbook : Workbook = {
    worksheet_owner_token: [()],
    sheets: [],
    chart_sheets: [],
    sheet_order: [],
    styles,
    conditional_styles,
    defined_names,
    core_properties,
    app_properties,
    custom_properties,
    vba_project: None,
    workbook_props,
    calc_props,
    default_font,
    default_table_style,
    default_pivot_style,
    theme_colors,
    theme_xml,
    indexed_colors,
    mru_colors_xml,
    styles_ext_lst_xml,
    io_context: workbook_io_context_with_transcoder(
      empty_workbook_io_context(),
      transcoder,
    ),
    options,
    workbook_protection,
    active_sheet_index: safe_active_index,
    cell_images,
    rich_value_images,
    rich_value_media,
    package_features: detect_package_features(part_names, name => {
      match (try? content_types.content_type_for(name)) {
        Ok(Some(value)) => Some(value)
        _ => None
      }
    }),
  }
  let slicer_cache_defs = parse_slicer_cache_definitions(
    workbook_rels,
    workbook_xml_part_path,
    part_names,
    content_types,
    archive,
    decode,
    budget=read_budget,
    cancelled~,
  )
  let vba_targets = parse_internal_relationship_targets(
    workbook_rels,
    rel_vba_project,
    budget=read_budget,
    cancelled~,
  )
  let vba_path = match
    first_existing_workbook_rel_target_path(
      vba_targets,
      workbook_xml_part_path,
      part_names,
      cancelled~,
    ) {
    Some(path) => Some(path)
    None =>
      match first_relationship_target(vba_targets) {
        Some(target) =>
          Some(
            actual_archive_part_path(
              part_names,
              resolve_part_rel_target(
                logical_archive_part_path(workbook_xml_part_path),
                target,
                cancelled~,
              ),
            ),
          )
        None => None
      }
  }
  match vba_path {
    Some(path) => {
      let vba_bytes = load_typed_part(
        archive,
        content_types,
        path,
        [ct_vba_project],
        "VBA project",
        cancelled~,
      )
      workbook.vba_project = Some(vba_bytes.to_owned())
    }
    None => ()
  }
  let worksheet_targets = parse_internal_relationship_targets(
    workbook_rels,
    rel_worksheet,
    budget=read_budget,
    cancelled~,
  )
  let chartsheet_targets = parse_internal_relationship_targets(
    workbook_rels,
    rel_chartsheet,
    budget=read_budget,
    cancelled~,
  )
  let seen_sheet_parts : Set[String] = Set([])
  let resolved_sheet_parts : Map[String, String] = Map([])
  for entry in sheets {
    let rel_id = entry.rel_id
    let is_worksheet = worksheet_targets.contains(rel_id)
    let is_chartsheet = chartsheet_targets.contains(rel_id)
    if is_worksheet == is_chartsheet {
      raise InvalidXml(msg="sheet relationship missing or ambiguous")
    }
    let target = if is_worksheet {
      match worksheet_targets.get(rel_id) {
        Some(value) => value
        None => raise InvalidXml(msg="worksheet target missing")
      }
    } else {
      match chartsheet_targets.get(rel_id) {
        Some(value) => value
        None => raise InvalidXml(msg="chartsheet target missing")
      }
    }
    let path = actual_archive_part_path(
      part_names,
      resolve_part_rel_target(
        logical_archive_part_path(workbook_xml_part_path),
        target,
        cancelled~,
      ),
    )
    let part_key = @ooxml.package_part_name_key(path)
    if seen_sheet_parts.contains(part_key) {
      raise InvalidXml(msg="sheet part referenced more than once")
    }
    seen_sheet_parts.add(part_key)
    resolved_sheet_parts[rel_id] = path
  }
  for entry in sheets {
    let name = entry.name
    let state = entry.state
    let rel_id = entry.rel_id
    let is_worksheet = worksheet_targets.contains(rel_id)
    if is_worksheet {
      let sheet_path = match resolved_sheet_parts.get(rel_id) {
        Some(value) => value
        None => raise InvalidXml(msg="worksheet target missing")
      }
      let sheet_bytes = load_typed_part(
        archive,
        content_types,
        sheet_path,
        [ct_worksheet],
        "worksheet",
        cancelled~,
      )
      let sheet_raw_xml = decode_source(sheet_bytes)
      read_budget.charge_work(sheet_raw_xml.length())
      let sheet_source_xml = project_xlsx_markup_compatibility(
        sheet_raw_xml,
        limits.max_xml_part_bytes,
        cancelled~,
      )
      if sheet_source_xml != sheet_raw_xml {
        read_budget.charge_work(sheet_source_xml.length())
      }
      let sheet_xml = canonicalize_xlsx_xml(
        sheet_source_xml,
        max_output_chars=limits.max_xml_part_bytes,
        cancelled~,
      )
      read_budget.checkpoint()
      read_budget.charge_work(sheet_xml.length())
      let sheet_core_xml = canonicalize_xlsx_worksheet_core(
        sheet_source_xml,
        sheet_xml,
        limits.max_xml_part_bytes,
        cancelled~,
      )
      read_budget.checkpoint()
      if sheet_core_xml != sheet_xml {
        read_budget.charge_work(sheet_core_xml.length())
      }
      let sheet_feature_xml = canonicalize_xlsx_worksheet_features(
        sheet_source_xml,
        limits.max_xml_part_bytes,
        cancelled~,
      )
      read_budget.checkpoint()
      read_budget.charge_work(sheet_feature_xml.length())
      let (
        cells,
        shared_formula_masters_index,
        merged_cells,
        auto_filter,
        row_dimensions,
        col_dimensions,
        page_margins,
        page_layout,
        header_footer,
        sheet_protection,
        row_breaks,
        col_breaks,
        sheet_views,
        sheet_props,
        dimension_ref,
        picture_rel_id,
      ) = parse_worksheet(
        sheet_core_xml,
        shared_strings,
        style_count,
        materialized_cells,
        limits.max_materialized_cells,
        materialized_dimensions,
        limits.max_materialized_row_column_dimensions,
        dimension_work,
        limits.max_row_column_dimension_work,
        budget=read_budget,
      )
      let hyperlink_elements = parse_hyperlink_elements(sheet_core_xml)
      let table_part_ids = parse_table_part_ids(sheet_core_xml)
      let pivot_part_ids = parse_pivot_table_part_ids(sheet_core_xml)
      let slicer_rel_ids = parse_sheet_slicer_rel_ids(sheet_feature_xml)
      let (drawing_rel_id, legacy_drawing_rel_id, legacy_drawing_hf_rel_id) = parse_sheet_drawing_rel_ids(
        sheet_source_xml,
        "worksheet",
        budget=read_budget,
        cancelled~,
      )
      let sparkline_groups = parse_sparkline_groups(sheet_feature_xml)
      let data_validations = parse_data_validations(
        sheet_core_xml,
        budget=read_budget,
      )
      for
        dv in parse_data_validations_x14(sheet_feature_xml, budget=read_budget) {
        data_validations.push(dv)
      }
      let conditional_formats = parse_conditional_formats(
        // Base data-bar rules carry their x14 correlation id in a nested
        // extLst, so this parser needs the preserved extension view. Cell and
        // row parsing above still uses the extension-free structural view.
        sheet_feature_xml,
        budget=read_budget,
      )
      for
        cf in parse_conditional_formats_x14(
          sheet_feature_xml,
          budget=read_budget,
        ) {
        conditional_formats.push(cf)
      }
      let x14_data_bars = parse_x14_data_bars(
        sheet_feature_xml,
        budget=read_budget,
      )
      let unknown_ext_blocks = parse_unknown_worksheet_ext_blocks(sheet_xml)
      let ignored_errors = parse_ignored_errors(
        sheet_core_xml,
        budget=read_budget,
      )
      let mut needs_hyperlink_rels = false
      for link in hyperlink_elements {
        if link.r_id is Some(_) {
          needs_hyperlink_rels = true
          break
        }
      }
      let needs_rels = table_part_ids.length() > 0 ||
        pivot_part_ids.length() > 0 ||
        slicer_rel_ids.length() > 0 ||
        needs_hyperlink_rels ||
        drawing_rel_id is Some(_) ||
        legacy_drawing_rel_id is Some(_) ||
        legacy_drawing_hf_rel_id is Some(_) ||
        picture_rel_id is Some(_)
      let rels_path = actual_relationship_part_path(sheet_path, part_names)
      let rels_xml = match
        load_optional_relationship_part(
          archive,
          content_types,
          rels_path,
          "worksheet relationships",
          cancelled~,
        ) {
        Some(rels_bytes) => decode(rels_bytes)
        None => if needs_rels { raise MissingPart(path=rels_path) } else { "" }
      }
      let slicer_part_entries = if slicer_rel_ids.length() == 0 {
        []
      } else {
        if rels_xml == "" {
          raise InvalidXml(msg="slicer relationship missing")
        }
        let rel_targets = parse_internal_relationship_targets(
          rels_xml,
          rel_slicer,
          budget=read_budget,
          cancelled~,
        )
        let parsed : Array[ParsedSlicerPartEntry] = []
        let seen_paths : Map[String, Bool] = Map([])
        for rel_id in slicer_rel_ids {
          let target = match rel_targets.get(rel_id) {
            Some(value) => value
            None => raise InvalidXml(msg="slicer relationship missing")
          }
          let slicer_path = actual_relationship_target_path(
            sheet_path,
            target,
            part_names,
            cancelled~,
          )
          require_part_content_type(
            content_types,
            logical_archive_part_path(slicer_path),
            [ct_slicer],
            "slicer",
            cancelled~,
          )
          if seen_paths.contains(slicer_path) {
            continue
          }
          seen_paths[slicer_path] = true
          let slicer_bytes = match archive.get(slicer_path) {
            Some(value) => value
            None => raise MissingPart(path=slicer_path)
          }
          let slicer_xml = decode(slicer_bytes)
          parsed.append(parse_slicer_part_entries(slicer_xml))
        }
        parsed
      }
      let sheet_background = match picture_rel_id {
        Some(rel_id) => {
          if rels_xml == "" {
            raise InvalidXml(msg="picture relationship missing")
          }
          let image_targets = parse_internal_relationship_targets(
            rels_xml,
            rel_image,
            budget=read_budget,
            cancelled~,
          )
          let target = match image_targets.get(rel_id) {
            Some(value) => value
            None => raise InvalidXml(msg="picture target missing")
          }
          let image_path = actual_relationship_target_path(
            sheet_path,
            target,
            part_names,
            cancelled~,
          )
          let (image_bytes, identity) = load_image_part(
            archive,
            content_types,
            image_path,
            "sheet background image",
            cancelled~,
          )
          Some({
            data: image_bytes.to_owned(),
            extension: identity.extension,
            content_type: identity.content_type,
          })
        }
        None => None
      }
      let hyperlinks = if hyperlink_elements.length() == 0 {
        []
      } else {
        let rel_targets = if needs_hyperlink_rels {
          parse_external_relationship_targets(
            rels_xml,
            rel_hyperlink,
            budget=read_budget,
            cancelled~,
          )
        } else {
          Map([])
        }
        let parsed_links : Array[Hyperlink] = []
        for link in hyperlink_elements {
          match link.r_id {
            Some(rel_id) => {
              let target = match rel_targets.get(rel_id) {
                Some(value) => value
                None => raise InvalidXml(msg="hyperlink relationship missing")
              }
              parsed_links.push({
                reference: link.reference,
                target,
                link_type: External,
                location: link.location,
                display: link.display,
                tooltip: link.tooltip,
              })
            }
            None =>
              match link.location {
                Some(location) =>
                  parsed_links.push({
                    reference: link.reference,
                    target: location,
                    link_type: Location,
                    location: Some(location),
                    display: link.display,
                    tooltip: link.tooltip,
                  })
                None => raise InvalidXml(msg="hyperlink target missing")
              }
          }
        }
        canonicalize_loaded_hyperlinks_for_merges(
          parsed_links, merged_cells, read_budget,
        )
        parsed_links
      }
      let tables = if table_part_ids.length() == 0 {
        []
      } else {
        let rel_targets = parse_internal_relationship_targets(
          rels_xml,
          rel_table,
          budget=read_budget,
          cancelled~,
        )
        let parsed_tables : Array[Table] = []
        for rel_id in table_part_ids {
          let target = match rel_targets.get(rel_id) {
            Some(value) => value
            None => raise InvalidXml(msg="table relationship missing")
          }
          let table_path = actual_relationship_target_path(
            sheet_path,
            target,
            part_names,
            cancelled~,
          )
          require_part_content_type(
            content_types,
            logical_archive_part_path(table_path),
            [ct_table],
            "table",
            cancelled~,
          )
          let table_bytes = match archive.get(table_path) {
            Some(value) => value
            None => raise MissingPart(path=table_path)
          }
          let table_xml = decode(table_bytes)
          parsed_tables.push(parse_table_xml(table_xml))
        }
        parsed_tables
      }
      let pivot_tables = {
        let rel_targets = if rels_xml == "" {
          Map([])
        } else {
          parse_internal_relationship_targets(
            rels_xml,
            rel_pivot_table,
            budget=read_budget,
            cancelled~,
          )
        }
        let pivot_rel_ids : Array[String] = []
        if pivot_part_ids.length() > 0 {
          for rel_id in pivot_part_ids {
            pivot_rel_ids.push(rel_id)
          }
        } else {
          for rel_id, _target in rel_targets {
            pivot_rel_ids.push(rel_id)
          }
        }
        if pivot_rel_ids.length() == 0 {
          []
        } else {
          let parsed_pivots : Array[PivotTable] = []
          for rel_id in pivot_rel_ids {
            let target = match rel_targets.get(rel_id) {
              Some(value) => value
              None => raise InvalidXml(msg="pivot table relationship missing")
            }
            let pivot_path = actual_relationship_target_path(
              sheet_path,
              target,
              part_names,
              cancelled~,
            )
            require_part_content_type(
              content_types,
              logical_archive_part_path(pivot_path),
              [ct_pivot_table],
              "pivot table",
              cancelled~,
            )
            let pivot_bytes = match archive.get(pivot_path) {
              Some(value) => value
              None => raise MissingPart(path=pivot_path)
            }
            let pivot_xml = decode(pivot_bytes)
            let pivot_rels_path = actual_relationship_part_path(
              pivot_path, part_names,
            )
            let pivot_rels_bytes = load_relationship_part(
              archive,
              content_types,
              pivot_rels_path,
              "pivot table relationships",
              cancelled~,
            )
            let pivot_rels_xml = decode(pivot_rels_bytes)
            let cache_targets = parse_internal_relationship_targets(
              pivot_rels_xml,
              rel_pivot_cache,
              budget=read_budget,
              cancelled~,
            )
            let cache_path = match
              first_existing_rel_target_path(
                cache_targets,
                pivot_path,
                part_names,
                cancelled~,
              ) {
              Some(path) => path
              None => {
                let cache_target = match
                  first_relationship_target(cache_targets) {
                  Some(value) => value
                  None =>
                    raise InvalidXml(msg="pivot cache relationship missing")
                }
                actual_relationship_target_path(
                  pivot_path,
                  cache_target,
                  part_names,
                  cancelled~,
                )
              }
            }
            require_part_content_type(
              content_types,
              logical_archive_part_path(cache_path),
              [ct_pivot_cache_def],
              "pivot cache definition",
              cancelled~,
            )
            let cache_bytes = match archive.get(cache_path) {
              Some(value) => value
              None => raise MissingPart(path=cache_path)
            }
            let cache_xml = decode(cache_bytes)
            let cache_id = parse_id_from_path(
              cache_path, "xl/pivotCache/pivotCacheDefinition",
            )
            let pivot_id = parse_id_from_path(
              pivot_path, "xl/pivotTables/pivotTable",
            )
            let cache_rels_path = actual_relationship_part_path(
              cache_path, part_names,
            )
            let cache_records_xml = match
              load_optional_relationship_part(
                archive,
                content_types,
                cache_rels_path,
                "pivot cache relationships",
                cancelled~,
              ) {
              Some(value) => {
                let cache_rels_xml = decode(value)
                let record_targets = parse_internal_relationship_targets(
                  cache_rels_xml,
                  rel_pivot_cache_records,
                  budget=read_budget,
                  cancelled~,
                )
                let record_path = match
                  first_existing_rel_target_path(
                    record_targets,
                    cache_path,
                    part_names,
                    cancelled~,
                  ) {
                  Some(path) => Some(path)
                  None =>
                    match first_relationship_target(record_targets) {
                      Some(record_target) =>
                        Some(
                          actual_relationship_target_path(
                            cache_path,
                            record_target,
                            part_names,
                            cancelled~,
                          ),
                        )
                      None => None
                    }
                }
                match record_path {
                  Some(path) => {
                    require_part_content_type(
                      content_types,
                      logical_archive_part_path(path),
                      [ct_pivot_cache_records],
                      "pivot cache records",
                      cancelled~,
                    )
                    let record_bytes = match archive.get(path) {
                      Some(value) => value
                      None => raise MissingPart(path~)
                    }
                    Some(decode(record_bytes))
                  }
                  None => None
                }
              }
              None => None
            }
            parsed_pivots.push({
              name: parse_pivot_table_name(pivot_xml),
              table_id: pivot_id,
              cache_id,
              table_xml: pivot_xml,
              cache_definition_xml: cache_xml,
              cache_records_xml,
            })
          }
          parsed_pivots
        }
      }
      let vml_targets = if legacy_drawing_rel_id is Some(_) ||
        legacy_drawing_hf_rel_id is Some(_) {
        parse_internal_relationship_targets(
          rels_xml,
          rel_vml,
          budget=read_budget,
          cancelled~,
        )
      } else {
        Map([])
      }
      let vml_drawing_xml = match legacy_drawing_rel_id {
        Some(rel_id) => {
          let target = match vml_targets.get(rel_id) {
            Some(value) => value
            None => raise InvalidXml(msg="vml drawing relationship missing")
          }
          let vml_path = actual_relationship_target_path(
            sheet_path,
            target,
            part_names,
            cancelled~,
          )
          require_part_content_type(
            content_types,
            logical_archive_part_path(vml_path),
            [ct_vml],
            "VML drawing",
            cancelled~,
          )
          let vml_bytes = match archive.get(vml_path) {
            Some(value) => value
            None => raise MissingPart(path=vml_path)
          }
          Some(decode(vml_bytes))
        }
        None => None
      }
      let vml_drawing_hf_xml = match legacy_drawing_hf_rel_id {
        Some(rel_id) => {
          let target = match vml_targets.get(rel_id) {
            Some(value) => value
            None => raise InvalidXml(msg="vml drawing hf relationship missing")
          }
          let vml_path = actual_relationship_target_path(
            sheet_path,
            target,
            part_names,
            cancelled~,
          )
          require_part_content_type(
            content_types,
            logical_archive_part_path(vml_path),
            [ct_vml],
            "VML drawing",
            cancelled~,
          )
          let vml_bytes = match archive.get(vml_path) {
            Some(value) => value
            None => raise MissingPart(path=vml_path)
          }
          Some((decode(vml_bytes), vml_path))
        }
        None => None
      }
      let (vml_drawing_hf_xml, vml_drawing_hf_path) = match vml_drawing_hf_xml {
        Some((xml, path)) => (Some(xml), Some(path))
        None => (None, None)
      }
      let header_footer_images = match
        (vml_drawing_hf_xml, vml_drawing_hf_path) {
        (Some(xml), Some(path)) => {
          let vml_rels_xml = match
            load_optional_relationship_part(
              archive,
              content_types,
              actual_relationship_part_path(path, part_names),
              "VML relationships",
              cancelled~,
            ) {
            Some(value) => Some(decode(value))
            None => None
          }
          parse_header_footer_images_from_vml(
            xml,
            vml_rels_xml,
            path,
            part_names,
            content_types,
            archive,
            budget=read_budget,
            cancelled~,
          )
        }
        _ => []
      }
      let comments = if rels_xml == "" {
        []
      } else {
        let comment_targets = parse_internal_relationship_targets(
          rels_xml,
          rel_comments,
          budget=read_budget,
          cancelled~,
        )
        let parsed_comments : Array[Comment] = []
        for _rel_id, target in comment_targets {
          let comment_path = actual_relationship_target_path(
            sheet_path,
            target,
            part_names,
            cancelled~,
          )
          require_part_content_type(
            content_types,
            logical_archive_part_path(comment_path),
            [ct_comments],
            "comments",
            cancelled~,
          )
          let comment_bytes = match archive.get(comment_path) {
            Some(value) => value
            None => raise MissingPart(path=comment_path)
          }
          let comment_xml = decode(comment_bytes)
          let entries = parse_comments_xml(comment_xml)
          parsed_comments.append(entries)
        }
        parsed_comments
      }
      let form_controls = match vml_drawing_xml {
        Some(xml) => parse_form_controls_vml(xml)
        None => []
      }
      let cell_index = build_bounded_worksheet_cell_index(cells, read_budget)
      let sheet = {
        workbook_owner_token: Some(workbook.worksheet_owner_token),
        name,
        sheet_views,
        dimension_ref,
        cells,
        cell_index,
        cell_index_valid: true,
        shared_formula_masters_index,
        shared_formula_masters_index_valid: true,
        merged_cells,
        auto_filter,
        page_margins,
        page_layout,
        header_footer,
        sheet_protection,
        sheet_props,
        sheet_background,
        row_breaks,
        col_breaks,
        hyperlinks,
        tables,
        sparkline_groups,
        pivot_tables,
        vml_drawing_xml,
        vml_drawing_hf_xml,
        images: [],
        header_footer_images,
        charts: [],
        shapes: [],
        form_controls,
        slicers: [],
        data_validations,
        conditional_formats,
        x14_data_bars,
        unknown_ext_blocks,
        x14_cf_rule_id_counter: 1,
        ignored_errors,
        comments,
        row_dimensions,
        col_dimensions,
        state,
        stream_state: Idle,
        next_drawing_order: 0,
        preserved_drawing_anchors: [],
        preserved_drawing_relationships: [],
        preserved_drawing_parts: [],
        cell_vm: if rich_value_images is Some(_) {
          parse_cell_vm_map(sheet_xml)
        } else {
          Map([])
        },
      }
      let mut slicer_anchor_info : Map[String, SlicerAnchorInfo] = Map([])
      match drawing_rel_id {
        Some(rel_id) => {
          if rels_xml == "" {
            raise InvalidXml(msg="drawing relationship missing")
          }
          let drawing_targets = parse_internal_relationship_targets(
            rels_xml,
            rel_drawing,
            budget=read_budget,
            cancelled~,
          )
          let drawing_target = match drawing_targets.get(rel_id) {
            Some(value) => value
            None => raise InvalidXml(msg="drawing target missing")
          }
          let drawing_path = actual_relationship_target_path(
            sheet_path,
            drawing_target,
            part_names,
            cancelled~,
          )
          require_part_content_type(
            content_types,
            logical_archive_part_path(drawing_path),
            [ct_drawing],
            "drawing",
            cancelled~,
          )
          let drawing_bytes = match archive.get(drawing_path) {
            Some(value) => value
            None => raise MissingPart(path=drawing_path)
          }
          let drawing_source_xml = decode_source(drawing_bytes)
          let drawing_xml = canonicalize_xlsx_drawing_xml(
            drawing_source_xml,
            limits.max_xml_part_bytes,
            budget=read_budget,
            cancelled~,
          )
          let drawing_metrics = drawing_metrics_for_sheet(sheet)
          let drawing_anchors = parse_drawing_anchors(
            drawing_xml,
            budget=read_budget,
          )
          sheet.next_drawing_order = drawing_anchors.length()
          if slicer_part_entries.length() > 0 {
            slicer_anchor_info = parse_drawing_slicer_anchors(
              drawing_anchors,
              drawing_metrics,
              budget=read_budget,
            )
          }
          let drawing_rels_xml = match
            load_optional_relationship_part(
              archive,
              content_types,
              actual_relationship_part_path(drawing_path, part_names),
              "drawing relationships",
              cancelled~,
            ) {
            Some(value) => decode(value)
            None => ""
          }
          let (preserved_anchors, preserved_relationships, preserved_parts) = preserved_unsupported_drawing_state(
            drawing_anchors,
            drawing_rels_xml,
            drawing_path,
            archive,
            part_names,
            content_types,
            decode,
            budget=read_budget,
            cancelled~,
          )
          sheet.preserved_drawing_anchors.append(preserved_anchors)
          sheet.preserved_drawing_relationships.append(preserved_relationships)
          sheet.preserved_drawing_parts.append(preserved_parts)
          let drawing_images = parse_drawing_images(
            drawing_anchors,
            drawing_rels_xml,
            drawing_path,
            part_names,
            content_types,
            drawing_metrics,
            archive,
            media_cache=drawing_media_cache,
            budget=read_budget,
            cancelled~,
          )
          sheet.images.append(drawing_images)
          let drawing_charts = parse_drawing_charts(
            drawing_anchors,
            drawing_rels_xml,
            drawing_path,
            part_names,
            content_types,
            drawing_metrics,
            archive,
            decode,
            budget=read_budget,
            cancelled~,
          )
          sheet.charts.append(drawing_charts)
          let drawing_shapes = parse_drawing_shapes(
            drawing_anchors,
            drawing_metrics,
            budget=read_budget,
          )
          sheet.shapes.append(drawing_shapes)
        }
        None => ()
      }
      for entry in slicer_part_entries {
        let cache_def = slicer_cache_defs.get(entry.cache)
        let source_name = match cache_def {
          Some(def) =>
            if def.source_name != "" {
              def.source_name
            } else {
              entry.name
            }
          None => entry.name
        }
        let item_desc = match cache_def {
          Some(def) => def.item_desc
          None => false
        }
        let mut cell = ""
        let mut width = 200
        let mut height = 200
        let mut macro_name = ""
        let format = GraphicOptions::new()
        match slicer_anchor_info.get(entry.name) {
          Some(info) => {
            cell = info.cell
            if info.width > 0 {
              width = info.width
            }
            if info.height > 0 {
              height = info.height
            }
            macro_name = info.macro_name
            if info.offset_x != 0 {
              format.offset_x = Some(info.offset_x)
            }
            if info.offset_y != 0 {
              format.offset_y = Some(info.offset_y)
            }
            if info.alt_text != "" {
              format.alt_text = Some(info.alt_text)
            }
            format.positioning = Some(info.positioning)
            format.locked = Some(info.locked)
            format.print_object = Some(info.print_object)
          }
          None => {
            format.positioning = Some(TwoCell)
            format.locked = Some(true)
            format.print_object = Some(true)
          }
        }
        sheet.slicers.push({
          name: entry.name,
          cache: entry.cache,
          source_name,
          cell,
          table_sheet: "",
          table_name: "",
          caption: entry.caption,
          macro_name,
          width,
          height,
          display_header: entry.display_header,
          item_desc,
          format,
          drawing_offset_x_emu: match slicer_anchor_info.get(entry.name) {
            Some(info) => Some(info.offset_x_emu)
            None => None
          },
          drawing_offset_y_emu: match slicer_anchor_info.get(entry.name) {
            Some(info) => Some(info.offset_y_emu)
            None => None
          },
          drawing_width_emu: match slicer_anchor_info.get(entry.name) {
            Some(info) => Some(info.width_emu)
            None => None
          },
          drawing_height_emu: match slicer_anchor_info.get(entry.name) {
            Some(info) => Some(info.height_emu)
            None => None
          },
          drawing_order: match slicer_anchor_info.get(entry.name) {
            Some(info) => Some(info.order)
            None => None
          },
        })
      }
      workbook.sheets.push(sheet)
      workbook.sheet_order.push(Worksheet(workbook.sheets.length() - 1))
    } else {
      let chartsheet_path = match resolved_sheet_parts.get(rel_id) {
        Some(value) => value
        None => raise InvalidXml(msg="chartsheet target missing")
      }
      let chartsheet_bytes = load_typed_part(
        archive,
        content_types,
        chartsheet_path,
        [ct_chartsheet],
        "chartsheet",
        cancelled~,
      )
      let chartsheet_raw_xml = decode_source(chartsheet_bytes)
      read_budget.charge_work(chartsheet_raw_xml.length())
      let chartsheet_source_xml = project_xlsx_markup_compatibility(
        chartsheet_raw_xml,
        limits.max_xml_part_bytes,
        cancelled~,
      )
      if chartsheet_source_xml != chartsheet_raw_xml {
        read_budget.charge_work(chartsheet_source_xml.length())
      }
      let chartsheet_xml = canonicalize_xlsx_xml(
        chartsheet_source_xml,
        max_output_chars=limits.max_xml_part_bytes,
        cancelled~,
      )
      read_budget.checkpoint()
      read_budget.charge_work(chartsheet_xml.length())
      let (drawing_rel_id, _, _) = parse_sheet_drawing_rel_ids(
        chartsheet_source_xml,
        "chartsheet",
        budget=read_budget,
        cancelled~,
      )
      let drawing_rel = match drawing_rel_id {
        Some(value) => value
        None => raise InvalidXml(msg="chartsheet drawing missing")
      }
      let chartsheet_rels_path = actual_relationship_part_path(
        chartsheet_path, part_names,
      )
      let chartsheet_rels_bytes = load_relationship_part(
        archive,
        content_types,
        chartsheet_rels_path,
        "chartsheet relationships",
        cancelled~,
      )
      let chartsheet_rels_xml = decode(chartsheet_rels_bytes)
      let drawing_targets = parse_internal_relationship_targets(
        chartsheet_rels_xml,
        rel_drawing,
        budget=read_budget,
        cancelled~,
      )
      let drawing_target = match drawing_targets.get(drawing_rel) {
        Some(value) => value
        None => raise InvalidXml(msg="drawing target missing")
      }
      let drawing_path = actual_relationship_target_path(
        chartsheet_path,
        drawing_target,
        part_names,
        cancelled~,
      )
      let drawing_bytes = load_typed_part(
        archive,
        content_types,
        drawing_path,
        [ct_drawing],
        "drawing",
        cancelled~,
      )
      let drawing_source_xml = decode_source(drawing_bytes)
      let drawing_xml = canonicalize_xlsx_drawing_xml(
        drawing_source_xml,
        limits.max_xml_part_bytes,
        budget=read_budget,
        cancelled~,
      )
      let drawing_anchors = parse_drawing_anchors(
        drawing_xml,
        budget=read_budget,
      )
      let chart_rel_id = single_drawing_chart_rel_id(
        drawing_anchors,
        budget=read_budget,
      )
      let drawing_rels_path = actual_relationship_part_path(
        drawing_path, part_names,
      )
      let drawing_rels_bytes = load_relationship_part(
        archive,
        content_types,
        drawing_rels_path,
        "drawing relationships",
        cancelled~,
      )
      let drawing_rels_xml = decode(drawing_rels_bytes)
      let chart_targets = parse_internal_relationship_targets(
        drawing_rels_xml,
        rel_chart,
        budget=read_budget,
        cancelled~,
      )
      let chart_target = match chart_targets.get(chart_rel_id) {
        Some(value) => value
        None => raise InvalidXml(msg="chartsheet chart target missing")
      }
      let chart_path = actual_relationship_target_path(
        drawing_path,
        chart_target,
        part_names,
        cancelled~,
      )
      let chart_bytes = load_typed_part(
        archive,
        content_types,
        chart_path,
        [ct_chart],
        "chart",
        cancelled~,
      )
      let chart_xml = decode(chart_bytes)
      let chart_sheet = ChartSheet::new(name, chart_xml)
      chart_sheet.state = state
      workbook.chart_sheets.push(chart_sheet)
      workbook.sheet_order.push(ChartSheet(workbook.chart_sheets.length() - 1))
    }
  }
  resolve_slicer_sources_after_read(workbook, slicer_cache_defs)
  read_budget.checkpoint()
  workbook
}

///|
fn read_zip_archive_core(
  archive : @zip.Archive,
  options : Options,
  limits : ReadLimits,
  cancelled : () -> Bool,
  transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
  let workbook = match transcoder {
    Some(decode) =>
      read_zip_archive_core_unchecked(
        archive,
        options,
        limits,
        cancelled,
        transcoder=decode,
      ) catch {
        InvalidCellRef(value~) =>
          raise InvalidXml(msg="coordinate reference invalid: \{value}")
        other => raise other
      }
    None =>
      read_zip_archive_core_unchecked(archive, options, limits, cancelled) catch {
        InvalidCellRef(value~) =>
          raise InvalidXml(msg="coordinate reference invalid: \{value}")
        other => raise other
      }
  }
  check_read_cancelled(cancelled)
  workbook
}

///|
fn checked_read_workbook(
  workbook : Workbook,
  cancelled : () -> Bool,
) -> Workbook raise XlsxError {
  check_read_cancelled(cancelled)
  workbook
}

///|
fn read_zip_bytes(
  bytes : BytesView,
  options? : Options = Options::new(),
  limits? : ReadLimits = ReadLimits::new(),
  cancelled? : () -> Bool = () => false,
  transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
  check_read_cancelled(cancelled)
  let archive = read_limited_archive(bytes, limits, cancelled~)
  require_bounded_archive(archive, limits, cancelled~)
  let workbook = match transcoder {
    Some(value) =>
      read_zip_archive_core(
        archive,
        options,
        limits,
        cancelled,
        transcoder=value,
      )
    None => read_zip_archive_core(archive, options, limits, cancelled)
  }
  checked_read_workbook(workbook, cancelled)
}

///|
/// Reads a workbook from an already-inflated ZIP archive without a second
/// decompression pass. The archive must be pristine and carry non-forgeable
/// `zip.read_limited` provenance at least as strict as `limits`; constructed,
/// compatibility-read, or mutated archives fail with `UnboundedArchive`.
pub fn read_bounded_archive(
  archive : @zip.Archive,
  options? : Options = Options::new(),
  limits? : ReadLimits = ReadLimits::new(),
  cancelled? : () -> Bool = () => false,
  transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
  check_read_cancelled(cancelled)
  require_bounded_archive(archive, limits, cancelled~)
  let workbook = match transcoder {
    Some(value) =>
      read_zip_archive_core(
        archive,
        options,
        limits,
        cancelled,
        transcoder=value,
      )
    None => read_zip_archive_core(archive, options, limits, cancelled)
  }
  checked_read_workbook(workbook, cancelled)
}

///|
/// Reads an XLSX package under a fail-closed resource policy.
///
/// `limits` applies to the compressed source, ZIP structure and expansion, and
/// individual XML-like OOXML parts. Encrypted packages require
/// `read_with_password` or an `Options` value containing a password.
pub fn read(
  bytes : BytesView,
  options? : Options = Options::new(),
  limits? : ReadLimits = ReadLimits::new(),
  cancelled? : () -> Bool = () => false,
  transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
  check_read_cancelled(cancelled)
  check_source_package_size(bytes, limits)
  if options.password != "" {
    match transcoder {
      Some(value) =>
        return checked_read_workbook(
          read_with_password(
            bytes,
            options.password,
            options~,
            limits~,
            cancelled~,
            transcoder=value,
          ),
          cancelled,
        )
      None =>
        return checked_read_workbook(
          read_with_password(
            bytes,
            options.password,
            options~,
            limits~,
            cancelled~,
          ),
          cancelled,
        )
    }
  }
  if is_encrypted_package(bytes) {
    raise EncryptedPackage
  }
  let workbook = match transcoder {
    Some(value) =>
      read_zip_bytes(bytes, options~, limits~, cancelled~, transcoder=value)
    None => read_zip_bytes(bytes, options~, limits~, cancelled~)
  }
  checked_read_workbook(workbook, cancelled)
}

///|
/// Reads an XLSX package with an explicit password under a fail-closed resource
/// policy. Resource failures remain `ResourceLimitExceeded`; a verified
/// password preserves malformed-package errors, while an unverified payload
/// that is not a valid ZIP is classified as `InvalidPassword`.
pub fn read_with_password(
  bytes : BytesView,
  password : String,
  options? : Options = Options::new(),
  limits? : ReadLimits = ReadLimits::new(),
  cancelled? : () -> Bool = () => false,
  transcoder? : (String, Bytes) -> String raise XlsxError,
) -> Workbook raise XlsxError {
  check_read_cancelled(cancelled)
  check_source_package_size(bytes, limits)
  let resolved_options = options_with_password(options, password)
  let workbook = if is_encrypted_package(bytes) {
    let archive = decrypt_encrypted_archive(
      bytes,
      password,
      limits~,
      cancelled~,
    )
    require_bounded_archive(archive, limits, cancelled~)
    match transcoder {
      Some(value) =>
        read_zip_archive_core(
          archive,
          resolved_options,
          limits,
          cancelled,
          transcoder=value,
        )
      None =>
        read_zip_archive_core(archive, resolved_options, limits, cancelled)
    }
  } else {
    match transcoder {
      Some(value) =>
        read_zip_bytes(
          bytes,
          options=resolved_options,
          limits~,
          cancelled~,
          transcoder=value,
        )
      None =>
        read_zip_bytes(bytes, options=resolved_options, limits~, cancelled~)
    }
  }
  checked_read_workbook(workbook, cancelled)
}

///|
fn is_invalid_xml_error(
  result : Result[Font, XlsxError],
  expected_msg : String,
) -> Bool {
  match result {
    Err(InvalidXml(msg~)) => msg == expected_msg
    _ => false
  }
}

///|
fn is_invalid_xml_fill_error(
  result : Result[Fill, XlsxError],
  expected_msg : String,
) -> Bool {
  match result {
    Err(InvalidXml(msg~)) => msg == expected_msg
    _ => false
  }
}

///|
fn is_invalid_xml_alignment_error(
  result : Result[Alignment?, XlsxError],
  expected_msg : String,
) -> Bool {
  match result {
    Err(InvalidXml(msg~)) => msg == expected_msg
    _ => false
  }
}

///|
fn has_border_type(borders : Array[Border], typ : String) -> Bool {
  for border in borders {
    if border.typ == typ {
      return true
    }
  }
  false
}

///|
fn border_color_by_type(borders : Array[Border], typ : String) -> String? {
  for border in borders {
    if border.typ == typ {
      return border.color
    }
  }
  None
}

///|
fn is_invalid_xml_string_array_error(
  result : Result[Array[String], XlsxError],
  expected_msg : String,
) -> Bool {
  match result {
    Err(InvalidXml(msg~)) => msg == expected_msg
    _ => false
  }
}

///|
fn is_invalid_xml_defined_name_error(
  result : Result[Array[DefinedName], XlsxError],
  expected_msg : String,
) -> Bool {
  match result {
    Err(InvalidXml(msg~)) => msg == expected_msg
    _ => false
  }
}

///|
test "read wb: xml encoding parser guard paths" {
  inspect(xml_encoding(b"abc") is None, content="true")
  inspect(xml_encoding(b"") is None, content="true")
  inspect(
    xml_encoding(b"") is None,
    content="true",
  )

  let with_bom_single_quote = b"\xEF\xBB\xBF"
  inspect(
    match xml_encoding(with_bom_single_quote) {
      Some(value) => value == "UTF-16"
      None => false
    },
    content="true",
  )

  inspect(
    xml_encoding(b"") is None,
    content="true",
  )
  inspect(
    xml_encoding(b"") is None,
    content="true",
  )
}

///|
test "read wb: decode_utf8 invalid paths" {
  let requires_transcoder = Ok(
    decode_utf8(b"", None),
  ) catch {
    e => Err(e)
  }
  inspect(
    match requires_transcoder {
      Err(InvalidXml(msg~)) => msg.contains("requires charset transcoder")
      _ => false
    },
    content="true",
  )

  let invalid_utf8 = Ok(decode_utf8(b"\xFF\xFF\xFF", None)) catch {
    e => Err(e)
  }
  inspect(
    match invalid_utf8 {
      Err(InvalidXml(msg~)) => msg == "invalid utf8"
      _ => false
    },
    content="true",
  )
}

///|
test "read wb: parse_default_font fallback matrix" {
  inspect(parse_default_font(""), content="Calibri")
  inspect(parse_default_font(""), content="Calibri")
  inspect(parse_default_font("(?[^\/<>]+?)\<\/a\>/g
          },
          hljs.COMMENT(
            '//', // begin
            '\n', // end
          )
        ]
      }
    }

    hljs.registerLanguage('moonbit', moonbitLanguageFn);
    hljs.highlightAll();
    hljs.initLineNumbersOnLoad();

    const number = window.location.href.split('#')[1];

    function waitForLineNumbers() {
      setTimeout(function () {
        const target = document.querySelector(`.hljs-ln-line[data-line-number="${number}"]`);
        if (target == null) waitForLineNumbers();
        else target.scrollIntoView();
      }, 50);
    }

    waitForLineNumbers()