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