///|
let word_content_types : Array[String] = [
  docm_ct, docx_ct, vba_ct, word_vba_data_ct, "application/vnd.openxmlformats-package.relationships+xml",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml",
  "application/vnd.openxmlformats-officedocument.theme+xml", "application/vnd.openxmlformats-package.core-properties+xml",
  "application/vnd.openxmlformats-officedocument.extended-properties+xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml", "image/png",
  "image/jpeg", ole_content_type, "application/vnd.ms-excel", "application/msword",
  "image/x-emf", "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
  "application/xml", custom_properties_ct,
]

///|
let word_relationship_types : Array[String] = [
  office_rel + "officeDocument",
  office_rel + "styles",
  office_rel + "settings",
  office_rel + "fontTable",
  office_rel + "webSettings",
  office_rel + "theme",
  office_rel + "extended-properties",
  office_rel + "header",
  office_rel + "footer",
  office_rel + "image",
  office_rel + "attachedTemplate",
  office_rel + "footnotes",
  office_rel + "endnotes",
  office_rel + "numbering",
  office_rel + "customXml",
  office_rel + "customXmlProps",
  vba_rel,
  word_vba_data_rel,
  "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
]

///|
let word_attribute_names : Array[String] = [
  "space", "val", "rsidR", "rsidRPr", "rsidRDefault", "rsidP", "rsidDel", "rsidSect",
  "w", "h", "orient", "top", "right", "bottom", "left", "header", "footer", "gutter",
  "num", "equalWidth", "sep", "linePitch", "charSpace", "type", "ascii", "hAnsi",
  "cs", "eastAsia", "asciiTheme", "hAnsiTheme", "cstheme", "eastAsiaTheme", "hint",
  "bidi", "firstLine", "hanging", "before", "after", "line", "lineRule", "styleId",
  "default", "customStyle", "name", "count", "defLockedState", "defUIPriority", "defSemiHidden",
  "defUnhideWhenUsed", "defQFormat", "locked", "uiPriority", "semiHidden", "unhideWhenUsed",
  "qFormat", "percent", "spelling", "grammar", "bg1", "t1", "bg2", "t2", "accent1",
  "accent2", "accent3", "accent4", "accent5", "accent6", "hyperlink", "followedHyperlink",
  "usb0", "usb1", "usb2", "usb3", "csb0", "csb1", "clear", "sz", "color", "fill",
  "themeColor", "themeTint", "themeShade", "gridSpan", "afterAutospacing", "beforeAutospacing",
  "tblLook", "firstRow", "lastRow", "firstColumn", "lastColumn", "noHBand", "noVBand",
  "hRule", "tblStyleRowBandSize",
]

///|
fn controlled_vba_parts(pkg : Package) -> Array[String] {
  let result : Array[String] = []
  for finding in pkg.audit.findings {
    result.push(finding.part)
    if is_word_format(pkg.audit.format) {
      let companion = relationship_part(finding.part)
      if pkg.archive.get(companion[:]) is Some(_) {
        result.push(companion)
        for
          i in pkg.graph.graph.nodes[pkg.graph.node_index[finding.part]].outbound {
          result.push(pkg.audit.relationships[i].resolved)
        }
      }
    }
  }
  result.sort()
  result
}

