mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
fix(selectors): honor heading offsets and resets
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+83
@@ -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"}"#
|
||||
);
|
||||
}
|
||||
|
||||
+48
@@ -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)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user