Skip to main content

devtools/actors/
stylesheets.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/. */
4use std::sync::Arc;
5
6use devtools_traits::DevtoolScriptControlMsg;
7use log::warn;
8use malloc_size_of_derive::MallocSizeOf;
9use serde::Serialize;
10use serde_json::{Map, Value};
11use servo_base::generic_channel;
12use servo_base::generic_channel::GenericSender;
13
14use crate::StreamId;
15use crate::actor::{Actor, ActorError, ActorRegistry, new_actor_name};
16use crate::actors::browsing_context::BrowsingContextActor;
17use crate::actors::long_string::{LongStringActor, LongStringObj};
18use crate::protocol::ClientRequest;
19
20/// <https://searchfox.org/mozilla-central/source/devtools/server/actors/resources/stylesheets.js>
21#[derive(Serialize)]
22#[serde(rename_all = "camelCase")]
23pub(crate) struct StyleSheetData {
24    /// Unique identifier for this stylesheet.
25    pub(crate) resource_id: String,
26    /// Current browsing context id.
27    pub(crate) browsing_context_id: u32,
28    /// The URL of the stylesheet. Optional for inline stylesheets.
29    pub(crate) href: Option<String>,
30    /// The URL of the document that owns this stylesheet.
31    pub(crate) node_href: String,
32    /// Whether the stylesheet is disabled.
33    pub(crate) disabled: bool,
34    /// The title of the stylesheet.
35    pub(crate) title: Option<String>,
36    /// Whether this is a browser stylesheet.
37    pub(crate) system: bool,
38    /// Whether this stylesheet was created by DevTools.
39    pub(crate) is_new: bool,
40    /// Optional file name used for local files.
41    pub(crate) file_name: Option<String>,
42    /// Optional source map URL.
43    #[serde(rename = "sourceMapURL")]
44    pub(crate) source_map_url: Option<String>,
45    #[serde(rename = "sourceMapBaseURL")]
46    pub(crate) source_map_base_url: Option<String>,
47    /// The index of this stylesheet in the document's stylesheet list.
48    pub(crate) style_sheet_index: i32,
49    /// whether the stylesheet was constructed using Web APIs.
50    pub(crate) constructed: bool,
51    /// Total count of individual CSS rules within that specific stylesheet.
52    pub(crate) rule_count: u32,
53    /// List of media query metadata (ex: @media, @keyframes).
54    pub(crate) at_rules: Vec<String>,
55}
56
57#[derive(Serialize)]
58#[serde(rename_all = "camelCase")]
59struct GetStyleSheetsReply {
60    from: String,
61    style_sheets: Vec<StyleSheetData>,
62}
63
64#[derive(Serialize)]
65#[serde(rename_all = "camelCase")]
66struct GetTextReply {
67    from: String,
68    text: LongStringObj,
69}
70
71#[derive(MallocSizeOf)]
72pub(crate) struct StyleSheetsActor {
73    name: String,
74    script_sender: GenericSender<DevtoolScriptControlMsg>,
75    browsing_context_name: String,
76}
77
78impl Actor for StyleSheetsActor {
79    fn name(&self) -> &str {
80        &self.name
81    }
82    fn handle_message(
83        &self,
84        request: ClientRequest,
85        registry: &ActorRegistry,
86        msg_type: &str,
87        msg: &Map<String, Value>,
88        _id: StreamId,
89    ) -> Result<(), ActorError> {
90        let browsing_context_actor =
91            registry.find::<BrowsingContextActor>(&self.browsing_context_name);
92        match msg_type {
93            "getStyleSheets" => {
94                let style_sheets = self.get_stylesheets_data(&browsing_context_actor);
95                let msg = GetStyleSheetsReply {
96                    from: self.name().into(),
97                    style_sheets,
98                };
99                request.reply_final(&msg)?
100            },
101            // TODO: Improve CSS text formatting for remote stylesheets to match as source.
102            "getText" => {
103                let resource_id = msg.get("resourceId").and_then(|v| v.as_str()).unwrap_or("");
104                let index = resource_id
105                    .split('-')
106                    .next_back()
107                    .unwrap_or("0")
108                    .parse::<i32>()
109                    .unwrap_or(0);
110                let (tx, rx) = generic_channel::channel().unwrap();
111                let _ = self
112                    .script_sender
113                    .send(DevtoolScriptControlMsg::GetStyleSheetText(
114                        browsing_context_actor.pipeline_id(),
115                        index,
116                        tx,
117                    ));
118                let css_text = rx
119                    .recv()
120                    .map_err(|_| ActorError::Internal)?
121                    .unwrap_or_else(|| {
122                        warn!("Stylesheet fetched without text content");
123                        "Error fetching CSS text".to_string()
124                    });
125                let long_string_actor = LongStringActor::register(registry, css_text);
126                let msg = GetTextReply {
127                    from: self.name().into(),
128                    text: long_string_actor.long_string_obj(),
129                };
130                request.reply_final(&msg)?
131            },
132            _ => return Err(ActorError::UnrecognizedPacketType),
133        };
134        Ok(())
135    }
136}
137
138impl StyleSheetsActor {
139    pub fn register(
140        registry: &ActorRegistry,
141        script_sender: GenericSender<DevtoolScriptControlMsg>,
142        browsing_context_name: String,
143    ) -> Arc<Self> {
144        let name = new_actor_name::<Self>();
145        let actor = StyleSheetsActor {
146            name,
147            script_sender,
148            browsing_context_name,
149        };
150        registry.register::<Self>(actor)
151    }
152
153    pub(crate) fn get_stylesheets_data(
154        &self,
155        browsing_context_actor: &BrowsingContextActor,
156    ) -> Vec<StyleSheetData> {
157        let (tx, rx) = generic_channel::channel().unwrap();
158        let _ = self
159            .script_sender
160            .send(DevtoolScriptControlMsg::GetStyleSheets(
161                browsing_context_actor.pipeline_id(),
162                tx,
163            ));
164        let style_sheets = rx.recv().unwrap();
165        let url = browsing_context_actor.url();
166        let browsing_context_id = browsing_context_actor.browsing_context_id.value();
167        style_sheets
168            .into_iter()
169            .map(|info| StyleSheetData {
170                resource_id: format!("{}-{}", browsing_context_id, info.style_sheet_index),
171                browsing_context_id,
172                href: info.href.clone(),
173                node_href: url.clone(),
174                disabled: info.disabled,
175                title: (!info.title.is_empty()).then_some(info.title),
176                system: info.system,
177                is_new: false,
178                file_name: None,
179                source_map_url: Some(String::new()),
180                source_map_base_url: Some(info.href.unwrap_or_else(|| url.clone())),
181                style_sheet_index: info.style_sheet_index,
182                constructed: false,
183                rule_count: info.rule_count,
184                at_rules: vec![], // TODO: Populate with media query metadata for the Style Editor sidebar.
185            })
186            .collect()
187    }
188}