369 lines
12 KiB
Rust
369 lines
12 KiB
Rust
/* 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<Self, ParseError<'i>> {
|
|
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<u32> {
|
|
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<W>(&self, dest: &mut CssWriter<W>) -> 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<AtomIdent, ParseError<'i>> {
|
|
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<Self, ParseError<'i>> {
|
|
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<PageSelector>) -> 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<Self, ParseError<'i>> {
|
|
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<Locked<CssRules>>,
|
|
/// The declaration block this page rule contains.
|
|
pub block: Arc<Locked<PropertyDeclarationBlock>>,
|
|
/// 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<u32> {
|
|
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(),
|
|
}
|
|
}
|
|
}
|