///|
fn is_document_head_element_name(name : StringView) -> Bool {
  match name {
    "base"
    | "basefont"
    | "bgsound"
    | "link"
    | "meta"
    | "noframes"
    | "noscript"
    | "script"
    | "style"
    | "template"
    | "title" => true
    _ => false
  }
}

///|
fn Parser::document_doctype_is_initial(self : Parser) -> Bool {
  for child in self.root.children {
    if child.kind == Comment {
      ()
    } else if child.kind == Text && is_whitespace_text_node(child) {
      ()
    } else {
      return false
    }
  }
  true
}

///|
fn scaffold_document(
  root : @dom.Node,
  foster_parented_texts : Array[@dom.Node],
  forced_body_nodes : Array[@dom.Node],
  after_body_comments : Array[@dom.Node],
  after_html_comments : Array[@dom.Node],
  after_head_whitespace : Array[@dom.Node],
) -> Unit {
  match document_html_element(root) {
    Some(html) => {
      // Routed comments may sit at the root (parsed before a later
      // explicit ) or inside the html element; strip them from both
      // places and re-attach from the recorded arrays so the original
      // comment order is preserved.
      strip_routed_comments(root, after_body_comments, after_html_comments)
      strip_routed_comments(html, after_body_comments, after_html_comments)
      absorb_root_html_siblings(
        root,
        html,
        forced_body_nodes,
        after_head_whitespace~,
      )
      scaffold_existing_html_element(
        html,
        foster_parented_texts,
        forced_body_nodes,
        after_head_whitespace~,
      )
      append_children_to(html, after_body_comments)
      append_children_to(root, after_html_comments)
      return
    }
    None => ()
  }
  let original = root.children.copy()
  root.children.clear()
  let mut head : @dom.Node? = None
  let mut body : @dom.Node? = None
  let mut frameset : @dom.Node? = None
  let between_head_body_children : Array[@dom.Node] = []
  let head_children : Array[@dom.Node] = []
  let body_children : Array[@dom.Node] = []
  let after_frameset_children : Array[@dom.Node] = []
  let mut body_started = false
  for child in original {
    if child.kind == Doctype {
      root.append_child(child)
    } else if child.kind == Element &&
      child.name == "head" &&
      head is None &&
      !body_started {
      head = Some(child)
    } else if child.kind == Element && child.name == "body" && body is None {
      body = Some(child)
      body_started = true
      append_body_node_contents(child, child, body_children)
    } else if child.kind == Element && child.name == "body" {
      append_body_node_contents(body.unwrap(), child, body_children)
      body_started = true
    } else if child.kind == Element &&
      child.name == "frameset" &&
      body is None &&
      frameset is None &&
      !body_started {
      frameset = Some(child)
      body_started = true
    } else if frameset is Some(_) && body is None {
      after_frameset_children.push(child)
    } else if child.kind == Comment &&
      (
        nodes_contain(after_body_comments, child) ||
        nodes_contain(after_html_comments, child)
      ) {
      // Routed at parse time: re-attached after the body (or after the
      // html element) during assembly below.
      ()
    } else if child.kind == Comment && !body_started && head is Some(_) {
      between_head_body_children.push(child)
    } else if child.kind == Comment && !body_started && head_children.is_empty() {
      root.append_child(child)
    } else if child.kind == Element && child.name == "head" {
      let existing = child.children.copy()
      for grandchild in existing {
        body_children.push(grandchild)
      }
      body_started = true
    } else if child.kind == Element &&
      is_document_head_element_name(child.name) &&
      !body_started &&
      !nodes_contain(forced_body_nodes, child) {
      head_children.push(child)
    } else if child.kind == Comment && !body_started {
      head_children.push(child)
    } else if is_whitespace_text_node(child) &&
      !body_started &&
      !nodes_contain(forced_body_nodes, child) {
      if head is Some(_) || nodes_contain(after_head_whitespace, child) {
        between_head_body_children.push(child)
      } else if !head_children.is_empty() {
        head_children.push(child)
      }
    } else if child.kind == Text && !body_started {
      if nodes_contain(foster_parented_texts, child) ||
        nodes_contain(forced_body_nodes, child) {
        // Foster-parented table text was produced after body content
        // started (a table is open); keep it verbatim, including leading
        // whitespace.
        body_started = true
        body_children.push(child)
      } else if head is None && !head_children.is_empty() {
        let (leading, rest) = split_text_node_leading_ascii_whitespace(child)
        if leading is Some(node) {
          head_children.push(node)
        }
        body_started = true
        body_children.push(rest.unwrap())
      } else if head is None {
        body_started = true
        body_children.push(
          text_node_without_leading_ascii_whitespace(child).unwrap(),
        )
      } else {
        body_started = true
        body_children.push(child)
      }
    } else {
      body_started = true
      body_children.push(child)
    }
  }
  let html = @dom.element("html")
  let head_node = head.unwrap_or(@dom.element("head"))
  html.append_child(head_node)
  append_children_to(head_node, head_children)
  append_children_to(html, between_head_body_children)
  match frameset {
    Some(frameset_node) if body is None && body_children.is_empty() => {
      html.append_child(frameset_node)
      append_children_to(html, after_frameset_children)
    }
    _ => {
      let body_node = body.unwrap_or(@dom.element("body"))
      append_children_to(body_node, body_children)
      html.append_child(body_node)
    }
  }
  append_children_to(html, after_body_comments)
  root.append_child(html)
  append_children_to(root, after_html_comments)
}

///|
fn append_post_body_html_children(
  root : @dom.Node,
  children : Array[@dom.Node],
) -> Unit {
  if children.is_empty() {
    return
  }
  match document_html_element(root) {
    Some(html) => append_children_to(html, children)
    None => append_children_to(root, children)
  }
}

///|
fn nodes_contain(nodes : Array[@dom.Node], node : @dom.Node) -> Bool {
  nodes[:].any(candidate => physical_equal(candidate, node))
}

///|
fn strip_routed_comments(
  parent : @dom.Node,
  after_body_comments : Array[@dom.Node],
  after_html_comments : Array[@dom.Node],
) -> Unit {
  let kept : Array[@dom.Node] = []
  for child in parent.children {
    if child.kind == Comment &&
      (
        nodes_contain(after_body_comments, child) ||
        nodes_contain(after_html_comments, child)
      ) {
      child.parent = None
    } else {
      kept.push(child)
    }
  }
  if kept.length() != parent.children.length() {
    parent.children.clear()
    append_children_to(parent, kept)
  }
}