///|
fn validate_word_profile(pkg : Package) -> Unit raise {
  let report = pkg.audit
  guard report.relationships.filter(is_template_relationship).length() <= 1 else {
    raise Refused("multiple attached template declarations unsupported")
  }
  guard pkg.graph.graph.coverage_gaps.length() == 0 else {
    raise Refused(
      "Word graph coverage gap: " + pkg.graph.graph.coverage_gaps[0],
    )
  }
  validate_ole_source(pkg)
  let bibliography = validate_word_bibliography(pkg)
  for r in report.relationships {
    if [office_rel + "customXml", office_rel + "customXmlProps"].contains(
        r.kind,
      ) {
      guard bibliography.contains(r.resolved) else {
        raise Invalid("unowned bibliography relationship")
      }
      continue
    }
    let previews = ole_relationships(pkg).filter(fn(o) {
      ole_preview(pkg, o) == r
    })
    if previews.length() > 0 {
      validate_ole(pkg, previews[0])
      continue
    }
    if ole_declared(r, pkg.graph.ole_objects) {
      validate_ole(pkg, r)
      continue
    }
    if hyperlink_declared(r, report.parts, pkg.xml_nodes) {
      validate_hyperlink(pkg, r)
      continue
    }
    if r.kind == office_rel + "attachedTemplate" {
      validate_template_relationship(pkg, r)
      continue
    }
    guard !r.external && word_relationship_types.contains(r.kind) else {
      raise Refused("external or unsupported Word relationship")
    }
    let target = report.parts[pkg.graph.part_index[r.resolved]]
    let expected = if r.kind == vba_rel {
      target.content_type == vba_ct
    } else if r.kind == word_vba_data_rel {
      target.content_type == word_vba_data_ct
    } else if r.kind == office_rel + "officeDocument" {
      target.content_type == docm_ct || target.content_type == docx_ct
    } else if r.kind == office_rel + "image" {
      target.content_type == "image/png" || target.content_type == "image/jpeg"
    } else if r.kind == office_rel + "theme" {
      target.content_type ==
      "application/vnd.openxmlformats-officedocument.theme+xml"
    } else if r.kind == office_rel + "extended-properties" {
      target.content_type ==
      "application/vnd.openxmlformats-officedocument.extended-properties+xml"
    } else if r.kind.has_suffix("/core-properties") {
      target.content_type ==
      "application/vnd.openxmlformats-package.core-properties+xml"
    } else {
      target.content_type ==
      "application/vnd.openxmlformats-officedocument.wordprocessingml." +
      r.kind[office_rel.length():].to_owned() +
      "+xml"
    }
    guard expected else {
      raise Refused("Word relationship/content-type conflict")
    }
    if r.kind == word_vba_data_rel {
      guard report.findings.any(fn(f) { f.part == r.source }) else {
        raise Refused("Word VBA metadata not owned by a declared VBA project")
      }
    } else if [
        office_rel + "officeDocument",
        office_rel + "extended-properties",
        "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
      ].contains(r.kind) {
      guard r.source == "/" else {
        raise Refused("Word package metadata source mismatch")
      }
    } else if r.kind == office_rel + "image" {
      let source_type = match pkg.graph.part_index.get(r.source) {
        Some(i) => report.parts[i].content_type
        None => ""
      }
      guard r.source == report.main_part ||
        source_type ==
        "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" ||
        source_type ==
        "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" else {
        raise Refused("Word image relationship has unsupported source")
      }
    } else {
      guard r.source == report.main_part else {
        raise Refused("unsupported Word relationship source")
      }
    }
  }
  guard report.findings.length() <= 1 else {
    raise Refused("multiple Word VBA projects unsupported")
  }
  for f in report.findings {
    guard report.format == "DOCM" else {
      raise Refused("VBA in non-macro Word main type")
    }
    let incoming = graph_incoming(pkg.graph, f.part)
    guard incoming.length() == 1 &&
      incoming[0].source == report.main_part &&
      incoming[0].kind == vba_rel else {
      raise Refused("shared/orphan Word VBA or conflicting evidence")
    }
    guard get_part(pkg.archive, f.part).has_prefix(
      b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1",
    ) else {
      raise Refused("Word VBA payload is not CFB")
    }
    let outgoing = pkg.graph.graph.nodes[pkg.graph.node_index[f.part]].outbound
    guard outgoing.length() <= 1 else {
      raise Refused("unsupported Word VBA dependency set")
    }
    for i in outgoing {
      let relation = report.relationships[i]
      guard relation.kind == word_vba_data_rel && !relation.external else {
        raise Refused("unsupported Word VBA companion")
      }
      let owners = graph_incoming(pkg.graph, relation.resolved)
      guard owners.length() == 1 && owners[0].source == f.part else {
        raise Refused("shared Word VBA metadata")
      }
      guard pkg.archive.get(relationship_part(relation.resolved)[:]) is None else {
        raise Refused("nested Word VBA metadata dependencies unsupported")
      }
    }
    guard !pkg.graph.graph.xml_references.any(fn(reference) {
      let r = report.relationships[reference.relationship_index]
      r.resolved == f.part ||
      outgoing.any(fn(i) { report.relationships[i].resolved == r.resolved })
    }) else {
      raise Refused("retained XML references Word VBA controlled parts")
    }
  }
  let removed = controlled_vba_parts(pkg)
  validate_word_references(pkg)
  validate_word_structures(pkg)
  for part in report.parts {
    if part.name == "[Content_Types].xml" {
      continue
    }
    if bibliography.contains(part.name) {
      continue
    }
    guard word_content_types.contains(part.content_type) else {
      raise Refused("unknown Word part content type")
    }
    if ole_payload_type(part.content_type) || part.content_type == "image/x-emf" {
      guard controlled_parts(pkg).contains(part.name) else {
        raise Refused("unowned Word OLE part")
      }
      continue
    }
    if part.content_type == vba_ct {
      continue
    }
    if part.content_type == "image/png" || part.content_type == "image/jpeg" {
      let bytes = get_part(pkg.archive, part.name)
      guard (
          part.content_type == "image/png" &&
          bytes.has_prefix(b"\x89PNG\x0d\x0a\x1a\x0a")
        ) ||
        (part.content_type == "image/jpeg" && bytes.has_prefix(b"\xff\xd8\xff")) else {
        raise Invalid("embedded image signature/content-type mismatch")
      }
      continue
    }
    if part.content_type == word_vba_data_ct {
      guard removed.contains(part.name) else {
        raise Refused("orphan Word VBA supporting metadata")
      }
    }
    if part.name.has_suffix(".rels") {
      guard part.content_type ==
        "application/vnd.openxmlformats-package.relationships+xml" else {
        raise Refused("Word relationship part content type mismatch")
      }
      continue
    }
    if part.content_type.contains(".theme+") ||
      part.content_type.contains("core-properties") ||
      part.content_type.contains("extended-properties") {
      validate_static_xml_part(pkg, part)
    } else {
      validate_word_xml_part(pkg, part)
    }
  }
}

