1use 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 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 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![], 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 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 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 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 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}