Skip to main content

script/event_loop/
devtools.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Ref, RefCell, RefMut};
6use std::collections::HashMap;
7use std::fmt::Write as _;
8use std::str;
9
10use devtools_traits::{
11    AncestorData, AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty,
12    EventListenerInfo, GetHTMLType, MatchedRule, NodeInfo, NodeStyle, RuleModification,
13    StyleSheetInfo, TimelineMarker, TimelineMarkerType,
14};
15use js::context::JSContext;
16use markup5ever::{LocalName, ns};
17use rustc_hash::FxHashMap;
18use script_bindings::codegen::GenericBindings::CSSRuleBinding::CSSRuleMethods;
19use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
20use script_bindings::root::Dom;
21use servo_base::generic_channel::GenericSender;
22use servo_base::id::PipelineId;
23use servo_config::pref;
24use style::attr::AttrValue;
25use style::stylesheets::Origin;
26
27use crate::conversions::Convert;
28use crate::dom::bindings::codegen::Bindings::CSSGroupingRuleBinding::CSSGroupingRuleMethods;
29use crate::dom::bindings::codegen::Bindings::CSSLayerBlockRuleBinding::CSSLayerBlockRuleMethods;
30use crate::dom::bindings::codegen::Bindings::CSSRuleListBinding::CSSRuleListMethods;
31use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
32use crate::dom::bindings::codegen::Bindings::CSSStyleRuleBinding::CSSStyleRuleMethods;
33use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::CSSStyleSheetMethods;
34use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
35use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
36use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
37use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
38use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants;
39use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
40use crate::dom::bindings::inheritance::Castable;
41use crate::dom::bindings::root::DomRoot;
42use crate::dom::bindings::str::DOMString;
43use crate::dom::bindings::trace::NoTrace;
44use crate::dom::css::cssstyledeclaration::ENABLED_LONGHAND_PROPERTIES;
45use crate::dom::css::cssstylerule::CSSStyleRule;
46use crate::dom::document::AnimationFrameCallback;
47use crate::dom::element::Element;
48use crate::dom::iterators::ShadowIncluding;
49use crate::dom::node::{Node, NodeTraits};
50use crate::dom::types::{
51    CSSGroupingRule, CSSLayerBlockRule, EventTarget, HTMLElement, TrustedHTML,
52};
53use crate::event_loop::document_collection::DocumentCollection;
54use crate::realms::enter_auto_realm;
55
56#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
57#[derive(JSTraceable)]
58pub(crate) struct PerPipelineState {
59    #[no_trace]
60    pipeline: PipelineId,
61
62    /// Maps from a node's unique ID to the Node itself
63    known_nodes: FxHashMap<String, Dom<Node>>,
64}
65
66#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
67#[derive(JSTraceable, Default)]
68pub(crate) struct DevtoolsState {
69    per_pipeline_state: RefCell<FxHashMap<NoTrace<PipelineId>, PerPipelineState>>,
70}
71
72impl PerPipelineState {
73    fn register_node(&mut self, node: &Node) {
74        let unique_id = node.unique_id(self.pipeline);
75        self.known_nodes
76            .entry(unique_id)
77            .or_insert_with(|| Dom::from_ref(node));
78    }
79}
80
81impl DevtoolsState {
82    pub(crate) fn notify_pipeline_created(&self, pipeline: PipelineId) {
83        self.per_pipeline_state.borrow_mut().insert(
84            NoTrace(pipeline),
85            PerPipelineState {
86                pipeline,
87                known_nodes: Default::default(),
88            },
89        );
90    }
91    pub(crate) fn notify_pipeline_exited(&self, pipeline: PipelineId) {
92        self.per_pipeline_state
93            .borrow_mut()
94            .remove(&NoTrace(pipeline));
95    }
96
97    fn pipeline_state_for(&self, pipeline: PipelineId) -> Option<Ref<'_, PerPipelineState>> {
98        Ref::filter_map(self.per_pipeline_state.borrow(), |state| {
99            state.get(&NoTrace(pipeline))
100        })
101        .ok()
102    }
103
104    fn mut_pipeline_state_for(&self, pipeline: PipelineId) -> Option<RefMut<'_, PerPipelineState>> {
105        RefMut::filter_map(self.per_pipeline_state.borrow_mut(), |state| {
106            state.get_mut(&NoTrace(pipeline))
107        })
108        .ok()
109    }
110
111    pub(crate) fn wants_updates_for_node(&self, pipeline: PipelineId, node: &Node) -> bool {
112        let Some(unique_id) = node.unique_id_if_already_present() else {
113            // This node does not have a unique id, so clearly the devtools inspector
114            // hasn't seen it before.
115            return false;
116        };
117        self.pipeline_state_for(pipeline)
118            .is_some_and(|pipeline_state| pipeline_state.known_nodes.contains_key(&unique_id))
119    }
120
121    fn find_node_by_unique_id(&self, pipeline: PipelineId, node_id: &str) -> Option<DomRoot<Node>> {
122        self.pipeline_state_for(pipeline)?
123            .known_nodes
124            .get(node_id)
125            .map(|node: &Dom<Node>| node.as_rooted())
126    }
127}
128
129pub(crate) fn handle_set_timeline_markers(
130    documents: &DocumentCollection,
131    pipeline: PipelineId,
132    marker_types: Vec<TimelineMarkerType>,
133    reply: GenericSender<Option<TimelineMarker>>,
134) {
135    match documents.find_window(pipeline) {
136        None => reply.send(None).unwrap(),
137        Some(window) => window.set_devtools_timeline_markers(marker_types, reply),
138    }
139}
140
141pub(crate) fn handle_drop_timeline_markers(
142    documents: &DocumentCollection,
143    pipeline: PipelineId,
144    marker_types: Vec<TimelineMarkerType>,
145) {
146    if let Some(window) = documents.find_window(pipeline) {
147        window.drop_devtools_timeline_markers(marker_types);
148    }
149}
150
151pub(crate) fn handle_request_animation_frame(
152    documents: &DocumentCollection,
153    id: PipelineId,
154    actor_name: String,
155) {
156    if let Some(doc) = documents.find_document(id) {
157        doc.request_animation_frame(AnimationFrameCallback::DevtoolsFramerateTick { actor_name });
158    }
159}
160
161pub(crate) fn handle_get_css_database(reply: GenericSender<HashMap<String, CssDatabaseProperty>>) {
162    let database: HashMap<_, _> = ENABLED_LONGHAND_PROPERTIES
163        .iter()
164        .map(|l| {
165            (
166                l.name().into(),
167                CssDatabaseProperty {
168                    is_inherited: l.inherited(),
169                    values: vec![], // TODO: Get allowed values for each property
170                    supports: vec![],
171                    subproperties: vec![l.name().into()],
172                },
173            )
174        })
175        .collect();
176    let _ = reply.send(database);
177}
178
179pub(crate) fn handle_get_event_listener_info(
180    state: &DevtoolsState,
181    pipeline: PipelineId,
182    node_id: &str,
183    reply: GenericSender<Vec<EventListenerInfo>>,
184) {
185    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
186        reply.send(vec![]).unwrap();
187        return;
188    };
189
190    let event_listeners = node
191        .upcast::<EventTarget>()
192        .summarize_event_listeners_for_devtools();
193    reply.send(event_listeners).unwrap();
194}
195
196pub(crate) fn handle_get_root_node(
197    cx: &mut JSContext,
198    state: &DevtoolsState,
199    documents: &DocumentCollection,
200    pipeline: PipelineId,
201    reply: GenericSender<Option<NodeInfo>>,
202) {
203    let info = documents
204        .find_document(pipeline)
205        .map(DomRoot::upcast::<Node>)
206        .inspect(|node| {
207            state
208                .mut_pipeline_state_for(pipeline)
209                .unwrap()
210                .register_node(node)
211        })
212        .map(|document| document.upcast::<Node>().summarize(cx));
213    reply.send(info).unwrap();
214}
215
216pub(crate) fn handle_get_document_element(
217    cx: &mut JSContext,
218    state: &DevtoolsState,
219    documents: &DocumentCollection,
220    pipeline: PipelineId,
221    reply: GenericSender<Option<NodeInfo>>,
222) {
223    let info = documents
224        .find_document(pipeline)
225        .and_then(|document| document.GetDocumentElement())
226        .inspect(|element| {
227            state
228                .mut_pipeline_state_for(pipeline)
229                .unwrap()
230                .register_node(element.upcast())
231        })
232        .map(|element| element.upcast::<Node>().summarize(cx));
233    reply.send(info).unwrap();
234}
235
236pub(crate) fn handle_get_stylesheets(
237    cx: &mut JSContext,
238    documents: &DocumentCollection,
239    pipeline: PipelineId,
240    reply: GenericSender<Vec<StyleSheetInfo>>,
241) {
242    let mut stylesheets = vec![];
243    if let Some(document) = documents.find_document(pipeline) {
244        let node = document.upcast::<Node>();
245        for i in 0..node.stylesheet_list_owner().stylesheet_count() {
246            if let Some(s) = node.stylesheet_list_owner().stylesheet_at(cx, i) {
247                stylesheets.push(StyleSheetInfo {
248                    href: s.href().map(String::from),
249                    disabled: s.disabled(),
250                    title: String::from(s.title()),
251                    style_sheet_index: i as i32,
252                    system: s.origin() == Origin::UserAgent,
253                    rule_count: s.get_rule_count(),
254                });
255            }
256        }
257    }
258    reply.send(stylesheets).unwrap();
259}
260
261pub(crate) fn handle_get_stylesheet_text(
262    cx: &mut JSContext,
263    documents: &DocumentCollection,
264    pipeline: PipelineId,
265    index: i32,
266    reply: GenericSender<Option<String>>,
267) {
268    let text = (|| {
269        let document = documents.find_document(pipeline)?;
270        let stylesheet = document
271            .upcast::<Node>()
272            .stylesheet_list_owner()
273            .stylesheet_at(cx, index as usize)?;
274
275        // For inline, Prefer the original "authored" source from the owner node (e.g., <style> tag).
276        if let Some(node) = stylesheet.owner_node() {
277            let text = node.upcast::<Node>().GetTextContent().unwrap_or_default();
278            if !text.is_empty() {
279                return Some(String::from(text));
280            }
281        }
282
283        // For styles which are not inline, Reconstruct the CSS from rules.
284        let rules = stylesheet.rulelist(cx);
285        let mut css_text = String::new();
286        for i in 0..rules.Length() {
287            if let Some(rule) = rules.Item(cx, i) {
288                css_text.push_str(&rule.CssText().str());
289                css_text.push('\n');
290            }
291        }
292        Some(css_text)
293    })();
294    reply.send(text).unwrap();
295}
296
297pub(crate) fn handle_get_children(
298    cx: &mut JSContext,
299    state: &DevtoolsState,
300    pipeline: PipelineId,
301    node_id: &str,
302    reply: GenericSender<Option<Vec<NodeInfo>>>,
303) {
304    let Some(parent) = state.find_node_by_unique_id(pipeline, node_id) else {
305        reply.send(None).unwrap();
306        return;
307    };
308    let is_whitespace = |node: &NodeInfo| {
309        node.node_type == NodeConstants::TEXT_NODE &&
310            node.node_value.as_ref().is_none_or(|v| v.trim().is_empty())
311    };
312    let mut pipeline_state = state.mut_pipeline_state_for(pipeline).unwrap();
313
314    let inline: Vec<_> = parent
315        .children()
316        .map(|child| {
317            let window = child.owner_window();
318            let Some(elem) = child.downcast::<Element>() else {
319                return false;
320            };
321            let computed_style = window.GetComputedStyle(cx, elem, None);
322            let display = computed_style.Display();
323            display == "inline"
324        })
325        .collect();
326
327    let mut children = vec![];
328    if let Some(shadow_root) = parent.downcast::<Element>().and_then(Element::shadow_root) &&
329        (!shadow_root.is_user_agent_widget() ||
330            pref!(inspector_show_servo_internal_shadow_roots))
331    {
332        children.push(shadow_root.upcast::<Node>().summarize(cx));
333        pipeline_state.register_node(shadow_root.upcast::<Node>());
334    }
335    let children_iter = parent.children().enumerate().filter_map(|(i, child)| {
336        // Filter whitespace only text nodes that are not inline level
337        // https://firefox-source-docs.mozilla.org/devtools-user/page_inspector/how_to/examine_and_edit_html/index.html#whitespace-only-text-nodes
338        let prev_inline = i > 0 && inline[i - 1];
339        let next_inline = i < inline.len() - 1 && inline[i + 1];
340        let is_inline_level = prev_inline && next_inline;
341
342        let info = child.summarize(cx);
343        if is_whitespace(&info) && !is_inline_level {
344            return None;
345        }
346        pipeline_state.register_node(&child);
347
348        Some(info)
349    });
350    children.extend(children_iter);
351
352    reply.send(Some(children)).unwrap();
353}
354
355pub(crate) fn handle_get_attribute_style(
356    cx: &mut JSContext,
357    state: &DevtoolsState,
358    pipeline: PipelineId,
359    node_id: &str,
360    reply: GenericSender<Option<Vec<NodeStyle>>>,
361) {
362    let node = match state.find_node_by_unique_id(pipeline, node_id) {
363        None => return reply.send(None).unwrap(),
364        Some(found_node) => found_node,
365    };
366
367    let Some(elem) = node.downcast::<HTMLElement>() else {
368        // the style attribute only works on html elements
369        reply.send(None).unwrap();
370        return;
371    };
372    let style = elem.Style(cx);
373
374    let msg = (0..style.Length())
375        .map(|i| {
376            let name = style.Item(i);
377            NodeStyle {
378                // This code has to clone the name values, even though
379                // these function actually would only need to borrow,
380                // but the binding generator forces an owned DOMString
381                // in the signature.
382                // It'd be nice to not have to do this here and in the
383                // similar cases below, but I don't see how.
384                value: String::from(style.GetPropertyValue(name.clone())),
385                priority: String::from(style.GetPropertyPriority(name.clone())),
386                name: String::from(name),
387            }
388        })
389        .collect();
390
391    reply.send(Some(msg)).unwrap();
392}
393
394fn build_rule_map(
395    cx: &mut JSContext,
396    list: &crate::dom::css::cssrulelist::CSSRuleList,
397    stylesheet_index: usize,
398    ancestors: &[AncestorData],
399    map: &mut HashMap<usize, MatchedRule>,
400) {
401    for i in 0..list.Length() {
402        let Some(rule) = list.Item(cx, i) else {
403            continue;
404        };
405
406        if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
407            let block_id = style_rule.block_id();
408            map.entry(block_id).or_insert_with(|| MatchedRule {
409                selector: style_rule.SelectorText().into(),
410                stylesheet_index,
411                block_id,
412                ancestor_data: ancestors.to_vec(),
413            });
414            continue;
415        }
416
417        if let Some(layer_rule) = rule.downcast::<CSSLayerBlockRule>() {
418            let name = String::from(layer_rule.Name());
419            let mut next = ancestors.to_vec();
420            next.push(AncestorData::Layer {
421                actor_id: None,
422                value: (!name.is_empty()).then_some(name),
423            });
424            let inner = layer_rule.upcast::<CSSGroupingRule>().CssRules(cx);
425            build_rule_map(cx, &inner, stylesheet_index, &next, map);
426            continue;
427        }
428
429        if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
430            let inner = group_rule.CssRules(cx);
431            build_rule_map(cx, &inner, stylesheet_index, ancestors, map);
432        }
433    }
434}
435
436fn find_rule_by_block_id(
437    cx: &mut JSContext,
438    list: &crate::dom::css::cssrulelist::CSSRuleList,
439    target_block_id: usize,
440) -> Option<DomRoot<CSSStyleRule>> {
441    for i in 0..list.Length() {
442        let Some(rule) = list.Item(cx, i) else {
443            continue;
444        };
445
446        if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
447            if style_rule.block_id() == target_block_id {
448                return Some(DomRoot::from_ref(style_rule));
449            }
450            continue;
451        }
452
453        if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
454            let inner = group_rule.CssRules(cx);
455            if let Some(found) = find_rule_by_block_id(cx, &inner, target_block_id) {
456                return Some(found);
457            }
458        }
459    }
460    None
461}
462
463#[cfg_attr(crown, expect(crown::unrooted_must_root))]
464pub(crate) fn handle_get_selectors(
465    cx: &mut JSContext,
466    state: &DevtoolsState,
467    documents: &DocumentCollection,
468    pipeline: PipelineId,
469    node_id: &str,
470    reply: GenericSender<Option<Vec<MatchedRule>>>,
471) {
472    let msg = (|| {
473        let node = state.find_node_by_unique_id(pipeline, node_id)?;
474        let elem = node.downcast::<Element>()?;
475        let document = documents.find_document(pipeline)?;
476        let mut realm = enter_auto_realm(cx, document.window());
477        let cx = &mut realm.current_realm();
478        let owner = node.stylesheet_list_owner();
479
480        let mut decl_map = HashMap::new();
481        for i in 0..owner.stylesheet_count() {
482            let Some(stylesheet) = owner.stylesheet_at(cx, i) else {
483                continue;
484            };
485            let Ok(list) = stylesheet.GetCssRules(cx) else {
486                continue;
487            };
488            build_rule_map(cx, &list, i, &[], &mut decl_map);
489        }
490
491        let mut rules = Vec::new();
492        let computed = elem.style()?;
493
494        if let Some(rule_node) = computed.rules.as_ref() {
495            for rn in rule_node.self_and_ancestors() {
496                if let Some(source) = rn.style_source() {
497                    let ptr = source.get().raw_ptr().as_ptr() as usize;
498
499                    if let Some(matched) = decl_map.get(&ptr) {
500                        rules.push(matched.clone());
501                    }
502                }
503            }
504        }
505
506        Some(rules)
507    })();
508
509    reply.send(msg).unwrap();
510}
511
512#[cfg_attr(crown, expect(crown::unrooted_must_root))]
513#[allow(clippy::too_many_arguments)]
514pub(crate) fn handle_get_stylesheet_style(
515    cx: &mut JSContext,
516    state: &DevtoolsState,
517    documents: &DocumentCollection,
518    pipeline: PipelineId,
519    node_id: &str,
520    matched_rule: MatchedRule,
521    reply: GenericSender<Option<Vec<NodeStyle>>>,
522) {
523    let msg = (|| {
524        let node = state.find_node_by_unique_id(pipeline, node_id)?;
525        let document = documents.find_document(pipeline)?;
526        let mut realm = enter_auto_realm(cx, document.window());
527        let cx = &mut realm.current_realm();
528        let owner = node.stylesheet_list_owner();
529
530        let stylesheet = owner.stylesheet_at(cx, matched_rule.stylesheet_index)?;
531        let list = stylesheet.GetCssRules(cx).ok()?;
532
533        let style_rule = find_rule_by_block_id(cx, &list, matched_rule.block_id)?;
534        let declaration = style_rule.Style(cx);
535
536        Some(
537            (0..declaration.Length())
538                .map(|i| {
539                    let name = declaration.Item(i);
540                    NodeStyle {
541                        value: String::from(declaration.GetPropertyValue(name.clone())),
542                        priority: String::from(declaration.GetPropertyPriority(name.clone())),
543                        name: String::from(name),
544                    }
545                })
546                .collect(),
547        )
548    })();
549
550    reply.send(msg).unwrap();
551}
552
553pub(crate) fn handle_get_computed_style(
554    cx: &mut JSContext,
555    state: &DevtoolsState,
556    pipeline: PipelineId,
557    node_id: &str,
558    reply: GenericSender<Option<Vec<NodeStyle>>>,
559) {
560    let node = match state.find_node_by_unique_id(pipeline, node_id) {
561        None => return reply.send(None).unwrap(),
562        Some(found_node) => found_node,
563    };
564
565    let window = node.owner_window();
566    let elem = node
567        .downcast::<Element>()
568        .expect("This should be an element");
569    let computed_style = window.GetComputedStyle(cx, elem, None);
570
571    let msg = (0..computed_style.Length())
572        .map(|i| {
573            let name = computed_style.Item(i);
574            NodeStyle {
575                value: String::from(computed_style.GetPropertyValue(name.clone())),
576                priority: String::from(computed_style.GetPropertyPriority(name.clone())),
577                name: String::from(name),
578            }
579        })
580        .collect();
581
582    reply.send(Some(msg)).unwrap();
583}
584
585pub(crate) fn handle_get_layout(
586    cx: &mut JSContext,
587    state: &DevtoolsState,
588    pipeline: PipelineId,
589    node_id: &str,
590    reply: GenericSender<Option<(ComputedNodeLayout, AutoMargins)>>,
591) {
592    let node = match state.find_node_by_unique_id(pipeline, node_id) {
593        None => return reply.send(None).unwrap(),
594        Some(found_node) => found_node,
595    };
596
597    let element = node
598        .downcast::<Element>()
599        .expect("should be getting layout of element");
600
601    let rect = element.GetBoundingClientRect(cx);
602    let width = rect.Width() as f32;
603    let height = rect.Height() as f32;
604
605    let window = node.owner_window();
606    let computed_style = window.GetComputedStyle(cx, element, None);
607    let computed_layout = ComputedNodeLayout {
608        display: computed_style.Display().into(),
609        position: computed_style.Position().into(),
610        z_index: computed_style.ZIndex().into(),
611        box_sizing: computed_style.BoxSizing().into(),
612        margin_top: computed_style.MarginTop().into(),
613        margin_right: computed_style.MarginRight().into(),
614        margin_bottom: computed_style.MarginBottom().into(),
615        margin_left: computed_style.MarginLeft().into(),
616        border_top_width: computed_style.BorderTopWidth().into(),
617        border_right_width: computed_style.BorderRightWidth().into(),
618        border_bottom_width: computed_style.BorderBottomWidth().into(),
619        border_left_width: computed_style.BorderLeftWidth().into(),
620        padding_top: computed_style.PaddingTop().into(),
621        padding_right: computed_style.PaddingRight().into(),
622        padding_bottom: computed_style.PaddingBottom().into(),
623        padding_left: computed_style.PaddingLeft().into(),
624        width,
625        height,
626    };
627
628    let auto_margins = element.determine_auto_margins();
629    reply.send(Some((computed_layout, auto_margins))).unwrap();
630}
631
632pub(crate) fn handle_get_xpath(
633    state: &DevtoolsState,
634    pipeline: PipelineId,
635    node_id: &str,
636    reply: GenericSender<String>,
637) {
638    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
639        return reply.send(Default::default()).unwrap();
640    };
641
642    let selector = node
643        .inclusive_ancestors(ShadowIncluding::Yes)
644        .filter_map(|ancestor| {
645            let Some(element) = ancestor.downcast::<Element>() else {
646                // TODO: figure out how to handle shadow roots here
647                return None;
648            };
649
650            let mut result = "/".to_owned();
651            if *element.namespace() != ns!(html) {
652                result.push_str(element.namespace());
653                result.push(':');
654            }
655
656            result.push_str(element.local_name());
657
658            let would_node_also_match_selector = |sibling: &Node| {
659                let Some(sibling) = sibling.downcast::<Element>() else {
660                    return false;
661                };
662                sibling.namespace() == element.namespace() &&
663                    sibling.local_name() == element.local_name()
664            };
665
666            let matching_elements_before = ancestor
667                .preceding_siblings()
668                .filter(|node| would_node_also_match_selector(node))
669                .count();
670            let matching_elements_after = ancestor
671                .following_siblings()
672                .filter(|node| would_node_also_match_selector(node))
673                .count();
674
675            if matching_elements_before + matching_elements_after != 0 {
676                // Need to add an index (note that XPath uses 1-based indexing)
677                write!(result, "[{}]", matching_elements_before + 1).unwrap();
678            }
679
680            Some(result)
681        })
682        .collect::<Vec<_>>()
683        .into_iter()
684        .rev()
685        .collect::<Vec<_>>()
686        .join("");
687
688    reply.send(selector).unwrap();
689}
690
691pub(crate) fn handle_get_inner_or_outer_html(
692    cx: &mut JSContext,
693    state: &DevtoolsState,
694    pipeline_id: PipelineId,
695    node_id: &str,
696    reply: GenericSender<Option<String>>,
697    html_type: GetHTMLType,
698) {
699    let node = state.find_node_by_unique_id(pipeline_id, node_id);
700
701    let selector = node.and_then(|node| {
702        let element = node.downcast::<Element>();
703
704        if let Some(element) = element {
705            let inner_or_outer_html = match html_type {
706                GetHTMLType::InnerHTML => element.GetInnerHTML(cx),
707                GetHTMLType::OuterHTML => element.GetOuterHTML(cx),
708            };
709
710            if let Ok(trusted_html) = inner_or_outer_html {
711                let trusted_html_or_string = trusted_html.convert();
712
713                let Ok(html_dom_string) = TrustedHTML::get_trusted_type_compliant_string(
714                    cx,
715                    &element.owner_global(),
716                    trusted_html_or_string,
717                    "Devtools GetInnerOrOuterHTML",
718                ) else {
719                    return None;
720                };
721
722                return Some(html_dom_string.to_string());
723            };
724        }
725        Some(String::new())
726    });
727
728    reply.send(selector).unwrap();
729}
730
731pub(crate) fn handle_modify_attribute(
732    cx: &mut JSContext,
733    state: &DevtoolsState,
734    documents: &DocumentCollection,
735    pipeline: PipelineId,
736    node_id: &str,
737    modifications: Vec<AttrModification>,
738) {
739    let Some(document) = documents.find_document(pipeline) else {
740        return warn!("document for pipeline id {} is not found", pipeline);
741    };
742    let mut realm = enter_auto_realm(cx, document.window());
743    let cx = &mut realm.current_realm();
744
745    let node = match state.find_node_by_unique_id(pipeline, node_id) {
746        None => {
747            return warn!(
748                "node id {} for pipeline id {} is not found",
749                node_id, pipeline
750            );
751        },
752        Some(found_node) => found_node,
753    };
754
755    let elem = node
756        .downcast::<Element>()
757        .expect("should be getting layout of element");
758
759    for modification in modifications {
760        match modification.new_value {
761            Some(string) => {
762                elem.set_attribute(
763                    cx,
764                    &LocalName::from(modification.attribute_name),
765                    AttrValue::String(string),
766                );
767            },
768            None => elem.RemoveAttribute(cx, DOMString::from(modification.attribute_name)),
769        }
770    }
771}
772
773pub(crate) fn handle_modify_rule(
774    cx: &mut JSContext,
775    state: &DevtoolsState,
776    documents: &DocumentCollection,
777    pipeline: PipelineId,
778    node_id: &str,
779    modifications: Vec<RuleModification>,
780) {
781    let Some(document) = documents.find_document(pipeline) else {
782        return warn!("Document for pipeline id {} is not found", pipeline);
783    };
784    let mut realm = enter_auto_realm(cx, document.window());
785    let cx = &mut realm.current_realm();
786
787    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
788        return warn!(
789            "Node id {} for pipeline id {} is not found",
790            node_id, pipeline
791        );
792    };
793
794    let elem = node
795        .downcast::<HTMLElement>()
796        .expect("This should be an HTMLElement");
797    let style = elem.Style(cx);
798
799    for modification in modifications {
800        let _ = style.SetProperty(
801            cx,
802            modification.name.into(),
803            modification.value.into(),
804            modification.priority.into(),
805        );
806    }
807}
808
809pub(crate) fn handle_highlight_dom_node(
810    state: &DevtoolsState,
811    documents: &DocumentCollection,
812    id: PipelineId,
813    node_id: Option<&str>,
814) {
815    let node = node_id.and_then(|node_id| {
816        let node = state.find_node_by_unique_id(id, node_id);
817        if node.is_none() {
818            log::warn!("Node id {node_id} for pipeline id {id} is not found",);
819        }
820        node
821    });
822
823    if let Some(window) = documents.find_window(id) {
824        window.Document().highlight_dom_node(node.as_deref());
825    }
826}
827
828impl Element {
829    fn determine_auto_margins(&self) -> AutoMargins {
830        let Some(style) = self.style() else {
831            return AutoMargins::default();
832        };
833        let margin = style.get_margin();
834        AutoMargins {
835            top: margin.margin_top.is_auto(),
836            right: margin.margin_right.is_auto(),
837            bottom: margin.margin_bottom.is_auto(),
838            left: margin.margin_left.is_auto(),
839        }
840    }
841}