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