1use std::collections::HashMap;
6use std::str;
7
8use base::generic_channel::GenericSender;
9use base::id::PipelineId;
10use devtools_traits::{
11 AttrModification, AutoMargins, ComputedNodeLayout, CssDatabaseProperty, EvaluateJSReply,
12 EventListenerInfo, NodeInfo, NodeStyle, RuleModification, TimelineMarker, TimelineMarkerType,
13};
14use js::context::JSContext;
15use js::conversions::jsstr_to_string;
16use js::jsval::UndefinedValue;
17use js::rust::ToString;
18use markup5ever::{LocalName, ns};
19use servo_config::pref;
20use style::attr::AttrValue;
21use uuid::Uuid;
22
23use crate::document_collection::DocumentCollection;
24use crate::dom::bindings::codegen::Bindings::CSSRuleListBinding::CSSRuleListMethods;
25use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
26use crate::dom::bindings::codegen::Bindings::CSSStyleRuleBinding::CSSStyleRuleMethods;
27use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::CSSStyleSheetMethods;
28use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
29use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
30use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
31use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
32use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants;
33use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
34use crate::dom::bindings::conversions::{ConversionResult, FromJSValConvertible};
35use crate::dom::bindings::inheritance::Castable;
36use crate::dom::bindings::root::DomRoot;
37use crate::dom::bindings::str::DOMString;
38use crate::dom::css::cssstyledeclaration::ENABLED_LONGHAND_PROPERTIES;
39use crate::dom::css::cssstylerule::CSSStyleRule;
40use crate::dom::document::AnimationFrameCallback;
41use crate::dom::element::Element;
42use crate::dom::globalscope::GlobalScope;
43use crate::dom::node::{Node, NodeTraits, ShadowIncluding};
44use crate::dom::types::{EventTarget, HTMLElement};
45use crate::realms::{enter_auto_realm, enter_realm};
46use crate::script_runtime::{CanGc, IntroductionType};
47
48#[expect(unsafe_code)]
49pub(crate) fn handle_evaluate_js(
50 global: &GlobalScope,
51 eval: String,
52 reply: GenericSender<EvaluateJSReply>,
53 cx: &mut JSContext,
54) {
55 let result = unsafe {
56 let mut realm = enter_auto_realm(cx, global);
57 let cx = &mut realm;
58 rooted!(&in(cx) let mut rval = UndefinedValue());
59 _ = global.evaluate_js_on_global(
62 eval.into(),
63 "<eval>",
64 Some(IntroductionType::DEBUGGER_EVAL),
65 rval.handle_mut(),
66 CanGc::from_cx(cx),
67 );
68
69 if rval.is_undefined() {
70 EvaluateJSReply::VoidValue
71 } else if rval.is_boolean() {
72 EvaluateJSReply::BooleanValue(rval.to_boolean())
73 } else if rval.is_double() || rval.is_int32() {
74 EvaluateJSReply::NumberValue(
75 match FromJSValConvertible::from_jsval(cx.raw_cx(), rval.handle(), ()) {
76 Ok(ConversionResult::Success(v)) => v,
77 _ => unreachable!(),
78 },
79 )
80 } else if rval.is_string() {
81 let jsstr = std::ptr::NonNull::new(rval.to_string()).unwrap();
82 EvaluateJSReply::StringValue(jsstr_to_string(cx.raw_cx(), jsstr))
83 } else if rval.is_null() {
84 EvaluateJSReply::NullValue
85 } else {
86 assert!(rval.is_object());
87
88 let jsstr = std::ptr::NonNull::new(ToString(cx.raw_cx(), rval.handle())).unwrap();
89 let class_name = jsstr_to_string(cx.raw_cx(), jsstr);
90
91 EvaluateJSReply::ActorValue {
92 class: class_name,
93 uuid: Uuid::new_v4().to_string(),
94 }
95 }
96 };
97 reply.send(result).unwrap();
98}
99
100pub(crate) fn handle_get_event_listener_info(
101 documents: &DocumentCollection,
102 pipeline: PipelineId,
103 node_id: &str,
104 reply: GenericSender<Vec<EventListenerInfo>>,
105) {
106 let Some(node) = find_node_by_unique_id(documents, pipeline, node_id) else {
107 reply.send(vec![]).unwrap();
108 return;
109 };
110
111 let event_listeners = node
112 .upcast::<EventTarget>()
113 .summarize_event_listeners_for_devtools();
114 reply.send(event_listeners).unwrap();
115}
116
117pub(crate) fn handle_get_root_node(
118 documents: &DocumentCollection,
119 pipeline: PipelineId,
120 reply: GenericSender<Option<NodeInfo>>,
121 can_gc: CanGc,
122) {
123 let info = documents
124 .find_document(pipeline)
125 .map(|document| document.upcast::<Node>().summarize(can_gc));
126 reply.send(info).unwrap();
127}
128
129pub(crate) fn handle_get_document_element(
130 documents: &DocumentCollection,
131 pipeline: PipelineId,
132 reply: GenericSender<Option<NodeInfo>>,
133 can_gc: CanGc,
134) {
135 let info = documents
136 .find_document(pipeline)
137 .and_then(|document| document.GetDocumentElement())
138 .map(|element| element.upcast::<Node>().summarize(can_gc));
139 reply.send(info).unwrap();
140}
141
142fn find_node_by_unique_id(
143 documents: &DocumentCollection,
144 pipeline: PipelineId,
145 node_id: &str,
146) -> Option<DomRoot<Node>> {
147 documents.find_document(pipeline).and_then(|document| {
148 document
149 .upcast::<Node>()
150 .traverse_preorder(ShadowIncluding::Yes)
151 .find(|candidate| candidate.unique_id(pipeline) == node_id)
152 })
153}
154
155pub(crate) fn handle_get_children(
156 documents: &DocumentCollection,
157 pipeline: PipelineId,
158 node_id: String,
159 reply: GenericSender<Option<Vec<NodeInfo>>>,
160 can_gc: CanGc,
161) {
162 match find_node_by_unique_id(documents, pipeline, &node_id) {
163 None => reply.send(None).unwrap(),
164 Some(parent) => {
165 let is_whitespace = |node: &NodeInfo| {
166 node.node_type == NodeConstants::TEXT_NODE &&
167 node.node_value.as_ref().is_none_or(|v| v.trim().is_empty())
168 };
169
170 let inline: Vec<_> = parent
171 .children()
172 .map(|child| {
173 let window = child.owner_window();
174 let Some(elem) = child.downcast::<Element>() else {
175 return false;
176 };
177 let computed_style = window.GetComputedStyle(elem, None);
178 let display = computed_style.Display();
179 display == "inline"
180 })
181 .collect();
182
183 let mut children = vec![];
184 if let Some(shadow_root) = parent.downcast::<Element>().and_then(Element::shadow_root) {
185 if !shadow_root.is_user_agent_widget() ||
186 pref!(inspector_show_servo_internal_shadow_roots)
187 {
188 children.push(shadow_root.upcast::<Node>().summarize(can_gc));
189 }
190 }
191 let children_iter = parent.children().enumerate().filter_map(|(i, child)| {
192 let prev_inline = i > 0 && inline[i - 1];
195 let next_inline = i < inline.len() - 1 && inline[i + 1];
196
197 let info = child.summarize(can_gc);
198 if !is_whitespace(&info) {
199 return Some(info);
200 }
201
202 (prev_inline && next_inline).then_some(info)
203 });
204 children.extend(children_iter);
205
206 reply.send(Some(children)).unwrap();
207 },
208 };
209}
210
211pub(crate) fn handle_get_attribute_style(
212 documents: &DocumentCollection,
213 pipeline: PipelineId,
214 node_id: String,
215 reply: GenericSender<Option<Vec<NodeStyle>>>,
216 can_gc: CanGc,
217) {
218 let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
219 None => return reply.send(None).unwrap(),
220 Some(found_node) => found_node,
221 };
222
223 let Some(elem) = node.downcast::<HTMLElement>() else {
224 reply.send(None).unwrap();
226 return;
227 };
228 let style = elem.Style(can_gc);
229
230 let msg = (0..style.Length())
231 .map(|i| {
232 let name = style.Item(i);
233 NodeStyle {
234 name: name.to_string(),
235 value: style.GetPropertyValue(name.clone()).to_string(),
236 priority: style.GetPropertyPriority(name).to_string(),
237 }
238 })
239 .collect();
240
241 reply.send(Some(msg)).unwrap();
242}
243
244#[cfg_attr(crown, expect(crown::unrooted_must_root))]
245pub(crate) fn handle_get_stylesheet_style(
246 documents: &DocumentCollection,
247 pipeline: PipelineId,
248 node_id: String,
249 selector: String,
250 stylesheet: usize,
251 reply: GenericSender<Option<Vec<NodeStyle>>>,
252 can_gc: CanGc,
253) {
254 let msg = (|| {
255 let node = find_node_by_unique_id(documents, pipeline, &node_id)?;
256
257 let document = documents.find_document(pipeline)?;
258 let _realm = enter_realm(document.window());
259 let owner = node.stylesheet_list_owner();
260
261 let stylesheet = owner.stylesheet_at(stylesheet)?;
262 let list = stylesheet.GetCssRules(can_gc).ok()?;
263
264 let styles = (0..list.Length())
265 .filter_map(move |i| {
266 let rule = list.Item(i, can_gc)?;
267 let style = rule.downcast::<CSSStyleRule>()?;
268 if selector != style.SelectorText() {
269 return None;
270 };
271 Some(style.Style(can_gc))
272 })
273 .flat_map(|style| {
274 (0..style.Length()).map(move |i| {
275 let name = style.Item(i);
276 NodeStyle {
277 name: name.to_string(),
278 value: style.GetPropertyValue(name.clone()).to_string(),
279 priority: style.GetPropertyPriority(name).to_string(),
280 }
281 })
282 })
283 .collect();
284
285 Some(styles)
286 })();
287
288 reply.send(msg).unwrap();
289}
290
291#[cfg_attr(crown, expect(crown::unrooted_must_root))]
292pub(crate) fn handle_get_selectors(
293 documents: &DocumentCollection,
294 pipeline: PipelineId,
295 node_id: String,
296 reply: GenericSender<Option<Vec<(String, usize)>>>,
297 can_gc: CanGc,
298) {
299 let msg = (|| {
300 let node = find_node_by_unique_id(documents, pipeline, &node_id)?;
301
302 let document = documents.find_document(pipeline)?;
303 let _realm = enter_realm(document.window());
304 let owner = node.stylesheet_list_owner();
305
306 let rules = (0..owner.stylesheet_count())
307 .filter_map(|i| {
308 let stylesheet = owner.stylesheet_at(i)?;
309 let list = stylesheet.GetCssRules(can_gc).ok()?;
310 let elem = node.downcast::<Element>()?;
311
312 Some((0..list.Length()).filter_map(move |j| {
313 let rule = list.Item(j, can_gc)?;
314 let style = rule.downcast::<CSSStyleRule>()?;
315 let selector = style.SelectorText();
316 elem.Matches(selector.clone()).ok()?.then_some(())?;
317 Some((selector.into(), i))
318 }))
319 })
320 .flatten()
321 .collect();
322
323 Some(rules)
324 })();
325
326 reply.send(msg).unwrap();
327}
328
329pub(crate) fn handle_get_computed_style(
330 documents: &DocumentCollection,
331 pipeline: PipelineId,
332 node_id: String,
333 reply: GenericSender<Option<Vec<NodeStyle>>>,
334) {
335 let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
336 None => return reply.send(None).unwrap(),
337 Some(found_node) => found_node,
338 };
339
340 let window = node.owner_window();
341 let elem = node
342 .downcast::<Element>()
343 .expect("This should be an element");
344 let computed_style = window.GetComputedStyle(elem, None);
345
346 let msg = (0..computed_style.Length())
347 .map(|i| {
348 let name = computed_style.Item(i);
349 NodeStyle {
350 name: name.to_string(),
351 value: computed_style.GetPropertyValue(name.clone()).to_string(),
352 priority: computed_style.GetPropertyPriority(name).to_string(),
353 }
354 })
355 .collect();
356
357 reply.send(Some(msg)).unwrap();
358}
359
360pub(crate) fn handle_get_layout(
361 documents: &DocumentCollection,
362 pipeline: PipelineId,
363 node_id: String,
364 reply: GenericSender<Option<(ComputedNodeLayout, AutoMargins)>>,
365 can_gc: CanGc,
366) {
367 let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
368 None => return reply.send(None).unwrap(),
369 Some(found_node) => found_node,
370 };
371 let auto_margins = determine_auto_margins(&node);
372
373 let elem = node
374 .downcast::<Element>()
375 .expect("should be getting layout of element");
376 let rect = elem.GetBoundingClientRect(can_gc);
377 let width = rect.Width() as f32;
378 let height = rect.Height() as f32;
379
380 let window = node.owner_window();
381 let computed_style = window.GetComputedStyle(elem, None);
382 let computed_layout = ComputedNodeLayout {
383 display: computed_style.Display().into(),
384 position: computed_style.Position().into(),
385 z_index: computed_style.ZIndex().into(),
386 box_sizing: computed_style.BoxSizing().into(),
387 margin_top: computed_style.MarginTop().into(),
388 margin_right: computed_style.MarginRight().into(),
389 margin_bottom: computed_style.MarginBottom().into(),
390 margin_left: computed_style.MarginLeft().into(),
391 border_top_width: computed_style.BorderTopWidth().into(),
392 border_right_width: computed_style.BorderRightWidth().into(),
393 border_bottom_width: computed_style.BorderBottomWidth().into(),
394 border_left_width: computed_style.BorderLeftWidth().into(),
395 padding_top: computed_style.PaddingTop().into(),
396 padding_right: computed_style.PaddingRight().into(),
397 padding_bottom: computed_style.PaddingBottom().into(),
398 padding_left: computed_style.PaddingLeft().into(),
399 width,
400 height,
401 };
402
403 reply.send(Some((computed_layout, auto_margins))).unwrap();
404}
405
406pub(crate) fn handle_get_xpath(
407 documents: &DocumentCollection,
408 pipeline: PipelineId,
409 node_id: String,
410 reply: GenericSender<String>,
411) {
412 let Some(node) = find_node_by_unique_id(documents, pipeline, &node_id) else {
413 return reply.send(Default::default()).unwrap();
414 };
415
416 let selector = node
417 .inclusive_ancestors(ShadowIncluding::Yes)
418 .filter_map(|ancestor| {
419 let Some(element) = ancestor.downcast::<Element>() else {
420 return None;
422 };
423
424 let mut result = "/".to_owned();
425 if *element.namespace() != ns!(html) {
426 result.push_str(element.namespace());
427 result.push(':');
428 }
429
430 result.push_str(element.local_name());
431
432 let would_node_also_match_selector = |sibling: &Node| {
433 let Some(sibling) = sibling.downcast::<Element>() else {
434 return false;
435 };
436 sibling.namespace() == element.namespace() &&
437 sibling.local_name() == element.local_name()
438 };
439
440 let matching_elements_before = ancestor
441 .preceding_siblings()
442 .filter(|node| would_node_also_match_selector(node))
443 .count();
444 let matching_elements_after = ancestor
445 .following_siblings()
446 .filter(|node| would_node_also_match_selector(node))
447 .count();
448
449 if matching_elements_before + matching_elements_after != 0 {
450 result.push_str(&format!("[{}]", matching_elements_before + 1));
452 }
453
454 Some(result)
455 })
456 .collect::<Vec<_>>()
457 .into_iter()
458 .rev()
459 .collect::<Vec<_>>()
460 .join("");
461
462 reply.send(selector).unwrap();
463}
464
465fn determine_auto_margins(node: &Node) -> AutoMargins {
466 let Some(style) = node.style() else {
467 return AutoMargins::default();
468 };
469 let margin = style.get_margin();
470 AutoMargins {
471 top: margin.margin_top.is_auto(),
472 right: margin.margin_right.is_auto(),
473 bottom: margin.margin_bottom.is_auto(),
474 left: margin.margin_left.is_auto(),
475 }
476}
477
478pub(crate) fn handle_modify_attribute(
479 documents: &DocumentCollection,
480 pipeline: PipelineId,
481 node_id: String,
482 modifications: Vec<AttrModification>,
483 can_gc: CanGc,
484) {
485 let Some(document) = documents.find_document(pipeline) else {
486 return warn!("document for pipeline id {} is not found", &pipeline);
487 };
488 let _realm = enter_realm(document.window());
489
490 let node = match find_node_by_unique_id(documents, pipeline, &node_id) {
491 None => {
492 return warn!(
493 "node id {} for pipeline id {} is not found",
494 &node_id, &pipeline
495 );
496 },
497 Some(found_node) => found_node,
498 };
499
500 let elem = node
501 .downcast::<Element>()
502 .expect("should be getting layout of element");
503
504 for modification in modifications {
505 match modification.new_value {
506 Some(string) => {
507 elem.set_attribute(
508 &LocalName::from(modification.attribute_name),
509 AttrValue::String(string),
510 can_gc,
511 );
512 },
513 None => elem.RemoveAttribute(DOMString::from(modification.attribute_name), can_gc),
514 }
515 }
516}
517
518pub(crate) fn handle_modify_rule(
519 documents: &DocumentCollection,
520 pipeline: PipelineId,
521 node_id: String,
522 modifications: Vec<RuleModification>,
523 can_gc: CanGc,
524) {
525 let Some(document) = documents.find_document(pipeline) else {
526 return warn!("Document for pipeline id {} is not found", &pipeline);
527 };
528 let _realm = enter_realm(document.window());
529
530 let Some(node) = find_node_by_unique_id(documents, pipeline, &node_id) else {
531 return warn!(
532 "Node id {} for pipeline id {} is not found",
533 &node_id, &pipeline
534 );
535 };
536
537 let elem = node
538 .downcast::<HTMLElement>()
539 .expect("This should be an HTMLElement");
540 let style = elem.Style(can_gc);
541
542 for modification in modifications {
543 let _ = style.SetProperty(
544 modification.name.into(),
545 modification.value.into(),
546 modification.priority.into(),
547 can_gc,
548 );
549 }
550}
551
552pub(crate) fn handle_wants_live_notifications(global: &GlobalScope, send_notifications: bool) {
553 global.set_devtools_wants_updates(send_notifications);
554}
555
556pub(crate) fn handle_set_timeline_markers(
557 documents: &DocumentCollection,
558 pipeline: PipelineId,
559 marker_types: Vec<TimelineMarkerType>,
560 reply: GenericSender<Option<TimelineMarker>>,
561) {
562 match documents.find_window(pipeline) {
563 None => reply.send(None).unwrap(),
564 Some(window) => window.set_devtools_timeline_markers(marker_types, reply),
565 }
566}
567
568pub(crate) fn handle_drop_timeline_markers(
569 documents: &DocumentCollection,
570 pipeline: PipelineId,
571 marker_types: Vec<TimelineMarkerType>,
572) {
573 if let Some(window) = documents.find_window(pipeline) {
574 window.drop_devtools_timeline_markers(marker_types);
575 }
576}
577
578pub(crate) fn handle_request_animation_frame(
579 documents: &DocumentCollection,
580 id: PipelineId,
581 actor_name: String,
582) {
583 if let Some(doc) = documents.find_document(id) {
584 doc.request_animation_frame(AnimationFrameCallback::DevtoolsFramerateTick { actor_name });
585 }
586}
587
588pub(crate) fn handle_get_css_database(reply: GenericSender<HashMap<String, CssDatabaseProperty>>) {
589 let database: HashMap<_, _> = ENABLED_LONGHAND_PROPERTIES
590 .iter()
591 .map(|l| {
592 (
593 l.name().into(),
594 CssDatabaseProperty {
595 is_inherited: l.inherited(),
596 values: vec![], supports: vec![],
598 subproperties: vec![l.name().into()],
599 },
600 )
601 })
602 .collect();
603 let _ = reply.send(database);
604}
605
606pub(crate) fn handle_highlight_dom_node(
607 documents: &DocumentCollection,
608 id: PipelineId,
609 node_id: Option<String>,
610) {
611 let node = node_id.and_then(|node_id| {
612 let node = find_node_by_unique_id(documents, id, &node_id);
613 if node.is_none() {
614 log::warn!("Node id {node_id} for pipeline id {id} is not found",);
615 }
616 node
617 });
618
619 if let Some(window) = documents.find_window(id) {
620 window.Document().highlight_dom_node(node.as_deref());
621 }
622}