diff --git a/components/devtools/actors/inspector/node.rs b/components/devtools/actors/inspector/node.rs index a27c0637c58..041a7ee1618 100644 --- a/components/devtools/actors/inspector/node.rs +++ b/components/devtools/actors/inspector/node.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use std::net::TcpStream; use base::id::PipelineId; -use devtools_traits::DevtoolScriptControlMsg::{GetDocumentElement, ModifyAttribute}; +use devtools_traits::DevtoolScriptControlMsg::{GetChildren, GetDocumentElement, ModifyAttribute}; use devtools_traits::{DevtoolScriptControlMsg, NodeInfo}; use ipc_channel::ipc::{self, IpcSender}; use serde::Serialize; @@ -19,6 +19,13 @@ use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::protocol::JsonPacketStream; use crate::{EmptyReplyMsg, StreamId}; +/// Text node type constant. This is defined again to avoid depending on `script`, where it is defined originally. +/// See `script::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants`. +const TEXT_NODE: u16 = 3; + +/// The maximum length of a text node for it to appear as an inline child in the inspector. +const MAX_INLINE_LENGTH: usize = 50; + #[derive(Serialize)] struct GetUniqueSelectorReply { from: String, @@ -41,6 +48,8 @@ pub struct NodeActorMsg { container_type: Option<()>, pub display_name: String, display_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + inline_text_child: Option>, is_after_pseudo_element: bool, is_anonymous: bool, is_before_pseudo_element: bool, @@ -55,7 +64,7 @@ pub struct NodeActorMsg { is_top_level_document: bool, node_name: String, node_type: u16, - node_value: Option<()>, + node_value: Option, pub num_children: usize, #[serde(skip_serializing_if = "String::is_empty")] parent: String, @@ -156,7 +165,7 @@ impl NodeInfoToProtocol for NodeInfo { let name = actors.new_name("node"); let node_actor = NodeActor { name: name.clone(), - script_chan, + script_chan: script_chan.clone(), pipeline, }; actors.register_script_actor(self.unique_id, name.clone()); @@ -166,6 +175,38 @@ impl NodeInfoToProtocol for NodeInfo { actors.script_to_actor(self.unique_id) }; + let name = actors.actor_to_script(actor.clone()); + + // If a node only has a single text node as a child whith a small enough text, + // return it with this node as an `inlineTextChild`. + let inline_text_child = (|| { + // TODO: Also return if this node is a flex element. + if self.num_children != 1 || self.node_name == "SLOT" { + return None; + } + + let (tx, rx) = ipc::channel().ok()?; + script_chan + .send(GetChildren(pipeline, name.clone(), tx)) + .unwrap(); + let mut children = rx.recv().ok()??; + + let child = children.pop()?; + let msg = child.encode(actors, true, script_chan.clone(), pipeline); + + // If the node child is not a text node, do not represent it inline. + if msg.node_type != TEXT_NODE { + return None; + } + + // If the text node child is too big, do not represent it inline. + if msg.node_value.clone().unwrap_or_default().len() > MAX_INLINE_LENGTH { + return None; + } + + Some(Box::new(msg)) + })(); + NodeActorMsg { actor, base_uri: self.base_uri, @@ -173,6 +214,7 @@ impl NodeInfoToProtocol for NodeInfo { container_type: None, display_name: self.node_name.clone().to_lowercase(), display_type: Some("block".into()), + inline_text_child, is_after_pseudo_element: false, is_anonymous: false, is_before_pseudo_element: false, @@ -186,7 +228,7 @@ impl NodeInfoToProtocol for NodeInfo { is_top_level_document: self.is_top_level_document, node_name: self.node_name, node_type: self.node_type, - node_value: None, + node_value: self.node_value, num_children: self.num_children, parent: actors.script_to_actor(self.parent.clone()), shadow_root_mode: None, diff --git a/components/script/devtools.rs b/components/script/devtools.rs index 597ed25dfd5..14f792555a4 100644 --- a/components/script/devtools.rs +++ b/components/script/devtools.rs @@ -19,6 +19,7 @@ use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyl use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods; use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; +use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeConstants; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::conversions::{jsstring_to_str, ConversionResult, FromJSValConvertible}; use crate::dom::bindings::inheritance::Castable; @@ -125,7 +126,43 @@ pub fn handle_get_children( match find_node_by_unique_id(documents, pipeline, &node_id) { None => reply.send(None).unwrap(), Some(parent) => { - let children = parent.children().map(|child| child.summarize()).collect(); + let is_whitespace = |node: &NodeInfo| { + node.node_type == NodeConstants::TEXT_NODE && + node.node_value + .as_ref() + .map_or(true, |v| v.trim().is_empty()) + }; + + let inline: Vec<_> = parent + .children() + .map(|child| { + let window = window_from_node(&*child); + let Some(elem) = child.downcast::() else { + return false; + }; + let computed_style = window.GetComputedStyle(elem, None); + let display = computed_style.Display(); + display == "inline" + }) + .collect(); + + let children: Vec<_> = parent + .children() + .enumerate() + .filter_map(|(i, child)| { + // Filter whitespace only text nodes that are not inline level + // https://firefox-source-docs.mozilla.org/devtools-user/page_inspector/how_to/examine_and_edit_html/index.html#whitespace-only-text-nodes + let prev_inline = i > 0 && inline[i - 1]; + let next_inline = i < inline.len() - 1 && inline[i + 1]; + + let info = child.summarize(); + if !is_whitespace(&info) { + return Some(info); + } + + (prev_inline && next_inline).then_some(info) + }) + .collect(); reply.send(Some(children)).unwrap(); }, diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 5e818348665..418bc2e6b04 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -1123,6 +1123,7 @@ impl Node { node_type, is_top_level_document: node_type == NodeConstants::DOCUMENT_NODE, node_name: String::from(self.NodeName()), + node_value: self.GetNodeValue().map(|v| v.into()), num_children: self.ChildNodes().Length() as usize, attrs: self.downcast().map(Element::summarize).unwrap_or(vec![]), } diff --git a/components/shared/devtools/lib.rs b/components/shared/devtools/lib.rs index af0de598da3..2455a324712 100644 --- a/components/shared/devtools/lib.rs +++ b/components/shared/devtools/lib.rs @@ -125,6 +125,7 @@ pub struct NodeInfo { pub parent: String, pub node_type: u16, pub node_name: String, + pub node_value: Option, pub num_children: usize, pub attrs: Vec, pub is_top_level_document: bool,