///|
fn Parser::insert_normal_start_tag(
  self : Parser,
  name : String,
  attrs : Map[String, String?],
  self_closing : Bool,
  start : Int32,
  tag_close_pos : Int32,
) -> Unit {
  let node_ns = self.start_tag_namespace(name)
  if node_ns == "html" {
    self.prepare_active_formatting_start(name, start)
  }
  if !start_tag_closes_p(name) &&
    !start_tag_uses_in_head_rules(name) &&
    !self.table_context_start_skips_active_reconstruction(name) {
    self.reconstruct_active_formatting_elements()
  }
  let adjusted_name = if node_ns == "html" {
    if name == "image" {
      self.error_at("image-start-tag", start)
      "img"
    } else {
      name
    }
  } else {
    adjusted_start_tag_name_for_namespace(node_ns, name)
  }
  let adjusted_attrs = if node_ns == "html" {
    attrs
  } else {
    prepare_foreign_attrs(node_ns, attrs)
  }
  let node = self.node_with_start_tag_source(
    @dom.element(adjusted_name, attrs=adjusted_attrs, ns=node_ns),
    start,
    tag_close_pos,
  )
  self.track_form_start_element(adjusted_name, node)
  self.append_start_tag_node(node, adjusted_name, start)
  if node_ns == "html" && adjusted_name == "select" {
    self.select_insertion_mode = true
  }
  let self_closes_tree = if node_ns == "html" {
    self_closing && @ser.is_void_element(adjusted_name)
  } else {
    self_closing
  }
  let inserts_empty_nonvoid = self.start_tag_inserts_empty_nonvoid(
    adjusted_name,
  )
  if node_ns == "html" &&
    !self_closes_tree &&
    self.is_special_text_element(adjusted_name) {
    self.consume_special_element_content(node, adjusted_name)
  } else if !self_closes_tree &&
    !@ser.is_void_element(adjusted_name) &&
    !inserts_empty_nonvoid {
    self.push_open_element(node)
    if node_ns == "html" && active_formatting_element_name(adjusted_name) {
      let duplicate_index = self.active_formatting_duplicate_index(
        adjusted_name,
        node.attrs,
      )
      if duplicate_index >= 0 {
        self.remove_active_formatting_index(duplicate_index)
      }
      self.append_active_formatting_entry(node)
    }
    if node_ns == "html" && active_formatting_marker_element_name(adjusted_name) {
      self.push_active_formatting_marker()
    }
    if node_ns == "html" && start_tag_ignores_initial_lf(adjusted_name) {
      // A literal newline is skipped here; an entity-encoded one decodes
      // later, so the text path drops it via the pending flag.
      self.pending_initial_lf_skip = self.current() is Some('&')
      self.skip_one_initial_lf()
    }
  }
}

///|
/// In-body start tags handled by the in-head rules insert without
/// reconstructing active formatting.
fn start_tag_uses_in_head_rules(name : StringView) -> Bool {
  match name {
    "base"
    | "basefont"
    | "bgsound"
    | "link"
    | "meta"
    | "noframes"
    | "script"
    | "style"
    | "template"
    | "title" => true
    _ => false
  }
}

///|
/// Append `node`, which the parser has just created, to `parent`.
///
/// Equivalent to `parent.append_child(node)` for a node that has no parent,
/// no children and no template contents, but without its O(depth) walk up
/// `parent`'s ancestors, which made parsing deeply nested markup quadratic.
/// The walk rejects appends that would create a cycle; every node in a tree
/// the parser builds is reachable from its parent's child list or template
/// contents, so a node with neither cannot be an ancestor of `parent`.
/// Anything else, including an HTML `template` parent (whose children go to
/// its contents), takes the `append_child` path.
fn append_new_node(parent : @dom.Node, node : @dom.Node) -> Unit {
  if node.parent is None &&
    node.children.is_empty() &&
    node.template_contents is None &&
    !physical_equal(parent, node) &&
    (
      parent.kind is (Document | Fragment) ||
      table_normalize_target_is_plain(parent)
    ) {
    node.parent = Some(parent)
    parent.children.push(node)
  } else {
    parent.append_child(node)
  }
}