style/stylesheets/
stylesheet.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/. */
4
5use crate::context::QuirksMode;
6use crate::error_reporting::{ContextualParseError, ParseErrorReporter};
7use crate::media_queries::{Device, MediaList};
8use crate::parser::ParserContext;
9use crate::shared_lock::{DeepCloneWithLock, Locked};
10use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard};
11use crate::stylesheets::loader::StylesheetLoader;
12use crate::stylesheets::rule_parser::{State, TopLevelRuleParser};
13use crate::stylesheets::rules_iterator::{EffectiveRules, EffectiveRulesIterator};
14use crate::stylesheets::rules_iterator::{NestedRuleIterationCondition, RulesIterator};
15use crate::stylesheets::{CssRule, CssRules, Origin, UrlExtraData};
16use crate::use_counters::UseCounters;
17use crate::{Namespace, Prefix};
18use cssparser::{Parser, ParserInput, StyleSheetParser};
19use fxhash::FxHashMap;
20#[cfg(feature = "gecko")]
21use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
22use parking_lot::RwLock;
23use servo_arc::Arc;
24use std::sync::atomic::{AtomicBool, Ordering};
25use style_traits::ParsingMode;
26
27use super::scope_rule::ImplicitScopeRoot;
28
29/// This structure holds the user-agent and user stylesheets.
30pub struct UserAgentStylesheets {
31    /// The lock used for user-agent stylesheets.
32    pub shared_lock: SharedRwLock,
33    /// The user or user agent stylesheets.
34    pub user_or_user_agent_stylesheets: Vec<DocumentStyleSheet>,
35    /// The quirks mode stylesheet.
36    pub quirks_mode_stylesheet: DocumentStyleSheet,
37}
38
39/// A set of namespaces applying to a given stylesheet.
40///
41/// The namespace id is used in gecko
42#[derive(Clone, Debug, Default, MallocSizeOf)]
43#[allow(missing_docs)]
44pub struct Namespaces {
45    pub default: Option<Namespace>,
46    pub prefixes: FxHashMap<Prefix, Namespace>,
47}
48
49/// The contents of a given stylesheet. This effectively maps to a
50/// StyleSheetInner in Gecko.
51#[derive(Debug)]
52pub struct StylesheetContents {
53    /// List of rules in the order they were found (important for
54    /// cascading order)
55    pub rules: Arc<Locked<CssRules>>,
56    /// The origin of this stylesheet.
57    pub origin: Origin,
58    /// The url data this stylesheet should use.
59    pub url_data: RwLock<UrlExtraData>,
60    /// The namespaces that apply to this stylesheet.
61    pub namespaces: RwLock<Namespaces>,
62    /// The quirks mode of this stylesheet.
63    pub quirks_mode: QuirksMode,
64    /// This stylesheet's source map URL.
65    pub source_map_url: RwLock<Option<String>>,
66    /// This stylesheet's source URL.
67    pub source_url: RwLock<Option<String>>,
68    /// The use counters of the original stylesheet.
69    pub use_counters: UseCounters,
70
71    /// We don't want to allow construction outside of this file, to guarantee
72    /// that all contents are created with Arc<>.
73    _forbid_construction: (),
74}
75
76impl StylesheetContents {
77    /// Parse a given CSS string, with a given url-data, origin, and
78    /// quirks mode.
79    pub fn from_str(
80        css: &str,
81        url_data: UrlExtraData,
82        origin: Origin,
83        shared_lock: &SharedRwLock,
84        stylesheet_loader: Option<&dyn StylesheetLoader>,
85        error_reporter: Option<&dyn ParseErrorReporter>,
86        quirks_mode: QuirksMode,
87        allow_import_rules: AllowImportRules,
88        sanitization_data: Option<&mut SanitizationData>,
89    ) -> Arc<Self> {
90        let use_counters = UseCounters::default();
91        let (namespaces, rules, source_map_url, source_url) = Stylesheet::parse_rules(
92            css,
93            &url_data,
94            origin,
95            &shared_lock,
96            stylesheet_loader,
97            error_reporter,
98            quirks_mode,
99            Some(&use_counters),
100            allow_import_rules,
101            sanitization_data,
102        );
103
104        Arc::new(Self {
105            rules: CssRules::new(rules, &shared_lock),
106            origin,
107            url_data: RwLock::new(url_data),
108            namespaces: RwLock::new(namespaces),
109            quirks_mode,
110            source_map_url: RwLock::new(source_map_url),
111            source_url: RwLock::new(source_url),
112            use_counters,
113            _forbid_construction: (),
114        })
115    }
116
117    /// Creates a new StylesheetContents with the specified pre-parsed rules,
118    /// origin, URL data, and quirks mode.
119    ///
120    /// Since the rules have already been parsed, and the intention is that
121    /// this function is used for read only User Agent style sheets, an empty
122    /// namespace map is used, and the source map and source URLs are set to
123    /// None.
124    ///
125    /// An empty namespace map should be fine, as it is only used for parsing,
126    /// not serialization of existing selectors.  Since UA sheets are read only,
127    /// we should never need the namespace map.
128    pub fn from_data(
129        rules: Arc<Locked<CssRules>>,
130        origin: Origin,
131        url_data: UrlExtraData,
132        quirks_mode: QuirksMode,
133    ) -> Arc<Self> {
134        Arc::new(Self {
135            rules,
136            origin,
137            url_data: RwLock::new(url_data),
138            namespaces: RwLock::new(Namespaces::default()),
139            quirks_mode,
140            source_map_url: RwLock::new(None),
141            source_url: RwLock::new(None),
142            use_counters: UseCounters::default(),
143            _forbid_construction: (),
144        })
145    }
146
147    /// Same as above, but ensuring that the rules are static.
148    pub fn from_shared_data(
149        rules: Arc<Locked<CssRules>>,
150        origin: Origin,
151        url_data: UrlExtraData,
152        quirks_mode: QuirksMode,
153    ) -> Arc<Self> {
154        debug_assert!(rules.is_static());
155        Self::from_data(rules, origin, url_data, quirks_mode)
156    }
157
158    /// Returns a reference to the list of rules.
159    #[inline]
160    pub fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] {
161        &self.rules.read_with(guard).0
162    }
163
164    /// Measure heap usage.
165    #[cfg(feature = "gecko")]
166    pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
167        if self.rules.is_static() {
168            return 0;
169        }
170        // Measurement of other fields may be added later.
171        self.rules.unconditional_shallow_size_of(ops)
172            + self.rules.read_with(guard).size_of(guard, ops)
173    }
174}
175
176impl DeepCloneWithLock for StylesheetContents {
177    fn deep_clone_with_lock(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self {
178        // Make a deep clone of the rules, using the new lock.
179        let rules = self
180            .rules
181            .read_with(guard)
182            .deep_clone_with_lock(lock, guard);
183
184        Self {
185            rules: Arc::new(lock.wrap(rules)),
186            quirks_mode: self.quirks_mode,
187            origin: self.origin,
188            url_data: RwLock::new((*self.url_data.read()).clone()),
189            namespaces: RwLock::new((*self.namespaces.read()).clone()),
190            source_map_url: RwLock::new((*self.source_map_url.read()).clone()),
191            source_url: RwLock::new((*self.source_url.read()).clone()),
192            use_counters: self.use_counters.clone(),
193            _forbid_construction: (),
194        }
195    }
196}
197
198/// The structure servo uses to represent a stylesheet.
199#[derive(Debug)]
200pub struct Stylesheet {
201    /// The contents of this stylesheet.
202    pub contents: Arc<StylesheetContents>,
203    /// The lock used for objects inside this stylesheet
204    pub shared_lock: SharedRwLock,
205    /// List of media associated with the Stylesheet.
206    pub media: Arc<Locked<MediaList>>,
207    /// Whether this stylesheet should be disabled.
208    pub disabled: AtomicBool,
209}
210
211/// A trait to represent a given stylesheet in a document.
212pub trait StylesheetInDocument: ::std::fmt::Debug {
213    /// Get whether this stylesheet is enabled.
214    fn enabled(&self) -> bool;
215
216    /// Get the media associated with this stylesheet.
217    fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList>;
218
219    /// Returns a reference to the list of rules in this stylesheet.
220    fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] {
221        self.contents().rules(guard)
222    }
223
224    /// Returns a reference to the contents of the stylesheet.
225    fn contents(&self) -> &StylesheetContents;
226
227    /// Return an iterator using the condition `C`.
228    #[inline]
229    fn iter_rules<'a, 'b, C>(
230        &'a self,
231        device: &'a Device,
232        guard: &'a SharedRwLockReadGuard<'b>,
233    ) -> RulesIterator<'a, 'b, C>
234    where
235        C: NestedRuleIterationCondition,
236    {
237        let contents = self.contents();
238        RulesIterator::new(
239            device,
240            contents.quirks_mode,
241            guard,
242            contents.rules(guard).iter(),
243        )
244    }
245
246    /// Returns whether the style-sheet applies for the current device.
247    fn is_effective_for_device(&self, device: &Device, guard: &SharedRwLockReadGuard) -> bool {
248        match self.media(guard) {
249            Some(medialist) => medialist.evaluate(device, self.contents().quirks_mode),
250            None => true,
251        }
252    }
253
254    /// Return an iterator over the effective rules within the style-sheet, as
255    /// according to the supplied `Device`.
256    #[inline]
257    fn effective_rules<'a, 'b>(
258        &'a self,
259        device: &'a Device,
260        guard: &'a SharedRwLockReadGuard<'b>,
261    ) -> EffectiveRulesIterator<'a, 'b> {
262        self.iter_rules::<EffectiveRules>(device, guard)
263    }
264
265    /// Return the implicit scope root for this stylesheet, if one exists.
266    fn implicit_scope_root(&self) -> Option<ImplicitScopeRoot>;
267}
268
269impl StylesheetInDocument for Stylesheet {
270    fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
271        Some(self.media.read_with(guard))
272    }
273
274    fn enabled(&self) -> bool {
275        !self.disabled()
276    }
277
278    #[inline]
279    fn contents(&self) -> &StylesheetContents {
280        &self.contents
281    }
282
283    fn implicit_scope_root(&self) -> Option<ImplicitScopeRoot> {
284        None
285    }
286}
287
288/// A simple wrapper over an `Arc<Stylesheet>`, with pointer comparison, and
289/// suitable for its use in a `StylesheetSet`.
290#[derive(Clone, Debug)]
291#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
292pub struct DocumentStyleSheet(
293    #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] pub Arc<Stylesheet>,
294);
295
296impl PartialEq for DocumentStyleSheet {
297    fn eq(&self, other: &Self) -> bool {
298        Arc::ptr_eq(&self.0, &other.0)
299    }
300}
301
302impl StylesheetInDocument for DocumentStyleSheet {
303    fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> {
304        self.0.media(guard)
305    }
306
307    fn enabled(&self) -> bool {
308        self.0.enabled()
309    }
310
311    #[inline]
312    fn contents(&self) -> &StylesheetContents {
313        self.0.contents()
314    }
315
316    fn implicit_scope_root(&self) -> Option<ImplicitScopeRoot> {
317        None
318    }
319}
320
321/// The kind of sanitization to use when parsing a stylesheet.
322#[repr(u8)]
323#[derive(Clone, Copy, Debug, PartialEq)]
324pub enum SanitizationKind {
325    /// Perform no sanitization.
326    None,
327    /// Allow only @font-face, style rules, and @namespace.
328    Standard,
329    /// Allow everything but conditional rules.
330    NoConditionalRules,
331}
332
333/// Whether @import rules are allowed.
334#[repr(u8)]
335#[derive(Clone, Copy, Debug, PartialEq)]
336pub enum AllowImportRules {
337    /// @import rules will be parsed.
338    Yes,
339    /// @import rules will not be parsed.
340    No,
341}
342
343impl SanitizationKind {
344    fn allows(self, rule: &CssRule) -> bool {
345        debug_assert_ne!(self, SanitizationKind::None);
346        // NOTE(emilio): If this becomes more complex (not filtering just by
347        // top-level rules), we should thread all the data through nested rules
348        // and such. But this doesn't seem necessary at the moment.
349        let is_standard = matches!(self, SanitizationKind::Standard);
350        match *rule {
351            CssRule::Document(..) |
352            CssRule::Media(..) |
353            CssRule::Supports(..) |
354            CssRule::Import(..) |
355            CssRule::Container(..) |
356            // TODO(emilio): Perhaps Layer should not be always sanitized? But
357            // we sanitize @media and co, so this seems safer for now.
358            CssRule::LayerStatement(..) |
359            CssRule::LayerBlock(..) |
360            // TODO(dshin): Same comment as Layer applies - shouldn't give away
361            // something like display size - erring on the side of "safe" for now.
362            CssRule::Scope(..) |
363            CssRule::StartingStyle(..) => false,
364
365            CssRule::FontFace(..) |
366            CssRule::Namespace(..) |
367            CssRule::Style(..) |
368            CssRule::NestedDeclarations(..) |
369            CssRule::PositionTry(..) => true,
370
371            CssRule::Keyframes(..) |
372            CssRule::Page(..) |
373            CssRule::Margin(..) |
374            CssRule::Property(..) |
375            CssRule::FontFeatureValues(..) |
376            CssRule::FontPaletteValues(..) |
377            CssRule::CounterStyle(..) => !is_standard,
378        }
379    }
380}
381
382/// A struct to hold the data relevant to style sheet sanitization.
383#[derive(Debug)]
384pub struct SanitizationData {
385    kind: SanitizationKind,
386    output: String,
387}
388
389impl SanitizationData {
390    /// Create a new input for sanitization.
391    #[inline]
392    pub fn new(kind: SanitizationKind) -> Option<Self> {
393        if matches!(kind, SanitizationKind::None) {
394            return None;
395        }
396        Some(Self {
397            kind,
398            output: String::new(),
399        })
400    }
401
402    /// Take the sanitized output.
403    #[inline]
404    pub fn take(self) -> String {
405        self.output
406    }
407}
408
409impl Stylesheet {
410    /// Updates an empty stylesheet from a given string of text.
411    pub fn update_from_str(
412        existing: &Stylesheet,
413        css: &str,
414        url_data: UrlExtraData,
415        stylesheet_loader: Option<&dyn StylesheetLoader>,
416        error_reporter: Option<&dyn ParseErrorReporter>,
417        allow_import_rules: AllowImportRules,
418    ) {
419        let use_counters = UseCounters::default();
420        let (namespaces, rules, source_map_url, source_url) = Self::parse_rules(
421            css,
422            &url_data,
423            existing.contents.origin,
424            &existing.shared_lock,
425            stylesheet_loader,
426            error_reporter,
427            existing.contents.quirks_mode,
428            Some(&use_counters),
429            allow_import_rules,
430            /* sanitization_data = */ None,
431        );
432
433        *existing.contents.url_data.write() = url_data;
434        *existing.contents.namespaces.write() = namespaces;
435
436        // Acquire the lock *after* parsing, to minimize the exclusive section.
437        let mut guard = existing.shared_lock.write();
438        *existing.contents.rules.write_with(&mut guard) = CssRules(rules);
439        *existing.contents.source_map_url.write() = source_map_url;
440        *existing.contents.source_url.write() = source_url;
441        existing.contents.use_counters.merge(&use_counters);
442    }
443
444    fn parse_rules(
445        css: &str,
446        url_data: &UrlExtraData,
447        origin: Origin,
448        shared_lock: &SharedRwLock,
449        stylesheet_loader: Option<&dyn StylesheetLoader>,
450        error_reporter: Option<&dyn ParseErrorReporter>,
451        quirks_mode: QuirksMode,
452        use_counters: Option<&UseCounters>,
453        allow_import_rules: AllowImportRules,
454        mut sanitization_data: Option<&mut SanitizationData>,
455    ) -> (Namespaces, Vec<CssRule>, Option<String>, Option<String>) {
456        let mut input = ParserInput::new(css);
457        let mut input = Parser::new(&mut input);
458
459        let context = ParserContext::new(
460            origin,
461            url_data,
462            None,
463            ParsingMode::DEFAULT,
464            quirks_mode,
465            /* namespaces = */ Default::default(),
466            error_reporter,
467            use_counters,
468        );
469
470        let mut rule_parser = TopLevelRuleParser {
471            shared_lock,
472            loader: stylesheet_loader,
473            context,
474            state: State::Start,
475            dom_error: None,
476            insert_rule_context: None,
477            allow_import_rules,
478            declaration_parser_state: Default::default(),
479            first_declaration_block: Default::default(),
480            wants_first_declaration_block: false,
481            error_reporting_state: Default::default(),
482            rules: Vec::new(),
483        };
484
485        {
486            let mut iter = StyleSheetParser::new(&mut input, &mut rule_parser);
487            while let Some(result) = iter.next() {
488                match result {
489                    Ok(rule_start) => {
490                        // TODO(emilio, nesting): sanitize nested CSS rules, probably?
491                        if let Some(ref mut data) = sanitization_data {
492                            if let Some(ref rule) = iter.parser.rules.last() {
493                                if !data.kind.allows(rule) {
494                                    iter.parser.rules.pop();
495                                    continue;
496                                }
497                            }
498                            let end = iter.input.position().byte_index();
499                            data.output.push_str(&css[rule_start.byte_index()..end]);
500                        }
501                    },
502                    Err((error, slice)) => {
503                        let location = error.location;
504                        let error = ContextualParseError::InvalidRule(slice, error);
505                        iter.parser.context.log_css_error(location, error);
506                    },
507                }
508            }
509        }
510
511        let source_map_url = input.current_source_map_url().map(String::from);
512        let source_url = input.current_source_url().map(String::from);
513        (
514            rule_parser.context.namespaces.into_owned(),
515            rule_parser.rules,
516            source_map_url,
517            source_url,
518        )
519    }
520
521    /// Creates an empty stylesheet and parses it with a given base url, origin
522    /// and media.
523    ///
524    /// Effectively creates a new stylesheet and forwards the hard work to
525    /// `Stylesheet::update_from_str`.
526    pub fn from_str(
527        css: &str,
528        url_data: UrlExtraData,
529        origin: Origin,
530        media: Arc<Locked<MediaList>>,
531        shared_lock: SharedRwLock,
532        stylesheet_loader: Option<&dyn StylesheetLoader>,
533        error_reporter: Option<&dyn ParseErrorReporter>,
534        quirks_mode: QuirksMode,
535        allow_import_rules: AllowImportRules,
536    ) -> Self {
537        // FIXME: Consider adding use counters to Servo?
538        let contents = StylesheetContents::from_str(
539            css,
540            url_data,
541            origin,
542            &shared_lock,
543            stylesheet_loader,
544            error_reporter,
545            quirks_mode,
546            allow_import_rules,
547            /* sanitized_output = */ None,
548        );
549
550        Stylesheet {
551            contents,
552            shared_lock,
553            media,
554            disabled: AtomicBool::new(false),
555        }
556    }
557
558    /// Returns whether the stylesheet has been explicitly disabled through the
559    /// CSSOM.
560    pub fn disabled(&self) -> bool {
561        self.disabled.load(Ordering::SeqCst)
562    }
563
564    /// Records that the stylesheet has been explicitly disabled through the
565    /// CSSOM.
566    ///
567    /// Returns whether the the call resulted in a change in disabled state.
568    ///
569    /// Disabled stylesheets remain in the document, but their rules are not
570    /// added to the Stylist.
571    pub fn set_disabled(&self, disabled: bool) -> bool {
572        self.disabled.swap(disabled, Ordering::SeqCst) != disabled
573    }
574}
575
576#[cfg(feature = "servo")]
577impl Clone for Stylesheet {
578    fn clone(&self) -> Self {
579        // Create a new lock for our clone.
580        let lock = self.shared_lock.clone();
581        let guard = self.shared_lock.read();
582
583        // Make a deep clone of the media, using the new lock.
584        let media = self.media.read_with(&guard).clone();
585        let media = Arc::new(lock.wrap(media));
586        let contents = Arc::new(self.contents.deep_clone_with_lock(&lock, &guard));
587
588        Stylesheet {
589            contents,
590            media,
591            shared_lock: lock,
592            disabled: AtomicBool::new(self.disabled.load(Ordering::SeqCst)),
593        }
594    }
595}