fix(selectors): honor heading offsets and resets

This commit is contained in:
ldm0
2026-09-27 19:22:01 +08:00
parent d63c7adaf7
commit 8d64772cce
13 changed files with 557 additions and 41 deletions
@@ -6904,6 +6904,7 @@ html/semantics/scripting-1/the-template-element/template-element/template-descen
html/semantics/scripting-1/the-template-element/template-element/template-element-clone-into-inactive-document-crash.html
html/semantics/scripting-1/the-template-element/template-element/template-set-inner-html-in-inactive-document-crash.html
html/semantics/scripting-1/the-template-element/template-table-crash.html
html/semantics/sections/headingoffset-and-headingreset.html
html/semantics/selectors/pseudo-classes/checked-type-change.html
html/semantics/selectors/pseudo-classes/checked.html
html/semantics/selectors/pseudo-classes/default.html
+50
View File
@@ -230,6 +230,27 @@ impl Element {
self.attribute_ns(namespace, local_name).is_some()
}
/// Returns the reflected `headingOffset` value.
///
/// HTML defines this unsigned-long reflection with `ReflectRange=(0, 8)`:
/// malformed and negative values fall back to zero, while larger parsed
/// values are clamped to eight.
pub fn heading_offset(&self) -> u32 {
self.attribute("headingoffset")
.and_then(parse_html_non_negative_integer)
.map(|value| value.min(8))
.unwrap_or(0)
}
/// Returns the reflected `headingReset` state.
///
/// Modal dialogs are heading-offset boundaries even without an explicit
/// `headingreset` content attribute.
pub fn heading_reset(&self) -> bool {
self.has_attribute("headingreset")
|| (self.is_html_element("dialog") && self.dialog_modal())
}
pub fn custom_element_state(&self) -> CustomElementState {
self.rare_data.custom_element_state()
}
@@ -1368,6 +1389,35 @@ impl Element {
}
}
fn parse_html_non_negative_integer(value: &str) -> Option<u32> {
let mut chars = value
.chars()
.skip_while(|ch| matches!(ch, '\t' | '\n' | '\u{000c}' | '\r' | ' '));
let negative = match chars.clone().next() {
Some('+') => {
chars.next();
false
}
Some('-') => {
chars.next();
true
}
_ => false,
};
let mut value = 0_u32;
let mut had_digit = false;
for ch in chars {
let Some(digit) = ch.to_digit(10) else {
break;
};
had_digit = true;
value = value.saturating_mul(10).saturating_add(digit);
}
(had_digit && (!negative || value == 0)).then_some(value)
}
fn utf16_units_contain_unpaired_surrogate(units: &[u16]) -> bool {
let mut index = 0;
while index < units.len() {
+41
View File
@@ -367,6 +367,47 @@ fn input_type_change_sanitizes_without_dirtying_default_value() {
}
#[test]
fn heading_reflections_parse_range_and_modal_state() {
let mut heading = Element::new_html("h1");
assert_eq!(heading.heading_offset(), 0);
assert!(!heading.heading_reset());
for (raw, expected) in [
("3", 3),
(" +7tail", 7),
("20", 8),
("429496729600", 8),
("-0", 0),
("-3", 0),
("invalid", 0),
("\u{000b}7", 0),
] {
assert!(heading.set_attribute(
"headingoffset".to_owned(),
String::new(),
None,
raw.to_owned(),
));
assert_eq!(heading.heading_offset(), expected, "{raw:?}");
}
assert!(heading.set_attribute(
"headingreset".to_owned(),
String::new(),
None,
String::new(),
));
assert!(heading.heading_reset());
let mut dialog = Element::new_html("dialog");
assert!(!dialog.heading_reset());
assert!(dialog.set_dialog_modal(true));
assert!(dialog.heading_reset());
assert!(dialog.set_dialog_modal(false));
assert!(!dialog.heading_reset());
}
#[test]
fn script_element_state_distinguishes_dynamic_and_parser_created_scripts() {
let dynamic = Element::new_html("script");
@@ -8,6 +8,7 @@ use crate::{
util::{utf16_split_units_lossy, utf16_units},
};
use moli_dom::native::{Element, Node, NodeType};
use moli_selector::stylo_flat_tree_heading_descendants;
use super::*;
@@ -1470,6 +1471,17 @@ impl DocumentRuntime {
self.push_validity_container_impacts(descendant, &mut impacts);
}
}
if matches!(attribute_name, "headingoffset" | "headingreset")
&& state.intersects(StyloElementState::HEADING_LEVEL_BITS)
{
for descendant in stylo_flat_tree_heading_descendants(&self.dom_host, handle) {
push_state_impact(
&mut impacts,
descendant,
StyloElementState::HEADING_LEVEL_BITS,
);
}
}
impacts
}
@@ -2347,6 +2359,9 @@ fn style_state_impact_for_attribute(name: &str) -> StyloElementState {
| StyloElementState::HAS_DIR_ATTR_RTL
| StyloElementState::HAS_DIR_ATTR_LIKE_AUTO;
}
if name == "headingoffset" || name == "headingreset" {
return StyloElementState::HEADING_LEVEL_BITS;
}
StyloElementState::empty()
}
+100 -2
View File
@@ -17,8 +17,7 @@ use super::node::{
throw_incompatible_getter_receiver, throw_incompatible_method_receiver,
throw_incompatible_setter_receiver,
};
use crate::document_runtime::DomHandle;
use crate::web_api_interfaces;
use crate::{document_runtime::DomHandle, web_api_interfaces, webidl};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiTemplateValue};
mod activation;
@@ -694,6 +693,20 @@ struct ElementPrototypeReflectionDeclaration {
namespace_uri: (),
#[webapi(accessor_property, enumerable, getter = element_prefix_getter_function)]
prefix: (),
#[webapi(
accessor_property = "headingOffset",
enumerable,
getter = element_heading_offset_getter_function,
setter = element_heading_offset_setter_function
)]
heading_offset: (),
#[webapi(
accessor_property = "headingReset",
enumerable,
getter = element_heading_reset_getter_function,
setter = element_heading_reset_setter_function
)]
heading_reset: (),
#[webapi(
accessor_property = "innerHTML",
enumerable,
@@ -6714,6 +6727,91 @@ fn element_id_getter_function<'s>(
set_element_string_return_value(scope, &mut rv, &value);
}
fn element_heading_offset_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some((runtime_ptr, handle)) = element_getter_receiver(scope, args.this(), "headingOffset")
else {
return;
};
let value = unsafe { &*runtime_ptr }
.dom_host()
.node(handle)
.and_then(|node| node.as_element())
.map(|element| element.heading_offset())
.unwrap_or(0);
rv.set_uint32(value);
}
fn element_heading_offset_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some((runtime_ptr, handle)) = element_setter_receiver(scope, args.this(), "headingOffset")
else {
return;
};
let value = match webidl::convert::<webidl::UnsignedLong>(
scope,
args.get(0),
webidl::Context::member("Element", "headingOffset"),
) {
Ok(value) => value.0,
Err(error) => {
webidl::throw_error(scope, &error);
return;
}
};
let value = if value <= i32::MAX as u32 { value } else { 0 };
set_reflected_attribute(
scope,
runtime_ptr,
handle,
"headingoffset",
&value.to_string(),
);
rv.set_undefined();
}
fn element_heading_reset_getter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some((runtime_ptr, handle)) = element_getter_receiver(scope, args.this(), "headingReset")
else {
return;
};
let value = unsafe { &*runtime_ptr }
.dom_host()
.node(handle)
.and_then(|node| node.as_element())
.is_some_and(|element| element.heading_reset());
rv.set_bool(value);
}
fn element_heading_reset_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some((runtime_ptr, handle)) = element_setter_receiver(scope, args.this(), "headingReset")
else {
return;
};
set_reflected_boolean_attribute(
scope,
runtime_ptr,
handle,
"headingreset",
args.get(0).boolean_value(scope),
);
rv.set_undefined();
}
fn element_id_setter_function<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -7,6 +7,8 @@ use crate::{
util::v8_string,
webidl,
};
use dom::ElementState as StyloElementState;
use moli_selector::stylo_flat_tree_heading_descendants;
use super::super::{
JsContextHost, node::node_runtime_and_handle_from_object_or_detached, throw_dom_exception,
@@ -234,10 +236,29 @@ fn dialog_set_open_state_for_handle(
modal: bool,
) {
set_reflected_boolean_attribute(scope, runtime_ptr, handle, "open", open);
let _ = set_dialog_modal_state(runtime_ptr, handle, open && modal);
}
fn set_dialog_modal_state(runtime_ptr: *mut JsContextHost, handle: DomHandle, modal: bool) -> bool {
let old_heading_states = {
let runtime = unsafe { &*runtime_ptr };
stylo_flat_tree_heading_descendants(runtime.dom_host(), handle)
.into_iter()
.map(|heading| (heading, runtime.retained_current_element_state(heading)))
.collect::<Vec<_>>()
};
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime
.dom_host_mut()
.set_dialog_modal(handle, open && modal);
if !runtime.dom_host_mut().set_dialog_modal(handle, modal) {
return false;
}
for (heading, old_state) in old_heading_states {
runtime.note_element_state_style_activity_with_old_state(
heading,
StyloElementState::HEADING_LEVEL_BITS,
old_state,
);
}
true
}
fn dialog_is_modal(runtime: &JsContextHost, handle: DomHandle) -> bool {
@@ -434,8 +455,8 @@ pub(in crate::native_bridge::element) fn close_dialog_element(
}
set_reflected_boolean_attribute(scope, runtime_ptr, handle, "open", false);
let _ = set_dialog_modal_state(runtime_ptr, handle, false);
let runtime = unsafe { &mut *runtime_ptr };
let _ = runtime.dom_host_mut().set_dialog_modal(handle, false);
if let Some(return_value) = return_value {
let _ = runtime
.dom_host_mut()
@@ -2648,3 +2648,86 @@ fn detached_plain_document_all_matches_chromium_htmldda_surface() {
r#"{"ownAll":false,"protoGetter":"function","allType":"undefined","loose":true,"strict":false,"bool":false,"string":"[object HTMLAllCollection]","tag":"[object HTMLAllCollection]","length":0,"noArgNull":true,"itemZeroNull":true,"itemMissNull":true,"itemMethodNull":true,"namedMethodNull":true}"#
);
}
#[test]
fn element_heading_reflections_drive_flat_tree_heading_matching() {
let mut vm = new_storage_test_vm("https://heading-offset.test/");
let result = vm
.eval(
r#"
(() => {
const descriptor = name => Object.getOwnPropertyDescriptor(Element.prototype, name);
const outcome = callback => {
try {
return `ok:${String(callback())}`;
} catch (error) {
return `throw:${error && error.name}`;
}
};
const parent = document.createElement("div");
const heading = document.createElement("h1");
parent.append(heading);
const mount = document.body || document.documentElement ||
document.appendChild(document.createElement("html"));
mount.append(parent);
const initial = [heading.headingOffset, heading.headingReset, heading.matches(":heading(1)")];
parent.headingOffset = 3;
const parentOffset = [parent.getAttribute("headingoffset"), heading.matches(":heading(4)")];
heading.headingReset = true;
const reset = [heading.hasAttribute("headingreset"), heading.matches(":heading(1)")];
heading.headingReset = false;
heading.headingOffset = 20;
const clamped = [heading.getAttribute("headingoffset"), heading.headingOffset, heading.matches(":heading(9)")];
const host = document.createElement("section");
host.headingOffset = 1;
const root = host.attachShadow({ mode: "open" });
const container = document.createElement("div");
container.headingOffset = 1;
const slot = document.createElement("slot");
container.append(slot);
root.append(container);
const slotted = document.createElement("h2");
host.append(slotted);
mount.append(host);
const modalParent = document.createElement("div");
modalParent.headingOffset = 8;
const modal = document.createElement("dialog");
const modalHeading = document.createElement("h1");
modal.append(modalHeading);
modalParent.append(modal);
mount.append(modalParent);
const modalBefore = modal.headingReset;
modal.showModal();
const modalState = [modalBefore, modal.headingReset, modalHeading.matches(":heading(1)")];
modal.close();
return JSON.stringify({
owner: [
Object.prototype.hasOwnProperty.call(Element.prototype, "headingOffset"),
Object.prototype.hasOwnProperty.call(HTMLElement.prototype, "headingOffset"),
descriptor("headingOffset").enumerable,
descriptor("headingReset").enumerable
],
initial,
parentOffset,
reset,
clamped,
slotted: slotted.matches(":heading(4)"),
modalState,
badGetter: outcome(() => descriptor("headingOffset").get.call({})),
badSetter: outcome(() => descriptor("headingReset").set.call({}, true))
});
})()
"#,
)
.expect("heading reflection and selector probe should evaluate");
assert_eq!(
result,
r#"{"owner":[true,false,true,true],"initial":[0,false,true],"parentOffset":["3",true],"reset":[true,true],"clamped":["20",8,true],"slotted":true,"modalState":[false,true,true],"badGetter":"throw:TypeError","badSetter":"throw:TypeError"}"#
);
}
@@ -2010,3 +2010,51 @@ fn focus_dependent_selector_invalidates_computed_style_cache() {
"targeted focus invalidation should not bump the retained style generation"
);
}
#[test]
fn heading_offset_and_modal_reset_invalidate_live_computed_styles() {
let mut vm = new_parsed_test_vm(
"https://heading-state-invalidation.test/",
r#"<!doctype html>
<html><head><style>
:heading(1) { color: rgb(1, 2, 3); }
:heading(4) { color: rgb(4, 5, 6); }
:heading(9) { color: rgb(9, 10, 11); }
</style></head><body>
<div id="parent"><h1 id="target"></h1></div>
<div headingoffset="8"><dialog id="modal"><h1 id="modal-heading"></h1></dialog></div>
</body></html>"#,
);
let result = vm
.eval(
r#"
(() => {
const parent = document.getElementById('parent');
const target = document.getElementById('target');
const modal = document.getElementById('modal');
const modalHeading = document.getElementById('modal-heading');
const targetStyle = getComputedStyle(target);
const modalStyle = getComputedStyle(modalHeading);
const values = [targetStyle.color, modalStyle.color];
parent.headingOffset = 3;
values.push(targetStyle.color);
target.headingReset = true;
values.push(targetStyle.color);
modal.showModal();
values.push(modalStyle.color);
modal.close();
values.push(modalStyle.color);
return values.join('|');
})()
"#,
)
.expect("heading state invalidation probe should evaluate");
assert_eq!(
result,
"rgb(1, 2, 3)|rgb(9, 10, 11)|rgb(4, 5, 6)|rgb(1, 2, 3)|rgb(1, 2, 3)|rgb(9, 10, 11)"
);
}
+66 -2
View File
@@ -59,8 +59,9 @@ pub use stylo::{
StyloStylesheetSourceScopeFallbackInput, is_svg_presentation_attribute_name,
stylo_attribute_change_can_skip_fallback_without_dependency,
stylo_attribute_change_can_use_retained_invalidator, stylo_element_dependency_snapshot,
stylo_fallback_roots_plan, stylo_focus_change_invalidation_roots,
stylo_focus_state_matches_handle, stylo_focus_within_state_matches_handle,
stylo_fallback_roots_plan, stylo_flat_tree_heading_descendants,
stylo_focus_change_invalidation_roots, stylo_focus_state_matches_handle,
stylo_focus_within_state_matches_handle,
stylo_merge_retained_source_invalidation_fallback_kind,
stylo_merge_retained_source_invalidation_kind,
stylo_merge_source_dependency_request_requirement, stylo_removed_element_dependency_snapshots,
@@ -773,10 +774,33 @@ mod tests {
let ancestor = host.create_element("section");
let nested_h1 = host.create_element("h1");
let subject = host.create_element("div");
let offset_parent = host.create_element("section");
let offset_h1 = host.create_element("h1");
let reset_boundary = host.create_element("section");
let nested_offset = host.create_element("div");
let reset_h1 = host.create_element("h1");
let shadow_host = host.create_element("section");
let shadow_h1 = host.create_element("h1");
let slot_container = host.create_element("div");
let slot = host.create_element("slot");
let slotted_h2 = host.create_element("h2");
let modal_parent = host.create_element("div");
let modal = host.create_element("dialog");
let modal_h1 = host.create_element("h1");
let aria_h1 = host.create_element("h1");
assert!(host.set_attribute(role_heading, "role", "heading"));
assert!(host.set_attribute(role_heading, "aria-level", "1"));
assert!(host.set_attribute(ancestor, "id", "ancestor"));
assert!(host.set_attribute(subject, "id", "subject"));
assert!(host.set_attribute(offset_parent, "headingoffset", "3"));
assert!(host.set_attribute(reset_boundary, "headingoffset", "2"));
assert!(host.set_attribute(reset_boundary, "headingreset", ""));
assert!(host.set_attribute(nested_offset, "headingoffset", "2"));
assert!(host.set_attribute(shadow_host, "headingoffset", "1"));
assert!(host.set_attribute(slot_container, "headingoffset", "1"));
assert!(host.set_attribute(modal_parent, "headingoffset", "8"));
assert!(host.set_attribute(aria_h1, "headingoffset", "8"));
assert!(host.set_attribute(aria_h1, "aria-level", "3"));
assert!(host.append_child(body, h1));
assert!(host.append_child(body, h2));
assert!(host.append_child(body, h7));
@@ -784,6 +808,22 @@ mod tests {
assert!(host.append_child(body, ancestor));
assert!(host.append_child(ancestor, nested_h1));
assert!(host.append_child(body, subject));
assert!(host.append_child(body, offset_parent));
assert!(host.append_child(offset_parent, offset_h1));
assert!(host.append_child(body, reset_boundary));
assert!(host.append_child(reset_boundary, nested_offset));
assert!(host.append_child(nested_offset, reset_h1));
assert!(host.append_child(body, shadow_host));
let shadow_root = host.attach_shadow_root(shadow_host, "open").unwrap();
assert!(host.append_child(shadow_root, shadow_h1));
assert!(host.append_child(shadow_root, slot_container));
assert!(host.append_child(slot_container, slot));
assert!(host.append_child(shadow_host, slotted_h2));
assert!(host.append_child(body, modal_parent));
assert!(host.append_child(modal_parent, modal));
assert!(host.append_child(modal, modal_h1));
assert!(host.set_dialog_modal(modal, true));
assert!(host.append_child(body, aria_h1));
let engine = QueryEngine;
assert!(engine.matches_host(&host, h1, ":heading").unwrap());
@@ -809,6 +849,30 @@ mod tests {
.matches_host(&host, subject, "#ancestor:has(:heading(1)) ~ #subject")
.unwrap()
);
assert!(
engine
.matches_host(&host, offset_h1, ":heading(4)")
.unwrap()
);
assert!(host.set_attribute(offset_parent, "headingoffset", "5"));
assert!(
engine
.matches_host(&host, offset_h1, ":heading(6)")
.unwrap()
);
assert!(engine.matches_host(&host, reset_h1, ":heading(5)").unwrap());
assert!(
engine
.matches_host(&host, shadow_h1, ":heading(2)")
.unwrap()
);
assert!(
engine
.matches_host(&host, slotted_h2, ":heading(4)")
.unwrap()
);
assert!(engine.matches_host(&host, modal_h1, ":heading(1)").unwrap());
assert!(engine.matches_host(&host, aria_h1, ":heading(9)").unwrap());
assert!(engine.query_selector_host(&host, ":heading()").is_err());
assert!(engine.query_selector_host(&host, ":heading(2n)").is_err());
}
+9
View File
@@ -12,6 +12,15 @@ use crate::{
selector::SelectorError,
};
/// Returns heading elements below `root` in flat-tree order.
///
/// Renderer state invalidation uses the same traversal as `:heading()` so a
/// `headingoffset` or `headingreset` mutation cannot leave computed styles on
/// shadow/slotted descendants stale.
pub fn stylo_flat_tree_heading_descendants(host: &DomHost, root: NodeId) -> Vec<NodeId> {
query::flat_tree_heading_descendants(host, root)
}
mod atoms;
mod invalidation;
mod presentation;
+12 -19
View File
@@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet};
use dom::{ElementState, HEADING_LEVEL_OFFSET};
use dom::ElementState;
use indexmap::{IndexMap, IndexSet};
use selectors::{
OpaqueElement,
@@ -72,7 +72,10 @@ use crate::{
NodeId,
native::{ConnectedShadowRootSnapshot, DomHost, Element},
},
stylo::style_traversal::{StyleDomHostBinding, StyleElement},
stylo::{
query::heading_state_for_element,
style_traversal::{StyleDomHostBinding, StyleElement},
},
};
pub use style::moli_invalidation::{
@@ -559,7 +562,7 @@ pub fn stylo_element_dependency_snapshot(
Some(StyloElementDependencySnapshot::new(
handle,
element.local_name().to_owned(),
stylo_retained_dependency_state_for_element(element),
stylo_retained_dependency_state_for_element(host, handle, element),
element
.attributes()
.iter()
@@ -1564,22 +1567,12 @@ fn ascii_whitespace_tokens(value: &str) -> Vec<String> {
tokens
}
fn stylo_retained_dependency_state_for_element(element: &Element) -> ElementState {
let mut state = ElementState::empty();
const HEADING_NAMES: [(&str, u64); 6] = [
("h1", 1),
("h2", 2),
("h3", 3),
("h4", 4),
("h5", 5),
("h6", 6),
];
if let Some(level) = HEADING_NAMES
.iter()
.find_map(|(name, level)| element.is_html_element(name).then_some(*level))
{
state |= ElementState::from_bits_retain(level << HEADING_LEVEL_OFFSET);
}
fn stylo_retained_dependency_state_for_element(
host: &DomHost,
handle: NodeId,
element: &Element,
) -> ElementState {
let mut state = heading_state_for_element(host, handle);
if element.is_html_media() {
state |= ElementState::PAUSED;
if element.media_muted() {
+3 -1
View File
@@ -18,7 +18,9 @@ mod element;
// This logic is intentionally split from `element` because it changes with web
// platform semantics, while the element adapter should stay mostly structural.
mod pseudo;
pub(crate) use pseudo::html_directionality;
pub(crate) use pseudo::{
flat_tree_heading_descendants, heading_state_for_element, html_directionality,
};
use style::{
context::QuirksMode,
+104 -13
View File
@@ -1,3 +1,5 @@
use std::collections::HashSet;
use crate::{CssDirection, first_strong_text_direction};
use dom::{ElementState, HEADING_LEVEL_OFFSET};
@@ -102,19 +104,7 @@ impl<'a> QueryElement<'a> {
}
pub(super) fn heading_state(self) -> ElementState {
const HEADING_NAMES: [(&str, u64); 6] = [
("h1", 1),
("h2", 2),
("h3", 3),
("h4", 4),
("h5", 5),
("h6", 6),
];
HEADING_NAMES
.iter()
.find_map(|(name, level)| self.element().is_html_element(name).then_some(*level))
.map(|level| ElementState::from_bits_retain(level << HEADING_LEVEL_OFFSET))
.unwrap_or_else(ElementState::empty)
heading_state_for_element(self.host, self.handle)
}
pub(in crate::stylo) fn resolved_direction(self) -> CssDirection {
@@ -597,6 +587,107 @@ impl<'a> QueryElement<'a> {
}
}
pub(crate) fn heading_state_for_element(host: &DomHost, handle: NodeId) -> ElementState {
let Some(base_level) = host
.node(handle)
.and_then(Node::as_element)
.and_then(heading_base_level)
else {
return ElementState::empty();
};
let mut offset = 0_u32;
let mut current = Some(handle);
let mut visited = HashSet::new();
while let Some(candidate) = current {
if !visited.insert(candidate) {
break;
}
if let Some(element) = host.node(candidate).and_then(Node::as_element)
&& element.namespace() == "http://www.w3.org/1999/xhtml"
{
offset = offset.saturating_add(element.heading_offset());
if element.heading_reset() {
break;
}
}
current = flat_tree_parent(host, candidate);
}
let level = base_level.saturating_add(offset).min(9) as u64;
ElementState::from_bits_retain(level << HEADING_LEVEL_OFFSET)
}
pub(crate) fn flat_tree_heading_descendants(host: &DomHost, root: NodeId) -> Vec<NodeId> {
let mut stack = flat_tree_children(host, root);
stack.reverse();
let mut seen = HashSet::new();
let mut headings = Vec::new();
while let Some(candidate) = stack.pop() {
if !seen.insert(candidate) {
continue;
}
if host
.node(candidate)
.and_then(Node::as_element)
.and_then(heading_base_level)
.is_some()
{
headings.push(candidate);
}
let mut children = flat_tree_children(host, candidate);
children.reverse();
stack.extend(children);
}
headings
}
fn heading_base_level(element: &Element) -> Option<u32> {
["h1", "h2", "h3", "h4", "h5", "h6"]
.iter()
.position(|name| element.is_html_element(name))
.map(|index| index as u32 + 1)
}
fn flat_tree_parent(host: &DomHost, handle: NodeId) -> Option<NodeId> {
if let Some(slot) = host.assigned_slot_for_node(handle) {
return Some(slot);
}
let parent = host.node(handle).and_then(Node::parent_node)?;
if host.is_shadow_root(parent) {
return host.shadow_root_host(parent);
}
if host.is_html_element_named(parent, "slot")
&& !host
.assigned_nodes_for_slot_with_options(parent, false)
.is_empty()
{
return None;
}
if host.shadow_root_handle(parent).is_some()
&& host
.node(handle)
.is_some_and(|node| node.is_element() || node.is_text())
{
return None;
}
Some(parent)
}
fn flat_tree_children(host: &DomHost, handle: NodeId) -> Vec<NodeId> {
if host.is_html_element_named(handle, "slot") {
let assigned = host.assigned_nodes_for_slot_with_options(handle, false);
if !assigned.is_empty() {
return assigned;
}
}
if let Some(shadow_root) = host.shadow_root_handle(handle) {
return host.child_handles(shadow_root).collect();
}
host.child_handles(handle).collect()
}
pub(crate) fn html_directionality(host: &DomHost, handle: NodeId) -> CssDirection {
let mut current = Some(handle);
while let Some(handle) = current {