///|
/// Stack mutations route through these wrappers so cheap open-element
/// counters stay accurate; the hot index walks early-out on them.
fn Parser::track_open_element(
self : Parser,
node : @dom.Node,
delta : Int,
) -> Unit {
if node.kind != Element {
return
}
let len = node.name.length()
if len == 8 &&
node.name == "template" &&
(node.ns is None || node.ns is Some("html")) {
self.open_template_count += delta
} else if len == 5 && node.name == "table" {
self.open_table_count += delta
} else if len == 6 && node.name == "select" && node.ns is Some("html") {
// Mirrors the has_open_html_select predicate exactly: the count is
// used as the boolean answer, not just as an early-out.
self.open_select_count += delta
}
}
///|
fn Parser::push_open_element(self : Parser, node : @dom.Node) -> Unit {
self.track_open_element(node, 1)
self.stack.push(node)
}
///|
fn Parser::pop_open_element(self : Parser) -> @dom.Node? {
// Return the popped option as-is instead of rebuilding a new Some.
let popped = self.stack.pop()
if popped is Some(node) {
self.track_open_element(node, -1)
}
popped
}
///|
fn Parser::remove_open_element_at(self : Parser, index : Int) -> @dom.Node {
let node = self.stack.remove(index)
self.track_open_element(node, -1)
node
}
///|
fn Parser::insert_open_element_at(
self : Parser,
index : Int,
node : @dom.Node,
) -> Unit {
self.track_open_element(node, 1)
self.stack.insert(index, node)
}
///|
fn Parser::replace_open_element_at(
self : Parser,
index : Int,
node : @dom.Node,
) -> Unit {
self.track_open_element(self.stack[index], -1)
self.track_open_element(node, 1)
self.stack[index] = node
}
///|
fn Parser::clear_open_elements(self : Parser) -> Unit {
self.stack.clear()
self.open_template_count = 0
self.open_table_count = 0
self.open_select_count = 0
}
///|
fn Parser::pop_until(
self : Parser,
name : String,
pos : Int,
source_end_tag? : String,
) -> Unit {
if !self.end_tag_target_visible_for_pop(name) {
self.error_at("unexpected-end-tag", pos)
return
}
if (!is_special_element_name(name) || special_name_without_end_tag_rule(name)) &&
!active_formatting_element_name(name) &&
self.special_blocks_end_tag(name) {
// Names with dedicated end-tag rules (div, ul, ...) close through
// their scope, and formatting end tags use the adoption agency
// rules; only the any-other-end-tag walk stops at a special element.
// Special elements without a dedicated end-tag rule (menuitem,
// iframe, the voids) also take the any-other-end-tag walk.
self.error_at("unexpected-end-tag", pos)
return
}
while self.stack.length() > 1 {
match self.stack.last() {
Some(node) if node.kind == Element &&
node.name != name &&
is_implied_end_tag_name(node.name) => ignore(self.pop_open_element())
_ => break
}
}
match self.stack.last() {
Some(node) if node.kind == Element && names_match_end_tag(node, name) => ()
_ => self.error_at("end-tag-too-early", pos)
}
while self.stack.length() > 1 {
let node = self.pop_open_element().unwrap()
if node.kind == Element && names_match_end_tag(node, name) {
if source_end_tag is Some(raw) {
set_node_source_end_tag(node, raw)
}
break
}
}
}
///|
fn is_implied_end_tag_name(name : StringView) -> Bool {
match name {
"dd"
| "dt"
| "li"
| "option"
| "optgroup"
| "p"
| "rb"
| "rp"
| "rt"
| "rtc" => true
_ => false
}
}
///|
fn Parser::generate_implied_end_tags(self : Parser, exclude : String?) -> Unit {
while self.stack.length() > 1 {
match self.stack.last() {
Some(node) if node.kind == Element &&
is_implied_end_tag_name(node.name) &&
!(exclude is Some(excluded) && node.name == excluded) =>
ignore(self.pop_open_element())
_ => break
}
}
}
///|
///|
/// True when an element with this name is on the stack above the nearest
/// open HTML template. Template contents are a separate tree: end tags
/// inside a template must not see (or close) elements outside it.
fn Parser::end_tag_target_visible(self : Parser, name : String) -> Bool {
let mut index = self.stack.length()
while index > 0 {
index -= 1
let node = self.stack[index]
if node.kind == Element && node.name == name {
return true
}
if name != "template" &&
node.kind == Element &&
node.name == "template" &&
(node.ns is None || node.ns is Some("html")) {
return false
}
}
false
}
///|
/// The spec's "special" element category (HTML namespace), used by the
/// any-other-end-tag rule: walking the open-elements stack, a special
/// element encountered before the target means the end tag is ignored.
fn is_special_element_name(name : StringView) -> Bool {
match name {
"address"
| "applet"
| "area"
| "article"
| "aside"
| "base"
| "basefont"
| "bgsound"
| "blockquote"
| "body"
| "br"
| "button"
| "caption"
| "center"
| "col"
| "colgroup"
| "dd"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "dt"
| "embed"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "form"
| "frame"
| "frameset"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "head"
| "header"
| "hgroup"
| "hr"
| "html"
| "iframe"
| "img"
| "input"
| "keygen"
| "li"
| "link"
| "listing"
| "main"
| "marquee"
| "menu"
| "menuitem"
| "meta"
| "nav"
| "noembed"
| "noframes"
| "noscript"
| "object"
| "ol"
| "p"
| "param"
| "plaintext"
| "pre"
| "script"
| "search"
| "section"
| "select"
| "source"
| "style"
| "summary"
| "table"
| "tbody"
| "td"
| "template"
| "textarea"
| "tfoot"
| "th"
| "thead"
| "title"
| "tr"
| "track"
| "ul"
| "wbr" => true
_ => false
}
}
///|
fn Parser::close_open_element(self : Parser, name : String) -> Bool {
if !self.end_tag_target_visible(name) {
return false
}
while self.stack.length() > 1 {
let node = self.pop_open_element().unwrap()
if node.kind == Element && node.name == name {
break
}
}
true
}
///|
fn Parser::pop_current_element_if_name(self : Parser, name : String) -> Bool {
match self.stack.last() {
Some(node) if node.kind == Element && node.name == name => {
ignore(self.pop_open_element())
true
}
_ => false
}
}
///|
fn Parser::close_current_element_if_name(self : Parser, name : String) -> Unit {
ignore(self.pop_current_element_if_name(name))
}
///|
fn Parser::last_stack_index_of(self : Parser, name : String) -> Int {
let mut index = self.stack.length()
while index > 0 {
index -= 1
let node = self.stack[index]
if node.kind == Element && node.name == name {
return index
}
}
-1
}
///|
fn Parser::last_stack_index_of_node(self : Parser, target : @dom.Node) -> Int {
let mut index = self.stack.length()
while index > 0 {
index -= 1
if physical_equal(self.stack[index], target) {
return index
}
}
-1
}
///|
fn Parser::open_html_element(self : Parser) -> @dom.Node? {
for node in self.stack {
if node.kind == Element && node.name == "html" {
return Some(node)
}
}
document_html_element(self.root)
}
///|
fn Parser::stack_contains_node(self : Parser, target : @dom.Node) -> Bool {
self.stack[:].any(node => physical_equal(node, target))
}
///|
fn Parser::remove_open_node(self : Parser, target : @dom.Node) -> Bool {
let mut index = 0
while index < self.stack.length() {
if physical_equal(self.stack[index], target) {
ignore(self.remove_open_element_at(index))
return true
}
index += 1
}
false
}
///|
fn Parser::insert_element_without_push(
self : Parser,
name : StringView,
attrs : Map[String, String?],
start? : Int,
) -> Unit {
let node = match start {
Some(offset) => self.node_with_origin(@dom.element(name, attrs~), offset)
None => @dom.element(name, attrs~)
}
self.current_node().append_child(node)
}
///|
fn Parser::insert_element_maybe_push(
self : Parser,
name : StringView,
attrs : Map[String, String?],
push : Bool,
start? : Int,
) -> Unit {
let node = match start {
Some(offset) => self.node_with_origin(@dom.element(name, attrs~), offset)
None => @dom.element(name, attrs~)
}
self.current_node().append_child(node)
if push {
self.push_open_element(node)
}
}
///|
/// Like end_tag_target_visible, but matches the same name rules as
/// pop_until (names_match_end_tag).
fn Parser::end_tag_target_visible_for_pop(self : Parser, name : String) -> Bool {
let mut index = self.stack.length()
while index > 0 {
index -= 1
let node = self.stack[index]
if node.kind == Element && names_match_end_tag(node, name) {
return true
}
// The dedicated block end tags only close an element in (default)
// scope: applet-like elements, table cells, templates, and the
// foreign integration points terminate the search, so e.g.
// cannot pop through an open