Skip to main content

style/stylesheets/
page_rule.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
5//! A [`@page`][page] rule.
6//!
7//! [page]: https://drafts.csswg.org/css2/page.html#page-box
8
9use crate::derives::*;
10use crate::parser::{Parse, ParserContext};
11use crate::properties::PropertyDeclarationBlock;
12use crate::shared_lock::{
13    DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
14};
15use crate::stylesheets::{style_or_page_rule_to_css, CssRules};
16use crate::values::{AtomIdent, CustomIdent};
17use cssparser::{match_ignore_ascii_case, Parser, SourceLocation, Token};
18#[cfg(feature = "gecko")]
19use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
20use servo_arc::Arc;
21use smallvec::SmallVec;
22use std::fmt::{self, Write};
23use style_traits::{CssStringWriter, CssWriter, ParseError, ToCss};
24
25macro_rules! page_pseudo_classes {
26    ($($(#[$($meta:tt)+])* $id:ident => $val:literal,)+) => {
27        /// [`@page`][page] rule pseudo-classes.
28        ///
29        /// https://drafts.csswg.org/css-page-3/#page-selectors
30        #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
31        #[repr(u8)]
32        pub enum PagePseudoClass {
33            $($(#[$($meta)+])* $id,)+
34        }
35        impl PagePseudoClass {
36            fn parse(
37                input: &mut Parser,
38            ) -> Result<Self, ParseError> {
39                let colon = input.next_including_whitespace()?;
40                if *colon != Token::Colon {
41                    return Err(ParseError::unexpected_token());
42                }
43
44                let ident = input.next_including_whitespace()?;
45                if let Token::Ident(s) = ident {
46                    return match_ignore_ascii_case! { &**s,
47                        $($val => Ok(PagePseudoClass::$id),)+
48                        _ => Err(ParseError::unexpected_token()),
49                    };
50                }
51                Err(ParseError::unexpected_token())
52            }
53            #[inline]
54            fn to_str(&self) -> &'static str {
55                match *self {
56                    $(PagePseudoClass::$id => concat!(':', $val),)+
57                }
58            }
59        }
60    }
61}
62
63page_pseudo_classes! {
64    /// [`:first`][first] pseudo-class
65    ///
66    /// [first] https://drafts.csswg.org/css-page-3/#first-pseudo
67    First => "first",
68    /// [`:blank`][blank] pseudo-class
69    ///
70    /// [blank] https://drafts.csswg.org/css-page-3/#blank-pseudo
71    Blank => "blank",
72    /// [`:left`][left] pseudo-class
73    ///
74    /// [left]: https://drafts.csswg.org/css-page-3/#spread-pseudos
75    Left => "left",
76    /// [`:right`][right] pseudo-class
77    ///
78    /// [right]: https://drafts.csswg.org/css-page-3/#spread-pseudos
79    Right => "right",
80}
81
82bitflags! {
83    /// Bit-flags for pseudo-class. This should only be used for querying if a
84    /// page-rule applies.
85    ///
86    /// https://drafts.csswg.org/css-page-3/#page-selectors
87    #[derive(Clone, Copy)]
88    #[repr(C)]
89    pub struct PagePseudoClassFlags : u8 {
90        /// No pseudo-classes
91        const NONE = 0;
92        /// Flag for PagePseudoClass::First
93        const FIRST = 1 << 0;
94        /// Flag for PagePseudoClass::Blank
95        const BLANK = 1 << 1;
96        /// Flag for PagePseudoClass::Left
97        const LEFT = 1 << 2;
98        /// Flag for PagePseudoClass::Right
99        const RIGHT = 1 << 3;
100    }
101}
102
103impl PagePseudoClassFlags {
104    /// Creates a pseudo-class flags object with a single pseudo-class.
105    #[inline]
106    pub fn new(other: &PagePseudoClass) -> Self {
107        match *other {
108            PagePseudoClass::First => PagePseudoClassFlags::FIRST,
109            PagePseudoClass::Blank => PagePseudoClassFlags::BLANK,
110            PagePseudoClass::Left => PagePseudoClassFlags::LEFT,
111            PagePseudoClass::Right => PagePseudoClassFlags::RIGHT,
112        }
113    }
114    /// Checks if the given pseudo class applies to this set of flags.
115    #[inline]
116    pub fn contains_class(self, other: &PagePseudoClass) -> bool {
117        self.intersects(PagePseudoClassFlags::new(other))
118    }
119}
120
121type PagePseudoClasses = SmallVec<[PagePseudoClass; 4]>;
122
123/// Type of a single [`@page`][page selector]
124///
125/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
126#[derive(Clone, Debug, MallocSizeOf, ToShmem)]
127pub struct PageSelector {
128    /// Page name
129    ///
130    /// https://drafts.csswg.org/css-page-3/#page-type-selector
131    pub name: AtomIdent,
132    /// Pseudo-classes for [`@page`][page-selectors]
133    ///
134    /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
135    pub pseudos: PagePseudoClasses,
136}
137
138/// Computes the [specificity] given the g, h, and f values as in the spec.
139///
140/// g is number of `:first` or `:blank`, h is number of `:left` or `:right`,
141/// f is if the selector includes a page-name (selectors can only include one
142/// or zero page-names).
143///
144/// This places hard limits of 65535 on h and 32767 on g, at which point all
145/// higher values are treated as those limits respectively.
146///
147/// [specificity]: https://drafts.csswg.org/css-page/#specificity
148#[inline]
149fn selector_specificity(g: usize, h: usize, f: bool) -> u32 {
150    let h = h.min(0xFFFF) as u32;
151    let g = (g.min(0x7FFF) as u32) << 16;
152    let f = if f { 0x80000000 } else { 0 };
153    h + g + f
154}
155
156impl PageSelector {
157    /// Checks if the ident matches a page-name's ident.
158    ///
159    /// This does not take pseudo selectors into account.
160    #[inline]
161    pub fn ident_matches(&self, other: &CustomIdent) -> bool {
162        self.name.0 == other.0
163    }
164
165    /// Checks that this selector matches the ident and all pseudo classes are
166    /// present in the provided flags.
167    #[inline]
168    pub fn matches(&self, name: &CustomIdent, flags: PagePseudoClassFlags) -> bool {
169        self.ident_matches(name) && self.flags_match(flags)
170    }
171
172    /// Checks that all pseudo classes in this selector are present in the
173    /// provided flags.
174    ///
175    /// Equivalent to, but may be more efficient than:
176    ///
177    /// ```
178    /// match_specificity(flags).is_some()
179    /// ```
180    pub fn flags_match(&self, flags: PagePseudoClassFlags) -> bool {
181        self.pseudos.iter().all(|pc| flags.contains_class(pc))
182    }
183
184    /// Implements specificity calculation for a page selector given a set of
185    /// page pseudo-classes to match with.
186    /// If this selector includes any pseudo-classes that are not in the flags,
187    /// then this will return None.
188    ///
189    /// To fit the specificity calculation into a 32-bit value, this limits the
190    /// maximum count of :first and :blank to 32767, and the maximum count of
191    /// :left and :right to 65535.
192    ///
193    /// https://drafts.csswg.org/css-page-3/#cascading-and-page-context
194    pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> {
195        let mut g: usize = 0;
196        let mut h: usize = 0;
197        for pc in self.pseudos.iter() {
198            if !flags.contains_class(pc) {
199                return None;
200            }
201            match pc {
202                PagePseudoClass::First | PagePseudoClass::Blank => g += 1,
203                PagePseudoClass::Left | PagePseudoClass::Right => h += 1,
204            }
205        }
206        Some(selector_specificity(g, h, !self.name.0.is_empty()))
207    }
208}
209
210impl ToCss for PageSelector {
211    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
212    where
213        W: Write,
214    {
215        self.name.to_css(dest)?;
216        for pc in self.pseudos.iter() {
217            dest.write_str(pc.to_str())?;
218        }
219        Ok(())
220    }
221}
222
223fn parse_page_name(input: &mut Parser) -> Result<AtomIdent, ParseError> {
224    let s = input.expect_ident()?;
225    Ok(AtomIdent::from(&**s))
226}
227
228impl Parse for PageSelector {
229    fn parse(_context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
230        let name = input.try_parse(parse_page_name);
231        let mut pseudos = PagePseudoClasses::default();
232        while let Ok(pc) = input.try_parse(PagePseudoClass::parse) {
233            pseudos.push(pc);
234        }
235        // If the result was empty, then we didn't get a selector.
236        let name = match name {
237            Ok(name) => name,
238            Err(..) if !pseudos.is_empty() => AtomIdent::new(atom!("")),
239            Err(err) => return Err(err),
240        };
241        Ok(PageSelector { name, pseudos })
242    }
243}
244
245/// A list of [`@page`][page selectors]
246///
247/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
248#[derive(Clone, Debug, Default, MallocSizeOf, ToCss, ToShmem)]
249#[css(comma)]
250pub struct PageSelectors(#[css(iterable)] pub Box<[PageSelector]>);
251
252impl PageSelectors {
253    /// Creates a new PageSelectors from a Vec, as from parse_comma_separated
254    #[inline]
255    pub fn new(s: Vec<PageSelector>) -> Self {
256        PageSelectors(s.into())
257    }
258    /// Returns true iff there are any page selectors
259    #[inline]
260    pub fn is_empty(&self) -> bool {
261        self.as_slice().is_empty()
262    }
263    /// Get the underlying PageSelector data as a slice
264    #[inline]
265    pub fn as_slice(&self) -> &[PageSelector] {
266        &self.0
267    }
268}
269
270impl Parse for PageSelectors {
271    fn parse(context: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
272        Ok(PageSelectors::new(input.parse_comma_separated(|i| {
273            PageSelector::parse(context, i)
274        })?))
275    }
276}
277
278/// A [`@page`][page] rule.
279///
280/// This implements only a limited subset of the CSS
281/// 2.2 syntax.
282///
283/// [page]: https://drafts.csswg.org/css2/page.html#page-box
284/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors
285#[derive(Clone, Debug, ToShmem)]
286pub struct PageRule {
287    /// Selectors of the page-rule
288    pub selectors: PageSelectors,
289    /// Nested rules.
290    pub rules: Arc<Locked<CssRules>>,
291    /// The declaration block this page rule contains.
292    pub block: Arc<Locked<PropertyDeclarationBlock>>,
293    /// The source position this rule was found at.
294    pub source_location: SourceLocation,
295}
296
297impl PageRule {
298    /// Measure heap usage.
299    #[cfg(feature = "gecko")]
300    pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
301        // Measurement of other fields may be added later.
302        self.rules.unconditional_shallow_size_of(ops)
303            + self.rules.read_with(guard).size_of(guard, ops)
304            + self.block.unconditional_shallow_size_of(ops)
305            + self.block.read_with(guard).size_of(ops)
306            + self.selectors.size_of(ops)
307    }
308    /// Computes the specificity of this page rule when matched with flags.
309    ///
310    /// Computing this value has linear-complexity with the size of the
311    /// selectors, so the caller should usually call this once and cache the
312    /// result.
313    ///
314    /// Returns None if the flags do not match this page rule.
315    ///
316    /// The return type is ordered by page-rule specificity.
317    pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> {
318        if self.selectors.is_empty() {
319            // A page-rule with no selectors matches all pages, but with the
320            // lowest possible specificity.
321            return Some(selector_specificity(0, 0, false));
322        }
323        let mut specificity = None;
324        for s in self.selectors.0.iter().map(|s| s.match_specificity(flags)) {
325            specificity = s.max(specificity);
326        }
327        specificity
328    }
329}
330
331impl ToCssWithGuard for PageRule {
332    /// Serialization of PageRule is not specced, adapted from steps for StyleRule.
333    fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
334        // https://drafts.csswg.org/cssom/#serialize-a-css-rule
335        dest.write_str("@page ")?;
336        if !self.selectors.is_empty() {
337            self.selectors.to_css(&mut CssWriter::new(dest))?;
338            dest.write_char(' ')?;
339        }
340        style_or_page_rule_to_css(Some(&self.rules), &self.block, guard, dest)
341    }
342}
343
344impl DeepCloneWithLock for PageRule {
345    fn deep_clone_with_lock(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self {
346        let rules = self.rules.read_with(guard);
347        PageRule {
348            selectors: self.selectors.clone(),
349            block: Arc::new(lock.wrap(self.block.read_with(guard).clone())),
350            rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))),
351            source_location: self.source_location,
352        }
353    }
354}