mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 00:01:34 +00:00
fix(html): update auto directionality after mutations
This commit is contained in:
+76
@@ -2092,3 +2092,79 @@ fn slotted_nodes_inherit_css_direction_from_slot_without_changing_html_direction
|
||||
|
||||
assert_eq!(result, "true:rtl:rgb(1, 2, 3)|true:ltr:ltr");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computed_direction_tracks_textarea_auto_value() {
|
||||
let mut vm = new_storage_test_vm("https://textarea-auto-direction.test/");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
if (!document.documentElement) {
|
||||
document.appendChild(document.createElement('html'));
|
||||
}
|
||||
if (!document.body) {
|
||||
document.documentElement.appendChild(document.createElement('body'));
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.dir = 'auto';
|
||||
document.body.appendChild(textarea);
|
||||
const empty = `${textarea.matches(':dir(ltr)')}:${getComputedStyle(textarea).direction}`;
|
||||
textarea.value = '\u05ea';
|
||||
const rtl = `${textarea.matches(':dir(rtl)')}:${getComputedStyle(textarea).direction}`;
|
||||
textarea.value = 'A';
|
||||
const ltr = `${textarea.matches(':dir(ltr)')}:${getComputedStyle(textarea).direction}`;
|
||||
|
||||
return `${empty}|${rtl}|${ltr}`;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("textarea dir=auto value direction should evaluate");
|
||||
|
||||
assert_eq!(result, "true:ltr|true:rtl|true:ltr");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computed_direction_tracks_dir_auto_tree_mutations() {
|
||||
let mut vm = new_storage_test_vm("https://dir-auto-tree-mutation.test/");
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
if (!document.documentElement) {
|
||||
document.appendChild(document.createElement('html'));
|
||||
}
|
||||
if (!document.head) {
|
||||
document.documentElement.appendChild(document.createElement('head'));
|
||||
}
|
||||
if (!document.body) {
|
||||
document.documentElement.appendChild(document.createElement('body'));
|
||||
}
|
||||
document.head.appendChild(document.createElement('style')).textContent =
|
||||
'#source:dir(rtl) + #target { display: none; }';
|
||||
|
||||
const source = document.createElement('div');
|
||||
source.id = 'source';
|
||||
source.dir = 'auto';
|
||||
const target = document.createElement('div');
|
||||
target.id = 'target';
|
||||
document.body.append(source, target);
|
||||
|
||||
const before = `${getComputedStyle(source).direction}:${getComputedStyle(target).display}`;
|
||||
const text = document.createTextNode('\u0627\u062e\u062a\u0628\u0631');
|
||||
source.appendChild(text);
|
||||
const afterAppend = `${getComputedStyle(source).direction}:${getComputedStyle(target).display}`;
|
||||
text.data = 'A';
|
||||
const afterText = `${getComputedStyle(source).direction}:${getComputedStyle(target).display}`;
|
||||
|
||||
return `${before}|${afterAppend}|${afterText}`;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("dir=auto tree mutation direction should evaluate");
|
||||
|
||||
assert_eq!(result, "ltr:block|rtl:none|ltr:block");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use dom::ElementState as StyloElementState;
|
||||
use moli_selector::StyloStyleSourceScope as StyleSourceScope;
|
||||
use indexmap::IndexSet;
|
||||
use moli_selector::{
|
||||
StyloStyleSourceScope as StyleSourceScope, html_auto_directionality_invalidation_root,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
document_runtime::DomHandle, dom::native::DomHost, protocol_types::EmulatedMediaOverrides,
|
||||
@@ -51,6 +54,12 @@ impl MoliStyleEngine {
|
||||
emulated_media: &EmulatedMediaOverrides,
|
||||
viewport: StyleViewport,
|
||||
) {
|
||||
self.invalidate_html_auto_directionality_for_mutations(
|
||||
host,
|
||||
effects,
|
||||
emulated_media,
|
||||
viewport,
|
||||
);
|
||||
let profile_enabled = moli_trace::cpu_profile_enabled();
|
||||
let total_started = profile_enabled.then(std::time::Instant::now);
|
||||
let grouping_started = profile_enabled.then(std::time::Instant::now);
|
||||
@@ -126,6 +135,44 @@ impl MoliStyleEngine {
|
||||
}
|
||||
}
|
||||
|
||||
fn invalidate_html_auto_directionality_for_mutations(
|
||||
&mut self,
|
||||
host: &DomHost,
|
||||
effects: &[StyleMutationEffect],
|
||||
emulated_media: &EmulatedMediaOverrides,
|
||||
viewport: StyleViewport,
|
||||
) {
|
||||
for root in html_auto_directionality_roots_for_mutations(host, effects) {
|
||||
let Some(document) = owner_document_for_handle(host, root) else {
|
||||
continue;
|
||||
};
|
||||
let world = self.world_for_document(document);
|
||||
if !world_has_style_state(self, &world) {
|
||||
continue;
|
||||
}
|
||||
// Tree and character-data mutation effects do not retain the old
|
||||
// resolved direction. Present the retained invalidator with the
|
||||
// conservative opposite direction so it checks both sides of
|
||||
// :dir() dependencies; the scoped cache cleanup below separately
|
||||
// rebuilds the HTML directionality presentation hint.
|
||||
let old_state = self
|
||||
.retained_current_element_state(host, root)
|
||||
.map(synthetic_opposite_directionality_state);
|
||||
if let Some(old_state) = old_state {
|
||||
self.invalidate_for_element_state_change_with_old_state_and_viewport(
|
||||
host,
|
||||
root,
|
||||
StyloElementState::LTR | StyloElementState::RTL,
|
||||
Some(old_state),
|
||||
emulated_media,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
self.invalidation_cleanup_for_world(&world)
|
||||
.invalidate_subtrees(host, [root]);
|
||||
}
|
||||
}
|
||||
|
||||
fn style_invalidation_without_cached_styles_is_complete(
|
||||
&self,
|
||||
effects: &[StyleMutationEffect],
|
||||
@@ -702,6 +749,50 @@ fn owner_document_for_mutation_effect(
|
||||
}
|
||||
}
|
||||
|
||||
fn html_auto_directionality_roots_for_mutations(
|
||||
host: &DomHost,
|
||||
effects: &[StyleMutationEffect],
|
||||
) -> IndexSet<DomHandle> {
|
||||
let mut roots = IndexSet::new();
|
||||
let mut record_root = |start| {
|
||||
if let Some(root) = html_auto_directionality_invalidation_root(host, start) {
|
||||
roots.insert(root);
|
||||
}
|
||||
};
|
||||
for effect in effects {
|
||||
match effect {
|
||||
StyleMutationEffect::ChildList { parent, .. } => record_root(*parent),
|
||||
StyleMutationEffect::CharacterData { node } => {
|
||||
if let Some(parent) = host.parent_node(*node) {
|
||||
record_root(parent);
|
||||
}
|
||||
}
|
||||
StyleMutationEffect::SlotAssignment { slot, .. } => record_root(*slot),
|
||||
StyleMutationEffect::Attribute { element, name, .. } if name == "dir" => {
|
||||
record_root(*element);
|
||||
if let Some(parent) = host.parent_node(*element) {
|
||||
record_root(parent);
|
||||
}
|
||||
}
|
||||
StyleMutationEffect::Attribute { .. }
|
||||
| StyleMutationEffect::ConnectedSubtrees { .. }
|
||||
| StyleMutationEffect::DisconnectedSubtrees { .. } => {}
|
||||
}
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
fn synthetic_opposite_directionality_state(mut state: StyloElementState) -> StyloElementState {
|
||||
let opposite = if state.contains(StyloElementState::RTL) {
|
||||
StyloElementState::LTR
|
||||
} else {
|
||||
StyloElementState::RTL
|
||||
};
|
||||
state.remove(StyloElementState::LTR | StyloElementState::RTL);
|
||||
state.insert(opposite);
|
||||
state
|
||||
}
|
||||
|
||||
fn owner_document_for_handle(host: &DomHost, handle: DomHandle) -> Option<DomHandle> {
|
||||
host.owner_document_handle(handle)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,151 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn child_list_under_dir_auto_invalidates_directionality_subtree() {
|
||||
let mut host = test_host();
|
||||
let document = host.document_handle();
|
||||
let auto = host.create_element("div");
|
||||
let descendant = host.create_element("span");
|
||||
let unrelated = host.create_element("aside");
|
||||
|
||||
assert!(host.set_attribute(auto, "dir", "auto"));
|
||||
assert!(host.append_child(document, auto));
|
||||
assert!(host.append_child(auto, descendant));
|
||||
assert!(host.append_child(document, unrelated));
|
||||
|
||||
let mut engine = MoliStyleEngine::new();
|
||||
let document_url = url::Url::parse("https://example.test/").unwrap();
|
||||
let inputs = FullStyleWorldSnapshot::default();
|
||||
for handle in [auto, descendant, unrelated] {
|
||||
assert_eq!(
|
||||
engine.computed_style_property_value(
|
||||
&host,
|
||||
&document_url,
|
||||
handle,
|
||||
"direction",
|
||||
None,
|
||||
&inputs,
|
||||
None,
|
||||
),
|
||||
Some("ltr".into())
|
||||
);
|
||||
}
|
||||
|
||||
let text = host.create_text_node("\u{0627}\u{062e}\u{062a}\u{0628}\u{0631}");
|
||||
assert!(host.append_child(descendant, text));
|
||||
let media = crate::protocol_types::EmulatedMediaOverrides::default();
|
||||
engine.invalidate_for_mutations(
|
||||
&host,
|
||||
&[StyleMutationEffect::ChildList {
|
||||
parent: descendant,
|
||||
added_nodes: vec![text],
|
||||
removed_nodes: Vec::new(),
|
||||
removed_element_snapshots: Vec::new(),
|
||||
previous_sibling: None,
|
||||
next_sibling: None,
|
||||
}],
|
||||
&media,
|
||||
);
|
||||
|
||||
assert!(!engine.computed_style_cache_contains_handle_for_document_for_test(document, auto));
|
||||
assert!(
|
||||
engine.computed_style_cache_contains_handle_for_document_for_test(document, descendant),
|
||||
"lazy subtree invalidation keeps descendants published until observation"
|
||||
);
|
||||
assert!(
|
||||
engine.retained_style_invalidation_root_count_for_document_for_test(document) > 0,
|
||||
"directionality invalidation must retain a lazy subtree root"
|
||||
);
|
||||
assert!(engine.computed_style_cache_contains_handle_for_document_for_test(document, unrelated));
|
||||
assert_eq!(
|
||||
engine.computed_style_property_value(
|
||||
&host,
|
||||
&document_url,
|
||||
auto,
|
||||
"direction",
|
||||
None,
|
||||
&inputs,
|
||||
None,
|
||||
),
|
||||
Some("rtl".into())
|
||||
);
|
||||
assert_eq!(
|
||||
engine.computed_style_property_value(
|
||||
&host,
|
||||
&document_url,
|
||||
descendant,
|
||||
"direction",
|
||||
None,
|
||||
&inputs,
|
||||
None,
|
||||
),
|
||||
Some("rtl".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_data_under_dir_auto_invalidates_dir_pseudo_sibling_dependency() {
|
||||
let mut host = test_host();
|
||||
let document = host.document_handle();
|
||||
let auto = host.create_element("div");
|
||||
let text = host.create_text_node("A");
|
||||
let target = host.create_element("div");
|
||||
|
||||
assert!(host.set_attribute(auto, "id", "source"));
|
||||
assert!(host.set_attribute(auto, "dir", "auto"));
|
||||
assert!(host.set_attribute(target, "id", "target"));
|
||||
assert!(host.append_child(document, auto));
|
||||
assert!(host.append_child(auto, text));
|
||||
assert!(host.append_child(document, target));
|
||||
|
||||
let mut engine = MoliStyleEngine::new();
|
||||
let document_url = url::Url::parse("https://example.test/").unwrap();
|
||||
let source = StyloStylesheetSource::new(
|
||||
"#source:dir(rtl) + #target { display: none; }".into(),
|
||||
document_url.clone(),
|
||||
);
|
||||
engine.set_document_adopted_style_sheet_sources(document, vec![source.clone()]);
|
||||
let mut inputs = FullStyleWorldSnapshot::default();
|
||||
inputs.document_stylesheet_sources.push(source);
|
||||
assert_eq!(
|
||||
engine.computed_style_property_value(
|
||||
&host,
|
||||
&document_url,
|
||||
target,
|
||||
"display",
|
||||
None,
|
||||
&inputs,
|
||||
None,
|
||||
),
|
||||
Some("block".into())
|
||||
);
|
||||
|
||||
host.node_mut(text)
|
||||
.and_then(|node| node.data_mut().as_text_mut())
|
||||
.expect("text node")
|
||||
.set_data("\u{0627}\u{062e}\u{062a}\u{0628}\u{0631}");
|
||||
let media = crate::protocol_types::EmulatedMediaOverrides::default();
|
||||
engine.invalidate_for_mutations(
|
||||
&host,
|
||||
&[StyleMutationEffect::CharacterData { node: text }],
|
||||
&media,
|
||||
);
|
||||
engine.drain_pending_style_invalidations_for_document_for_test(&host, document);
|
||||
|
||||
assert_eq!(
|
||||
engine.computed_style_property_value(
|
||||
&host,
|
||||
&document_url,
|
||||
target,
|
||||
"display",
|
||||
None,
|
||||
&inputs,
|
||||
None,
|
||||
),
|
||||
Some("none".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_data_without_structural_selector_preserves_cache_entries() {
|
||||
let mut host = test_host();
|
||||
|
||||
@@ -279,6 +279,10 @@ pub fn html_directionality(host: &DomHost, handle: NodeId) -> CssDirection {
|
||||
stylo::html_directionality(host, handle)
|
||||
}
|
||||
|
||||
pub fn html_auto_directionality_invalidation_root(host: &DomHost, start: NodeId) -> Option<NodeId> {
|
||||
stylo::html_auto_directionality_invalidation_root(host, start)
|
||||
}
|
||||
|
||||
pub fn validate_supports_selector_condition_argument(selector: &str) -> Result<(), SelectorError> {
|
||||
stylo::validate_supports_selector_condition_argument(selector)
|
||||
}
|
||||
@@ -374,8 +378,9 @@ fn serialize_cssom_style_rule_selector_text(
|
||||
mod tests {
|
||||
use super::{
|
||||
QueryEngine, StyleRuleNamespaceContext, StyleRuleSelectorContext, StyloDomStyleAdapter,
|
||||
canonicalize_cssom_style_rule_selector_text, normalize_scope_end_selector_list,
|
||||
normalize_scope_selector_list, validate_supports_selector_condition_argument,
|
||||
canonicalize_cssom_style_rule_selector_text, html_auto_directionality_invalidation_root,
|
||||
normalize_scope_end_selector_list, normalize_scope_selector_list,
|
||||
validate_supports_selector_condition_argument,
|
||||
};
|
||||
use crate::dom::native::{DomHost, NativeDom};
|
||||
use crate::stylo::{
|
||||
@@ -1551,6 +1556,46 @@ mod tests {
|
||||
assert!(engine.matches_host(&host, parent, ":dir(rtl)").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dir_auto_mutation_root_stops_at_contained_text_boundaries() {
|
||||
let url = url::Url::parse("https://example.test/").unwrap();
|
||||
let mut host = DomHost::from_dom(NativeDom::new_html(url));
|
||||
host.reset_html_document_shell();
|
||||
let body = host.document_body_handle().unwrap();
|
||||
|
||||
let auto = host.create_element("div");
|
||||
let transparent = host.create_element("span");
|
||||
let explicit = host.create_element("section");
|
||||
let explicit_child = host.create_element("span");
|
||||
let bdi = host.create_element("bdi");
|
||||
let script = host.create_element("script");
|
||||
assert!(host.set_attribute(auto, "dir", "auto"));
|
||||
assert!(host.set_attribute(explicit, "dir", "rtl"));
|
||||
assert!(host.append_child(body, auto));
|
||||
assert!(host.append_child(auto, transparent));
|
||||
assert!(host.append_child(auto, explicit));
|
||||
assert!(host.append_child(explicit, explicit_child));
|
||||
assert!(host.append_child(auto, bdi));
|
||||
assert!(host.append_child(auto, script));
|
||||
|
||||
assert_eq!(
|
||||
html_auto_directionality_invalidation_root(&host, transparent),
|
||||
Some(auto)
|
||||
);
|
||||
assert_eq!(
|
||||
html_auto_directionality_invalidation_root(&host, explicit_child),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
html_auto_directionality_invalidation_root(&host, bdi),
|
||||
Some(bdi)
|
||||
);
|
||||
assert_eq!(
|
||||
html_auto_directionality_invalidation_root(&host, script),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dom_api_selectors_dir_auto_tracks_clean_and_dirty_textarea_values() {
|
||||
let url = url::Url::parse("https://example.test/").unwrap();
|
||||
@@ -1628,6 +1673,17 @@ mod tests {
|
||||
assert!(host.set_attribute(auto, "type", "hidden"));
|
||||
assert!(!engine.matches_host(&host, auto, ":dir(ltr)").unwrap());
|
||||
assert!(engine.matches_host(&host, auto, ":dir(rtl)").unwrap());
|
||||
|
||||
let textarea = host.create_element("textarea");
|
||||
assert!(host.set_attribute(textarea, "dir", "auto"));
|
||||
assert!(host.set_input_value(textarea, "\u{05ea}"));
|
||||
assert!(host.append_child(body, textarea));
|
||||
assert!(!engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
||||
assert!(engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
||||
|
||||
assert!(host.set_input_value(textarea, "A"));
|
||||
assert!(engine.matches_host(&host, textarea, ":dir(ltr)").unwrap());
|
||||
assert!(!engine.matches_host(&host, textarea, ":dir(rtl)").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -86,8 +86,8 @@ pub use invalidation::{
|
||||
stylo_stylesheet_source_scope_fallback_roots,
|
||||
};
|
||||
pub use presentation::is_svg_presentation_attribute_name;
|
||||
pub(crate) use query::html_directionality;
|
||||
use query::{QueryDocument, QueryElement, QueryNode};
|
||||
pub(crate) use query::{html_auto_directionality_invalidation_root, html_directionality};
|
||||
#[cfg(test)]
|
||||
pub(crate) use selector_parse::validate_supports_selector_list;
|
||||
pub(crate) use selector_parse::{
|
||||
|
||||
@@ -19,7 +19,8 @@ mod element;
|
||||
// platform semantics, while the element adapter should stay mostly structural.
|
||||
mod pseudo;
|
||||
pub(crate) use pseudo::{
|
||||
flat_tree_heading_descendants, heading_state_for_element, html_directionality,
|
||||
flat_tree_heading_descendants, heading_state_for_element,
|
||||
html_auto_directionality_invalidation_root, html_directionality,
|
||||
};
|
||||
|
||||
use style::{
|
||||
|
||||
@@ -714,6 +714,43 @@ pub(crate) fn html_directionality(host: &DomHost, handle: NodeId) -> CssDirectio
|
||||
CssDirection::Ltr
|
||||
}
|
||||
|
||||
/// Return the nearest auto-directionality element whose resolved direction may
|
||||
/// depend on a mutation below `start`.
|
||||
///
|
||||
/// Contained-text auto directionality stops at the same HTML boundaries, so a
|
||||
/// mutation below an explicit `dir`, `bdi`, `script`, `style`, or `textarea`
|
||||
/// element must not invalidate an outer `dir=auto` element.
|
||||
pub(crate) fn html_auto_directionality_invalidation_root(
|
||||
host: &DomHost,
|
||||
start: NodeId,
|
||||
) -> Option<NodeId> {
|
||||
let mut current = Some(start);
|
||||
while let Some(handle) = current {
|
||||
let node = host.node(handle)?;
|
||||
if let Some(element) = node.as_element()
|
||||
&& element.namespace() == "http://www.w3.org/1999/xhtml"
|
||||
{
|
||||
let dir = element.attribute("dir");
|
||||
let has_auto_direction = dir.is_some_and(|value| value.eq_ignore_ascii_case("auto"))
|
||||
|| (element.is_html_element("bdi")
|
||||
&& !dir.is_some_and(|value| {
|
||||
normalized_direction(value).is_some() || value.eq_ignore_ascii_case("auto")
|
||||
}));
|
||||
if has_auto_direction {
|
||||
return Some(handle);
|
||||
}
|
||||
if element.is_html_element("bdi")
|
||||
|| dir.and_then(normalized_direction).is_some()
|
||||
|| matches!(element.local_name(), "script" | "style" | "textarea")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
current = node.parent_node();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn auto_direction_for_element(host: &DomHost, root: NodeId) -> Option<CssDirection> {
|
||||
if let Some(element) = host.node(root).and_then(Node::as_element) {
|
||||
if element.is_html_textarea() {
|
||||
|
||||
Reference in New Issue
Block a user