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    documents: &DocumentCollection,
237    pipeline: PipelineId,
238    reply: GenericSender<Vec<StyleSheetInfo>>,
239) {
240    let mut stylesheets = vec![];
241    if let Some(document) = documents.find_document(pipeline) {
242        let node = document.upcast::<Node>();
243        for i in 0..node.stylesheet_list_owner().stylesheet_count() {
244            if let Some(s) = node.stylesheet_list_owner().stylesheet_at(i) {
245                stylesheets.push(StyleSheetInfo {
246                    href: s.href().map(String::from),
247                    disabled: s.disabled(),
248                    title: String::from(s.title()),
249                    style_sheet_index: i as i32,
250                    system: s.origin() == Origin::UserAgent,
251                    rule_count: s.get_rule_count(),
252                });
253            }
254        }
255    }
256    reply.send(stylesheets).unwrap();
257}
258
259pub(crate) fn handle_get_stylesheet_text(
260    cx: &mut JSContext,
261    documents: &DocumentCollection,
262    pipeline: PipelineId,
263    index: i32,
264    reply: GenericSender<Option<String>>,
265) {
266    let text = (|| {
267        let document = documents.find_document(pipeline)?;
268        let stylesheet = document
269            .upcast::<Node>()
270            .stylesheet_list_owner()
271            .stylesheet_at(index as usize)?;
272
273        // For inline, Prefer the original "authored" source from the owner node (e.g., <style> tag).
274        if let Some(node) = stylesheet.owner_node() {
275            let text = node.upcast::<Node>().GetTextContent().unwrap_or_default();
276            if !text.is_empty() {
277                return Some(String::from(text));
278            }
279        }
280
281        // For styles which are not inline, Reconstruct the CSS from rules.
282        let rules = stylesheet.rulelist(cx);
283        let mut css_text = String::new();
284        for i in 0..rules.Length() {
285            if let Some(rule) = rules.Item(cx, i) {
286                css_text.push_str(&rule.CssText().str());
287                css_text.push('\n');
288            }
289        }
290        Some(css_text)
291    })();
292    reply.send(text).unwrap();
293}
294
295pub(crate) fn handle_get_children(
296    cx: &mut JSContext,
297    state: &DevtoolsState,
298    pipeline: PipelineId,
299    node_id: &str,
300    reply: GenericSender<Option<Vec<NodeInfo>>>,
301) {
302    let Some(parent) = state.find_node_by_unique_id(pipeline, node_id) else {
303        reply.send(None).unwrap();
304        return;
305    };
306    let is_whitespace = |node: &NodeInfo| {
307        node.node_type == NodeConstants::TEXT_NODE &&
308            node.node_value.as_ref().is_none_or(|v| v.trim().is_empty())
309    };
310    let mut pipeline_state = state.mut_pipeline_state_for(pipeline).unwrap();
311
312    let inline: Vec<_> = parent
313        .children()
314        .map(|child| {
315            let window = child.owner_window();
316            let Some(elem) = child.downcast::<Element>() else {
317                return false;
318            };
319            let computed_style = window.GetComputedStyle(cx, elem, None);
320            let display = computed_style.Display();
321            display == "inline"
322        })
323        .collect();
324
325    let mut children = vec![];
326    if let Some(shadow_root) = parent.downcast::<Element>().and_then(Element::shadow_root) &&
327        (!shadow_root.is_user_agent_widget() ||
328            pref!(inspector_show_servo_internal_shadow_roots))
329    {
330        children.push(shadow_root.upcast::<Node>().summarize(cx));
331    }
332    let children_iter = parent.children().enumerate().filter_map(|(i, child)| {
333        // Filter whitespace only text nodes that are not inline level
334        // https://firefox-source-docs.mozilla.org/devtools-user/page_inspector/how_to/examine_and_edit_html/index.html#whitespace-only-text-nodes
335        let prev_inline = i > 0 && inline[i - 1];
336        let next_inline = i < inline.len() - 1 && inline[i + 1];
337        let is_inline_level = prev_inline && next_inline;
338
339        let info = child.summarize(cx);
340        if is_whitespace(&info) && !is_inline_level {
341            return None;
342        }
343        pipeline_state.register_node(&child);
344
345        Some(info)
346    });
347    children.extend(children_iter);
348
349    reply.send(Some(children)).unwrap();
350}
351
352pub(crate) fn handle_get_attribute_style(
353    cx: &mut JSContext,
354    state: &DevtoolsState,
355    pipeline: PipelineId,
356    node_id: &str,
357    reply: GenericSender<Option<Vec<NodeStyle>>>,
358) {
359    let node = match state.find_node_by_unique_id(pipeline, node_id) {
360        None => return reply.send(None).unwrap(),
361        Some(found_node) => found_node,
362    };
363
364    let Some(elem) = node.downcast::<HTMLElement>() else {
365        // the style attribute only works on html elements
366        reply.send(None).unwrap();
367        return;
368    };
369    let style = elem.Style(cx);
370
371    let msg = (0..style.Length())
372        .map(|i| {
373            let name = style.Item(i);
374            NodeStyle {
375                // This code has to clone the name values, even though
376                // these function actually would only need to borrow,
377                // but the binding generator forces an owned DOMString
378                // in the signature.
379                // It'd be nice to not have to do this here and in the
380                // similar cases below, but I don't see how.
381                value: String::from(style.GetPropertyValue(name.clone())),
382                priority: String::from(style.GetPropertyPriority(name.clone())),
383                name: String::from(name),
384            }
385        })
386        .collect();
387
388    reply.send(Some(msg)).unwrap();
389}
390
391fn build_rule_map(
392    cx: &mut JSContext,
393    list: &crate::dom::css::cssrulelist::CSSRuleList,
394    stylesheet_index: usize,
395    ancestors: &[AncestorData],
396    map: &mut HashMap<usize, MatchedRule>,
397) {
398    for i in 0..list.Length() {
399        let Some(rule) = list.Item(cx, i) else {
400            continue;
401        };
402
403        if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
404            let block_id = style_rule.block_id();
405            map.entry(block_id).or_insert_with(|| MatchedRule {
406                selector: style_rule.SelectorText().into(),
407                stylesheet_index,
408                block_id,
409                ancestor_data: ancestors.to_vec(),
410            });
411            continue;
412        }
413
414        if let Some(layer_rule) = rule.downcast::<CSSLayerBlockRule>() {
415            let name = String::from(layer_rule.Name());
416            let mut next = ancestors.to_vec();
417            next.push(AncestorData::Layer {
418                actor_id: None,
419                value: (!name.is_empty()).then_some(name),
420            });
421            let inner = layer_rule.upcast::<CSSGroupingRule>().CssRules(cx);
422            build_rule_map(cx, &inner, stylesheet_index, &next, map);
423            continue;
424        }
425
426        if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
427            let inner = group_rule.CssRules(cx);
428            build_rule_map(cx, &inner, stylesheet_index, ancestors, map);
429        }
430    }
431}
432
433fn find_rule_by_block_id(
434    cx: &mut JSContext,
435    list: &crate::dom::css::cssrulelist::CSSRuleList,
436    target_block_id: usize,
437) -> Option<DomRoot<CSSStyleRule>> {
438    for i in 0..list.Length() {
439        let Some(rule) = list.Item(cx, i) else {
440            continue;
441        };
442
443        if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
444            if style_rule.block_id() == target_block_id {
445                return Some(DomRoot::from_ref(style_rule));
446            }
447            continue;
448        }
449
450        if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
451            let inner = group_rule.CssRules(cx);
452            if let Some(found) = find_rule_by_block_id(cx, &inner, target_block_id) {
453                return Some(found);
454            }
455        }
456    }
457    None
458}
459
460#[cfg_attr(crown, expect(crown::unrooted_must_root))]
461pub(crate) fn handle_get_selectors(
462    cx: &mut JSContext,
463    state: &DevtoolsState,
464    documents: &DocumentCollection,
465    pipeline: PipelineId,
466    node_id: &str,
467    reply: GenericSender<Option<Vec<MatchedRule>>>,
468) {
469    let msg = (|| {
470        let node = state.find_node_by_unique_id(pipeline, node_id)?;
471        let elem = node.downcast::<Element>()?;
472        let document = documents.find_document(pipeline)?;
473        let mut realm = enter_auto_realm(cx, document.window());
474        let cx = &mut realm.current_realm();
475        let owner = node.stylesheet_list_owner();
476
477        let mut decl_map = HashMap::new();
478        for i in 0..owner.stylesheet_count() {
479            let Some(stylesheet) = owner.stylesheet_at(i) else {
480                continue;
481            };
482            let Ok(list) = stylesheet.GetCssRules(cx) else {
483                continue;
484            };
485            build_rule_map(cx, &list, i, &[], &mut decl_map);
486        }
487
488        let mut rules = Vec::new();
489        let computed = elem.style()?;
490
491        if let Some(rule_node) = computed.rules.as_ref() {
492            for rn in rule_node.self_and_ancestors() {
493                if let Some(source) = rn.style_source() {
494                    let ptr = source.get().raw_ptr().as_ptr() as usize;
495
496                    if let Some(matched) = decl_map.get(&ptr) {
497                        rules.push(matched.clone());
498                    }
499                }
500            }
501        }
502
503        Some(rules)
504    })();
505
506    reply.send(msg).unwrap();
507}
508
509#[cfg_attr(crown, expect(crown::unrooted_must_root))]
510#[allow(clippy::too_many_arguments)]
511pub(crate) fn handle_get_stylesheet_style(
512    cx: &mut JSContext,
513    state: &DevtoolsState,
514    documents: &DocumentCollection,
515    pipeline: PipelineId,
516    node_id: &str,
517    matched_rule: MatchedRule,
518    reply: GenericSender<Option<Vec<NodeStyle>>>,
519) {
520    let msg = (|| {
521        let node = state.find_node_by_unique_id(pipeline, node_id)?;
522        let document = documents.find_document(pipeline)?;
523        let mut realm = enter_auto_realm(cx, document.window());
524        let cx = &mut realm.current_realm();
525        let owner = node.stylesheet_list_owner();
526
527        let stylesheet = owner.stylesheet_at(matched_rule.stylesheet_index)?;
528        let list = stylesheet.GetCssRules(cx).ok()?;
529
530        let style_rule = find_rule_by_block_id(cx, &list, matched_rule.block_id)?;
531        let declaration = style_rule.Style(cx);
532
533        Some(
534            (0..declaration.Length())
535                .map(|i| {
536                    let name = declaration.Item(i);
537                    NodeStyle {
538                        value: String::from(declaration.GetPropertyValue(name.clone())),
539                        priority: String::from(declaration.GetPropertyPriority(name.clone())),
540                        name: String::from(name),
541                    }
542                })
543                .collect(),
544        )
545    })();
546
547    reply.send(msg).unwrap();
548}
549
550pub(crate) fn handle_get_computed_style(
551    cx: &mut JSContext,
552    state: &DevtoolsState,
553    pipeline: PipelineId,
554    node_id: &str,
555    reply: GenericSender<Option<Vec<NodeStyle>>>,
556) {
557    let node = match state.find_node_by_unique_id(pipeline, node_id) {
558        None => return reply.send(None).unwrap(),
559        Some(found_node) => found_node,
560    };
561
562    let window = node.owner_window();
563    let elem = node
564        .downcast::<Element>()
565        .expect("This should be an element");
566    let computed_style = window.GetComputedStyle(cx, elem, None);
567
568    let msg = (0..computed_style.Length())
569        .map(|i| {
570            let name = computed_style.Item(i);
571            NodeStyle {
572                value: String::from(computed_style.GetPropertyValue(name.clone())),
573                priority: String::from(computed_style.GetPropertyPriority(name.clone())),
574                name: String::from(name),
575            }
576        })
577        .collect();
578
579    reply.send(Some(msg)).unwrap();
580}
581
582pub(crate) fn handle_get_layout(
583    cx: &mut JSContext,
584    state: &DevtoolsState,
585    pipeline: PipelineId,
586    node_id: &str,
587    reply: GenericSender<Option<(ComputedNodeLayout, AutoMargins)>>,
588) {
589    let node = match state.find_node_by_unique_id(pipeline, node_id) {
590        None => return reply.send(None).unwrap(),
591        Some(found_node) => found_node,
592    };
593
594    let element = node
595        .downcast::<Element>()
596        .expect("should be getting layout of element");
597
598    let rect = element.GetBoundingClientRect(cx);
599    let width = rect.Width() as f32;
600    let height = rect.Height() as f32;
601
602    let window = node.owner_window();
603    let computed_style = window.GetComputedStyle(cx, element, None);
604    let computed_layout = ComputedNodeLayout {
605        display: computed_style.Display().into(),
606        position: computed_style.Position().into(),
607        z_index: computed_style.ZIndex().into(),
608        box_sizing: computed_style.BoxSizing().into(),
609        margin_top: computed_style.MarginTop().into(),
610        margin_right: computed_style.MarginRight().into(),
611        margin_bottom: computed_style.MarginBottom().into(),
612        margin_left: computed_style.MarginLeft().into(),
613        border_top_width: computed_style.BorderTopWidth().into(),
614        border_right_width: computed_style.BorderRightWidth().into(),
615        border_bottom_width: computed_style.BorderBottomWidth().into(),
616        border_left_width: computed_style.BorderLeftWidth().into(),
617        padding_top: computed_style.PaddingTop().into(),
618        padding_right: computed_style.PaddingRight().into(),
619        padding_bottom: computed_style.PaddingBottom().into(),
620        padding_left: computed_style.PaddingLeft().into(),
621        width,
622        height,
623    };
624
625    let auto_margins = element.determine_auto_margins();
626    reply.send(Some((computed_layout, auto_margins))).unwrap();
627}
628
629pub(crate) fn handle_get_xpath(
630    state: &DevtoolsState,
631    pipeline: PipelineId,
632    node_id: &str,
633    reply: GenericSender<String>,
634) {
635    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
636        return reply.send(Default::default()).unwrap();
637    };
638
639    let selector = node
640        .inclusive_ancestors(ShadowIncluding::Yes)
641        .filter_map(|ancestor| {
642            let Some(element) = ancestor.downcast::<Element>() else {
643                // TODO: figure out how to handle shadow roots here
644                return None;
645            };
646
647            let mut result = "/".to_owned();
648            if *element.namespace() != ns!(html) {
649                result.push_str(element.namespace());
650                result.push(':');
651            }
652
653            result.push_str(element.local_name());
654
655            let would_node_also_match_selector = |sibling: &Node| {
656                let Some(sibling) = sibling.downcast::<Element>() else {
657                    return false;
658                };
659                sibling.namespace() == element.namespace() &&
660                    sibling.local_name() == element.local_name()
661            };
662
663            let matching_elements_before = ancestor
664                .preceding_siblings()
665                .filter(|node| would_node_also_match_selector(node))
666                .count();
667            let matching_elements_after = ancestor
668                .following_siblings()
669                .filter(|node| would_node_also_match_selector(node))
670                .count();
671
672            if matching_elements_before + matching_elements_after != 0 {
673                // Need to add an index (note that XPath uses 1-based indexing)
674                result.push_str(&format!("[{}]", matching_elements_before + 1));
675            }
676
677            Some(result)
678        })
679        .collect::<Vec<_>>()
680        .into_iter()
681        .rev()
682        .collect::<Vec<_>>()
683        .join("");
684
685    reply.send(selector).unwrap();
686}
687
688pub(crate) fn handle_get_inner_or_outer_html(
689    cx: &mut JSContext,
690    state: &DevtoolsState,
691    pipeline_id: PipelineId,
692    node_id: &str,
693    reply: GenericSender<Option<String>>,
694    html_type: GetHTMLType,
695) {
696    let node = state.find_node_by_unique_id(pipeline_id, node_id);
697
698    let selector = node.and_then(|node| {
699        let element = node.downcast::<Element>();
700
701        if let Some(element) = element {
702            let inner_or_outer_html = match html_type {
703                GetHTMLType::InnerHTML => element.GetInnerHTML(cx),
704                GetHTMLType::OuterHTML => element.GetOuterHTML(cx),
705            };
706
707            if let Ok(trusted_html) = inner_or_outer_html {
708                let trusted_html_or_string = trusted_html.convert();
709
710                let Ok(html_dom_string) = TrustedHTML::get_trusted_type_compliant_string(
711                    cx,
712                    &element.owner_global(),
713                    trusted_html_or_string,
714                    "Devtools GetInnerOrOuterHTML",
715                ) else {
716                    return None;
717                };
718
719                return Some(html_dom_string.to_string());
720            };
721        }
722        Some("".to_owned())
723    });
724
725    reply.send(selector).unwrap();
726}
727
728pub(crate) fn handle_modify_attribute(
729    cx: &mut JSContext,
730    state: &DevtoolsState,
731    documents: &DocumentCollection,
732    pipeline: PipelineId,
733    node_id: &str,
734    modifications: Vec<AttrModification>,
735) {
736    let Some(document) = documents.find_document(pipeline) else {
737        return warn!("document for pipeline id {} is not found", &pipeline);
738    };
739    let mut realm = enter_auto_realm(cx, document.window());
740    let cx = &mut realm.current_realm();
741
742    let node = match state.find_node_by_unique_id(pipeline, node_id) {
743        None => {
744            return warn!(
745                "node id {} for pipeline id {} is not found",
746                &node_id, &pipeline
747            );
748        },
749        Some(found_node) => found_node,
750    };
751
752    let elem = node
753        .downcast::<Element>()
754        .expect("should be getting layout of element");
755
756    for modification in modifications {
757        match modification.new_value {
758            Some(string) => {
759                elem.set_attribute(
760                    cx,
761                    &LocalName::from(modification.attribute_name),
762                    AttrValue::String(string),
763                );
764            },
765            None => elem.RemoveAttribute(cx, DOMString::from(modification.attribute_name)),
766        }
767    }
768}
769
770pub(crate) fn handle_modify_rule(
771    cx: &mut JSContext,
772    state: &DevtoolsState,
773    documents: &DocumentCollection,
774    pipeline: PipelineId,
775    node_id: &str,
776    modifications: Vec<RuleModification>,
777) {
778    let Some(document) = documents.find_document(pipeline) else {
779        return warn!("Document for pipeline id {} is not found", &pipeline);
780    };
781    let mut realm = enter_auto_realm(cx, document.window());
782    let cx = &mut realm.current_realm();
783
784    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
785        return warn!(
786            "Node id {} for pipeline id {} is not found",
787            &node_id, &pipeline
788        );
789    };
790
791    let elem = node
792        .downcast::<HTMLElement>()
793        .expect("This should be an HTMLElement");
794    let style = elem.Style(cx);
795
796    for modification in modifications {
797        let _ = style.SetProperty(
798            cx,
799            modification.name.into(),
800            modification.value.into(),
801            modification.priority.into(),
802        );
803    }
804}
805
806pub(crate) fn handle_highlight_dom_node(
807    state: &DevtoolsState,
808    documents: &DocumentCollection,
809    id: PipelineId,
810    node_id: Option<&str>,
811) {
812    let node = node_id.and_then(|node_id| {
813        let node = state.find_node_by_unique_id(id, node_id);
814        if node.is_none() {
815            log::warn!("Node id {node_id} for pipeline id {id} is not found",);
816        }
817        node
818    });
819
820    if let Some(window) = documents.find_window(id) {
821        window.Document().highlight_dom_node(node.as_deref());
822    }
823}
824
825impl Element {
826    fn determine_auto_margins(&self) -> AutoMargins {
827        let Some(style) = self.style() else {
828            return AutoMargins::default();
829        };
830        let margin = style.get_margin();
831        AutoMargins {
832            top: margin.margin_top.is_auto(),
833            right: margin.margin_right.is_auto(),
834            bottom: margin.margin_bottom.is_auto(),
835            left: margin.margin_left.is_auto(),
836        }
837    }
838}