///|
fn Parser::parse_comment(self : Parser) -> Unit {
  let comment_start = self.pos
  self.pos += 4
  let scan = @tok.scan_comment_data(self.input, self.pos)
  for error in scan.errors {
    self.error_at(error.code, error.offset)
  }
  self.pos = scan.end
  if scan.eof {
    self.append_sanitize_escape_source_text(comment_start, self.pos)
  }
  let data = if self.xml_coercion {
    coerce_comment_for_xml(scan.data)
  } else {
    scan.data
  }
  self.current_node().append_child(self.comment_at(data, comment_start))
}

///|
fn Parser::parse_bogus_comment(self : Parser, origin_start : Int) -> Unit {
  let data_start = self.pos
  while !self.is_eof() && self.current() != Some('>') {
    ignore(self.advance_char())
  }
  let view = self.input[data_start:self.pos]
  let data = normalize_bogus_comment_data(view)
  let data = if self.xml_coercion { coerce_comment_for_xml(data) } else { data }
  self.current_node().append_child(self.comment_at(data, origin_start))
  if self.current() == Some('>') {
    ignore(self.advance_char())
  }
}

///|
fn Parser::parse_markup_declaration(self : Parser) -> Unit {
  let origin_start = self.pos
  self.pos += 2
  if self.starts_with("[CDATA[") {
    if self.current_node_is_foreign_namespace() {
      self.parse_foreign_cdata_section()
      return
    }
    self.error_at("cdata-in-html-content", self.pos + "[CDATA[".length() - 1)
  } else {
    self.error_at("incorrectly-opened-comment", self.pos - 1)
  }
  self.parse_bogus_comment(origin_start)
  self.append_sanitize_escape_source_text(origin_start, self.pos)
}

///|
fn Parser::parse_foreign_cdata_section(self : Parser) -> Unit {
  self.pos += "[CDATA[".length()
  let start = self.pos
  while !self.is_eof() && !self.starts_with("]]>") {
    ignore(self.advance_char())
  }
  let view = self.input[start:self.pos]
  let cleaned = if self.current_node_uses_foreign_content_for_text() {
    self.clean_foreign_content_text(view, start)
  } else {
    self.remove_null_characters(view, start)
  }
  if !cleaned.is_empty() {
    let cleaned = normalize_tree_builder_text(cleaned)
    let data = if self.xml_coercion {
      coerce_text_for_xml(cleaned)
    } else {
      cleaned
    }
    self.current_node().append_child(self.text_at(data, start))
  }
  if self.starts_with("]]>") {
    self.pos += 3
  }
}

///|
fn Parser::parse_doctype(self : Parser) -> Unit {
  let doctype_start = self.pos
  self.pos += "') | None => None
    Some(ch) if ch.is_ascii_whitespace() => {
      self.skip_ascii_whitespace()
      None
    }
    _ => Some(self.pos)
  }
  let name_start = self.pos
  while !self.is_eof() {
    let ch = self.current().unwrap()
    if ch == '>' || ch.is_ascii_whitespace() {
      break
    } else {
      ignore(self.advance_char())
    }
  }
  let (name, name_errors) = @tok.parse_lower_name_value_replacing_nulls(
    self.input,
    name_start,
    self.pos,
  )
  let ids_start = self.pos
  while !self.is_eof() && !self.starts_with(">") {
    ignore(self.advance_char())
  }
  let external = @tok.parse_doctype_external(
    self.input[ids_start:self.pos],
    self.starts_with(">"),
  )
  let public_id = external.public_id
  let system_id = external.system_id
  let has_external_error = !external.errors.is_empty()
  let eof_doctype = name != "" && !has_external_error && !self.starts_with(">")
  let error_pos = if self.starts_with(">") {
    self.pos
  } else {
    self.eof_error_pos()
  }
  let force_quirks = name == "" || external.force_quirks || eof_doctype
  match missing_before_name {
    Some(pos) => self.error_at("missing-whitespace-before-doctype-name", pos)
    None => ()
  }
  for error in name_errors {
    self.error_at(error.code, error.offset)
  }
  if name == "" {
    if self.starts_with(">") {
      self.error_at("expected-doctype-name-but-got-right-bracket", error_pos)
    } else {
      self.error_at_eof("eof-in-doctype")
    }
  } else {
    for error in external.errors {
      self.error_at(error.code, ids_start + error.offset)
    }
  }
  if eof_doctype {
    self.error_at_eof("eof-in-doctype")
  }
  if self.starts_with(">") {
    self.pos += 1
  }
  if self.root.kind == Fragment {
    self.error_at("unexpected-doctype", error_pos)
    self.append_sanitize_escape_source_text(doctype_start, self.pos)
    return
  }
  if !self.document_doctype_is_initial() {
    self.error_at("unexpected-doctype", error_pos)
    return
  }
  if !is_known_doctype(name, public_id, system_id) {
    self.error_at("unknown-doctype", error_pos)
  }
  let doctype_node = match (public_id, system_id) {
    (Some(public_id), Some(system_id)) =>
      @dom.doctype(name~, public_id~, system_id~, force_quirks~)
    (Some(public_id), None) => @dom.doctype(name~, public_id~, force_quirks~)
    (None, Some(system_id)) => @dom.doctype(name~, system_id~, force_quirks~)
    (None, None) => @dom.doctype(name~, force_quirks~)
  }
  self.root.append_child(self.node_with_origin(doctype_node, doctype_start))
}

///|
fn is_known_doctype(
  name : StringView,
  public_id : String?,
  system_id : String?,
) -> Bool {
  if name != "html" {
    return false
  }
  match (public_id, system_id) {
    (None, None) => true
    (None, Some("about:legacy-compat")) => true
    (Some("-//W3C//DTD HTML 4.0//EN"), None) => true
    (
      Some("-//W3C//DTD HTML 4.0//EN"),
      Some("http://www.w3.org/TR/REC-html40/strict.dtd"),
    ) => true
    (Some("-//W3C//DTD HTML 4.01//EN"), None) => true
    (
      Some("-//W3C//DTD HTML 4.01//EN"),
      Some("http://www.w3.org/TR/html4/strict.dtd"),
    ) => true
    (
      Some("-//W3C//DTD XHTML 1.0 Strict//EN"),
      Some("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"),
    ) => true
    (
      Some("-//W3C//DTD XHTML 1.1//EN"),
      Some("http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"),
    ) => true
    _ => false
  }
}