/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! A [`@page`][page] rule. //! //! [page]: https://drafts.csswg.org/css2/page.html#page-box use crate::parser::{Parse, ParserContext}; use crate::properties::PropertyDeclarationBlock; use crate::shared_lock::{ DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, }; use crate::str::CssStringWriter; use crate::stylesheets::{CssRules, style_or_page_rule_to_css}; use crate::values::{AtomIdent, CustomIdent}; use cssparser::{Parser, SourceLocation, Token}; #[cfg(feature = "gecko")] use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; use servo_arc::Arc; use smallvec::SmallVec; use std::fmt::{self, Write}; use style_traits::{CssWriter, ParseError, ToCss}; macro_rules! page_pseudo_classes { ($($(#[$($meta:tt)+])* $id:ident => $val:literal,)+) => { /// [`@page`][page] rule pseudo-classes. /// /// https://drafts.csswg.org/css-page-3/#page-selectors #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] #[repr(u8)] pub enum PagePseudoClass { $($(#[$($meta)+])* $id,)+ } impl PagePseudoClass { fn parse<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result> { let loc = input.current_source_location(); let colon = input.next_including_whitespace()?; if *colon != Token::Colon { return Err(loc.new_unexpected_token_error(colon.clone())); } let ident = input.next_including_whitespace()?; if let Token::Ident(s) = ident { return match_ignore_ascii_case! { &**s, $($val => Ok(PagePseudoClass::$id),)+ _ => Err(loc.new_unexpected_token_error(Token::Ident(s.clone()))), }; } Err(loc.new_unexpected_token_error(ident.clone())) } #[inline] fn to_str(&self) -> &'static str { match *self { $(PagePseudoClass::$id => concat!(':', $val),)+ } } } } } page_pseudo_classes! { /// [`:first`][first] pseudo-class /// /// [first] https://drafts.csswg.org/css-page-3/#first-pseudo First => "first", /// [`:blank`][blank] pseudo-class /// /// [blank] https://drafts.csswg.org/css-page-3/#blank-pseudo Blank => "blank", /// [`:left`][left] pseudo-class /// /// [left]: https://drafts.csswg.org/css-page-3/#spread-pseudos Left => "left", /// [`:right`][right] pseudo-class /// /// [right]: https://drafts.csswg.org/css-page-3/#spread-pseudos Right => "right", } bitflags! { /// Bit-flags for pseudo-class. This should only be used for querying if a /// page-rule applies. /// /// https://drafts.csswg.org/css-page-3/#page-selectors #[derive(Clone, Copy)] #[repr(C)] pub struct PagePseudoClassFlags : u8 { /// No pseudo-classes const NONE = 0; /// Flag for PagePseudoClass::First const FIRST = 1 << 0; /// Flag for PagePseudoClass::Blank const BLANK = 1 << 1; /// Flag for PagePseudoClass::Left const LEFT = 1 << 2; /// Flag for PagePseudoClass::Right const RIGHT = 1 << 3; } } impl PagePseudoClassFlags { /// Creates a pseudo-class flags object with a single pseudo-class. #[inline] pub fn new(other: &PagePseudoClass) -> Self { match *other { PagePseudoClass::First => PagePseudoClassFlags::FIRST, PagePseudoClass::Blank => PagePseudoClassFlags::BLANK, PagePseudoClass::Left => PagePseudoClassFlags::LEFT, PagePseudoClass::Right => PagePseudoClassFlags::RIGHT, } } /// Checks if the given pseudo class applies to this set of flags. #[inline] pub fn contains_class(self, other: &PagePseudoClass) -> bool { self.intersects(PagePseudoClassFlags::new(other)) } } type PagePseudoClasses = SmallVec<[PagePseudoClass; 4]>; /// Type of a single [`@page`][page selector] /// /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors #[derive(Clone, Debug, MallocSizeOf, ToShmem)] pub struct PageSelector { /// Page name /// /// https://drafts.csswg.org/css-page-3/#page-type-selector pub name: AtomIdent, /// Pseudo-classes for [`@page`][page-selectors] /// /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors pub pseudos: PagePseudoClasses, } /// Computes the [specificity] given the g, h, and f values as in the spec. /// /// g is number of `:first` or `:blank`, h is number of `:left` or `:right`, /// f is if the selector includes a page-name (selectors can only include one /// or zero page-names). /// /// This places hard limits of 65535 on h and 32767 on g, at which point all /// higher values are treated as those limits respectively. /// /// [specificity]: https://drafts.csswg.org/css-page/#specificity #[inline] fn selector_specificity(g: usize, h: usize, f: bool) -> u32 { let h = h.min(0xFFFF) as u32; let g = (g.min(0x7FFF) as u32) << 16; let f = if f { 0x80000000 } else { 0 }; h + g + f } impl PageSelector { /// Checks if the ident matches a page-name's ident. /// /// This does not take pseudo selectors into account. #[inline] pub fn ident_matches(&self, other: &CustomIdent) -> bool { self.name.0 == other.0 } /// Checks that this selector matches the ident and all pseudo classes are /// present in the provided flags. #[inline] pub fn matches(&self, name: &CustomIdent, flags: PagePseudoClassFlags) -> bool { self.ident_matches(name) && self.flags_match(flags) } /// Checks that all pseudo classes in this selector are present in the /// provided flags. /// /// Equivalent to, but may be more efficient than: /// /// ``` /// match_specificity(flags).is_some() /// ``` pub fn flags_match(&self, flags: PagePseudoClassFlags) -> bool { self.pseudos.iter().all(|pc| flags.contains_class(pc)) } /// Implements specificity calculation for a page selector given a set of /// page pseudo-classes to match with. /// If this selector includes any pseudo-classes that are not in the flags, /// then this will return None. /// /// To fit the specificity calculation into a 32-bit value, this limits the /// maximum count of :first and :blank to 32767, and the maximum count of /// :left and :right to 65535. /// /// https://drafts.csswg.org/css-page-3/#cascading-and-page-context pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option { let mut g: usize = 0; let mut h: usize = 0; for pc in self.pseudos.iter() { if !flags.contains_class(pc) { return None; } match pc { PagePseudoClass::First | PagePseudoClass::Blank => g += 1, PagePseudoClass::Left | PagePseudoClass::Right => h += 1, } } Some(selector_specificity(g, h, !self.name.0.is_empty())) } } impl ToCss for PageSelector { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { self.name.to_css(dest)?; for pc in self.pseudos.iter() { dest.write_str(pc.to_str())?; } Ok(()) } } fn parse_page_name<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { let s = input.expect_ident()?; Ok(AtomIdent::from(&**s)) } impl Parse for PageSelector { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let name = input.try_parse(parse_page_name); let mut pseudos = PagePseudoClasses::default(); while let Ok(pc) = input.try_parse(PagePseudoClass::parse) { pseudos.push(pc); } // If the result was empty, then we didn't get a selector. let name = match name { Ok(name) => name, Err(..) if !pseudos.is_empty() => AtomIdent::new(atom!("")), Err(err) => return Err(err), }; Ok(PageSelector { name, pseudos }) } } /// A list of [`@page`][page selectors] /// /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors #[derive(Clone, Debug, Default, MallocSizeOf, ToCss, ToShmem)] #[css(comma)] pub struct PageSelectors(#[css(iterable)] pub Box<[PageSelector]>); impl PageSelectors { /// Creates a new PageSelectors from a Vec, as from parse_comma_separated #[inline] pub fn new(s: Vec) -> Self { PageSelectors(s.into()) } /// Returns true iff there are any page selectors #[inline] pub fn is_empty(&self) -> bool { self.as_slice().is_empty() } /// Get the underlying PageSelector data as a slice #[inline] pub fn as_slice(&self) -> &[PageSelector] { &*self.0 } } impl Parse for PageSelectors { fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { Ok(PageSelectors::new(input.parse_comma_separated(|i| { PageSelector::parse(context, i) })?)) } } /// A [`@page`][page] rule. /// /// This implements only a limited subset of the CSS /// 2.2 syntax. /// /// [page]: https://drafts.csswg.org/css2/page.html#page-box /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors #[derive(Clone, Debug, ToShmem)] pub struct PageRule { /// Selectors of the page-rule pub selectors: PageSelectors, /// Nested rules. pub rules: Arc>, /// The declaration block this page rule contains. pub block: Arc>, /// The source position this rule was found at. pub source_location: SourceLocation, } impl PageRule { /// Measure heap usage. #[cfg(feature = "gecko")] pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { // Measurement of other fields may be added later. self.rules.unconditional_shallow_size_of(ops) + self.rules.read_with(guard).size_of(guard, ops) + self.block.unconditional_shallow_size_of(ops) + self.block.read_with(guard).size_of(ops) + self.selectors.size_of(ops) } /// Computes the specificity of this page rule when matched with flags. /// /// Computing this value has linear-complexity with the size of the /// selectors, so the caller should usually call this once and cache the /// result. /// /// Returns None if the flags do not match this page rule. /// /// The return type is ordered by page-rule specificity. pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option { if self.selectors.is_empty() { // A page-rule with no selectors matches all pages, but with the // lowest possible specificity. return Some(selector_specificity(0, 0, false)); } let mut specificity = None; for s in self.selectors.0.iter().map(|s| s.match_specificity(flags)) { specificity = s.max(specificity); } specificity } } impl ToCssWithGuard for PageRule { /// Serialization of PageRule is not specced, adapted from steps for StyleRule. fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { // https://drafts.csswg.org/cssom/#serialize-a-css-rule dest.write_str("@page ")?; if !self.selectors.is_empty() { self.selectors.to_css(&mut CssWriter::new(dest))?; dest.write_char(' ')?; } style_or_page_rule_to_css(Some(&self.rules), &self.block, guard, dest) } } impl DeepCloneWithLock for PageRule { fn deep_clone_with_lock( &self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard, ) -> Self { let rules = self.rules.read_with(&guard); PageRule { selectors: self.selectors.clone(), block: Arc::new(lock.wrap(self.block.read_with(&guard).clone())), rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))), source_location: self.source_location.clone(), } } }