Skip to main content

blitz_dom/
cssom.rs

1//! CSSOM stylesheet access (`document.styleSheets`, `CSSStyleSheet`,
2//! `CSSRuleList`, `CSSRule` and friends) backed by stylo's stylesheet objects.
3//!
4//! Stylesheets are identified by the `NodeId` of their owner node (`<style>` or
5//! `<link>`), and rules by a *path* of indices from the stylesheet's top-level
6//! rule list down through nested rule lists (`@media`, `@supports`, nested
7//! style rules, `@keyframes`, ...). An empty path denotes the stylesheet's own
8//! top-level rule list.
9
10use cssparser::Parser;
11use selectors::matching::QuirksMode;
12use selectors::parser::{ParseRelative, SelectorList};
13use style::font_face::FontFaceRule;
14use style::invalidation::stylesheets::RuleChangeKind;
15use style::parser::ParserContext;
16use style::properties::font_face::DescriptorId as FontFaceDescriptorId;
17use style::properties::{
18    Importance, PropertyDeclarationBlock, PropertyId, SourcePropertyDeclaration,
19    parse_one_declaration_into,
20};
21use style::selector_parser::SelectorParser;
22use style::servo_arc::Arc as ServoArc;
23use style::shared_lock::{Locked, SharedRwLockReadGuard, ToCssWithGuard};
24use style::stylesheets::keyframes_rule::{Keyframe, KeyframesRule};
25use style::stylesheets::{
26    AllowImportRules, CssRule, CssRuleRef, CssRuleType, CssRuleTypes, CssRules, DocumentStyleSheet,
27    Origin, OriginSet, RulesMutateError, StylesheetInDocument,
28};
29use style_traits::{CssStringWriter, ParsingMode, ToCss};
30// For `SelectorList::to_css_string`
31use cssparser::ToCss as _;
32
33use blitz_traits::node_id::NodeId;
34
35use crate::BaseDocument;
36use crate::net::StylesheetLoader;
37
38/// Errors from CSSOM mutation operations, mirroring the `DOMException` names
39/// which the corresponding JavaScript APIs throw.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum CssomError {
42    /// The owner node has no associated stylesheet, or the rule path does not
43    /// resolve to a rule (list).
44    NotFound,
45    /// `SyntaxError`
46    Syntax,
47    /// `IndexSizeError`
48    IndexSize,
49    /// `HierarchyRequestError`
50    HierarchyRequest,
51    /// `InvalidStateError`
52    InvalidState,
53    /// `NotSupportedError`
54    NotSupported,
55}
56
57impl CssomError {
58    /// The `DOMException` name corresponding to this error
59    pub fn dom_exception_name(self) -> &'static str {
60        match self {
61            Self::NotFound => "NotFoundError",
62            Self::Syntax => "SyntaxError",
63            Self::IndexSize => "IndexSizeError",
64            Self::HierarchyRequest => "HierarchyRequestError",
65            Self::InvalidState => "InvalidStateError",
66            Self::NotSupported => "NotSupportedError",
67        }
68    }
69}
70
71impl From<RulesMutateError> for CssomError {
72    fn from(err: RulesMutateError) -> Self {
73        match err {
74            RulesMutateError::Syntax => Self::Syntax,
75            RulesMutateError::IndexSize => Self::IndexSize,
76            RulesMutateError::HierarchyRequest => Self::HierarchyRequest,
77            RulesMutateError::InvalidState => Self::InvalidState,
78        }
79    }
80}
81
82/// A snapshot of the CSSOM-visible attributes of a rule
83#[derive(Debug, Clone)]
84pub struct CssRuleInfo {
85    /// The name of the CSSOM interface for the rule (e.g. `"CSSStyleRule"`)
86    pub interface: &'static str,
87    /// The legacy `CSSRule.type` constant (0 for rules without one)
88    pub rule_type: u16,
89    /// `CSSRule.cssText`
90    pub css_text: String,
91    /// Whether the rule has a child rule list (`cssRules`)
92    pub has_child_rules: bool,
93    /// Whether the rule has a `style` declaration block
94    pub has_style: bool,
95    /// Additional interface-specific string attributes (e.g. `selectorText`)
96    pub attributes: Vec<(&'static str, String)>,
97}
98
99/// A rule list that CSSOM rule paths can index into
100#[derive(Clone)]
101enum RuleList {
102    Css(ServoArc<Locked<CssRules>>),
103    Keyframes(ServoArc<Locked<KeyframesRule>>),
104}
105
106/// A rule resolved from a CSSOM rule path
107#[derive(Clone)]
108enum RuleHandle {
109    Rule(CssRule),
110    Keyframe(ServoArc<Locked<Keyframe>>),
111}
112
113/// The declarations of a rule (`CSSRule.style`)
114enum DeclarationTarget {
115    Block {
116        block: ServoArc<Locked<PropertyDeclarationBlock>>,
117        rule_type: CssRuleType,
118    },
119    FontFace(ServoArc<Locked<FontFaceRule>>),
120}
121
122fn child_rule_list(rule: &CssRule, guard: &SharedRwLockReadGuard) -> Option<RuleList> {
123    let rules = match rule {
124        CssRule::Style(r) => r.read_with(guard).rules.clone()?,
125        CssRule::Media(r) => r.rules.clone(),
126        CssRule::Supports(r) => r.rules.clone(),
127        CssRule::Container(r) => r.rules.clone(),
128        CssRule::Page(r) => r.read_with(guard).rules.clone(),
129        CssRule::Document(r) => r.rules.clone(),
130        CssRule::LayerBlock(r) => r.rules.clone(),
131        CssRule::Scope(r) => r.rules.clone(),
132        CssRule::StartingStyle(r) => r.rules.clone(),
133        CssRule::Keyframes(r) => return Some(RuleList::Keyframes(r.clone())),
134        _ => return None,
135    };
136    Some(RuleList::Css(rules))
137}
138
139fn declaration_target(
140    handle: &RuleHandle,
141    guard: &SharedRwLockReadGuard,
142) -> Option<DeclarationTarget> {
143    let (block, rule_type) = match handle {
144        RuleHandle::Keyframe(k) => (k.read_with(guard).block.clone(), CssRuleType::Keyframe),
145        RuleHandle::Rule(rule) => match rule {
146            CssRule::Style(r) => (r.read_with(guard).block.clone(), CssRuleType::Style),
147            CssRule::Page(r) => (r.read_with(guard).block.clone(), CssRuleType::Page),
148            CssRule::Margin(r) => (r.block.clone(), CssRuleType::Margin),
149            CssRule::NestedDeclarations(r) => (
150                r.read_with(guard).block.clone(),
151                CssRuleType::NestedDeclarations,
152            ),
153            CssRule::PositionTry(r) => (r.read_with(guard).block.clone(), CssRuleType::PositionTry),
154            CssRule::FontFace(r) => return Some(DeclarationTarget::FontFace(r.clone())),
155            _ => return None,
156        },
157    };
158    Some(DeclarationTarget::Block { block, rule_type })
159}
160
161fn css_rule_interface(rule: &CssRule) -> &'static str {
162    match rule {
163        CssRule::Style(_) => "CSSStyleRule",
164        CssRule::Namespace(_) => "CSSNamespaceRule",
165        CssRule::Import(_) => "CSSImportRule",
166        CssRule::Media(_) => "CSSMediaRule",
167        CssRule::CustomMedia(_) => "CSSCustomMediaRule",
168        CssRule::Container(_) => "CSSContainerRule",
169        CssRule::FontFace(_) => "CSSFontFaceRule",
170        CssRule::FontFeatureValues(_) => "CSSFontFeatureValuesRule",
171        CssRule::FontPaletteValues(_) => "CSSFontPaletteValuesRule",
172        CssRule::CounterStyle(_) => "CSSCounterStyleRule",
173        CssRule::Keyframes(_) => "CSSKeyframesRule",
174        CssRule::Margin(_) => "CSSMarginRule",
175        CssRule::Supports(_) => "CSSSupportsRule",
176        CssRule::Page(_) => "CSSPageRule",
177        CssRule::Property(_) => "CSSPropertyRule",
178        CssRule::Document(_) => "CSSMozDocumentRule",
179        CssRule::LayerBlock(_) => "CSSLayerBlockRule",
180        CssRule::LayerStatement(_) => "CSSLayerStatementRule",
181        CssRule::Scope(_) => "CSSScopeRule",
182        CssRule::StartingStyle(_) => "CSSStartingStyleRule",
183        CssRule::AppearanceBase(_) => "CSSAppearanceBaseRule",
184        CssRule::PositionTry(_) => "CSSPositionTryRule",
185        CssRule::NestedDeclarations(_) => "CSSNestedDeclarations",
186        CssRule::ViewTransition(_) => "CSSViewTransitionRule",
187    }
188}
189
190/// Join the CSS serializations of `items` with `", "`
191fn comma_separated<'a, T: ToCss + 'a>(items: impl IntoIterator<Item = &'a T>) -> String {
192    let mut css = CssStringWriter::new();
193    for (i, item) in items.into_iter().enumerate() {
194        if i > 0 {
195            css.push_str(", ");
196        }
197        let _ = item.to_css(&mut style_traits::CssWriter::new(&mut css));
198    }
199    css
200}
201
202fn rule_attributes(rule: &CssRule, guard: &SharedRwLockReadGuard) -> Vec<(&'static str, String)> {
203    match rule {
204        CssRule::Style(r) => {
205            vec![("selectorText", r.read_with(guard).selectors.to_css_string())]
206        }
207        CssRule::Namespace(r) => vec![
208            ("namespaceURI", r.url.to_string()),
209            (
210                "prefix",
211                r.prefix.as_ref().map(|p| p.to_string()).unwrap_or_default(),
212            ),
213        ],
214        CssRule::Import(r) => {
215            let r = r.read_with(guard);
216            vec![
217                ("href", r.url.as_str().to_string()),
218                (
219                    "media",
220                    r.stylesheet
221                        .media(guard)
222                        .map(|m| m.to_css_string())
223                        .unwrap_or_default(),
224                ),
225            ]
226        }
227        CssRule::Media(r) => vec![(
228            "conditionText",
229            r.media_queries.read_with(guard).to_css_string(),
230        )],
231        CssRule::Supports(r) => vec![("conditionText", r.condition.to_css_string())],
232        CssRule::Container(r) => vec![("conditionText", r.conditions.to_css_string())],
233        CssRule::Page(r) => vec![("selectorText", r.read_with(guard).selectors.to_css_string())],
234        CssRule::Keyframes(r) => vec![("name", r.read_with(guard).name.to_css_string())],
235        CssRule::LayerBlock(r) => vec![(
236            "name",
237            r.name
238                .as_ref()
239                .map(|n| n.to_css_string())
240                .unwrap_or_default(),
241        )],
242        CssRule::LayerStatement(r) => vec![("nameList", comma_separated(r.names.iter()))],
243        CssRule::Property(r) => {
244            let mut attrs = vec![("name", r.name.to_css_string())];
245            for (attr, id) in [
246                ("syntax", style::properties::property::DescriptorId::Syntax),
247                (
248                    "inherits",
249                    style::properties::property::DescriptorId::Inherits,
250                ),
251                (
252                    "initialValue",
253                    style::properties::property::DescriptorId::InitialValue,
254                ),
255            ] {
256                let mut css = CssStringWriter::new();
257                let _ = r.descriptors.get(id, &mut css);
258                attrs.push((attr, css));
259            }
260            attrs
261        }
262        CssRule::CounterStyle(r) => vec![("name", r.read_with(guard).name().to_css_string())],
263        CssRule::FontFeatureValues(r) => {
264            vec![("fontFamily", comma_separated(r.family_names.iter()))]
265        }
266        CssRule::FontPaletteValues(r) => vec![
267            ("name", r.name.to_css_string()),
268            ("fontFamily", comma_separated(r.family_names.iter())),
269            (
270                "basePalette",
271                r.base_palette
272                    .as_ref()
273                    .map(|b| b.to_css_string())
274                    .unwrap_or_default(),
275            ),
276            ("overrideColors", comma_separated(r.override_colors.iter())),
277        ],
278        CssRule::Margin(r) => vec![("name", format!("{:?}", r.rule_type))],
279        CssRule::PositionTry(r) => vec![("name", r.read_with(guard).name.to_css_string())],
280        _ => Vec::new(),
281    }
282}
283
284impl BaseDocument {
285    /// The owner nodes (`<style>` / `<link>` elements) of the document's author
286    /// stylesheets (`document.styleSheets`).
287    ///
288    /// Ordered by node id, which matches document order for stylesheets
289    /// created while parsing (and is the order the stylist uses too, see
290    /// `add_stylesheet_for_node`).
291    pub fn stylesheet_owner_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
292        self.nodes_to_stylesheet.keys().copied()
293    }
294
295    /// Whether `node_id` currently owns a stylesheet
296    pub fn node_has_stylesheet(&self, node_id: NodeId) -> bool {
297        self.nodes_to_stylesheet.contains_key(&node_id)
298    }
299
300    /// A counter which changes whenever the set of stylesheets (or the
301    /// stylesheet object owned by some node) changes. CSSOM wrappers use it to
302    /// detect that rule data cached on the script side is stale.
303    pub fn stylesheet_generation(&self) -> u64 {
304        self.stylesheet_generation
305    }
306
307    /// Whether the stylesheet owned by `node_id` has been disabled through the
308    /// CSSOM (`CSSStyleSheet.disabled` / `HTMLStyleElement.disabled` /
309    /// `HTMLLinkElement.disabled`). `false` if the node owns no stylesheet.
310    pub fn stylesheet_disabled(&self, node_id: NodeId) -> bool {
311        self.nodes_to_stylesheet
312            .get(&node_id)
313            .is_some_and(|sheet| sheet.0.disabled())
314    }
315
316    /// Set the disabled flag of the stylesheet owned by `node_id`. A disabled
317    /// stylesheet stays associated with its owner node but its rules no longer
318    /// apply to the document.
319    ///
320    /// Returns `false` if the node owns no stylesheet.
321    pub fn set_stylesheet_disabled(&mut self, node_id: NodeId, disabled: bool) -> bool {
322        let Some(sheet) = self.nodes_to_stylesheet.get(&node_id) else {
323            return false;
324        };
325        if sheet.0.set_disabled(disabled) {
326            self.stylist
327                .force_stylesheet_origins_dirty(OriginSet::all());
328        }
329        true
330    }
331
332    fn stylesheet_loader(&self) -> StylesheetLoader {
333        StylesheetLoader {
334            tx: self.tx.clone(),
335            doc_id: self.id(),
336            net_provider: self.net_provider.clone(),
337            shell_provider: self.shell_provider.clone(),
338            abort_signal: self.abort_signal.clone(),
339            import_depth: 0,
340        }
341    }
342
343    /// Resolve `path` to the rule it denotes. The rule's ancestors (outermost
344    /// first) are passed to `on_ancestor` as they are traversed.
345    fn resolve_rule(
346        sheet: &DocumentStyleSheet,
347        guard: &SharedRwLockReadGuard,
348        path: &[usize],
349        on_ancestor: impl FnMut(&CssRule),
350    ) -> Option<RuleHandle> {
351        let (index, parent_path) = path.split_last()?;
352        let list = Self::resolve_rule_list(sheet, guard, parent_path, on_ancestor)?;
353        Some(match list {
354            RuleList::Css(rules) => RuleHandle::Rule(rules.read_with(guard).0.get(*index)?.clone()),
355            RuleList::Keyframes(keyframes) => {
356                RuleHandle::Keyframe(keyframes.read_with(guard).keyframes.get(*index)?.clone())
357            }
358        })
359    }
360
361    /// Resolve `path` to the rule list owned by the rule it denotes (or the
362    /// stylesheet's top-level rule list for an empty path). The ancestor rules
363    /// of that list (outermost first, including the rule at `path` itself) are
364    /// passed to `on_ancestor` as they are traversed.
365    fn resolve_rule_list(
366        sheet: &DocumentStyleSheet,
367        guard: &SharedRwLockReadGuard,
368        path: &[usize],
369        mut on_ancestor: impl FnMut(&CssRule),
370    ) -> Option<RuleList> {
371        let mut list = RuleList::Css(sheet.contents(guard).rules.clone());
372        for &index in path {
373            let RuleList::Css(rules) = &list else {
374                return None;
375            };
376            let rules = rules.read_with(guard);
377            let rule = rules.0.get(index)?;
378            let child_list = child_rule_list(rule, guard)?;
379            on_ancestor(rule);
380            list = child_list;
381        }
382        Some(list)
383    }
384
385    /// [`Self::resolve_rule_list`], also collecting the ancestor rules
386    fn resolve_rule_list_with_ancestors(
387        sheet: &DocumentStyleSheet,
388        guard: &SharedRwLockReadGuard,
389        path: &[usize],
390    ) -> Option<(Vec<CssRule>, RuleList)> {
391        let mut ancestors = Vec::with_capacity(path.len());
392        let list =
393            Self::resolve_rule_list(sheet, guard, path, |rule| ancestors.push(rule.clone()))?;
394        Some((ancestors, list))
395    }
396
397    /// The number of rules in the rule list at `path` (`CSSRuleList.length`)
398    pub fn stylesheet_rule_count(&self, node_id: NodeId, path: &[usize]) -> Option<usize> {
399        let sheet = self.nodes_to_stylesheet.get(&node_id)?;
400        let guard = self.guard.read();
401        let list = Self::resolve_rule_list(sheet, &guard, path, |_| {})?;
402        Some(match list {
403            RuleList::Css(rules) => rules.read_with(&guard).0.len(),
404            RuleList::Keyframes(keyframes) => keyframes.read_with(&guard).keyframes.len(),
405        })
406    }
407
408    /// The CSSOM-visible attributes of the rule at `path`
409    pub fn stylesheet_rule_info(&self, node_id: NodeId, path: &[usize]) -> Option<CssRuleInfo> {
410        let sheet = self.nodes_to_stylesheet.get(&node_id)?;
411        let guard = self.guard.read();
412        let handle = Self::resolve_rule(sheet, &guard, path, |_| {})?;
413        let mut css_text = CssStringWriter::new();
414        let has_style = declaration_target(&handle, &guard).is_some();
415        Some(match handle {
416            RuleHandle::Rule(rule) => {
417                let _ = rule.to_css(&guard, &mut css_text);
418                CssRuleInfo {
419                    interface: css_rule_interface(&rule),
420                    rule_type: rule.rule_type() as u16,
421                    css_text,
422                    has_child_rules: child_rule_list(&rule, &guard).is_some(),
423                    has_style,
424                    attributes: rule_attributes(&rule, &guard),
425                }
426            }
427            RuleHandle::Keyframe(keyframe) => {
428                let keyframe = keyframe.read_with(&guard);
429                let _ = keyframe.to_css(&guard, &mut css_text);
430                CssRuleInfo {
431                    interface: "CSSKeyframeRule",
432                    rule_type: CssRuleType::Keyframe as u16,
433                    css_text,
434                    has_child_rules: false,
435                    has_style,
436                    attributes: vec![("keyText", keyframe.selector.to_css_string())],
437                }
438            }
439        })
440    }
441
442    /// Insert a rule parsed from `rule` at `index` in the rule list at `path`
443    /// (`CSSStyleSheet.insertRule` / `CSSGroupingRule.insertRule` /
444    /// `CSSKeyframesRule.appendRule`). Returns the index of the inserted rule.
445    pub fn stylesheet_insert_rule(
446        &mut self,
447        node_id: NodeId,
448        path: &[usize],
449        rule: &str,
450        index: usize,
451    ) -> Result<usize, CssomError> {
452        let sheet = self
453            .nodes_to_stylesheet
454            .get(&node_id)
455            .ok_or(CssomError::NotFound)?
456            .clone();
457        let loader = self.stylesheet_loader();
458        let lock = &self.guard;
459
460        let (ancestors, list) = {
461            let guard = lock.read();
462            Self::resolve_rule_list_with_ancestors(&sheet, &guard, path)
463                .ok_or(CssomError::NotFound)?
464        };
465
466        let (new_rule, index) = match &list {
467            RuleList::Css(rules) => {
468                let new_rule = {
469                    let guard = lock.read();
470                    let contents = sheet.contents(&guard);
471                    let containing_rule_types =
472                        ancestors
473                            .iter()
474                            .fold(CssRuleTypes::default(), |types, rule| {
475                                CssRuleTypes::from_bits(types.bits() | rule.rule_type().bit())
476                            });
477                    let parse_relative_rule_type = ancestors.last().map(|rule| rule.rule_type());
478                    rules.read_with(&guard).parse_rule_for_insert(
479                        lock,
480                        rule,
481                        contents,
482                        index,
483                        containing_rule_types,
484                        parse_relative_rule_type,
485                        Some(&loader),
486                        AllowImportRules::Yes,
487                    )?
488                };
489                {
490                    let mut guard = lock.write();
491                    rules
492                        .write_with(&mut guard)
493                        .0
494                        .insert(index, new_rule.clone());
495                }
496                (new_rule, index)
497            }
498            RuleList::Keyframes(keyframes) => {
499                let keyframe = {
500                    let guard = lock.read();
501                    let contents = sheet.contents(&guard);
502                    Keyframe::parse(rule, contents, lock).map_err(|_| CssomError::Syntax)?
503                };
504                let index = {
505                    let mut guard = lock.write();
506                    let keyframes = &mut keyframes.write_with(&mut guard).keyframes;
507                    keyframes.push(keyframe);
508                    keyframes.len() - 1
509                };
510                (CssRule::Keyframes(keyframes.clone()), index)
511            }
512        };
513
514        let guard = lock.read();
515        if let CssRule::FontFace(_) = &new_rule {
516            crate::net::fetch_font_face_rules(
517                std::iter::once(&new_rule),
518                self.tx.clone(),
519                self.id(),
520                Some(node_id),
521                &self.net_provider,
522                &self.shell_provider,
523                &guard,
524                self.abort_signal.as_ref(),
525            );
526        }
527        let (change_kind, ancestors) = match &list {
528            RuleList::Css(_) => (RuleChangeKind::Insertion, ancestors.as_slice()),
529            // The keyframes rule itself changed; its ancestors exclude it
530            RuleList::Keyframes(_) => (
531                RuleChangeKind::Generic,
532                &ancestors[..ancestors.len().saturating_sub(1)],
533            ),
534        };
535        let ancestor_refs: Vec<CssRuleRef> = ancestors.iter().map(CssRuleRef::from).collect();
536        self.stylist
537            .rule_changed(&sheet, &new_rule, &guard, change_kind, &ancestor_refs);
538        Ok(index)
539    }
540
541    /// Remove the rule at `index` from the rule list at `path`
542    /// (`CSSStyleSheet.deleteRule` / `CSSGroupingRule.deleteRule` /
543    /// `CSSKeyframesRule.deleteRule`).
544    pub fn stylesheet_delete_rule(
545        &mut self,
546        node_id: NodeId,
547        path: &[usize],
548        index: usize,
549    ) -> Result<(), CssomError> {
550        let sheet = self
551            .nodes_to_stylesheet
552            .get(&node_id)
553            .ok_or(CssomError::NotFound)?
554            .clone();
555        let lock = &self.guard;
556
557        let (ancestors, list) = {
558            let guard = lock.read();
559            Self::resolve_rule_list_with_ancestors(&sheet, &guard, path)
560                .ok_or(CssomError::NotFound)?
561        };
562
563        let (removed_rule, change_kind, ancestors) = match &list {
564            RuleList::Css(rules) => {
565                let mut guard = lock.write();
566                let rules = rules.write_with(&mut guard);
567                let removed = rules.0.get(index).cloned().ok_or(CssomError::IndexSize)?;
568                rules.remove_rule(index)?;
569                (removed, RuleChangeKind::Removal, ancestors.as_slice())
570            }
571            RuleList::Keyframes(keyframes) => {
572                let mut guard = lock.write();
573                let list = &mut keyframes.write_with(&mut guard).keyframes;
574                if index >= list.len() {
575                    return Err(CssomError::IndexSize);
576                }
577                list.remove(index);
578                (
579                    CssRule::Keyframes(keyframes.clone()),
580                    RuleChangeKind::Generic,
581                    &ancestors[..ancestors.len().saturating_sub(1)],
582                )
583            }
584        };
585
586        let guard = lock.read();
587        let ancestor_refs: Vec<CssRuleRef> = ancestors.iter().map(CssRuleRef::from).collect();
588        self.stylist
589            .rule_changed(&sheet, &removed_rule, &guard, change_kind, &ancestor_refs);
590        Ok(())
591    }
592
593    /// The declarations of the rule at `path`, for read-only access
594    fn rule_declaration_target(
595        &self,
596        node_id: NodeId,
597        path: &[usize],
598        guard: &SharedRwLockReadGuard,
599    ) -> Option<DeclarationTarget> {
600        let sheet = self.nodes_to_stylesheet.get(&node_id)?;
601        let handle = Self::resolve_rule(sheet, guard, path, |_| {})?;
602        declaration_target(&handle, guard)
603    }
604
605    /// The declarations of the rule at `path`, along with what is needed to
606    /// notify the stylist after mutating them: the stylesheet, the rule to
607    /// report as changed and its ancestors.
608    fn rule_declaration_target_mut(
609        &self,
610        node_id: NodeId,
611        path: &[usize],
612    ) -> Option<(DocumentStyleSheet, Vec<CssRule>, CssRule, DeclarationTarget)> {
613        let sheet = self.nodes_to_stylesheet.get(&node_id)?.clone();
614        let guard = self.guard.read();
615        let mut ancestors = Vec::with_capacity(path.len());
616        let handle = Self::resolve_rule(&sheet, &guard, path, |rule| ancestors.push(rule.clone()))?;
617        let target = declaration_target(&handle, &guard)?;
618        // For keyframes, the rule to report as changed is the owning
619        // `@keyframes` rule
620        let (changed_rule, ancestors) = match handle {
621            RuleHandle::Rule(rule) => (rule, ancestors),
622            RuleHandle::Keyframe(_) => {
623                let mut ancestors = ancestors;
624                let rule = ancestors.pop()?;
625                (rule, ancestors)
626            }
627        };
628        Some((sheet, ancestors, changed_rule, target))
629    }
630
631    /// The serialized declarations of the rule at `path` (`CSSRule.style.cssText`)
632    pub fn stylesheet_rule_style_css_text(
633        &self,
634        node_id: NodeId,
635        path: &[usize],
636    ) -> Option<String> {
637        let guard = self.guard.read();
638        let target = self.rule_declaration_target(node_id, path, &guard)?;
639        let mut css = CssStringWriter::new();
640        match target {
641            DeclarationTarget::Block { block, .. } => {
642                let _ = block.read_with(&guard).to_css(&mut css);
643            }
644            DeclarationTarget::FontFace(rule) => {
645                let _ = rule
646                    .read_with(&guard)
647                    .descriptors
648                    .to_css(&mut style_traits::CssWriter::new(&mut css));
649                // `Descriptors::to_css` emits a trailing space after each declaration
650                let trimmed = css.trim_end().len();
651                css.truncate(trimmed);
652            }
653        }
654        Some(css)
655    }
656
657    /// The names of the declarations of the rule at `path`, in order
658    /// (`CSSRule.style.length` / `.item()`)
659    pub fn stylesheet_rule_style_property_names(
660        &self,
661        node_id: NodeId,
662        path: &[usize],
663    ) -> Option<Vec<String>> {
664        let guard = self.guard.read();
665        let target = self.rule_declaration_target(node_id, path, &guard)?;
666        Some(match target {
667            DeclarationTarget::Block { block, .. } => block
668                .read_with(&guard)
669                .declarations()
670                .iter()
671                .map(|decl| decl.id().name().into_owned())
672                .collect(),
673            DeclarationTarget::FontFace(rule) => {
674                let descriptors = &rule.read_with(&guard).descriptors;
675                (0..descriptors.len())
676                    .filter_map(|i| descriptors.at(i))
677                    .map(|id| id.name().to_string())
678                    .collect()
679            }
680        })
681    }
682
683    /// The serialized value of `property` in the declarations of the rule at
684    /// `path`, and whether it is `!important`
685    /// (`CSSRule.style.getPropertyValue()` / `.getPropertyPriority()`).
686    /// Returns an empty string for unset or unknown properties.
687    pub fn stylesheet_rule_style_get_property(
688        &self,
689        node_id: NodeId,
690        path: &[usize],
691        property: &str,
692    ) -> Option<(String, bool)> {
693        let guard = self.guard.read();
694        let target = self.rule_declaration_target(node_id, path, &guard)?;
695        let mut css = CssStringWriter::new();
696        let important = match target {
697            DeclarationTarget::Block { block, .. } => {
698                let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
699                    return Some((String::new(), false));
700                };
701                let block = block.read_with(&guard);
702                let _ = block.property_value_to_css(&property_id, &mut css);
703                block.property_priority(&property_id).important()
704            }
705            DeclarationTarget::FontFace(rule) => {
706                let Ok(id) = FontFaceDescriptorId::from_ident(property) else {
707                    return Some((String::new(), false));
708                };
709                let _ = rule.read_with(&guard).descriptors.get(id, &mut css);
710                false
711            }
712        };
713        Some((css, important))
714    }
715
716    /// Set `property` to `value` in the declarations of the rule at `path`
717    /// (`CSSRule.style.setProperty()`). Invalid declarations are ignored, per
718    /// CSSOM. An empty `value` removes the property.
719    pub fn stylesheet_rule_style_set_property(
720        &mut self,
721        node_id: NodeId,
722        path: &[usize],
723        property: &str,
724        value: &str,
725        important: bool,
726    ) -> Result<(), CssomError> {
727        if value.trim().is_empty() {
728            self.stylesheet_rule_style_remove_property(node_id, path, property)?;
729            return Ok(());
730        }
731        let (sheet, ancestors, rule, target) = self
732            .rule_declaration_target_mut(node_id, path)
733            .ok_or(CssomError::NotFound)?;
734        let url_data = self.url.url_extra_data();
735        let lock = &self.guard;
736
737        let change_kind = match target {
738            DeclarationTarget::Block { block, rule_type } => {
739                let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
740                    return Ok(());
741                };
742                let mut source = SourcePropertyDeclaration::default();
743                if parse_one_declaration_into(
744                    &mut source,
745                    property_id,
746                    value,
747                    Origin::Author,
748                    &url_data,
749                    None,
750                    ParsingMode::DEFAULT,
751                    QuirksMode::NoQuirks,
752                    rule_type,
753                )
754                .is_err()
755                {
756                    return Ok(());
757                }
758                let importance = if important {
759                    Importance::Important
760                } else {
761                    Importance::Normal
762                };
763                let mut guard = lock.write();
764                block
765                    .write_with(&mut guard)
766                    .extend(source.drain(), importance);
767                match rule_type {
768                    CssRuleType::PositionTry => RuleChangeKind::PositionTryDeclarations,
769                    _ => RuleChangeKind::StyleRuleDeclarations,
770                }
771            }
772            DeclarationTarget::FontFace(font_face) => {
773                let Ok(id) = FontFaceDescriptorId::from_ident(property) else {
774                    return Ok(());
775                };
776                let context = ParserContext::new(
777                    Origin::Author,
778                    &url_data,
779                    Some(CssRuleType::FontFace),
780                    ParsingMode::DEFAULT,
781                    QuirksMode::NoQuirks,
782                    Default::default(),
783                    None,
784                    None,
785                    Default::default(),
786                );
787                let mut parser = Parser::new(value);
788                let mut guard = lock.write();
789                let descriptors = &mut font_face.write_with(&mut guard).descriptors;
790                if descriptors.set(id, &context, &mut parser).is_err() {
791                    return Ok(());
792                }
793                RuleChangeKind::Generic
794            }
795        };
796
797        let guard = lock.read();
798        let ancestor_refs: Vec<CssRuleRef> = ancestors.iter().map(CssRuleRef::from).collect();
799        self.stylist
800            .rule_changed(&sheet, &rule, &guard, change_kind, &ancestor_refs);
801        Ok(())
802    }
803
804    /// Remove `property` from the declarations of the rule at `path`
805    /// (`CSSRule.style.removeProperty()`). Returns the removed property's
806    /// previous serialized value.
807    pub fn stylesheet_rule_style_remove_property(
808        &mut self,
809        node_id: NodeId,
810        path: &[usize],
811        property: &str,
812    ) -> Result<String, CssomError> {
813        let (sheet, ancestors, rule, target) = self
814            .rule_declaration_target_mut(node_id, path)
815            .ok_or(CssomError::NotFound)?;
816        let lock = &self.guard;
817        let mut removed_value = CssStringWriter::new();
818
819        let change_kind = match target {
820            DeclarationTarget::Block { block, rule_type } => {
821                let Ok(property_id) = PropertyId::parse_enabled_for_all_content(property) else {
822                    return Ok(removed_value);
823                };
824                let mut guard = lock.write();
825                let block = block.write_with(&mut guard);
826                let _ = block.property_value_to_css(&property_id, &mut removed_value);
827                let Some(first_declaration) = block.first_declaration_to_remove(&property_id)
828                else {
829                    return Ok(removed_value);
830                };
831                block.remove_property(&property_id, first_declaration);
832                match rule_type {
833                    CssRuleType::PositionTry => RuleChangeKind::PositionTryDeclarations,
834                    _ => RuleChangeKind::StyleRuleDeclarations,
835                }
836            }
837            DeclarationTarget::FontFace(font_face) => {
838                let Ok(id) = FontFaceDescriptorId::from_ident(property) else {
839                    return Ok(removed_value);
840                };
841                let mut guard = lock.write();
842                let descriptors = &mut font_face.write_with(&mut guard).descriptors;
843                let _ = descriptors.get(id, &mut removed_value);
844                if !descriptors.remove(id) {
845                    return Ok(removed_value);
846                }
847                RuleChangeKind::Generic
848            }
849        };
850
851        let guard = lock.read();
852        let ancestor_refs: Vec<CssRuleRef> = ancestors.iter().map(CssRuleRef::from).collect();
853        self.stylist
854            .rule_changed(&sheet, &rule, &guard, change_kind, &ancestor_refs);
855        Ok(removed_value)
856    }
857
858    /// Replace the selectors of the style rule at `path` with those parsed
859    /// from `selectors` (`CSSStyleRule.selectorText` setter). Per CSSOM, an
860    /// invalid selector list leaves the rule unchanged (without throwing).
861    pub fn stylesheet_rule_set_selector_text(
862        &mut self,
863        node_id: NodeId,
864        path: &[usize],
865        selectors: &str,
866    ) -> Result<(), CssomError> {
867        let sheet = self
868            .nodes_to_stylesheet
869            .get(&node_id)
870            .ok_or(CssomError::NotFound)?
871            .clone();
872        let lock = &self.guard;
873
874        let (ancestors, style_rule) = {
875            let guard = lock.read();
876            let mut ancestors = Vec::with_capacity(path.len());
877            let handle =
878                Self::resolve_rule(&sheet, &guard, path, |rule| ancestors.push(rule.clone()))
879                    .ok_or(CssomError::NotFound)?;
880            let RuleHandle::Rule(CssRule::Style(style_rule)) = handle else {
881                return Err(CssomError::NotSupported);
882            };
883            (ancestors, style_rule)
884        };
885
886        // Nested style rules take relative selectors (`> .a`), resolved
887        // against the innermost enclosing style rule or `@scope`, mirroring
888        // stylo's `NestingContext`.
889        let parse_relative = ancestors
890            .iter()
891            .rev()
892            .find_map(|rule| match rule.rule_type() {
893                CssRuleType::Style => Some(ParseRelative::ForNesting),
894                CssRuleType::Scope => Some(ParseRelative::ForScope),
895                _ => None,
896            })
897            .unwrap_or(ParseRelative::No);
898
899        let new_selectors = {
900            let guard = lock.read();
901            let contents = sheet.contents(&guard);
902            let selector_parser = SelectorParser {
903                stylesheet_origin: contents.origin,
904                namespaces: &contents.namespaces,
905                url_data: &contents.url_data,
906                for_supports_rule: false,
907            };
908            let mut parser = Parser::new(selectors);
909            let Ok(new_selectors) =
910                SelectorList::parse(&selector_parser, &mut parser, parse_relative)
911            else {
912                return Ok(());
913            };
914            new_selectors
915        };
916
917        {
918            let mut guard = lock.write();
919            style_rule.write_with(&mut guard).selectors = new_selectors;
920        }
921
922        let guard = lock.read();
923        let rule = CssRule::Style(style_rule);
924        let ancestor_refs: Vec<CssRuleRef> = ancestors.iter().map(CssRuleRef::from).collect();
925        self.stylist.rule_changed(
926            &sheet,
927            &rule,
928            &guard,
929            RuleChangeKind::Generic,
930            &ancestor_refs,
931        );
932        Ok(())
933    }
934
935    /// Replace the declarations of the rule at `path` with those parsed from
936    /// `css` (`CSSRule.style.cssText` setter).
937    pub fn stylesheet_rule_style_set_css_text(
938        &mut self,
939        node_id: NodeId,
940        path: &[usize],
941        css: &str,
942    ) -> Result<(), CssomError> {
943        let (sheet, ancestors, rule, target) = self
944            .rule_declaration_target_mut(node_id, path)
945            .ok_or(CssomError::NotFound)?;
946        let DeclarationTarget::Block { block, rule_type } = target else {
947            return Err(CssomError::NotSupported);
948        };
949        let new_block = style::properties::declaration_block::parse_style_attribute(
950            css,
951            &self.url.url_extra_data(),
952            None,
953            QuirksMode::NoQuirks,
954            rule_type,
955        );
956        let lock = &self.guard;
957        {
958            let mut guard = lock.write();
959            *block.write_with(&mut guard) = new_block;
960        }
961        let guard = lock.read();
962        let ancestor_refs: Vec<CssRuleRef> = ancestors.iter().map(CssRuleRef::from).collect();
963        let change_kind = match rule_type {
964            CssRuleType::PositionTry => RuleChangeKind::PositionTryDeclarations,
965            _ => RuleChangeKind::StyleRuleDeclarations,
966        };
967        self.stylist
968            .rule_changed(&sheet, &rule, &guard, change_kind, &ancestor_refs);
969        Ok(())
970    }
971}