Files
servo/components/devtools/actors/inspector/node.rs
Abubakar Abdulazeez Usman 750fb41bdb devtools: Include layer rules in CSS panel using rule tree (#43912)
DevTools was collecting CSS rules by walking stylesheets and matching
selector text. This ignored cascade order and did not correctly handle
rules inside layer blocks.

This change uses computed values (rule tree) to get the actual applied
rules in cascade order. It then maps those rules back to CSSStyleRule
using the declaration block identity, and walks the CSSOM to get
selector text and layer ancestry.

This fills ancestor_data with layer names and lets the inspector show
layered rules correctly.


Testing: 
- Verified using the minimized testcase from the issue

- Verified on https://www.sharyap.com/

- Confirmed that rules inside layer blocks are now shown with correct
order and hierarchy.


Fixes: #43541

Signed-off-by: arabson99 <arabiusman99@gmail.com>
2026-04-07 11:26:12 +00:00

404 lines
13 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! This actor represents one DOM node. It is created by the Walker actor when it is traversing the
//! document tree.
use std::collections::HashMap;
use atomic_refcell::AtomicRefCell;
use devtools_traits::{
AttrModification, DevtoolScriptControlMsg, EventListenerInfo, MatchedRule, NodeInfo,
ShadowRootMode,
};
use malloc_size_of_derive::MallocSizeOf;
use serde::Serialize;
use serde_json::{self, Map, Value};
use servo_base::generic_channel::{self, GenericSender};
use servo_base::id::PipelineId;
use crate::actor::{Actor, ActorError, ActorRegistry};
use crate::protocol::ClientRequest;
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 GetEventListenerInfoReply {
from: String,
events: Vec<DevtoolsEventListenerInfo>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct DevtoolsEventListenerInfo {
r#type: String,
handler: String,
origin: String,
tags: String,
capturing: bool,
// This will always be an empty object, we just need a value that serializes to "{}".
hide: Value,
native: bool,
source_actor: String,
enabled: bool,
is_user_defined: bool,
event_listener_info_id: String,
}
#[derive(Serialize)]
struct GetUniqueSelectorReply {
from: String,
value: String,
}
#[derive(Serialize)]
struct GetXPathReply {
from: String,
value: String,
}
#[derive(Clone, Serialize)]
struct AttrMsg {
name: String,
value: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NodeActorMsg {
pub actor: String,
/// The ID of the shadow host of this node, if it is
/// a shadow root
host: Option<String>,
#[serde(rename = "baseURI")]
base_uri: String,
causes_overflow: bool,
container_type: Option<()>,
pub display_name: String,
display_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
inline_text_child: Option<Box<NodeActorMsg>>,
is_after_pseudo_element: bool,
is_anonymous: bool,
is_before_pseudo_element: bool,
is_direct_shadow_host_child: Option<bool>,
/// Whether or not this node is displayed.
///
/// Setting this value to `false` will cause the devtools to render the node name in gray.
is_displayed: bool,
#[serde(rename = "isInHTMLDocument")]
is_in_html_document: Option<bool>,
is_marker_pseudo_element: bool,
is_native_anonymous: bool,
is_scrollable: bool,
is_shadow_host: bool,
is_shadow_root: bool,
is_top_level_document: bool,
node_name: String,
node_type: u16,
node_value: Option<String>,
pub num_children: usize,
#[serde(skip_serializing_if = "String::is_empty")]
parent: String,
shadow_root_mode: Option<String>,
traits: HashMap<String, ()>,
attrs: Vec<AttrMsg>,
/// The `DOCTYPE` name if this is a `DocumentType` node, `None` otherwise
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
/// The `DOCTYPE` public identifier if this is a `DocumentType` node, `None` otherwise
#[serde(skip_serializing_if = "Option::is_none")]
public_id: Option<String>,
/// The `DOCTYPE` system identifier if this is a `DocumentType` node, `None` otherwise
#[serde(skip_serializing_if = "Option::is_none")]
system_id: Option<String>,
has_event_listeners: bool,
}
#[derive(MallocSizeOf)]
pub(crate) struct NodeActor {
name: String,
pub script_chan: GenericSender<DevtoolScriptControlMsg>,
pub pipeline: PipelineId,
pub walker: String,
pub style_rules: AtomicRefCell<HashMap<MatchedRule, String>>,
}
impl Actor for NodeActor {
fn name(&self) -> String {
self.name.clone()
}
/// The node actor can handle the following messages:
///
/// - `modifyAttributes`: Asks the script to change a value in the attribute of the
/// corresponding node
///
/// - `getUniqueSelector`: Returns the display name of this node
fn handle_message(
&self,
request: ClientRequest,
registry: &ActorRegistry,
msg_type: &str,
msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
match msg_type {
"modifyAttributes" => {
let mods = msg
.get("modifications")
.ok_or(ActorError::MissingParameter)?
.as_array()
.ok_or(ActorError::BadParameterType)?;
let modifications: Vec<AttrModification> = mods
.iter()
.filter_map(|json_mod| {
serde_json::from_str(&serde_json::to_string(json_mod).ok()?).ok()
})
.collect();
self.script_chan
.send(DevtoolScriptControlMsg::ModifyAttribute(
self.pipeline,
registry.actor_to_script(self.name()),
modifications,
))
.map_err(|_| ActorError::Internal)?;
let reply = EmptyReplyMsg { from: self.name() };
request.reply_final(&reply)?
},
"getEventListenerInfo" => {
let target = msg
.get("to")
.ok_or(ActorError::MissingParameter)?
.as_str()
.ok_or(ActorError::BadParameterType)?;
let (tx, rx) = generic_channel::channel().ok_or(ActorError::Internal)?;
self.script_chan
.send(DevtoolScriptControlMsg::GetEventListenerInfo(
self.pipeline,
registry.actor_to_script(target.to_owned()),
tx,
))
.unwrap();
let event_listeners = rx.recv().map_err(|_| ActorError::Internal)?;
let msg = GetEventListenerInfoReply {
from: self.name(),
events: event_listeners.into_iter().map(From::from).collect(),
};
request.reply_final(&msg)?
},
"getUniqueSelector" => {
let (tx, rx) = generic_channel::channel().unwrap();
self.script_chan
.send(DevtoolScriptControlMsg::GetDocumentElement(
self.pipeline,
tx,
))
.unwrap();
let doc_elem_info = rx
.recv()
.map_err(|_| ActorError::Internal)?
.ok_or(ActorError::Internal)?;
let node = doc_elem_info.encode(
registry,
self.script_chan.clone(),
self.pipeline,
self.walker.clone(),
);
let msg = GetUniqueSelectorReply {
from: self.name(),
value: node.display_name,
};
request.reply_final(&msg)?
},
"getXPath" => {
let target = msg
.get("to")
.ok_or(ActorError::MissingParameter)?
.as_str()
.ok_or(ActorError::BadParameterType)?;
let (tx, rx) = generic_channel::channel().unwrap();
self.script_chan
.send(DevtoolScriptControlMsg::GetXPath(
self.pipeline,
registry.actor_to_script(target.to_owned()),
tx,
))
.unwrap();
let xpath_selector = rx.recv().map_err(|_| ActorError::Internal)?;
let msg = GetXPathReply {
from: self.name(),
value: xpath_selector,
};
request.reply_final(&msg)?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}
pub trait NodeInfoToProtocol {
fn encode(
self,
registry: &ActorRegistry,
script_chan: GenericSender<DevtoolScriptControlMsg>,
pipeline: PipelineId,
walker: String,
) -> NodeActorMsg;
}
impl NodeInfoToProtocol for NodeInfo {
fn encode(
self,
registry: &ActorRegistry,
script_chan: GenericSender<DevtoolScriptControlMsg>,
pipeline: PipelineId,
walker: String,
) -> NodeActorMsg {
let get_or_register_node_actor = |id: &str| {
if !registry.script_actor_registered(id.to_string()) {
let node_name = registry.new_name::<NodeActor>();
registry.register_script_actor(id.to_string(), node_name.clone());
let node_actor = NodeActor {
name: node_name.clone(),
script_chan: script_chan.clone(),
pipeline,
walker: walker.clone(),
style_rules: AtomicRefCell::new(HashMap::new()),
};
registry.register(node_actor);
node_name
} else {
registry.script_to_actor(id.to_string())
}
};
let actor = get_or_register_node_actor(&self.unique_id);
let host = self
.host
.as_ref()
.map(|host_id| get_or_register_node_actor(host_id));
let name = registry.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) = generic_channel::channel()?;
script_chan
.send(DevtoolScriptControlMsg::GetChildren(
pipeline,
name.clone(),
tx,
))
.unwrap();
let mut children = rx.recv().ok()??;
let child = children.pop()?;
let msg = child.encode(registry, script_chan.clone(), pipeline, walker);
// 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,
host,
base_uri: self.base_uri,
causes_overflow: false,
container_type: None,
display_name: self.node_name.clone().to_lowercase(),
display_type: self.display,
inline_text_child,
is_after_pseudo_element: false,
is_anonymous: false,
is_before_pseudo_element: false,
is_direct_shadow_host_child: None,
is_displayed: self.is_displayed,
is_in_html_document: Some(true),
is_marker_pseudo_element: false,
is_native_anonymous: false,
is_scrollable: false,
is_shadow_host: self.is_shadow_host,
is_shadow_root: self.shadow_root_mode.is_some(),
is_top_level_document: self.is_top_level_document,
node_name: self.node_name,
node_type: self.node_type,
node_value: self.node_value,
num_children: self.num_children,
parent: registry.script_to_actor(self.parent.clone()),
shadow_root_mode: self
.shadow_root_mode
.as_ref()
.map(ShadowRootMode::to_string),
traits: HashMap::new(),
attrs: self
.attrs
.into_iter()
.map(|attr| AttrMsg {
name: attr.name,
value: attr.value,
})
.collect(),
name: self.doctype_name,
public_id: self.doctype_public_identifier,
system_id: self.doctype_system_identifier,
has_event_listeners: self.has_event_listeners,
}
}
}
impl From<EventListenerInfo> for DevtoolsEventListenerInfo {
fn from(event_listener_info: EventListenerInfo) -> Self {
Self {
r#type: event_listener_info.event_type,
handler: "todo".to_owned(),
capturing: event_listener_info.capturing,
origin: "todo".to_owned(),
tags: "".to_owned(),
hide: Value::Object(Default::default()),
native: false,
source_actor: "todo".to_owned(),
enabled: true,
is_user_defined: false,
event_listener_info_id: "todo".to_owned(),
}
}
}