mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
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>
404 lines
13 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|