1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! Shadow DOM "flattened tree" and slot distribution.
//!
//! The flattened tree is the tree that is actually laid out and painted. It is
//! derived from the regular DOM tree by:
//! - replacing a shadow host's light-DOM children with its shadow root's
//! children, and
//! - replacing each `<slot>` element with the light-DOM nodes assigned to it
//! (falling back to the slot's own children when nothing is assigned).
//!
//! The result is cached on each node in [`Node::flattened_children`] and
//! consulted by box construction, inline layout and painting via
//! [`Node::layout_dom_children`].
use markup5ever::local_name;
use crate::BaseDocument;
use blitz_traits::node_id::NodeId;
impl BaseDocument {
/// Recompute the flattened tree (shadow-root composition and `<slot>`
/// distribution) for every shadow host in the document.
///
/// This is cheap when there are no shadow hosts and is run once per
/// `resolve`.
pub(crate) fn compute_flattened_trees(&mut self) {
if self.shadow_host_nodes.is_empty() {
return;
}
let host_ids: Vec<NodeId> = self.shadow_host_nodes.iter().copied().collect();
for host_id in host_ids {
self.compute_flattened_tree_for_host(host_id);
}
}
fn compute_flattened_tree_for_host(&mut self, host_id: NodeId) {
let Some(shadow_root_id) = self
.get_node(host_id)
.and_then(|node| node.shadow_root_id())
else {
return;
};
// 1. Reset any previously-computed flattened state for this host's
// shadow tree and light children.
self.nodes[host_id].flattened_children = None;
self.clear_flattened_in_subtree(shadow_root_id);
let light_children = self.nodes[host_id].children.clone();
for &child_id in &light_children {
if let Some(el) = self.nodes[child_id].element_data_mut() {
el.assigned_slot = None;
}
}
// 2. The host's flattened children are the shadow root's children.
let shadow_children = self.nodes[shadow_root_id].children.to_vec();
self.nodes[host_id].flattened_children = Some(shadow_children);
// 3. Discover slots within the shadow tree.
let mut default_slot: Option<NodeId> = None;
let mut named_slots: Vec<(String, NodeId)> = Vec::new();
self.collect_slots(shadow_root_id, &mut default_slot, &mut named_slots);
// If there are no slots at all, there is nothing to distribute.
if default_slot.is_none() && named_slots.is_empty() {
return;
}
// 4. Assign each light-DOM child to a slot.
let mut assignments: Vec<(NodeId, Vec<NodeId>)> = Vec::new();
if let Some(slot_id) = default_slot {
assignments.push((slot_id, Vec::new()));
}
for (_, slot_id) in &named_slots {
assignments.push((*slot_id, Vec::new()));
}
for &child_id in &light_children {
let slot_name = self.nodes[child_id]
.element_data()
.and_then(|el| el.attr(local_name!("slot")))
.map(|s| s.to_string())
.filter(|s| !s.is_empty());
let target_slot = match slot_name {
Some(name) => named_slots
.iter()
.find(|(slot_name, _)| *slot_name == name)
.map(|(_, id)| *id)
.or(default_slot),
None => default_slot,
};
if let Some(slot_id) = target_slot {
if let Some(el) = self.nodes[child_id].element_data_mut() {
el.assigned_slot = Some(slot_id);
}
if let Some((_, list)) = assignments.iter_mut().find(|(id, _)| *id == slot_id) {
list.push(child_id);
}
}
}
// 5. Apply assignments to slots. A slot with no assigned nodes falls
// back to rendering its own children (so `flattened_children` is left
// as None).
for (slot_id, assigned) in assignments {
if !assigned.is_empty() {
self.nodes[slot_id].flattened_children = Some(assigned);
}
}
}
/// Recursively clear `flattened_children` on a shadow subtree, so stale slot
/// assignments don't persist across recomputes. Does not descend into
/// nested shadow roots.
fn clear_flattened_in_subtree(&mut self, node_id: NodeId) {
let children = self.nodes[node_id].children.clone();
self.nodes[node_id].flattened_children = None;
for child_id in children {
self.clear_flattened_in_subtree(child_id);
}
}
/// Walk the shadow subtree collecting `<slot>` elements. The first unnamed
/// slot becomes the default slot; the first slot with a given `name`
/// attribute wins for that name. Does not descend into nested shadow hosts'
/// shadow trees.
fn collect_slots(
&self,
node_id: NodeId,
default_slot: &mut Option<NodeId>,
named_slots: &mut Vec<(String, NodeId)>,
) {
let node = &self.nodes[node_id];
for &child_id in &node.children {
let child = &self.nodes[child_id];
if let Some(el) = child.element_data() {
if el.name.local == local_name!("slot") {
match el.attr(local_name!("name")).filter(|s| !s.is_empty()) {
Some(name) => {
if !named_slots.iter().any(|(n, _)| n == name) {
named_slots.push((name.to_string(), child_id));
}
}
None => {
if default_slot.is_none() {
*default_slot = Some(child_id);
}
}
}
}
}
// Recurse, but don't descend into nested shadow hosts' shadow trees
// (those slots belong to the nested host).
self.collect_slots(child_id, default_slot, named_slots);
}
}
}