page_rule.rs (12792B)
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 9 use crate::derives::*; 10 use crate::parser::{Parse, ParserContext}; 11 use crate::properties::PropertyDeclarationBlock; 12 use crate::shared_lock::{ 13 DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, 14 }; 15 use crate::stylesheets::{style_or_page_rule_to_css, CssRules}; 16 use crate::values::{AtomIdent, CustomIdent}; 17 use cssparser::{match_ignore_ascii_case, Parser, SourceLocation, Token}; 18 #[cfg(feature = "gecko")] 19 use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; 20 use servo_arc::Arc; 21 use smallvec::SmallVec; 22 use std::fmt::{self, Write}; 23 use style_traits::{CssStringWriter, CssWriter, ParseError, ToCss}; 24 25 macro_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 64 page_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 83 bitflags! { 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 104 impl 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 122 type 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)] 128 pub 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] 150 fn 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 { 0x80000000 } else { 0 }; 154 h + g + f 155 } 156 157 impl PageSelector { 158 /// Checks if the ident matches a page-name's ident. 159 /// 160 /// This does not take pseudo selectors into account. 161 #[inline] 162 pub fn ident_matches(&self, other: &CustomIdent) -> bool { 163 self.name.0 == other.0 164 } 165 166 /// Checks that this selector matches the ident and all pseudo classes are 167 /// present in the provided flags. 168 #[inline] 169 pub fn matches(&self, name: &CustomIdent, flags: PagePseudoClassFlags) -> bool { 170 self.ident_matches(name) && self.flags_match(flags) 171 } 172 173 /// Checks that all pseudo classes in this selector are present in the 174 /// provided flags. 175 /// 176 /// Equivalent to, but may be more efficient than: 177 /// 178 /// ``` 179 /// match_specificity(flags).is_some() 180 /// ``` 181 pub fn flags_match(&self, flags: PagePseudoClassFlags) -> bool { 182 self.pseudos.iter().all(|pc| flags.contains_class(pc)) 183 } 184 185 /// Implements specificity calculation for a page selector given a set of 186 /// page pseudo-classes to match with. 187 /// If this selector includes any pseudo-classes that are not in the flags, 188 /// then this will return None. 189 /// 190 /// To fit the specificity calculation into a 32-bit value, this limits the 191 /// maximum count of :first and :blank to 32767, and the maximum count of 192 /// :left and :right to 65535. 193 /// 194 /// https://drafts.csswg.org/css-page-3/#cascading-and-page-context 195 pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> { 196 let mut g: usize = 0; 197 let mut h: usize = 0; 198 for pc in self.pseudos.iter() { 199 if !flags.contains_class(pc) { 200 return None; 201 } 202 match pc { 203 PagePseudoClass::First | PagePseudoClass::Blank => g += 1, 204 PagePseudoClass::Left | PagePseudoClass::Right => h += 1, 205 } 206 } 207 Some(selector_specificity(g, h, !self.name.0.is_empty())) 208 } 209 } 210 211 impl ToCss for PageSelector { 212 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 213 where 214 W: Write, 215 { 216 self.name.to_css(dest)?; 217 for pc in self.pseudos.iter() { 218 dest.write_str(pc.to_str())?; 219 } 220 Ok(()) 221 } 222 } 223 224 fn parse_page_name<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AtomIdent, ParseError<'i>> { 225 let s = input.expect_ident()?; 226 Ok(AtomIdent::from(&**s)) 227 } 228 229 impl Parse for PageSelector { 230 fn parse<'i, 't>( 231 _context: &ParserContext, 232 input: &mut Parser<'i, 't>, 233 ) -> Result<Self, ParseError<'i>> { 234 let name = input.try_parse(parse_page_name); 235 let mut pseudos = PagePseudoClasses::default(); 236 while let Ok(pc) = input.try_parse(PagePseudoClass::parse) { 237 pseudos.push(pc); 238 } 239 // If the result was empty, then we didn't get a selector. 240 let name = match name { 241 Ok(name) => name, 242 Err(..) if !pseudos.is_empty() => AtomIdent::new(atom!("")), 243 Err(err) => return Err(err), 244 }; 245 Ok(PageSelector { name, pseudos }) 246 } 247 } 248 249 /// A list of [`@page`][page selectors] 250 /// 251 /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors 252 #[derive(Clone, Debug, Default, MallocSizeOf, ToCss, ToShmem)] 253 #[css(comma)] 254 pub struct PageSelectors(#[css(iterable)] pub Box<[PageSelector]>); 255 256 impl PageSelectors { 257 /// Creates a new PageSelectors from a Vec, as from parse_comma_separated 258 #[inline] 259 pub fn new(s: Vec<PageSelector>) -> Self { 260 PageSelectors(s.into()) 261 } 262 /// Returns true iff there are any page selectors 263 #[inline] 264 pub fn is_empty(&self) -> bool { 265 self.as_slice().is_empty() 266 } 267 /// Get the underlying PageSelector data as a slice 268 #[inline] 269 pub fn as_slice(&self) -> &[PageSelector] { 270 &*self.0 271 } 272 } 273 274 impl Parse for PageSelectors { 275 fn parse<'i, 't>( 276 context: &ParserContext, 277 input: &mut Parser<'i, 't>, 278 ) -> Result<Self, ParseError<'i>> { 279 Ok(PageSelectors::new(input.parse_comma_separated(|i| { 280 PageSelector::parse(context, i) 281 })?)) 282 } 283 } 284 285 /// A [`@page`][page] rule. 286 /// 287 /// This implements only a limited subset of the CSS 288 /// 2.2 syntax. 289 /// 290 /// [page]: https://drafts.csswg.org/css2/page.html#page-box 291 /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors 292 #[derive(Clone, Debug, ToShmem)] 293 pub struct PageRule { 294 /// Selectors of the page-rule 295 pub selectors: PageSelectors, 296 /// Nested rules. 297 pub rules: Arc<Locked<CssRules>>, 298 /// The declaration block this page rule contains. 299 pub block: Arc<Locked<PropertyDeclarationBlock>>, 300 /// The source position this rule was found at. 301 pub source_location: SourceLocation, 302 } 303 304 impl PageRule { 305 /// Measure heap usage. 306 #[cfg(feature = "gecko")] 307 pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { 308 // Measurement of other fields may be added later. 309 self.rules.unconditional_shallow_size_of(ops) 310 + self.rules.read_with(guard).size_of(guard, ops) 311 + self.block.unconditional_shallow_size_of(ops) 312 + self.block.read_with(guard).size_of(ops) 313 + self.selectors.size_of(ops) 314 } 315 /// Computes the specificity of this page rule when matched with flags. 316 /// 317 /// Computing this value has linear-complexity with the size of the 318 /// selectors, so the caller should usually call this once and cache the 319 /// result. 320 /// 321 /// Returns None if the flags do not match this page rule. 322 /// 323 /// The return type is ordered by page-rule specificity. 324 pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> { 325 if self.selectors.is_empty() { 326 // A page-rule with no selectors matches all pages, but with the 327 // lowest possible specificity. 328 return Some(selector_specificity(0, 0, false)); 329 } 330 let mut specificity = None; 331 for s in self.selectors.0.iter().map(|s| s.match_specificity(flags)) { 332 specificity = s.max(specificity); 333 } 334 specificity 335 } 336 } 337 338 impl ToCssWithGuard for PageRule { 339 /// Serialization of PageRule is not specced, adapted from steps for StyleRule. 340 fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { 341 // https://drafts.csswg.org/cssom/#serialize-a-css-rule 342 dest.write_str("@page ")?; 343 if !self.selectors.is_empty() { 344 self.selectors.to_css(&mut CssWriter::new(dest))?; 345 dest.write_char(' ')?; 346 } 347 style_or_page_rule_to_css(Some(&self.rules), &self.block, guard, dest) 348 } 349 } 350 351 impl DeepCloneWithLock for PageRule { 352 fn deep_clone_with_lock(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self { 353 let rules = self.rules.read_with(&guard); 354 PageRule { 355 selectors: self.selectors.clone(), 356 block: Arc::new(lock.wrap(self.block.read_with(&guard).clone())), 357 rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))), 358 source_location: self.source_location.clone(), 359 } 360 } 361 }