Skip to main content

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