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