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(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(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_unrooted(cx.no_gc())
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(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(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    for i in 0..list.Length() {
327        let Some(rule) = list.Item(cx, i) else {
328            continue;
329        };
330
331        if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
332            let block_id = style_rule.block_id();
333            map.entry(block_id).or_insert_with(|| MatchedRule {
334                selector: style_rule.SelectorText().into(),
335                stylesheet_index,
336                block_id,
337                ancestor_data: ancestors.to_vec(),
338            });
339            continue;
340        }
341
342        if let Some(layer_rule) = rule.downcast::<CSSLayerBlockRule>() {
343            let name = layer_rule.Name().to_string();
344            let mut next = ancestors.to_vec();
345            next.push(AncestorData::Layer {
346                actor_id: None,
347                value: (!name.is_empty()).then_some(name),
348            });
349            let inner = layer_rule.upcast::<CSSGroupingRule>().CssRules(cx);
350            build_rule_map(cx, &inner, stylesheet_index, &next, map);
351            continue;
352        }
353
354        if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
355            let inner = group_rule.CssRules(cx);
356            build_rule_map(cx, &inner, stylesheet_index, ancestors, map);
357        }
358    }
359}
360
361fn find_rule_by_block_id(
362    cx: &mut JSContext,
363    list: &crate::dom::css::cssrulelist::CSSRuleList,
364    target_block_id: usize,
365) -> Option<DomRoot<CSSStyleRule>> {
366    for i in 0..list.Length() {
367        let Some(rule) = list.Item(cx, i) else {
368            continue;
369        };
370
371        if let Some(style_rule) = rule.downcast::<CSSStyleRule>() {
372            if style_rule.block_id() == target_block_id {
373                return Some(DomRoot::from_ref(style_rule));
374            }
375            continue;
376        }
377
378        if let Some(group_rule) = rule.downcast::<CSSGroupingRule>() {
379            let inner = group_rule.CssRules(cx);
380            if let Some(found) = find_rule_by_block_id(cx, &inner, target_block_id) {
381                return Some(found);
382            }
383        }
384    }
385    None
386}
387
388#[cfg_attr(crown, expect(crown::unrooted_must_root))]
389pub(crate) fn handle_get_selectors(
390    cx: &mut JSContext,
391    state: &DevtoolsState,
392    documents: &DocumentCollection,
393    pipeline: PipelineId,
394    node_id: &str,
395    reply: GenericSender<Option<Vec<MatchedRule>>>,
396) {
397    let msg = (|| {
398        let node = state.find_node_by_unique_id(pipeline, node_id)?;
399        let elem = node.downcast::<Element>()?;
400        let document = documents.find_document(pipeline)?;
401        let _realm = enter_realm(document.window());
402        let owner = node.stylesheet_list_owner();
403
404        let mut decl_map = HashMap::new();
405        for i in 0..owner.stylesheet_count() {
406            let Some(stylesheet) = owner.stylesheet_at(i) else {
407                continue;
408            };
409            let Ok(list) = stylesheet.GetCssRules(cx) else {
410                continue;
411            };
412            build_rule_map(cx, &list, i, &[], &mut decl_map);
413        }
414
415        let mut rules = Vec::new();
416        let computed = elem.style()?;
417
418        if let Some(rule_node) = computed.rules.as_ref() {
419            for rn in rule_node.self_and_ancestors() {
420                if let Some(source) = rn.style_source() {
421                    let ptr = source.get().raw_ptr().as_ptr() as usize;
422
423                    if let Some(matched) = decl_map.get(&ptr) {
424                        rules.push(matched.clone());
425                    }
426                }
427            }
428        }
429
430        Some(rules)
431    })();
432
433    reply.send(msg).unwrap();
434}
435
436#[cfg_attr(crown, expect(crown::unrooted_must_root))]
437#[allow(clippy::too_many_arguments)]
438pub(crate) fn handle_get_stylesheet_style(
439    cx: &mut JSContext,
440    state: &DevtoolsState,
441    documents: &DocumentCollection,
442    pipeline: PipelineId,
443    node_id: &str,
444    matched_rule: MatchedRule,
445    reply: GenericSender<Option<Vec<NodeStyle>>>,
446) {
447    let msg = (|| {
448        let node = state.find_node_by_unique_id(pipeline, node_id)?;
449        let document = documents.find_document(pipeline)?;
450        let _realm = enter_realm(document.window());
451        let owner = node.stylesheet_list_owner();
452
453        let stylesheet = owner.stylesheet_at(matched_rule.stylesheet_index)?;
454        let list = stylesheet.GetCssRules(cx).ok()?;
455
456        let style_rule = find_rule_by_block_id(cx, &list, matched_rule.block_id)?;
457        let declaration = style_rule.Style(cx);
458
459        Some(
460            (0..declaration.Length())
461                .map(|i| {
462                    let name = declaration.Item(i);
463                    NodeStyle {
464                        name: name.to_string(),
465                        value: declaration.GetPropertyValue(name.clone()).to_string(),
466                        priority: declaration.GetPropertyPriority(name).to_string(),
467                    }
468                })
469                .collect(),
470        )
471    })();
472
473    reply.send(msg).unwrap();
474}
475
476pub(crate) fn handle_get_computed_style(
477    state: &DevtoolsState,
478    pipeline: PipelineId,
479    node_id: &str,
480    reply: GenericSender<Option<Vec<NodeStyle>>>,
481) {
482    let node = match state.find_node_by_unique_id(pipeline, node_id) {
483        None => return reply.send(None).unwrap(),
484        Some(found_node) => found_node,
485    };
486
487    let window = node.owner_window();
488    let elem = node
489        .downcast::<Element>()
490        .expect("This should be an element");
491    let computed_style = window.GetComputedStyle(elem, None);
492
493    let msg = (0..computed_style.Length())
494        .map(|i| {
495            let name = computed_style.Item(i);
496            NodeStyle {
497                name: name.to_string(),
498                value: computed_style.GetPropertyValue(name.clone()).to_string(),
499                priority: computed_style.GetPropertyPriority(name).to_string(),
500            }
501        })
502        .collect();
503
504    reply.send(Some(msg)).unwrap();
505}
506
507pub(crate) fn handle_get_layout(
508    cx: &mut JSContext,
509    state: &DevtoolsState,
510    pipeline: PipelineId,
511    node_id: &str,
512    reply: GenericSender<Option<(ComputedNodeLayout, AutoMargins)>>,
513) {
514    let node = match state.find_node_by_unique_id(pipeline, node_id) {
515        None => return reply.send(None).unwrap(),
516        Some(found_node) => found_node,
517    };
518
519    let element = node
520        .downcast::<Element>()
521        .expect("should be getting layout of element");
522
523    let rect = element.GetBoundingClientRect(cx);
524    let width = rect.Width() as f32;
525    let height = rect.Height() as f32;
526
527    let window = node.owner_window();
528    let computed_style = window.GetComputedStyle(element, None);
529    let computed_layout = ComputedNodeLayout {
530        display: computed_style.Display().into(),
531        position: computed_style.Position().into(),
532        z_index: computed_style.ZIndex().into(),
533        box_sizing: computed_style.BoxSizing().into(),
534        margin_top: computed_style.MarginTop().into(),
535        margin_right: computed_style.MarginRight().into(),
536        margin_bottom: computed_style.MarginBottom().into(),
537        margin_left: computed_style.MarginLeft().into(),
538        border_top_width: computed_style.BorderTopWidth().into(),
539        border_right_width: computed_style.BorderRightWidth().into(),
540        border_bottom_width: computed_style.BorderBottomWidth().into(),
541        border_left_width: computed_style.BorderLeftWidth().into(),
542        padding_top: computed_style.PaddingTop().into(),
543        padding_right: computed_style.PaddingRight().into(),
544        padding_bottom: computed_style.PaddingBottom().into(),
545        padding_left: computed_style.PaddingLeft().into(),
546        width,
547        height,
548    };
549
550    let auto_margins = element.determine_auto_margins();
551    reply.send(Some((computed_layout, auto_margins))).unwrap();
552}
553
554pub(crate) fn handle_get_xpath(
555    state: &DevtoolsState,
556    pipeline: PipelineId,
557    node_id: &str,
558    reply: GenericSender<String>,
559) {
560    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
561        return reply.send(Default::default()).unwrap();
562    };
563
564    let selector = node
565        .inclusive_ancestors(ShadowIncluding::Yes)
566        .filter_map(|ancestor| {
567            let Some(element) = ancestor.downcast::<Element>() else {
568                // TODO: figure out how to handle shadow roots here
569                return None;
570            };
571
572            let mut result = "/".to_owned();
573            if *element.namespace() != ns!(html) {
574                result.push_str(element.namespace());
575                result.push(':');
576            }
577
578            result.push_str(element.local_name());
579
580            let would_node_also_match_selector = |sibling: &Node| {
581                let Some(sibling) = sibling.downcast::<Element>() else {
582                    return false;
583                };
584                sibling.namespace() == element.namespace() &&
585                    sibling.local_name() == element.local_name()
586            };
587
588            let matching_elements_before = ancestor
589                .preceding_siblings()
590                .filter(|node| would_node_also_match_selector(node))
591                .count();
592            let matching_elements_after = ancestor
593                .following_siblings()
594                .filter(|node| would_node_also_match_selector(node))
595                .count();
596
597            if matching_elements_before + matching_elements_after != 0 {
598                // Need to add an index (note that XPath uses 1-based indexing)
599                result.push_str(&format!("[{}]", matching_elements_before + 1));
600            }
601
602            Some(result)
603        })
604        .collect::<Vec<_>>()
605        .into_iter()
606        .rev()
607        .collect::<Vec<_>>()
608        .join("");
609
610    reply.send(selector).unwrap();
611}
612
613pub(crate) fn handle_modify_attribute(
614    cx: &mut JSContext,
615    state: &DevtoolsState,
616    documents: &DocumentCollection,
617    pipeline: PipelineId,
618    node_id: &str,
619    modifications: Vec<AttrModification>,
620) {
621    let Some(document) = documents.find_document(pipeline) else {
622        return warn!("document for pipeline id {} is not found", &pipeline);
623    };
624    let _realm = enter_realm(document.window());
625
626    let node = match state.find_node_by_unique_id(pipeline, node_id) {
627        None => {
628            return warn!(
629                "node id {} for pipeline id {} is not found",
630                &node_id, &pipeline
631            );
632        },
633        Some(found_node) => found_node,
634    };
635
636    let elem = node
637        .downcast::<Element>()
638        .expect("should be getting layout of element");
639
640    for modification in modifications {
641        match modification.new_value {
642            Some(string) => {
643                elem.set_attribute(
644                    &LocalName::from(modification.attribute_name),
645                    AttrValue::String(string),
646                    CanGc::from_cx(cx),
647                );
648            },
649            None => elem.RemoveAttribute(
650                DOMString::from(modification.attribute_name),
651                CanGc::from_cx(cx),
652            ),
653        }
654    }
655}
656
657pub(crate) fn handle_modify_rule(
658    cx: &mut JSContext,
659    state: &DevtoolsState,
660    documents: &DocumentCollection,
661    pipeline: PipelineId,
662    node_id: &str,
663    modifications: Vec<RuleModification>,
664) {
665    let Some(document) = documents.find_document(pipeline) else {
666        return warn!("Document for pipeline id {} is not found", &pipeline);
667    };
668    let _realm = enter_realm(document.window());
669
670    let Some(node) = state.find_node_by_unique_id(pipeline, node_id) else {
671        return warn!(
672            "Node id {} for pipeline id {} is not found",
673            &node_id, &pipeline
674        );
675    };
676
677    let elem = node
678        .downcast::<HTMLElement>()
679        .expect("This should be an HTMLElement");
680    let style = elem.Style(CanGc::from_cx(cx));
681
682    for modification in modifications {
683        let _ = style.SetProperty(
684            cx,
685            modification.name.into(),
686            modification.value.into(),
687            modification.priority.into(),
688        );
689    }
690}
691
692pub(crate) fn handle_highlight_dom_node(
693    state: &DevtoolsState,
694    documents: &DocumentCollection,
695    id: PipelineId,
696    node_id: Option<&str>,
697) {
698    let node = node_id.and_then(|node_id| {
699        let node = state.find_node_by_unique_id(id, node_id);
700        if node.is_none() {
701            log::warn!("Node id {node_id} for pipeline id {id} is not found",);
702        }
703        node
704    });
705
706    if let Some(window) = documents.find_window(id) {
707        window.Document().highlight_dom_node(node.as_deref());
708    }
709}
710
711impl Element {
712    fn determine_auto_margins(&self) -> AutoMargins {
713        let Some(style) = self.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    }
724}