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::cell::RefCell;
9use std::collections::HashMap;
10
11use base::id::PipelineId;
12use devtools_traits::DevtoolScriptControlMsg::{GetChildren, GetDocumentElement, ModifyAttribute};
13use devtools_traits::{DevtoolScriptControlMsg, NodeInfo, ShadowRootMode};
14use ipc_channel::ipc::{self, IpcSender};
15use serde::Serialize;
16use serde_json::{self, Map, Value};
17
18use crate::actor::{Actor, ActorError, ActorRegistry};
19use crate::actors::inspector::walker::WalkerActor;
20use crate::protocol::ClientRequest;
21use crate::{EmptyReplyMsg, StreamId};
22
23/// Text node type constant. This is defined again to avoid depending on `script`, where it is defined originally.
24/// See `script::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants`.
25const TEXT_NODE: u16 = 3;
26
27/// The maximum length of a text node for it to appear as an inline child in the inspector.
28const MAX_INLINE_LENGTH: usize = 50;
29
30#[derive(Serialize)]
31struct GetUniqueSelectorReply {
32    from: String,
33    value: String,
34}
35
36#[derive(Clone, Serialize)]
37struct AttrMsg {
38    name: String,
39    value: String,
40}
41
42#[derive(Clone, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct NodeActorMsg {
45    pub actor: String,
46
47    /// The ID of the shadow host of this node, if it is
48    /// a shadow root
49    host: Option<String>,
50    #[serde(rename = "baseURI")]
51    base_uri: String,
52    causes_overflow: bool,
53    container_type: Option<()>,
54    pub display_name: String,
55    display_type: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    inline_text_child: Option<Box<NodeActorMsg>>,
58    is_after_pseudo_element: bool,
59    is_anonymous: bool,
60    is_before_pseudo_element: bool,
61    is_direct_shadow_host_child: Option<bool>,
62    /// Whether or not this node is displayed.
63    ///
64    /// Setting this value to `false` will cause the devtools to render the node name in gray.
65    is_displayed: bool,
66    #[serde(rename = "isInHTMLDocument")]
67    is_in_html_document: Option<bool>,
68    is_marker_pseudo_element: bool,
69    is_native_anonymous: bool,
70    is_scrollable: bool,
71    is_shadow_host: bool,
72    is_shadow_root: bool,
73    is_top_level_document: bool,
74    node_name: String,
75    node_type: u16,
76    node_value: Option<String>,
77    pub num_children: usize,
78    #[serde(skip_serializing_if = "String::is_empty")]
79    parent: String,
80    shadow_root_mode: Option<String>,
81    traits: HashMap<String, ()>,
82    attrs: Vec<AttrMsg>,
83
84    /// The `DOCTYPE` name if this is a `DocumentType` node, `None` otherwise
85    #[serde(skip_serializing_if = "Option::is_none")]
86    name: Option<String>,
87
88    /// The `DOCTYPE` public identifier if this is a `DocumentType` node, `None` otherwise
89    #[serde(skip_serializing_if = "Option::is_none")]
90    public_id: Option<String>,
91
92    /// The `DOCTYPE` system identifier if this is a `DocumentType` node, `None` otherwise
93    #[serde(skip_serializing_if = "Option::is_none")]
94    system_id: Option<String>,
95}
96
97pub struct NodeActor {
98    name: String,
99    pub script_chan: IpcSender<DevtoolScriptControlMsg>,
100    pub pipeline: PipelineId,
101    pub walker: String,
102    pub style_rules: RefCell<HashMap<(String, usize), String>>,
103}
104
105impl Actor for NodeActor {
106    fn name(&self) -> String {
107        self.name.clone()
108    }
109
110    /// The node actor can handle the following messages:
111    ///
112    /// - `modifyAttributes`: Asks the script to change a value in the attribute of the
113    ///   corresponding node
114    ///
115    /// - `getUniqueSelector`: Returns the display name of this node
116    fn handle_message(
117        &self,
118        mut request: ClientRequest,
119        registry: &ActorRegistry,
120        msg_type: &str,
121        msg: &Map<String, Value>,
122        _id: StreamId,
123    ) -> Result<(), ActorError> {
124        match msg_type {
125            "modifyAttributes" => {
126                let mods = msg
127                    .get("modifications")
128                    .ok_or(ActorError::MissingParameter)?
129                    .as_array()
130                    .ok_or(ActorError::BadParameterType)?;
131                let modifications: Vec<_> = mods
132                    .iter()
133                    .filter_map(|json_mod| {
134                        serde_json::from_str(&serde_json::to_string(json_mod).ok()?).ok()
135                    })
136                    .collect();
137
138                let walker = registry.find::<WalkerActor>(&self.walker);
139                walker.new_mutations(&mut request, &self.name, &modifications);
140
141                self.script_chan
142                    .send(ModifyAttribute(
143                        self.pipeline,
144                        registry.actor_to_script(self.name()),
145                        modifications,
146                    ))
147                    .map_err(|_| ActorError::Internal)?;
148
149                let reply = EmptyReplyMsg { from: self.name() };
150                request.reply_final(&reply)?
151            },
152
153            "getUniqueSelector" => {
154                let (tx, rx) = ipc::channel().unwrap();
155                self.script_chan
156                    .send(GetDocumentElement(self.pipeline, tx))
157                    .unwrap();
158                let doc_elem_info = rx
159                    .recv()
160                    .map_err(|_| ActorError::Internal)?
161                    .ok_or(ActorError::Internal)?;
162                let node = doc_elem_info.encode(
163                    registry,
164                    self.script_chan.clone(),
165                    self.pipeline,
166                    self.walker.clone(),
167                );
168
169                let msg = GetUniqueSelectorReply {
170                    from: self.name(),
171                    value: node.display_name,
172                };
173                request.reply_final(&msg)?
174            },
175
176            _ => return Err(ActorError::UnrecognizedPacketType),
177        };
178        Ok(())
179    }
180}
181
182pub trait NodeInfoToProtocol {
183    fn encode(
184        self,
185        actors: &ActorRegistry,
186        script_chan: IpcSender<DevtoolScriptControlMsg>,
187        pipeline: PipelineId,
188        walker: String,
189    ) -> NodeActorMsg;
190}
191
192impl NodeInfoToProtocol for NodeInfo {
193    fn encode(
194        self,
195        actors: &ActorRegistry,
196        script_chan: IpcSender<DevtoolScriptControlMsg>,
197        pipeline: PipelineId,
198        walker: String,
199    ) -> NodeActorMsg {
200        let get_or_register_node_actor = |id: &str| {
201            if !actors.script_actor_registered(id.to_string()) {
202                let name = actors.new_name("node");
203                actors.register_script_actor(id.to_string(), name.clone());
204
205                let node_actor = NodeActor {
206                    name: name.clone(),
207                    script_chan: script_chan.clone(),
208                    pipeline,
209                    walker: walker.clone(),
210                    style_rules: RefCell::new(HashMap::new()),
211                };
212                actors.register_later(Box::new(node_actor));
213                name
214            } else {
215                actors.script_to_actor(id.to_string())
216            }
217        };
218
219        let actor = get_or_register_node_actor(&self.unique_id);
220        let host = self
221            .host
222            .as_ref()
223            .map(|host_id| get_or_register_node_actor(host_id));
224
225        let name = actors.actor_to_script(actor.clone());
226
227        // If a node only has a single text node as a child whith a small enough text,
228        // return it with this node as an `inlineTextChild`.
229        let inline_text_child = (|| {
230            // TODO: Also return if this node is a flex element.
231            if self.num_children != 1 || self.node_name == "SLOT" {
232                return None;
233            }
234
235            let (tx, rx) = ipc::channel().ok()?;
236            script_chan
237                .send(GetChildren(pipeline, name.clone(), tx))
238                .unwrap();
239            let mut children = rx.recv().ok()??;
240
241            let child = children.pop()?;
242            let msg = child.encode(actors, script_chan.clone(), pipeline, walker);
243
244            // If the node child is not a text node, do not represent it inline.
245            if msg.node_type != TEXT_NODE {
246                return None;
247            }
248
249            // If the text node child is too big, do not represent it inline.
250            if msg.node_value.clone().unwrap_or_default().len() > MAX_INLINE_LENGTH {
251                return None;
252            }
253
254            Some(Box::new(msg))
255        })();
256
257        NodeActorMsg {
258            actor,
259            host,
260            base_uri: self.base_uri,
261            causes_overflow: false,
262            container_type: None,
263            display_name: self.node_name.clone().to_lowercase(),
264            display_type: self.display,
265            inline_text_child,
266            is_after_pseudo_element: false,
267            is_anonymous: false,
268            is_before_pseudo_element: false,
269            is_direct_shadow_host_child: None,
270            is_displayed: self.is_displayed,
271            is_in_html_document: Some(true),
272            is_marker_pseudo_element: false,
273            is_native_anonymous: false,
274            is_scrollable: false,
275            is_shadow_host: self.is_shadow_host,
276            is_shadow_root: self.shadow_root_mode.is_some(),
277            is_top_level_document: self.is_top_level_document,
278            node_name: self.node_name,
279            node_type: self.node_type,
280            node_value: self.node_value,
281            num_children: self.num_children,
282            parent: actors.script_to_actor(self.parent.clone()),
283            shadow_root_mode: self
284                .shadow_root_mode
285                .as_ref()
286                .map(ShadowRootMode::to_string),
287            traits: HashMap::new(),
288            attrs: self
289                .attrs
290                .into_iter()
291                .map(|attr| AttrMsg {
292                    name: attr.name,
293                    value: attr.value,
294                })
295                .collect(),
296            name: self.doctype_name,
297            public_id: self.doctype_public_identifier,
298            system_id: self.doctype_system_identifier,
299        }
300    }
301}