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