devtools/actors/inspector/
node.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
5//! This actor represents one DOM node. It is created by the Walker actor when it is traversing the
6//! document tree.
7
8use std::collections::HashMap;
9
10use atomic_refcell::AtomicRefCell;
11use devtools_traits::{
12    AttrModification, DevtoolScriptControlMsg, EventListenerInfo, NodeInfo, ShadowRootMode,
13};
14use malloc_size_of_derive::MallocSizeOf;
15use serde::Serialize;
16use serde_json::{self, Map, Value};
17use servo_base::generic_channel::{self, GenericSender};
18use servo_base::id::PipelineId;
19
20use crate::actor::{Actor, ActorError, ActorRegistry};
21use crate::protocol::ClientRequest;
22use crate::{EmptyReplyMsg, StreamId};
23
24/// Text node type constant. This is defined again to avoid depending on `script`, where it is defined originally.
25/// See `script::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants`.
26const TEXT_NODE: u16 = 3;
27
28/// The maximum length of a text node for it to appear as an inline child in the inspector.
29const MAX_INLINE_LENGTH: usize = 50;
30
31#[derive(Serialize)]
32struct GetEventListenerInfoReply {
33    from: String,
34    events: Vec<DevtoolsEventListenerInfo>,
35}
36
37#[derive(Serialize)]
38#[serde(rename_all = "camelCase")]
39struct DevtoolsEventListenerInfo {
40    r#type: String,
41    handler: String,
42    origin: String,
43    tags: String,
44    capturing: bool,
45    // This will always be an empty object, we just need a value that serializes to "{}".
46    hide: Value,
47    native: bool,
48    source_actor: String,
49    enabled: bool,
50    is_user_defined: bool,
51    event_listener_info_id: String,
52}
53
54#[derive(Serialize)]
55struct GetUniqueSelectorReply {
56    from: String,
57    value: String,
58}
59
60#[derive(Serialize)]
61struct GetXPathReply {
62    from: String,
63    value: String,
64}
65
66#[derive(Clone, Serialize)]
67struct AttrMsg {
68    name: String,
69    value: String,
70}
71
72#[derive(Clone, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub(crate) struct NodeActorMsg {
75    pub actor: String,
76
77    /// The ID of the shadow host of this node, if it is
78    /// a shadow root
79    host: Option<String>,
80    #[serde(rename = "baseURI")]
81    base_uri: String,
82    causes_overflow: bool,
83    container_type: Option<()>,
84    pub display_name: String,
85    display_type: Option<String>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    inline_text_child: Option<Box<NodeActorMsg>>,
88    is_after_pseudo_element: bool,
89    is_anonymous: bool,
90    is_before_pseudo_element: bool,
91    is_direct_shadow_host_child: Option<bool>,
92    /// Whether or not this node is displayed.
93    ///
94    /// Setting this value to `false` will cause the devtools to render the node name in gray.
95    is_displayed: bool,
96    #[serde(rename = "isInHTMLDocument")]
97    is_in_html_document: Option<bool>,
98    is_marker_pseudo_element: bool,
99    is_native_anonymous: bool,
100    is_scrollable: bool,
101    is_shadow_host: bool,
102    is_shadow_root: bool,
103    is_top_level_document: bool,
104    node_name: String,
105    node_type: u16,
106    node_value: Option<String>,
107    pub num_children: usize,
108    #[serde(skip_serializing_if = "String::is_empty")]
109    parent: String,
110    shadow_root_mode: Option<String>,
111    traits: HashMap<String, ()>,
112    attrs: Vec<AttrMsg>,
113
114    /// The `DOCTYPE` name if this is a `DocumentType` node, `None` otherwise
115    #[serde(skip_serializing_if = "Option::is_none")]
116    name: Option<String>,
117
118    /// The `DOCTYPE` public identifier if this is a `DocumentType` node, `None` otherwise
119    #[serde(skip_serializing_if = "Option::is_none")]
120    public_id: Option<String>,
121
122    /// The `DOCTYPE` system identifier if this is a `DocumentType` node, `None` otherwise
123    #[serde(skip_serializing_if = "Option::is_none")]
124    system_id: Option<String>,
125
126    has_event_listeners: bool,
127}
128
129#[derive(MallocSizeOf)]
130pub(crate) struct NodeActor {
131    name: String,
132    pub script_chan: GenericSender<DevtoolScriptControlMsg>,
133    pub pipeline: PipelineId,
134    pub walker: String,
135    pub style_rules: AtomicRefCell<HashMap<(String, usize), String>>,
136}
137
138impl Actor for NodeActor {
139    fn name(&self) -> String {
140        self.name.clone()
141    }
142
143    /// The node actor can handle the following messages:
144    ///
145    /// - `modifyAttributes`: Asks the script to change a value in the attribute of the
146    ///   corresponding node
147    ///
148    /// - `getUniqueSelector`: Returns the display name of this node
149    fn handle_message(
150        &self,
151        request: ClientRequest,
152        registry: &ActorRegistry,
153        msg_type: &str,
154        msg: &Map<String, Value>,
155        _id: StreamId,
156    ) -> Result<(), ActorError> {
157        match msg_type {
158            "modifyAttributes" => {
159                let mods = msg
160                    .get("modifications")
161                    .ok_or(ActorError::MissingParameter)?
162                    .as_array()
163                    .ok_or(ActorError::BadParameterType)?;
164                let modifications: Vec<AttrModification> = mods
165                    .iter()
166                    .filter_map(|json_mod| {
167                        serde_json::from_str(&serde_json::to_string(json_mod).ok()?).ok()
168                    })
169                    .collect();
170
171                self.script_chan
172                    .send(DevtoolScriptControlMsg::ModifyAttribute(
173                        self.pipeline,
174                        registry.actor_to_script(self.name()),
175                        modifications,
176                    ))
177                    .map_err(|_| ActorError::Internal)?;
178
179                let reply = EmptyReplyMsg { from: self.name() };
180                request.reply_final(&reply)?
181            },
182            "getEventListenerInfo" => {
183                let target = msg
184                    .get("to")
185                    .ok_or(ActorError::MissingParameter)?
186                    .as_str()
187                    .ok_or(ActorError::BadParameterType)?;
188
189                let (tx, rx) = generic_channel::channel().ok_or(ActorError::Internal)?;
190                self.script_chan
191                    .send(DevtoolScriptControlMsg::GetEventListenerInfo(
192                        self.pipeline,
193                        registry.actor_to_script(target.to_owned()),
194                        tx,
195                    ))
196                    .unwrap();
197                let event_listeners = rx.recv().map_err(|_| ActorError::Internal)?;
198
199                let msg = GetEventListenerInfoReply {
200                    from: self.name(),
201                    events: event_listeners.into_iter().map(From::from).collect(),
202                };
203                request.reply_final(&msg)?
204            },
205            "getUniqueSelector" => {
206                let (tx, rx) = generic_channel::channel().unwrap();
207                self.script_chan
208                    .send(DevtoolScriptControlMsg::GetDocumentElement(
209                        self.pipeline,
210                        tx,
211                    ))
212                    .unwrap();
213                let doc_elem_info = rx
214                    .recv()
215                    .map_err(|_| ActorError::Internal)?
216                    .ok_or(ActorError::Internal)?;
217                let node = doc_elem_info.encode(
218                    registry,
219                    self.script_chan.clone(),
220                    self.pipeline,
221                    self.walker.clone(),
222                );
223
224                let msg = GetUniqueSelectorReply {
225                    from: self.name(),
226                    value: node.display_name,
227                };
228                request.reply_final(&msg)?
229            },
230            "getXPath" => {
231                let target = msg
232                    .get("to")
233                    .ok_or(ActorError::MissingParameter)?
234                    .as_str()
235                    .ok_or(ActorError::BadParameterType)?;
236
237                let (tx, rx) = generic_channel::channel().unwrap();
238                self.script_chan
239                    .send(DevtoolScriptControlMsg::GetXPath(
240                        self.pipeline,
241                        registry.actor_to_script(target.to_owned()),
242                        tx,
243                    ))
244                    .unwrap();
245
246                let xpath_selector = rx.recv().map_err(|_| ActorError::Internal)?;
247                let msg = GetXPathReply {
248                    from: self.name(),
249                    value: xpath_selector,
250                };
251                request.reply_final(&msg)?
252            },
253
254            _ => return Err(ActorError::UnrecognizedPacketType),
255        };
256        Ok(())
257    }
258}
259
260pub trait NodeInfoToProtocol {
261    fn encode(
262        self,
263        registry: &ActorRegistry,
264        script_chan: GenericSender<DevtoolScriptControlMsg>,
265        pipeline: PipelineId,
266        walker: String,
267    ) -> NodeActorMsg;
268}
269
270impl NodeInfoToProtocol for NodeInfo {
271    fn encode(
272        self,
273        registry: &ActorRegistry,
274        script_chan: GenericSender<DevtoolScriptControlMsg>,
275        pipeline: PipelineId,
276        walker: String,
277    ) -> NodeActorMsg {
278        let get_or_register_node_actor = |id: &str| {
279            if !registry.script_actor_registered(id.to_string()) {
280                let node_name = registry.new_name::<NodeActor>();
281                registry.register_script_actor(id.to_string(), node_name.clone());
282
283                let node_actor = NodeActor {
284                    name: node_name.clone(),
285                    script_chan: script_chan.clone(),
286                    pipeline,
287                    walker: walker.clone(),
288                    style_rules: AtomicRefCell::new(HashMap::new()),
289                };
290                registry.register(node_actor);
291                node_name
292            } else {
293                registry.script_to_actor(id.to_string())
294            }
295        };
296
297        let actor = get_or_register_node_actor(&self.unique_id);
298        let host = self
299            .host
300            .as_ref()
301            .map(|host_id| get_or_register_node_actor(host_id));
302
303        let name = registry.actor_to_script(actor.clone());
304
305        // If a node only has a single text node as a child whith a small enough text,
306        // return it with this node as an `inlineTextChild`.
307        let inline_text_child = (|| {
308            // TODO: Also return if this node is a flex element.
309            if self.num_children != 1 || self.node_name == "SLOT" {
310                return None;
311            }
312
313            let (tx, rx) = generic_channel::channel()?;
314            script_chan
315                .send(DevtoolScriptControlMsg::GetChildren(
316                    pipeline,
317                    name.clone(),
318                    tx,
319                ))
320                .unwrap();
321            let mut children = rx.recv().ok()??;
322
323            let child = children.pop()?;
324            let msg = child.encode(registry, script_chan.clone(), pipeline, walker);
325
326            // If the node child is not a text node, do not represent it inline.
327            if msg.node_type != TEXT_NODE {
328                return None;
329            }
330
331            // If the text node child is too big, do not represent it inline.
332            if msg.node_value.clone().unwrap_or_default().len() > MAX_INLINE_LENGTH {
333                return None;
334            }
335
336            Some(Box::new(msg))
337        })();
338
339        NodeActorMsg {
340            actor,
341            host,
342            base_uri: self.base_uri,
343            causes_overflow: false,
344            container_type: None,
345            display_name: self.node_name.clone().to_lowercase(),
346            display_type: self.display,
347            inline_text_child,
348            is_after_pseudo_element: false,
349            is_anonymous: false,
350            is_before_pseudo_element: false,
351            is_direct_shadow_host_child: None,
352            is_displayed: self.is_displayed,
353            is_in_html_document: Some(true),
354            is_marker_pseudo_element: false,
355            is_native_anonymous: false,
356            is_scrollable: false,
357            is_shadow_host: self.is_shadow_host,
358            is_shadow_root: self.shadow_root_mode.is_some(),
359            is_top_level_document: self.is_top_level_document,
360            node_name: self.node_name,
361            node_type: self.node_type,
362            node_value: self.node_value,
363            num_children: self.num_children,
364            parent: registry.script_to_actor(self.parent.clone()),
365            shadow_root_mode: self
366                .shadow_root_mode
367                .as_ref()
368                .map(ShadowRootMode::to_string),
369            traits: HashMap::new(),
370            attrs: self
371                .attrs
372                .into_iter()
373                .map(|attr| AttrMsg {
374                    name: attr.name,
375                    value: attr.value,
376                })
377                .collect(),
378            name: self.doctype_name,
379            public_id: self.doctype_public_identifier,
380            system_id: self.doctype_system_identifier,
381            has_event_listeners: self.has_event_listeners,
382        }
383    }
384}
385
386impl From<EventListenerInfo> for DevtoolsEventListenerInfo {
387    fn from(event_listener_info: EventListenerInfo) -> Self {
388        Self {
389            r#type: event_listener_info.event_type,
390            handler: "todo".to_owned(),
391            capturing: event_listener_info.capturing,
392            origin: "todo".to_owned(),
393            tags: "".to_owned(),
394            hide: Value::Object(Default::default()),
395            native: false,
396            source_actor: "todo".to_owned(),
397            enabled: true,
398            is_user_defined: false,
399            event_listener_info_id: "todo".to_owned(),
400        }
401    }
402}