///|
fn validate_word_xml_part(pkg : Package, part : PartRecord) -> Unit raise {
  guard pkg.xml_nodes.get(part.name) is Some(nodes) else {
    raise Refused("Word XML payload name unsupported")
  }
  let root = if part.name == pkg.audit.main_part {
    "document"
  } else if part.content_type == word_vba_data_ct {
    "vbaSuppData"
  } else if part.content_type.contains(".header+") {
    "hdr"
  } else if part.content_type.contains(".footer+") {
    "ftr"
  } else if part.content_type.contains(".styles+") {
    "styles"
  } else if part.content_type.contains(".settings+") {
    "settings"
  } else if part.content_type.contains(".fontTable+") {
    "fonts"
  } else if part.content_type.contains(".footnotes+") {
    "footnotes"
  } else if part.content_type.contains(".endnotes+") {
    "endnotes"
  } else if part.content_type.contains(".numbering+") {
    "numbering"
  } else {
    "webSettings"
  }
  guard nodes[0].element.name.local_name == root else {
    raise Refused("Word XML root/content type mismatch")
  }
  let parents : Array[String] = []
  let mut table_style = false
  let mut style_table_properties = 0
  let mut style_border_seen = false
  let mut validated_end = 0
  let mut allow_png_seen = false
  let mut no_proof_seen = false
  let mut header_run_properties = 0
  let layout_settings : Map[String, Bool] = Map([])
  let modern = word_modern_catalog(part, nodes, pkg.control)
  let mask = if part.name == pkg.audit.main_part &&
    ole_relationships(pkg).length() > 0 {
    ole_catalog_mask(pkg.graph.ole_objects, part.name)
  } else {
    []
  }
  for ordinal, node in nodes {
    pkg.control.checkpoint(
      "word-profile-node",
      1 + node.element.attributes.length(),
    )
    if ordinal < validated_end ||
      mask.contains(ordinal) ||
      modern.contains(ordinal) {
      continue
    }
    let ns = node.element.name.namespace_uri.unwrap_or("")
    let name = node.element.name.local_name
    while parents.length() > node.depth {
      ignore(parents.pop())
    }
    let parent = if parents.length() > 0 {
      parents[parents.length() - 1]
    } else {
      ""
    }
    if root == "styles" && node.depth == 1 {
      table_style = ns == word_ns &&
        name == "style" &&
        word_attr_opt(node, "type") == Some("table")
      style_table_properties = 0
      style_border_seen = false
    }
    if table_style && node.depth == 2 && ns == word_ns && name == "tblPr" {
      guard !style_border_seen else {
        raise Refused("duplicate table properties in bordered Word style")
      }
      style_table_properties += 1
    }
    if root == "styles" && ns == word_ns && name == "tblBorders" {
      guard table_style &&
        style_table_properties == 1 &&
        node.depth == 3 &&
        parent == "tblPr" &&
        !style_border_seen else {
        raise Refused("misplaced or duplicate Word table style borders")
      }
      validated_end = validate_word_table_style_borders(nodes, ordinal)
      style_border_seen = true
      continue
    }
    if root == "hdr" {
      if ns == word_ns && name == "r" {
        no_proof_seen = false
        header_run_properties = 0
      }
      if ns == word_ns && name == "rPr" && parent == "r" {
        guard !no_proof_seen else {
          raise Refused("duplicate run properties in Word header")
        }
        header_run_properties += 1
      }
      if ns == word_ns && name == "noProof" {
        guard !no_proof_seen &&
          header_run_properties == 1 &&
          parent == "rPr" &&
          parents.length() >= 2 &&
          parents[parents.length() - 2] == "r" &&
          word_val_leaf(nodes, ordinal) &&
          ["0", "1", "true", "false", "on", "off"].contains(
            word_attr_opt(node, "val").unwrap_or("true"),
          ) else {
          raise Refused("invalid or duplicate Word header run proofing flag")
        }
        no_proof_seen = true
        continue
      }
      if ns == drawing_ns && name == "srcRect" && parent == "blipFill" {
        validate_header_source_rectangle(nodes, ordinal)
        continue
      }
      if ns == drawing_ns && name == "noFill" && parent == "spPr" {
        validated_end = validate_header_picture_no_fill(nodes, ordinal)
        continue
      }
    }
    if root == "webSettings" && ns == word_ns && name == "allowPNG" {
      guard !allow_png_seen &&
        node.depth == 1 &&
        parent == "webSettings" &&
        word_val_leaf(nodes, ordinal) &&
        ["0", "1", "true", "false", "on", "off"].contains(
          word_attr_opt(node, "val").unwrap_or("true"),
        ) else {
        raise Refused("invalid or duplicate Word allowPNG setting")
      }
      allow_png_seen = true
      continue
    }
    if root == "settings" &&
      ns == word_ns &&
      [
        "embedSystemFonts", "doNotHyphenateCaps", "noPunctuationKerning", "drawingGridHorizontalSpacing",
        "displayHorizontalDrawingGridEvery", "displayVerticalDrawingGridEvery",
      ].contains(name) {
      guard node.depth == 1 &&
        parent == "settings" &&
        !layout_settings.contains(name) &&
        word_val_leaf(nodes, ordinal) else {
        raise Refused("misplaced, malformed or duplicate Word layout setting")
      }
      match name {
        "drawingGridHorizontalSpacing" => {
          guard checked_word_decimal(word_attr(node, "val"), 31681, name) > 0 else {
            raise Invalid("Word drawing grid spacing must be positive")
          }
        }
        "displayHorizontalDrawingGridEvery"
        | "displayVerticalDrawingGridEvery" =>
          ignore(checked_word_decimal(word_attr(node, "val"), 256, name))
        _ => {
          // embedTrueTypeFonts and font payload relationships still refuse.
          guard ["0", "1", "true", "false", "on", "off"].contains(
            word_attr_opt(node, "val").unwrap_or("true"),
          ) else {
            raise Refused("unsupported Word layout flag value")
          }
        }
      }
      layout_settings[name] = true
      continue
    }
    let permitted = if ["document", "hdr", "ftr", "footnotes", "endnotes"].contains(
        root,
      ) {
      word_passive_element(node, parent, root)
    } else if root == "numbering" {
      word_numbering_element(node, parent)
    } else if root == "vbaSuppData" {
      ns == word_macro_ns &&
      (match name {
        "vbaSuppData" => node.depth == 0
        "mcds" => parent == "vbaSuppData"
        "mcd" => parent == "mcds"
        _ => false
      })
    } else if root == "settings" {
      if ns == word_math_ns {
        (name == "mathPr" && parent == "settings") ||
        (
          parent == "mathPr" &&
          [
            "mathFont", "brkBin", "brkBinSub", "smallFrac", "dispDef", "lMargin",
            "rMargin", "defJc", "wrapIndent", "intLim", "naryLim",
          ].contains(name)
        )
      } else if ns == word_office_ns {
        (
          ["shapeDefaults", "hdrShapeDefaults"].contains(parent) &&
          ["shapedefaults", "shapelayout"].contains(name)
        ) ||
        (name == "idmap" && parent == "shapelayout")
      } else if ns == word_ns {
        (name == "settings" && node.depth == 0) ||
        (
          parent == "settings" &&
          [
            "zoom", "proofState", "defaultTabStop", "characterSpacingControl", "compat",
            "rsids", "themeFontLang", "clrSchemeMapping", "shapeDefaults", "decimalSymbol",
            "listSeparator", "attachedTemplate", "hyphenationZone", "footnotePr",
            "endnotePr", "removePersonalInformation", "removeDateAndTime", "doNotAutoCompressPictures",
            "doNotDisplayPageBoundaries", "doNotIncludeSubdocsInStats", "hdrShapeDefaults",
          ].contains(name)
        ) ||
        (parent == "footnotePr" && name == "footnote") ||
        (parent == "endnotePr" && name == "endnote") ||
        (parent == "compat" && name == "compatSetting") ||
        (parent == "rsids" && ["rsidRoot", "rsid"].contains(name))
      } else {
        false
      }
    } else if root == "fonts" {
      ns == word_ns &&
      (
        (name == "fonts" && node.depth == 0) ||
        (name == "font" && parent == "fonts") ||
        (
          parent == "font" &&
          ["panose1", "charset", "family", "pitch", "sig", "altName"].contains(
            name,
          )
        )
      )
    } else if root == "webSettings" {
      ns == word_ns &&
      (
        (name == "webSettings" && node.depth == 0) ||
        (
          ["optimizeForBrowser", "divs"].contains(name) &&
          parent == "webSettings"
        ) ||
        (name == "div" && parent == "divs") ||
        (
          ["bodyDiv", "marLeft", "marRight", "marTop", "marBottom", "divBdr"].contains(
            name,
          ) &&
          parent == "div"
        ) ||
        (
          ["top", "left", "bottom", "right"].contains(name) &&
          parent == "divBdr"
        )
      )
    } else {
      (
        ns == word_ns &&
        [
          "styles", "docDefaults", "rPrDefault", "pPrDefault", "rPr", "pPr", "latentStyles",
          "lsdException", "style", "name", "basedOn", "next", "link", "uiPriority",
          "semiHidden", "unhideWhenUsed", "qFormat", "rsid", "rFonts", "lang", "sz",
          "szCs", "spacing", "tblPr", "tblInd", "tblCellMar", "top", "left", "bottom",
          "right", "b", "i", "u", "color", "jc", "ind", "keepNext", "keepLines",
          "widowControl",
        ].contains(name)
      ) ||
      (ns == word_ns && word_extra_layout_element(name, parent))
    }
    guard permitted else {
      raise Refused("unsupported Word XML feature or placement: " + name)
    }
    validate_word_structure_attributes(node, parent, root)
    if (
        ns == word_ns &&
        ["headerReference", "footerReference", "attachedTemplate", "hyperlink"].contains(
          name,
        )
      ) ||
      (ns == drawing_ns && name == "blip") {
      let required = if ns == drawing_ns { "embed" } else { "id" }
      guard node.element.attributes
        .filter(fn(a) {
          a.name.namespace_uri ==
          Some(office_rel[0:office_rel.length() - 1].to_owned()) &&
          a.name.local_name == required
        })
        .length() ==
        1 else {
        raise Refused(
          "Word content reference requires a source-local relationship",
        )
      }
    }
    if ns == word_ns && name == "hyperlink" {
      guard node.text_fragments.all(fn(t) { t.trim() == "" }) &&
        node.element.attributes.all(fn(a) {
          (
            a.name.namespace_uri ==
            Some(office_rel[0:office_rel.length() - 1].to_owned()) &&
            a.name.local_name == "id"
          ) ||
          (
            a.name.namespace_uri == Some(word_ns) &&
            ["history", "tooltip", "tgtFrame"].contains(a.name.local_name)
          )
        }) else {
        raise Refused("unsupported Word click hyperlink content or attributes")
      }
      for attribute in node.element.attributes {
        if attribute.name.namespace_uri == Some(word_ns) &&
          attribute.name.local_name == "history" {
          let value = attribute.value
          guard ["0", "1", "true", "false", "on", "off"].contains(value) else {
            raise Invalid("invalid hyperlink history flag")
          }
        }
      }
    }
    parents.push(name)
    for a in node.element.attributes {
      if word_modern_attribute(part, node, a) {
        continue
      }
      let ans = a.name.namespace_uri.unwrap_or("")
      if ans == office_rel[0:office_rel.length() - 1].to_owned() {
        validate_word_reference_attribute(pkg, part, node, a)
      } else if [word_drawing_ns, picture_ns, drawing_ns].contains(ns) {
        guard ans == "" &&
          (
            passive_drawing_attribute(ns, name, a.name.local_name) ||
            (
              root == "hdr" &&
              ns == picture_ns &&
              name == "spPr" &&
              a.name.local_name == "bwMode" &&
              a.value == "auto"
            )
          ) else {
          raise Refused("unsupported drawing attribute")
        }
        if ns == drawing_ns &&
          name == "graphicData" &&
          a.name.local_name == "uri" {
          guard a.value == picture_ns else {
            raise Refused("non-picture drawing")
          }
        }
      } else if ns == word_office_ns {
        guard (
            ans == "urn:schemas-microsoft-com:vml" &&
            a.name.local_name == "ext" &&
            a.value == "edit"
          ) ||
          (
            ans == "" &&
            (
              (name == "shapedefaults" && a.name.local_name == "spidmax") ||
              (name == "idmap" && a.name.local_name == "data")
            )
          ) else {
          raise Refused("unsupported Office layout attribute")
        }
      } else {
        guard ans == ns ||
          (
            ans == "http://www.w3.org/XML/1998/namespace" &&
            a.name.local_name == "space" &&
            name == "t"
          ) else {
          raise Refused("unsupported Word XML attribute namespace")
        }
        guard !["id", "embed", "link"].contains(a.name.local_name) ||
          (
            ns == word_ns &&
            (
              (root == "styles" && name == "link") ||
              [
                "footnote", "endnote", "footnoteReference", "endnoteReference", "bookmarkStart",
                "bookmarkEnd", "div",
              ].contains(name)
            )
          ) else {
          raise Refused("unsupported Word relationship attribute")
        }
        let permitted_attribute = if ns == word_ns &&
          a.name.local_name == "rsidTr" {
          name == "tr" && parent == "tbl" && hex_text(a.value, 8)
        } else if ns == word_macro_ns {
          ["macroName", "name", "bEncrypt", "cmg"].contains(a.name.local_name)
        } else if ns == word_ns && name == "hyperlink" {
          ["history", "tooltip", "tgtFrame"].contains(a.name.local_name)
        } else if ns == word_math_ns {
          a.name.local_name == "val"
        } else if ns == word_ns &&
          word_structure_attribute(name, a.name.local_name) {
          true
        } else {
          word_attribute_names.contains(a.name.local_name)
        }
        guard permitted_attribute else {
          raise Refused("unsupported Word attribute: " + a.name.local_name)
        }
      }
    }
  }
}

