Skip to main content

blitz_dom/
platform.rs

1//! CSSOM operations that use the same parser and computed values as rendering.
2
3use cssparser::{Parser, ParserInput, serialize_identifier};
4use selectors::matching::QuirksMode;
5use style::media_queries::MediaList;
6use style::parser::ParserContext;
7use style::properties::{
8    ComputedValues, Importance, IndexedId, LonghandId, PropertyDeclaration,
9    PropertyDeclarationBlock, PropertyDeclarationId, PropertyId, SourcePropertyDeclaration,
10    parse_style_attribute,
11};
12use style::selector_parser::PseudoElement;
13use style::servo_arc::Arc as ServoArc;
14use style::shared_lock::ToCssWithGuard;
15use style::stylesheets::supports_rule::{Declaration, SupportsCondition};
16use style::stylesheets::{
17    CssRuleType, CustomMediaEvaluator, DocumentStyleSheet, Origin, OriginSet, StylesheetInDocument,
18};
19use style_traits::{CssString, ParsingMode, ToCss};
20
21use crate::{BaseDocument, NodeId, local_name};
22
23/// A parsed media query owned by its MediaQueryList, rather than a parser cache.
24pub struct PlatformMediaQuery {
25    list: MediaList,
26}
27
28impl PlatformMediaQuery {
29    /// The CSSOM serialization of this query.
30    pub fn media(&self) -> String {
31        self.list.to_css_string()
32    }
33}
34
35/// Keeps a sheet alive if its owner is removed from the document.
36pub struct PlatformStyleSheet {
37    sheet: DocumentStyleSheet,
38    href: Option<String>,
39    origin_clean: bool,
40}
41
42impl PlatformStyleSheet {
43    /// The fetched sheet's URL, or None for an inline sheet.
44    pub fn href(&self) -> Option<&str> {
45        self.href.as_deref()
46    }
47
48    /// Whether CSSOM has disabled the sheet.
49    pub fn disabled(&self) -> bool {
50        self.sheet.0.disabled()
51    }
52
53    /// The sheet's current media text.
54    pub fn media(&self) -> String {
55        let guard = self.sheet.0.shared_lock.read();
56        self.sheet.0.media.read_with(&guard).to_css_string()
57    }
58
59    /// Individually serialized media queries.
60    pub fn media_items(&self) -> Vec<String> {
61        let guard = self.sheet.0.shared_lock.read();
62        self.sheet
63            .0
64            .media
65            .read_with(&guard)
66            .media_queries
67            .iter()
68            .map(ToCss::to_css_string)
69            .collect()
70    }
71
72    /// Serialize available rules. Cross-origin sheets require an origin-clean flag.
73    pub fn rules(&self) -> Option<Vec<String>> {
74        if !self.origin_clean {
75            return None;
76        }
77        let guard = self.sheet.0.shared_lock.read();
78        Some(
79            self.sheet
80                .contents(&guard)
81                .rules(&guard)
82                .iter()
83                .map(|rule| {
84                    let mut text = CssString::new();
85                    rule.to_css(&guard, &mut text)
86                        .expect("writing CSS to a string cannot fail");
87                    text.to_string()
88                })
89                .collect(),
90        )
91    }
92}
93
94impl BaseDocument {
95    fn platform_parser_context<'a>(
96        &'a self,
97        url_data: &'a style::stylesheets::UrlExtraData,
98    ) -> ParserContext<'a> {
99        ParserContext::new(
100            Origin::Author,
101            url_data,
102            Some(CssRuleType::Style),
103            ParsingMode::DEFAULT,
104            QuirksMode::NoQuirks,
105            Default::default(),
106            None,
107            None,
108            Default::default(),
109        )
110    }
111
112    /// Test a supports condition through Stylo.
113    pub fn platform_supports(&self, condition: &str) -> bool {
114        let url_data = self.url.url_extra_data();
115        let mut context = self.platform_parser_context(&url_data);
116        let mut input = ParserInput::new(condition);
117        if let Ok(condition) = Parser::new(&mut input).parse_entirely(SupportsCondition::parse) {
118            return condition.eval(&mut context);
119        }
120        // CSS.supports also accepts a declaration with implicit parentheses.
121        let mut input = ParserInput::new(condition);
122        Parser::new(&mut input)
123            .parse_entirely(Declaration::parse)
124            .is_ok_and(|declaration| declaration.eval(&mut context))
125    }
126
127    /// Test a property and value without allowing the property string to inject syntax.
128    pub fn platform_supports_property(&self, name: &str, value: &str) -> bool {
129        let mut declaration = String::new();
130        serialize_identifier(name, &mut declaration)
131            .expect("writing an identifier to a string cannot fail");
132        declaration.push(':');
133        declaration.push_str(value);
134        self.platform_supports(&declaration)
135    }
136
137    /// Parse a query with the document's CSS parser context.
138    pub fn platform_media_query(&self, text: &str) -> PlatformMediaQuery {
139        let url_data = self.url.url_extra_data();
140        let mut context = self.platform_parser_context(&url_data);
141        let mut input = ParserInput::new(text);
142        PlatformMediaQuery {
143            list: MediaList::parse(&mut context, &mut Parser::new(&mut input)),
144        }
145    }
146
147    /// Evaluate against the current Stylo device, including viewport and color scheme.
148    pub fn platform_media_matches(&self, query: &PlatformMediaQuery) -> bool {
149        query.list.evaluate(
150            self.stylist.device(),
151            QuirksMode::NoQuirks,
152            &mut CustomMediaEvaluator::none(),
153        )
154    }
155
156    fn platform_computed_values(
157        &self,
158        node_id: NodeId,
159        pseudo: &str,
160    ) -> Option<ServoArc<ComputedValues>> {
161        let node = self.get_node(node_id)?;
162        if !node.is_element() || !node.flags.is_in_document() {
163            return None;
164        }
165        if pseudo.is_empty() {
166            return node.primary_styles().map(|style| ServoArc::clone(&style));
167        }
168        let pseudo = match pseudo {
169            "::before" => PseudoElement::Before,
170            "::after" => PseudoElement::After,
171            _ => return None,
172        };
173        let data = node.stylo_element_data_opt()?.get()?;
174        data.styles.pseudos.get(&pseudo).cloned()
175    }
176
177    /// Canonical, enabled longhands followed by valid custom properties.
178    pub fn platform_computed_names(&self, node_id: NodeId, pseudo: &str) -> Vec<String> {
179        let Some(style) = self.platform_computed_values(node_id, pseudo) else {
180            return Vec::new();
181        };
182        let mut names = Vec::new();
183        for index in 0..LonghandId::COUNT {
184            // IndexedId guarantees contiguous valid ids below COUNT.
185            let id = unsafe { LonghandId::from_index_release_unchecked(index) };
186            if PropertyId::parse_enabled_for_all_content(id.name()).is_ok() {
187                names.push(id.name().to_string());
188            }
189        }
190        names.sort_unstable();
191        let properties = style.custom_properties();
192        let count = properties.inherited.len() + properties.non_inherited.len();
193        for index in 0..count {
194            if let Some((name, Some(_))) = properties.property_at(index) {
195                names.push(format!("--{name}"));
196            }
197        }
198        names
199    }
200
201    /// Serialize any enabled longhand or custom property from computed values.
202    pub fn platform_computed_value(&self, node_id: NodeId, pseudo: &str, name: &str) -> String {
203        let Some(style) = self.platform_computed_values(node_id, pseudo) else {
204            return String::new();
205        };
206        let Ok(property) = PropertyId::parse_enabled_for_all_content(name) else {
207            return String::new();
208        };
209        let declaration = match property.as_shorthand() {
210            Ok(_) => return String::new(),
211            Err(declaration) => declaration,
212        };
213
214        // Preserve the renderer's existing used box values for geometry.
215        let geometry_id = self.get_node(node_id).and_then(|node| match pseudo {
216            "" => Some(node_id),
217            "::before" => node.before(),
218            "::after" => node.after(),
219            _ => None,
220        });
221        if matches!(
222            name,
223            "width"
224                | "height"
225                | "padding-top"
226                | "padding-right"
227                | "padding-bottom"
228                | "padding-left"
229                | "margin-top"
230                | "margin-right"
231                | "margin-bottom"
232                | "margin-left"
233                | "border-top-width"
234                | "border-right-width"
235                | "border-bottom-width"
236                | "border-left-width"
237        ) {
238            if let Some(properties) = geometry_id
239                .and_then(|id| self.get_node(id))
240                .and_then(|node| node.computed_style_properties())
241            {
242                if let Some((_, value)) = properties.into_iter().find(|(key, _)| *key == name) {
243                    return value;
244                }
245            }
246        }
247        style.computed_value_to_string(declaration)
248    }
249
250    fn platform_inline_block(&self, node_id: NodeId) -> PropertyDeclarationBlock {
251        let css = self
252            .get_node(node_id)
253            .and_then(|node| node.attr(local_name!("style")))
254            .unwrap_or_default();
255        parse_style_attribute(
256            css,
257            &self.url.url_extra_data(),
258            None,
259            QuirksMode::NoQuirks,
260            CssRuleType::Style,
261        )
262    }
263
264    /// Parse and serialize cssText without splitting CSS on punctuation.
265    pub fn platform_inline_text(&self, node_id: NodeId) -> String {
266        let mut text = CssString::new();
267        self.platform_inline_block(node_id)
268            .to_css(&mut text)
269            .expect("writing CSS to a string cannot fail");
270        text.to_string()
271    }
272
273    /// The declaration's ordered longhand and custom-property names.
274    pub fn platform_inline_names(&self, node_id: NodeId) -> Vec<String> {
275        self.platform_inline_block(node_id)
276            .declarations()
277            .iter()
278            .map(|declaration| match declaration.id() {
279                PropertyDeclarationId::Longhand(id) => id.name().to_string(),
280                PropertyDeclarationId::Custom(name) => format!("--{name}"),
281            })
282            .collect()
283    }
284
285    /// Read a value or priority from Stylo's parsed declaration block.
286    pub fn platform_inline_value(&self, node_id: NodeId, name: &str, priority: bool) -> String {
287        let url_data = self.url.url_extra_data();
288        let context = self.platform_parser_context(&url_data);
289        let Ok(property) = PropertyId::parse(name, &context) else {
290            return String::new();
291        };
292        let block = self.platform_inline_block(node_id);
293        if priority {
294            return if block.property_priority(&property).important() {
295                "important".to_string()
296            } else {
297                String::new()
298            };
299        }
300        let mut text = CssString::new();
301        block
302            .property_value_to_css(&property, &mut text)
303            .expect("writing CSS to a string cannot fail");
304        text.to_string()
305    }
306
307    /// Return replacement cssText for a CSSOM edit, or None for an invalid edit.
308    ///
309    /// The binding applies the returned text through the ordinary attribute mutator.
310    pub fn platform_edit_inline(
311        &self,
312        node_id: NodeId,
313        name: &str,
314        value: &str,
315        priority: &str,
316    ) -> Option<String> {
317        let url_data = self.url.url_extra_data();
318        let context = self.platform_parser_context(&url_data);
319        let property = PropertyId::parse(name, &context).ok()?;
320        let mut source = SourcePropertyDeclaration::default();
321        let importance = if priority.is_empty() {
322            Importance::Normal
323        } else if priority.eq_ignore_ascii_case("important") {
324            Importance::Important
325        } else if !value.is_empty() {
326            return None;
327        } else {
328            Importance::Normal
329        };
330        if !value.is_empty() {
331            let mut input = ParserInput::new(value);
332            Parser::new(&mut input)
333                .parse_entirely(|input| {
334                    PropertyDeclaration::parse_into(&mut source, property.clone(), &context, input)
335                })
336                .ok()?;
337        }
338        let mut block = self.platform_inline_block(node_id);
339        if let Some(index) = block.first_declaration_to_remove(&property) {
340            block.remove_property(&property, index);
341        }
342        if !value.is_empty() {
343            block.extend(source.drain(), importance);
344        }
345        let mut text = CssString::new();
346        block
347            .to_css(&mut text)
348            .expect("writing CSS to a string cannot fail");
349        Some(text.to_string())
350    }
351
352    /// Sheet owners in DOM order, scoped to the requested document.
353    pub fn platform_sheet_owners(&self, root_id: NodeId) -> Vec<NodeId> {
354        let mut owners = Vec::new();
355        let mut stack = vec![root_id];
356        while let Some(id) = stack.pop() {
357            let Some(node) = self.get_node(id) else {
358                continue;
359            };
360            if self.nodes_to_stylesheet.contains_key(&id) {
361                owners.push(id);
362            }
363            stack.extend(node.children.iter().rev().copied());
364        }
365        owners
366    }
367
368    /// Retain the actual sheet, including its final fetched URL.
369    pub fn platform_sheet(&self, owner: NodeId) -> Option<PlatformStyleSheet> {
370        let sheet = self.nodes_to_stylesheet.get(&owner)?.clone();
371        let node = self.get_node(owner)?;
372        let guard = sheet.0.shared_lock.read();
373        let source_url = &sheet.contents(&guard).url_data.0;
374        let linked = node
375            .element_data()
376            .is_some_and(|element| element.name.local == local_name!("link"));
377        let href = linked.then(|| source_url.to_string());
378        let origin_clean = !linked || source_url.origin() == self.url.origin();
379        drop(guard);
380        Some(PlatformStyleSheet {
381            sheet,
382            href,
383            origin_clean,
384        })
385    }
386
387    /// Apply an owner's initial media attribute to its sheet.
388    pub(crate) fn platform_initial_sheet_media(&self, sheet: &DocumentStyleSheet, owner: NodeId) {
389        let text = self
390            .get_node(owner)
391            .and_then(|node| node.attr(local_name!("media")))
392            .unwrap_or_default();
393        let query = self.platform_media_query(text);
394        *sheet.0.media.write_with(&mut sheet.0.shared_lock.write()) = query.list;
395    }
396
397    /// Disable a sheet and invalidate the cascade.
398    pub fn platform_disable_sheet(&mut self, sheet: &PlatformStyleSheet, disabled: bool) {
399        if sheet.sheet.0.set_disabled(disabled) {
400            self.stylist
401                .force_stylesheet_origins_dirty(OriginSet::all());
402        }
403    }
404
405    /// Replace a sheet's media list and invalidate the cascade.
406    pub fn platform_set_sheet_media(&mut self, sheet: &PlatformStyleSheet, text: &str) {
407        let query = self.platform_media_query(text);
408        *sheet
409            .sheet
410            .0
411            .media
412            .write_with(&mut sheet.sheet.0.shared_lock.write()) = query.list;
413        self.stylist
414            .force_stylesheet_origins_dirty(OriginSet::all());
415    }
416
417    /// Append or delete a parsed medium. False means invalid or not found.
418    pub fn platform_edit_sheet_medium(
419        &mut self,
420        sheet: &PlatformStyleSheet,
421        medium: &str,
422        delete: bool,
423    ) -> bool {
424        let url_data = self.url.url_extra_data();
425        let context = self.platform_parser_context(&url_data);
426        let changed = {
427            let mut guard = sheet.sheet.0.shared_lock.write();
428            let list = sheet.sheet.0.media.write_with(&mut guard);
429            if delete {
430                list.delete_medium(&context, medium)
431            } else {
432                list.append_medium(&context, medium)
433            }
434        };
435        if changed {
436            self.stylist
437                .force_stylesheet_origins_dirty(OriginSet::all());
438        }
439        changed
440    }
441
442    /// Return all hit elements in front-to-back paint order.
443    pub fn platform_elements_from_point(&self, x: f32, y: f32) -> Vec<NodeId> {
444        let viewport = self.get_viewport();
445        let width = viewport.window_size.0 as f64 / viewport.scale_f64();
446        let height = viewport.window_size.1 as f64 / viewport.scale_f64();
447        if !x.is_finite()
448            || !y.is_finite()
449            || x < 0.0
450            || y < 0.0
451            || f64::from(x) >= width
452            || f64::from(y) >= height
453        {
454            return Vec::new();
455        }
456        let Some(root) = self.try_root_element() else {
457            return Vec::new();
458        };
459        let mut hits = Some(Vec::new());
460        root.hit_inner_collect(x, y, viewport.scale_f64(), &mut None, &mut hits);
461        let mut seen = std::collections::HashSet::new();
462        hits.unwrap_or_default()
463            .into_iter()
464            .filter_map(|hit| {
465                let mut id = self.nearest_non_anonymous_ancestor(hit.node_id)?;
466                loop {
467                    let node = self.get_node(id)?;
468                    if node.is_element() {
469                        return seen.insert(id).then_some(id);
470                    }
471                    id = node.parent?;
472                }
473            })
474            .collect()
475    }
476}