///|
priv struct Parser {
  input : StringView
  scripting_enabled : Bool
  xml_coercion : Bool
  track_node_locations : Bool
  mut pos : Int
  errors : Array[@core.ParseError]
  root : @dom.Node
  stack : Array[@dom.Node]
  mut form_element : @dom.Node?
  mut frameset_ok : Bool
  mut frameset_reprocesses_in_body_mode : Bool
  mut detached_frameset_mode : Bool
  mut before_html_phase : Bool
  mut after_frameset : Bool
  mut document_after_body : Bool
  mut select_insertion_mode : Bool
  mut foreign_table_row_recovery_mode : Bool
  foster_parented_start_error_nodes : Array[@dom.Node]
  mut fragment_context_table : @dom.Node?
  mut fragment_context_table_section : @dom.Node?
  mut fragment_context_table_row : @dom.Node?
  mut fragment_context_table_cell : @dom.Node?
  mut fragment_context_caption : Bool
  mut fragment_context_select : Bool
  mut open_template_count : Int
  mut open_table_count : Int
  mut open_select_count : Int
  mut fragment_context_colgroup : Bool
  mut fragment_context_html : Bool
  mut fragment_context_html_root : @dom.Node?
  mut fragment_context_html_after_body : Bool
  mut fragment_context_foreign : @dom.Node?
  template_column_group_templates : Array[@dom.Node]
  template_table_context_templates : Array[@dom.Node]
  mut template_table_context_detached_mode : Bool
  mut template_detached_column_group_mode : Bool
  mut deferred_initial_doctype_error : @core.ParseError?
  mut duplicate_head_element : @dom.Node?
  mut repeated_document_html_shell : Bool
  pending_document_html_attrs : Map[String, String?]
  pending_document_body_attrs : Map[String, String?]
  mut pending_table_view_implied_end_tag : Bool
  foster_parented_texts : Array[@dom.Node]
  forced_body_nodes : Array[@dom.Node]
  mut document_after_html : Bool
  mut end_tag_was_after_body : Bool
  mut document_entered_body : Bool
  iframe_srcdoc : Bool
  mut doctype_quirks_mode : Bool
  mut fragment_context_html_entered_body : Bool
  mut pending_initial_lf_skip : Bool
  mut document_head_closed : Bool
  mut document_entered_body_from_after_body : Bool
  after_head_whitespace : Array[@dom.Node]
  template_content_modes : Array[(@dom.Node, String)]
  after_body_comments : Array[@dom.Node]
  after_html_comments : Array[@dom.Node]
  mut pending_text_node : @dom.Node?
  mut pending_text_builder : StringBuilder
  mut pending_text_is_whitespace : Bool
  post_body_html_children : Array[@dom.Node]
  active_formatting : Array[FormattingEntry]
}

///|
priv struct FormattingEntry {
  marker : Bool
  name : String
  ns : String
  attrs : Map[String, String?]
  node : @dom.Node
}

///|
fn Parser::Parser(
  input : StringView,
  root : @dom.Node,
  scripting_enabled~ : Bool,
  xml_coercion~ : Bool,
  track_node_locations? : Bool = false,
  iframe_srcdoc? : Bool = false,
) -> Parser {
  let input = strip_leading_bom(input)
  {
    input,
    scripting_enabled,
    xml_coercion,
    track_node_locations,
    iframe_srcdoc,
    pos: 0,
    errors: [],
    root,
    stack: [root],
    form_element: None,
    frameset_ok: root.kind == Document,
    frameset_reprocesses_in_body_mode: false,
    detached_frameset_mode: false,
    before_html_phase: root.kind == Document,
    after_frameset: false,
    document_after_body: false,
    select_insertion_mode: false,
    foreign_table_row_recovery_mode: false,
    foster_parented_start_error_nodes: [],
    fragment_context_table: None,
    fragment_context_table_section: None,
    fragment_context_table_row: None,
    fragment_context_table_cell: None,
    fragment_context_caption: false,
    fragment_context_select: false,
    open_template_count: 0,
    open_table_count: 0,
    open_select_count: 0,
    fragment_context_colgroup: false,
    fragment_context_html: false,
    fragment_context_html_root: None,
    fragment_context_html_after_body: false,
    fragment_context_foreign: None,
    template_column_group_templates: [],
    template_table_context_templates: [],
    template_table_context_detached_mode: false,
    template_detached_column_group_mode: false,
    deferred_initial_doctype_error: None,
    duplicate_head_element: None,
    repeated_document_html_shell: false,
    pending_document_html_attrs: Map([]),
    pending_document_body_attrs: Map([]),
    pending_table_view_implied_end_tag: false,
    foster_parented_texts: [],
    forced_body_nodes: [],
    document_after_html: false,
    end_tag_was_after_body: false,
    document_entered_body: false,
    doctype_quirks_mode: true,
    fragment_context_html_entered_body: false,
    pending_initial_lf_skip: false,
    document_head_closed: false,
    document_entered_body_from_after_body: false,
    after_head_whitespace: [],
    template_content_modes: [],
    after_body_comments: [],
    after_html_comments: [],
    pending_text_node: None,
    pending_text_builder: StringBuilder(),
    pending_text_is_whitespace: true,
    post_body_html_children: [],
    active_formatting: [],
  }
}

///|
fn Parser::current(self : Parser) -> Char? {
  self.input.get_char(self.pos)
}

///|
fn Parser::is_eof(self : Parser) -> Bool {
  self.pos >= self.input.length()
}

///|
fn Parser::advance_char(self : Parser) -> Char? {
  match self.current() {
    Some(ch) => {
      self.pos += ch.utf16_len()
      Some(ch)
    }
    None => None
  }
}

///|
fn Parser::starts_with(self : Parser, prefix : StringView) -> Bool {
  let end = self.pos + prefix.length()
  match self.input.get_view(start=self.pos, end~) {
    Some(view) => view == prefix
    None => false
  }
}

///|
fn Parser::skip_ascii_whitespace(self : Parser) -> Unit {
  // Direct code-unit scan: ASCII whitespace never matches inside a
  // surrogate pair.
  let input = self.input
  let input_length = input.length()
  while self.pos < input_length {
    match input[self.pos] {
      ' ' | '\t' | '\n' | '\u{000B}' | '\u{000C}' | '\r' => self.pos += 1
      _ => return
    }
  }
}

///|
fn Parser::current_node(self : Parser) -> @dom.Node {
  self.stack.last().unwrap_or(self.root)
}