///|
fn word_val_leaf(nodes : Array[Node], ordinal : Int32) -> Bool {
  let node = nodes[ordinal]
  exact_node_attributes(node, Some(word_ns), ["val"]) &&
  empty_node(node) &&
  (ordinal + 1 == nodes.length() || nodes[ordinal + 1].depth <= node.depth)
}

///|
fn checked_word_decimal(
  value : String,
  exclusive : Int32,
  property : String,
) -> Int32 raise {
  checked_index(value, exclusive) catch {
    Invalid(_) => raise Invalid("invalid or out-of-range Word " + property)
    error => raise error
  }
}

///|
fn validate_word_table_style_borders(
  nodes : Array[Node],
  ordinal : Int32,
) -> Int32 raise {
  let container = nodes[ordinal]
  guard container.element.attributes.is_empty() && empty_node(container) else {
    raise Refused("unsupported Word table style borders attributes or text")
  }
  let sides = ["top", "left", "bottom", "right", "insideH", "insideV"]
  let mut last_side = -1
  let mut cursor = ordinal + 1
  while cursor < nodes.length() && nodes[cursor].depth > container.depth {
    let side = nodes[cursor]
    guard side.depth == container.depth + 1 &&
      side.element.name.namespace_uri == Some(word_ns) &&
      sides.search(side.element.name.local_name) is Some(index) &&
      index > last_side &&
      exact_node_attributes(side, Some(word_ns), ["val", "sz", "space", "color"]) &&
      word_attr_opt(side, "val") == Some("single") &&
      empty_node(side) else {
      raise Refused("unsupported, duplicate or out-of-order Word table border")
    }
    if word_attr_opt(side, "sz") is Some(size) {
      guard checked_word_decimal(size, 97, "table border size") >= 2 else {
        raise Invalid("Word table border size out of range")
      }
    }
    if word_attr_opt(side, "space") is Some(space) {
      ignore(checked_word_decimal(space, 32, "table border spacing"))
    }
    if word_attr_opt(side, "color") is Some(color) {
      guard color == "auto" || hex_text(color, 6) else {
        raise Invalid("invalid Word table border color")
      }
    }
    last_side = index
    cursor += 1
  }
  guard last_side >= 0 else { raise Refused("empty Word table style borders") }
  cursor
}