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