diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /servo/components/style/stylesheets | |
parent | Initial commit. (diff) | |
download | firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.tar.xz firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/components/style/stylesheets')
24 files changed, 7451 insertions, 0 deletions
diff --git a/servo/components/style/stylesheets/cascading_at_rule.rs b/servo/components/style/stylesheets/cascading_at_rule.rs new file mode 100644 index 0000000000..b23b0720fe --- /dev/null +++ b/servo/components/style/stylesheets/cascading_at_rule.rs @@ -0,0 +1,70 @@ +/* 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/. */ + +//! Cascading at-rule types and traits + +use crate::stylesheets::Origin; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// Computes the cascade precedence as according to +/// <http://dev.w3.org/csswg/css-cascade/#cascade-origin> +#[inline] +fn cascade_precendence(origin: Origin, important: bool) -> u8 { + match (origin, important) { + (Origin::UserAgent, true) => 1, + (Origin::User, true) => 2, + (Origin::Author, true) => 3, + (Origin::Author, false) => 4, + (Origin::User, false) => 5, + (Origin::UserAgent, false) => 6, + } +} + +/// Cascading rule descriptor implementation. +/// This is only used for at-rules which can cascade. These are @viewport and +/// @page, although we don't currently implement @page as such. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct DescriptorDeclaration<T> { + /// Origin of the declaration + pub origin: Origin, + /// Declaration value + pub descriptor: T, + /// Indicates the presence of a !important property. + pub important: bool, +} + +impl<T> DescriptorDeclaration<T> { + #[allow(missing_docs)] + pub fn new(origin: Origin, descriptor: T, important: bool) -> Self { + Self { + origin, + descriptor, + important, + } + } + /// Returns true iff self is equal or higher precedence to the other. + pub fn higher_or_equal_precendence(&self, other: &Self) -> bool { + let self_precedence = cascade_precendence(self.origin, self.important); + let other_precedence = cascade_precendence(other.origin, other.important); + + self_precedence <= other_precedence + } +} + +impl<T> ToCss for DescriptorDeclaration<T> +where + T: ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.descriptor.to_css(dest)?; + if self.important { + dest.write_str(" !important")?; + } + dest.write_char(';') + } +} diff --git a/servo/components/style/stylesheets/container_rule.rs b/servo/components/style/stylesheets/container_rule.rs new file mode 100644 index 0000000000..f9d488b9b4 --- /dev/null +++ b/servo/components/style/stylesheets/container_rule.rs @@ -0,0 +1,632 @@ +/* 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 [`@container`][container] rule. +//! +//! [container]: https://drafts.csswg.org/css-contain-3/#container-rule + +use crate::computed_value_flags::ComputedValueFlags; +use crate::dom::TElement; +use crate::logical_geometry::{LogicalSize, WritingMode}; +use crate::media_queries::Device; +use crate::parser::ParserContext; +use crate::properties::ComputedValues; +use crate::queries::condition::KleeneValue; +use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; +use crate::queries::values::Orientation; +use crate::queries::{FeatureType, QueryCondition}; +use crate::shared_lock::{ + DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, +}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use crate::values::computed::{CSSPixelLength, ContainerType, Context, Ratio}; +use crate::values::specified::ContainerName; +use app_units::Au; +use cssparser::{Parser, SourceLocation}; +use euclid::default::Size2D; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A container rule. +#[derive(Debug, ToShmem)] +pub struct ContainerRule { + /// The container query and name. + pub condition: Arc<ContainerCondition>, + /// The nested rules inside the block. + pub rules: Arc<Locked<CssRules>>, + /// The source position where this rule was found. + pub source_location: SourceLocation, +} + +impl ContainerRule { + /// Returns the query condition. + pub fn query_condition(&self) -> &QueryCondition { + &self.condition.condition + } + + /// Returns the query name filter. + pub fn container_name(&self) -> &ContainerName { + &self.condition.name + } + + /// 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) + } +} + +impl DeepCloneWithLock for ContainerRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(guard); + Self { + condition: self.condition.clone(), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + source_location: self.source_location.clone(), + } + } +} + +impl ToCssWithGuard for ContainerRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@container ")?; + { + let mut writer = CssWriter::new(dest); + if !self.condition.name.is_none() { + self.condition.name.to_css(&mut writer)?; + writer.write_char(' ')?; + } + self.condition.condition.to_css(&mut writer)?; + } + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +/// A container condition and filter, combined. +#[derive(Debug, ToShmem, ToCss)] +pub struct ContainerCondition { + #[css(skip_if = "ContainerName::is_none")] + name: ContainerName, + condition: QueryCondition, + #[css(skip)] + flags: FeatureFlags, +} + +/// The result of a successful container query lookup. +pub struct ContainerLookupResult<E> { + /// The relevant container. + pub element: E, + /// The sizing / writing-mode information of the container. + pub info: ContainerInfo, + /// The style of the element. + pub style: Arc<ComputedValues>, +} + +fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags { + match ty_ { + ContainerType::Size => FeatureFlags::all_container_axes(), + ContainerType::InlineSize => { + let physical_axis = if wm.is_vertical() { + FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS + } else { + FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS + }; + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis + }, + ContainerType::Normal => FeatureFlags::empty(), + } +} + +enum TraversalResult<T> { + InProgress, + StopTraversal, + Done(T), +} + +fn traverse_container<E, F, R>( + mut e: E, + originating_element_style: Option<&ComputedValues>, + evaluator: F, +) -> Option<(E, R)> +where + E: TElement, + F: Fn(E, Option<&ComputedValues>) -> TraversalResult<R>, +{ + if originating_element_style.is_some() { + match evaluator(e, originating_element_style) { + TraversalResult::InProgress => {}, + TraversalResult::StopTraversal => return None, + TraversalResult::Done(result) => return Some((e, result)), + } + } + while let Some(element) = e.traversal_parent() { + match evaluator(element, None) { + TraversalResult::InProgress => {}, + TraversalResult::StopTraversal => return None, + TraversalResult::Done(result) => return Some((element, result)), + } + e = element; + } + + None +} + +impl ContainerCondition { + /// Parse a container condition. + pub fn parse<'a>( + context: &ParserContext, + input: &mut Parser<'a, '_>, + ) -> Result<Self, ParseError<'a>> { + let name = input + .try_parse(|input| ContainerName::parse_for_query(context, input)) + .ok() + .unwrap_or_else(ContainerName::none); + let condition = QueryCondition::parse(context, input, FeatureType::Container)?; + let flags = condition.cumulative_flags(); + Ok(Self { + name, + condition, + flags, + }) + } + + fn valid_container_info<E>( + &self, + potential_container: E, + originating_element_style: Option<&ComputedValues>, + ) -> TraversalResult<ContainerLookupResult<E>> + where + E: TElement, + { + let data; + let style = match originating_element_style { + Some(s) => s, + None => { + data = match potential_container.borrow_data() { + Some(d) => d, + None => return TraversalResult::InProgress, + }; + &**data.styles.primary() + }, + }; + let wm = style.writing_mode; + let box_style = style.get_box(); + + // Filter by container-type. + let container_type = box_style.clone_container_type(); + let available_axes = container_type_axes(container_type, wm); + if !available_axes.contains(self.flags.container_axes()) { + return TraversalResult::InProgress; + } + + // Filter by container-name. + let container_name = box_style.clone_container_name(); + for filter_name in self.name.0.iter() { + if !container_name.0.contains(filter_name) { + return TraversalResult::InProgress; + } + } + + let size = potential_container.query_container_size(&box_style.clone_display()); + let style = style.to_arc(); + TraversalResult::Done(ContainerLookupResult { + element: potential_container, + info: ContainerInfo { size, wm }, + style, + }) + } + + /// Performs container lookup for a given element. + pub fn find_container<E>( + &self, + e: E, + originating_element_style: Option<&ComputedValues>, + ) -> Option<ContainerLookupResult<E>> + where + E: TElement, + { + match traverse_container( + e, + originating_element_style, + |element, originating_element_style| { + self.valid_container_info(element, originating_element_style) + }, + ) { + Some((_, result)) => Some(result), + None => None, + } + } + + /// Tries to match a container query condition for a given element. + pub(crate) fn matches<E>( + &self, + device: &Device, + element: E, + originating_element_style: Option<&ComputedValues>, + invalidation_flags: &mut ComputedValueFlags, + ) -> KleeneValue + where + E: TElement, + { + let result = self.find_container(element, originating_element_style); + let (container, info) = match result { + Some(r) => (Some(r.element), Some((r.info, r.style))), + None => (None, None), + }; + // Set up the lookup for the container in question, as the condition may be using container query lengths. + let size_query_container_lookup = ContainerSizeQuery::for_option_element(container, None); + Context::for_container_query_evaluation( + device, + info, + size_query_container_lookup, + |context| { + let matches = self.condition.matches(context); + if context + .style() + .flags() + .contains(ComputedValueFlags::USES_VIEWPORT_UNITS) + { + // TODO(emilio): Might need something similar to improve + // invalidation of font relative container-query lengths. + invalidation_flags + .insert(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES); + } + matches + }, + ) + } +} + +/// Information needed to evaluate an individual container query. +#[derive(Copy, Clone)] +pub struct ContainerInfo { + size: Size2D<Option<Au>>, + wm: WritingMode, +} + +impl ContainerInfo { + fn size(&self) -> Option<Size2D<Au>> { + Some(Size2D::new(self.size.width?, self.size.height?)) + } +} + +fn eval_width(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new(info.size.width?.to_f32_px())) +} + +fn eval_height(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new(info.size.height?.to_f32_px())) +} + +fn eval_inline_size(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new( + LogicalSize::from_physical(info.wm, info.size) + .inline? + .to_f32_px(), + )) +} + +fn eval_block_size(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new( + LogicalSize::from_physical(info.wm, info.size) + .block? + .to_f32_px(), + )) +} + +fn eval_aspect_ratio(context: &Context) -> Option<Ratio> { + let info = context.container_info.as_ref()?; + Some(Ratio::new( + info.size.width?.0 as f32, + info.size.height?.0 as f32, + )) +} + +fn eval_orientation(context: &Context, value: Option<Orientation>) -> KleeneValue { + let size = match context.container_info.as_ref().and_then(|info| info.size()) { + Some(size) => size, + None => return KleeneValue::Unknown, + }; + KleeneValue::from(Orientation::eval(size, value)) +} + +/// https://drafts.csswg.org/css-contain-3/#container-features +/// +/// TODO: Support style queries, perhaps. +pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_width), + FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS, + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_height), + FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS, + ), + feature!( + atom!("inline-size"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_inline_size), + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS, + ), + feature!( + atom!("block-size"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_block_size), + FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS, + ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::OptionalNumberRatio(eval_aspect_ratio), + // XXX from_bits_truncate is const, but the pipe operator isn't, so this + // works around it. + FeatureFlags::from_bits_truncate( + FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() | + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits() + ), + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + FeatureFlags::from_bits_truncate( + FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() | + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits() + ), + ), +]; + +/// Result of a container size query, signifying the hypothetical containment boundary in terms of physical axes. +/// Defined by up to two size containers. Queries on logical axes are resolved with respect to the querying +/// element's writing mode. +#[derive(Copy, Clone, Default)] +pub struct ContainerSizeQueryResult { + width: Option<Au>, + height: Option<Au>, +} + +impl ContainerSizeQueryResult { + fn get_viewport_size(context: &Context) -> Size2D<Au> { + use crate::values::specified::ViewportVariant; + context.viewport_size_for_viewport_unit_resolution(ViewportVariant::Small) + } + + fn get_logical_viewport_size(context: &Context) -> LogicalSize<Au> { + LogicalSize::from_physical( + context.builder.writing_mode, + Self::get_viewport_size(context), + ) + } + + /// Get the inline-size of the query container. + pub fn get_container_inline_size(&self, context: &Context) -> Au { + if context.builder.writing_mode.is_horizontal() { + if let Some(w) = self.width { + return w; + } + } else { + if let Some(h) = self.height { + return h; + } + } + Self::get_logical_viewport_size(context).inline + } + + /// Get the block-size of the query container. + pub fn get_container_block_size(&self, context: &Context) -> Au { + if context.builder.writing_mode.is_horizontal() { + self.get_container_height(context) + } else { + self.get_container_width(context) + } + } + + /// Get the width of the query container. + pub fn get_container_width(&self, context: &Context) -> Au { + if let Some(w) = self.width { + return w; + } + Self::get_viewport_size(context).width + } + + /// Get the height of the query container. + pub fn get_container_height(&self, context: &Context) -> Au { + if let Some(h) = self.height { + return h; + } + Self::get_viewport_size(context).height + } + + // Merge the result of a subsequent lookup, preferring the initial result. + fn merge(self, new_result: Self) -> Self { + let mut result = self; + if let Some(width) = new_result.width { + result.width.get_or_insert(width); + } + if let Some(height) = new_result.height { + result.height.get_or_insert(height); + } + result + } + + fn is_complete(&self) -> bool { + self.width.is_some() && self.height.is_some() + } +} + +/// Unevaluated lazy container size query. +pub enum ContainerSizeQuery<'a> { + /// Query prior to evaluation. + NotEvaluated(Box<dyn Fn() -> ContainerSizeQueryResult + 'a>), + /// Cached evaluated result. + Evaluated(ContainerSizeQueryResult), +} + +impl<'a> ContainerSizeQuery<'a> { + fn evaluate_potential_size_container<E>( + e: E, + originating_element_style: Option<&ComputedValues>, + ) -> TraversalResult<ContainerSizeQueryResult> + where + E: TElement, + { + let data; + let style = match originating_element_style { + Some(s) => s, + None => { + data = match e.borrow_data() { + Some(d) => d, + None => return TraversalResult::InProgress, + }; + &**data.styles.primary() + }, + }; + if !style + .flags + .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE) + { + // We know we won't find a size container. + return TraversalResult::StopTraversal; + } + + let wm = style.writing_mode; + let box_style = style.get_box(); + + let container_type = box_style.clone_container_type(); + let size = e.query_container_size(&box_style.clone_display()); + match container_type { + ContainerType::Size => TraversalResult::Done(ContainerSizeQueryResult { + width: size.width, + height: size.height, + }), + ContainerType::InlineSize => { + if wm.is_horizontal() { + TraversalResult::Done(ContainerSizeQueryResult { + width: size.width, + height: None, + }) + } else { + TraversalResult::Done(ContainerSizeQueryResult { + width: None, + height: size.height, + }) + } + }, + ContainerType::Normal => TraversalResult::InProgress, + } + } + + /// Find the query container size for a given element. Meant to be used as a callback for new(). + fn lookup<E>( + element: E, + originating_element_style: Option<&ComputedValues>, + ) -> ContainerSizeQueryResult + where + E: TElement + 'a, + { + match traverse_container( + element, + originating_element_style, + |e, originating_element_style| { + Self::evaluate_potential_size_container(e, originating_element_style) + }, + ) { + Some((container, result)) => { + if result.is_complete() { + result + } else { + // Traverse up from the found size container to see if we can get a complete containment. + result.merge(Self::lookup(container, None)) + } + }, + None => ContainerSizeQueryResult::default(), + } + } + + /// Create a new instance of the container size query for given element, with a deferred lookup callback. + pub fn for_element<E>(element: E, originating_element_style: Option<&'a ComputedValues>) -> Self + where + E: TElement + 'a, + { + let parent; + let data; + let style = match originating_element_style { + Some(s) => Some(s), + None => { + // No need to bother if we're the top element. + parent = match element.traversal_parent() { + Some(parent) => parent, + None => return Self::none(), + }; + data = parent.borrow_data(); + data.as_ref().map(|data| &**data.styles.primary()) + }, + }; + let should_traverse = match style { + Some(style) => style + .flags + .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE), + None => true, // `display: none`, still want to show a correct computed value, so give it a try. + }; + if should_traverse { + return Self::NotEvaluated(Box::new(move || { + Self::lookup(element, originating_element_style) + })); + } + Self::none() + } + + /// Create a new instance, but with optional element. + pub fn for_option_element<E>( + element: Option<E>, + originating_element_style: Option<&'a ComputedValues>, + ) -> Self + where + E: TElement + 'a, + { + if let Some(e) = element { + Self::for_element(e, originating_element_style) + } else { + Self::none() + } + } + + /// Create a query that evaluates to empty, for cases where container size query is not required. + pub fn none() -> Self { + ContainerSizeQuery::Evaluated(ContainerSizeQueryResult::default()) + } + + /// Get the result of the container size query, doing the lookup if called for the first time. + pub fn get(&mut self) -> ContainerSizeQueryResult { + match self { + Self::NotEvaluated(lookup) => { + *self = Self::Evaluated((lookup)()); + match self { + Self::Evaluated(info) => *info, + _ => unreachable!("Just evaluated but not set?"), + } + }, + Self::Evaluated(info) => *info, + } + } +} diff --git a/servo/components/style/stylesheets/counter_style_rule.rs b/servo/components/style/stylesheets/counter_style_rule.rs new file mode 100644 index 0000000000..974b76b806 --- /dev/null +++ b/servo/components/style/stylesheets/counter_style_rule.rs @@ -0,0 +1,7 @@ +/* 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/. */ + +#![allow(missing_docs)] + +pub use crate::counter_style::CounterStyleRuleData as CounterStyleRule; diff --git a/servo/components/style/stylesheets/document_rule.rs b/servo/components/style/stylesheets/document_rule.rs new file mode 100644 index 0000000000..75edab308d --- /dev/null +++ b/servo/components/style/stylesheets/document_rule.rs @@ -0,0 +1,305 @@ +/* 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/. */ + +//! [@document rules](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) +//! initially in CSS Conditional Rules Module Level 3, @document has been postponed to the level 4. +//! We implement the prefixed `@-moz-document`. + +use crate::media_queries::Device; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use crate::values::CssUrl; +use cssparser::{BasicParseErrorKind, Parser, SourceLocation}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +#[derive(Debug, ToShmem)] +/// A @-moz-document rule +pub struct DocumentRule { + /// The parsed condition + pub condition: DocumentCondition, + /// Child rules + pub rules: Arc<Locked<CssRules>>, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl DocumentRule { + /// 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) + } +} + +impl ToCssWithGuard for DocumentRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@-moz-document ")?; + self.condition.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" {")?; + for rule in self.rules.read_with(guard).0.iter() { + dest.write_char(' ')?; + rule.to_css(guard, dest)?; + } + dest.write_str(" }") + } +} + +impl DeepCloneWithLock for DocumentRule { + /// Deep clones this DocumentRule. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(guard); + DocumentRule { + condition: self.condition.clone(), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + source_location: self.source_location.clone(), + } + } +} + +/// The kind of media document that the rule will match. +#[derive(Clone, Copy, Debug, Parse, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum MediaDocumentKind { + All, + Plugin, + Image, + Video, +} + +/// A matching function for a `@document` rule's condition. +#[derive(Clone, Debug, ToCss, ToShmem)] +pub enum DocumentMatchingFunction { + /// Exact URL matching function. It evaluates to true whenever the + /// URL of the document being styled is exactly the URL given. + Url(CssUrl), + /// URL prefix matching function. It evaluates to true whenever the + /// URL of the document being styled has the argument to the + /// function as an initial substring (which is true when the two + /// strings are equal). When the argument is the empty string, + /// it evaluates to true for all documents. + #[css(function)] + UrlPrefix(String), + /// Domain matching function. It evaluates to true whenever the URL + /// of the document being styled has a host subcomponent and that + /// host subcomponent is exactly the argument to the ‘domain()’ + /// function or a final substring of the host component is a + /// period (U+002E) immediately followed by the argument to the + /// ‘domain()’ function. + #[css(function)] + Domain(String), + /// Regular expression matching function. It evaluates to true + /// whenever the regular expression matches the entirety of the URL + /// of the document being styled. + #[css(function)] + Regexp(String), + /// Matching function for a media document. + #[css(function)] + MediaDocument(MediaDocumentKind), + /// Matching function for a plain-text document. + #[css(function)] + PlainTextDocument(()), + /// Matching function for a document that can be observed by other content + /// documents. + #[css(function)] + UnobservableDocument(()), +} + +macro_rules! parse_quoted_or_unquoted_string { + ($input:ident, $url_matching_function:expr) => { + $input.parse_nested_block(|input| { + let start = input.position(); + input + .parse_entirely(|input| { + let string = input.expect_string()?; + Ok($url_matching_function(string.as_ref().to_owned())) + }) + .or_else(|_: ParseError| { + while let Ok(_) = input.next() {} + Ok($url_matching_function(input.slice_from(start).to_string())) + }) + }) + }; +} + +impl DocumentMatchingFunction { + /// Parse a URL matching function for a`@document` rule's condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(url) = input.try_parse(|input| CssUrl::parse(context, input)) { + return Ok(DocumentMatchingFunction::Url(url)); + } + + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + match_ignore_ascii_case! { &function, + "url-prefix" => { + parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::UrlPrefix) + }, + "domain" => { + parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::Domain) + }, + "regexp" => { + input.parse_nested_block(|input| { + Ok(DocumentMatchingFunction::Regexp( + input.expect_string()?.as_ref().to_owned(), + )) + }) + }, + "media-document" => { + input.parse_nested_block(|input| { + let kind = MediaDocumentKind::parse(input)?; + Ok(DocumentMatchingFunction::MediaDocument(kind)) + }) + }, + + "plain-text-document" => { + input.parse_nested_block(|input| { + input.expect_exhausted()?; + Ok(DocumentMatchingFunction::PlainTextDocument(())) + }) + }, + + "unobservable-document" => { + input.parse_nested_block(|input| { + input.expect_exhausted()?; + Ok(DocumentMatchingFunction::UnobservableDocument(())) + }) + }, + + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + } + + #[cfg(feature = "gecko")] + /// Evaluate a URL matching function. + pub fn evaluate(&self, device: &Device) -> bool { + use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation; + use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction; + use nsstring::nsCStr; + + let func = match *self { + DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL, + DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix, + DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain, + DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp, + DocumentMatchingFunction::MediaDocument(_) => { + GeckoDocumentMatchingFunction::MediaDocument + }, + DocumentMatchingFunction::PlainTextDocument(..) => { + GeckoDocumentMatchingFunction::PlainTextDocument + }, + DocumentMatchingFunction::UnobservableDocument(..) => { + GeckoDocumentMatchingFunction::UnobservableDocument + }, + }; + + let pattern = nsCStr::from(match *self { + DocumentMatchingFunction::Url(ref url) => url.as_str(), + DocumentMatchingFunction::UrlPrefix(ref pat) | + DocumentMatchingFunction::Domain(ref pat) | + DocumentMatchingFunction::Regexp(ref pat) => pat, + DocumentMatchingFunction::MediaDocument(kind) => match kind { + MediaDocumentKind::All => "all", + MediaDocumentKind::Image => "image", + MediaDocumentKind::Plugin => "plugin", + MediaDocumentKind::Video => "video", + }, + DocumentMatchingFunction::PlainTextDocument(()) | + DocumentMatchingFunction::UnobservableDocument(()) => "", + }); + unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) } + } + + #[cfg(not(feature = "gecko"))] + /// Evaluate a URL matching function. + pub fn evaluate(&self, _: &Device) -> bool { + false + } +} + +/// A `@document` rule's condition. +/// +/// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document> +/// +/// The `@document` rule's condition is written as a comma-separated list of +/// URL matching functions, and the condition evaluates to true whenever any +/// one of those functions evaluates to true. +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct DocumentCondition(#[css(iterable)] Vec<DocumentMatchingFunction>); + +impl DocumentCondition { + /// Parse a document condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let conditions = + input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?; + + let condition = DocumentCondition(conditions); + if !condition.allowed_in(context) { + return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid("-moz-document".into()))); + } + Ok(condition) + } + + /// Evaluate a document condition. + pub fn evaluate(&self, device: &Device) -> bool { + self.0 + .iter() + .any(|url_matching_function| url_matching_function.evaluate(device)) + } + + #[cfg(feature = "servo")] + fn allowed_in(&self, _: &ParserContext) -> bool { + false + } + + #[cfg(feature = "gecko")] + fn allowed_in(&self, context: &ParserContext) -> bool { + use static_prefs::pref; + + if context.in_ua_or_chrome_sheet() { + return true; + } + + if pref!("layout.css.moz-document.content.enabled") { + return true; + } + + // Allow a single url-prefix() for compatibility. + // + // See bug 1446470 and dependencies. + if self.0.len() != 1 { + return false; + } + + // NOTE(emilio): This technically allows url-prefix("") too, but... + match self.0[0] { + DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(), + _ => false, + } + } +} diff --git a/servo/components/style/stylesheets/font_face_rule.rs b/servo/components/style/stylesheets/font_face_rule.rs new file mode 100644 index 0000000000..78f3b338b2 --- /dev/null +++ b/servo/components/style/stylesheets/font_face_rule.rs @@ -0,0 +1,7 @@ +/* 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/. */ + +#![allow(missing_docs)] + +pub use crate::font_face::FontFaceRuleData as FontFaceRule; diff --git a/servo/components/style/stylesheets/font_feature_values_rule.rs b/servo/components/style/stylesheets/font_feature_values_rule.rs new file mode 100644 index 0000000000..06016ec2bd --- /dev/null +++ b/servo/components/style/stylesheets/font_feature_values_rule.rs @@ -0,0 +1,490 @@ +/* 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/. */ + +//! The [`@font-feature-values`][font-feature-values] at-rule. +//! +//! [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule + +use crate::error_reporting::ContextualParseError; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::bindings::Gecko_AppendFeatureValueHashEntry; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs::{self, gfxFontFeatureValueSet, nsTArray}; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRuleType; +use crate::values::computed::font::FamilyName; +use crate::values::serialize_atom_identifier; +use crate::Atom; +use cssparser::{ + AtRuleParser, BasicParseErrorKind, CowRcStr, DeclarationParser, Parser, ParserState, + QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, Token, +}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A @font-feature-values block declaration. +/// It is `<ident>: <integer>+`. +/// This struct can take 3 value types. +/// - `SingleValue` is to keep just one unsigned integer value. +/// - `PairValues` is to keep one or two unsigned integer values. +/// - `VectorValues` is to keep a list of unsigned integer values. +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FFVDeclaration<T> { + /// An `<ident>` for declaration name. + pub name: Atom, + /// An `<integer>+` for declaration value. + pub value: T, +} + +impl<T: ToCss> ToCss for FFVDeclaration<T> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.name, dest)?; + dest.write_str(": ")?; + self.value.to_css(dest)?; + dest.write_char(';') + } +} + +/// A trait for @font-feature-values rule to gecko values conversion. +#[cfg(feature = "gecko")] +pub trait ToGeckoFontFeatureValues { + /// Sets the equivalent of declaration to gecko `nsTArray<u32>` array. + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>); +} + +/// A @font-feature-values block declaration value that keeps one value. +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub struct SingleValue(pub u32); + +impl Parse for SingleValue { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SingleValue, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { + int_value: Some(v), .. + } if v >= 0 => Ok(SingleValue(v as u32)), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +#[cfg(feature = "gecko")] +impl ToGeckoFontFeatureValues for SingleValue { + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) { + unsafe { + array.set_len_pod(1); + } + array[0] = self.0 as u32; + } +} + +/// A @font-feature-values block declaration value that keeps one or two values. +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub struct PairValues(pub u32, pub Option<u32>); + +impl Parse for PairValues { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<PairValues, ParseError<'i>> { + let location = input.current_source_location(); + let first = match *input.next()? { + Token::Number { + int_value: Some(a), .. + } if a >= 0 => a as u32, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + let location = input.current_source_location(); + match input.next() { + Ok(&Token::Number { + int_value: Some(b), .. + }) if b >= 0 => Ok(PairValues(first, Some(b as u32))), + // It can't be anything other than number. + Ok(t) => Err(location.new_unexpected_token_error(t.clone())), + // It can be just one value. + Err(_) => Ok(PairValues(first, None)), + } + } +} + +#[cfg(feature = "gecko")] +impl ToGeckoFontFeatureValues for PairValues { + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) { + let len = if self.1.is_some() { 2 } else { 1 }; + + unsafe { + array.set_len_pod(len); + } + array[0] = self.0 as u32; + if let Some(second) = self.1 { + array[1] = second as u32; + }; + } +} + +/// A @font-feature-values block declaration value that keeps a list of values. +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub struct VectorValues(#[css(iterable)] pub Vec<u32>); + +impl Parse for VectorValues { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<VectorValues, ParseError<'i>> { + let mut vec = vec![]; + loop { + let location = input.current_source_location(); + match input.next() { + Ok(&Token::Number { + int_value: Some(a), .. + }) if a >= 0 => { + vec.push(a as u32); + }, + // It can't be anything other than number. + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(_) => break, + } + } + + if vec.len() == 0 { + return Err(input.new_error(BasicParseErrorKind::EndOfInput)); + } + + Ok(VectorValues(vec)) + } +} + +#[cfg(feature = "gecko")] +impl ToGeckoFontFeatureValues for VectorValues { + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) { + array.assign_from_iter_pod(self.0.iter().map(|v| *v)); + } +} + +/// Parses a list of `FamilyName`s. +pub fn parse_family_name_list<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Vec<FamilyName>, ParseError<'i>> { + input + .parse_comma_separated(|i| FamilyName::parse(context, i)) + .map_err(|e| e.into()) +} + +/// @font-feature-values inside block parser. Parses a list of `FFVDeclaration`. +/// (`<ident>: <integer>+`) +struct FFVDeclarationsParser<'a, 'b: 'a, T: 'a> { + context: &'a ParserContext<'b>, + declarations: &'a mut Vec<FFVDeclaration<T>>, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i, T> AtRuleParser<'i> for FFVDeclarationsParser<'a, 'b, T> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i, T> QualifiedRuleParser<'i> for FFVDeclarationsParser<'a, 'b, T> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i, T> DeclarationParser<'i> for FFVDeclarationsParser<'a, 'b, T> +where + T: Parse, +{ + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let value = input.parse_entirely(|i| T::parse(self.context, i))?; + let new = FFVDeclaration { + name: Atom::from(&*name), + value, + }; + update_or_push(&mut self.declarations, new); + Ok(()) + } +} + +impl<'a, 'b, 'i, T> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for FFVDeclarationsParser<'a, 'b, T> +where + T: Parse, +{ + fn parse_declarations(&self) -> bool { + true + } + fn parse_qualified(&self) -> bool { + false + } +} + +macro_rules! font_feature_values_blocks { + ( + blocks = [ + $( #[$doc: meta] $name: tt $ident: ident / $ident_camel: ident / $gecko_enum: ident: $ty: ty, )* + ] + ) => { + /// The [`@font-feature-values`][font-feature-values] at-rule. + /// + /// [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule + #[derive(Clone, Debug, PartialEq, ToShmem)] + pub struct FontFeatureValuesRule { + /// Font family list for @font-feature-values rule. + /// Family names cannot contain generic families. FamilyName + /// also accepts only non-generic names. + pub family_names: Vec<FamilyName>, + $( + #[$doc] + pub $ident: Vec<FFVDeclaration<$ty>>, + )* + /// The line and column of the rule's source code. + pub source_location: SourceLocation, + } + + impl FontFeatureValuesRule { + /// Creates an empty FontFeatureValuesRule with given location and family name list. + fn new(family_names: Vec<FamilyName>, location: SourceLocation) -> Self { + FontFeatureValuesRule { + family_names: family_names, + $( + $ident: vec![], + )* + source_location: location, + } + } + + /// Parses a `FontFeatureValuesRule`. + pub fn parse( + context: &ParserContext, + input: &mut Parser, + family_names: Vec<FamilyName>, + location: SourceLocation, + ) -> Self { + let mut rule = FontFeatureValuesRule::new(family_names, location); + let mut parser = FontFeatureValuesRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(result) = iter.next() { + if let Err((error, slice)) = result { + let location = error.location; + let error = ContextualParseError::UnsupportedRule(slice, error); + context.log_css_error(location, error); + } + } + rule + } + + /// Prints inside of `@font-feature-values` block. + pub fn value_to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + $( + if self.$ident.len() > 0 { + dest.write_str(concat!("@", $name, " {\n"))?; + let iter = self.$ident.iter(); + for val in iter { + val.to_css(dest)?; + dest.write_str("\n")? + } + dest.write_str("}\n")? + } + )* + Ok(()) + } + + /// Returns length of all at-rules. + pub fn len(&self) -> usize { + let mut len = 0; + $( + len += self.$ident.len(); + )* + len + } + + /// Convert to Gecko gfxFontFeatureValueSet. + #[cfg(feature = "gecko")] + pub fn set_at_rules(&self, dest: *mut gfxFontFeatureValueSet) { + for ref family in self.family_names.iter() { + let family = family.name.to_ascii_lowercase(); + $( + if self.$ident.len() > 0 { + for val in self.$ident.iter() { + let array = unsafe { + Gecko_AppendFeatureValueHashEntry( + dest, + family.as_ptr(), + structs::$gecko_enum, + val.name.as_ptr() + ) + }; + unsafe { + val.value.to_gecko_font_feature_values(&mut *array); + } + } + } + )* + } + } + } + + impl ToCssWithGuard for FontFeatureValuesRule { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@font-feature-values ")?; + self.family_names.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" {\n")?; + self.value_to_css(&mut CssWriter::new(dest))?; + dest.write_char('}') + } + } + + /// Updates with new value if same `ident` exists, otherwise pushes to the vector. + fn update_or_push<T>(vec: &mut Vec<FFVDeclaration<T>>, element: FFVDeclaration<T>) { + if let Some(item) = vec.iter_mut().find(|item| item.name == element.name) { + item.value = element.value; + } else { + vec.push(element); + } + } + + /// Keeps the information about block type like @swash, @styleset etc. + enum BlockType { + $( + $ident_camel, + )* + } + + /// Parser for `FontFeatureValuesRule`. Parses all blocks + /// <feature-type> { + /// <feature-value-declaration-list> + /// } + /// <feature-type> = @stylistic | @historical-forms | @styleset | + /// @character-variant | @swash | @ornaments | @annotation + struct FontFeatureValuesRuleParser<'a> { + context: &'a ParserContext<'a>, + rule: &'a mut FontFeatureValuesRule, + } + + /// Default methods reject all qualified rules. + impl<'a, 'i> QualifiedRuleParser<'i> for FontFeatureValuesRuleParser<'a> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; + } + + impl<'a, 'i> AtRuleParser<'i> for FontFeatureValuesRuleParser<'a> { + type Prelude = BlockType; + type AtRule = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<BlockType, ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name => Ok(BlockType::$ident_camel), + )* + _ => Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), + } + } + + fn parse_block<'t>( + &mut self, + prelude: BlockType, + _: &ParserState, + input: &mut Parser<'i, 't> + ) -> Result<Self::AtRule, ParseError<'i>> { + debug_assert!(self.context.rule_types().contains(CssRuleType::FontFeatureValues)); + match prelude { + $( + BlockType::$ident_camel => { + let mut parser = FFVDeclarationsParser { + context: &self.context, + declarations: &mut self.rule.$ident, + }; + + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedKeyframePropertyDeclaration( + slice, error + ); + self.context.log_css_error(location, error); + } + } + }, + )* + } + + Ok(()) + } + } + + impl<'a, 'i> DeclarationParser<'i> for FontFeatureValuesRuleParser<'a> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + } + + impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> for FontFeatureValuesRuleParser<'a> { + fn parse_declarations(&self) -> bool { false } + fn parse_qualified(&self) -> bool { true } + } + } +} + +font_feature_values_blocks! { + blocks = [ + #[doc = "A @swash blocksck. \ + Specifies a feature name that will work with the swash() \ + functional notation of font-variant-alternates."] + "swash" swash / Swash / NS_FONT_VARIANT_ALTERNATES_SWASH: SingleValue, + + #[doc = "A @stylistic block. \ + Specifies a feature name that will work with the annotation() \ + functional notation of font-variant-alternates."] + "stylistic" stylistic / Stylistic / NS_FONT_VARIANT_ALTERNATES_STYLISTIC: SingleValue, + + #[doc = "A @ornaments block. \ + Specifies a feature name that will work with the ornaments() ] \ + functional notation of font-variant-alternates."] + "ornaments" ornaments / Ornaments / NS_FONT_VARIANT_ALTERNATES_ORNAMENTS: SingleValue, + + #[doc = "A @annotation block. \ + Specifies a feature name that will work with the stylistic() \ + functional notation of font-variant-alternates."] + "annotation" annotation / Annotation / NS_FONT_VARIANT_ALTERNATES_ANNOTATION: SingleValue, + + #[doc = "A @character-variant block. \ + Specifies a feature name that will work with the styleset() \ + functional notation of font-variant-alternates. The value can be a pair."] + "character-variant" character_variant / CharacterVariant / NS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT: + PairValues, + + #[doc = "A @styleset block. \ + Specifies a feature name that will work with the character-variant() \ + functional notation of font-variant-alternates. The value can be a list."] + "styleset" styleset / Styleset / NS_FONT_VARIANT_ALTERNATES_STYLESET: VectorValues, + ] +} diff --git a/servo/components/style/stylesheets/font_palette_values_rule.rs b/servo/components/style/stylesheets/font_palette_values_rule.rs new file mode 100644 index 0000000000..400d348215 --- /dev/null +++ b/servo/components/style/stylesheets/font_palette_values_rule.rs @@ -0,0 +1,264 @@ +/* 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/. */ + +//! The [`@font-palette-values`][font-palette-values] at-rule. +//! +//! [font-palette-values]: https://drafts.csswg.org/css-fonts/#font-palette-values + +use crate::error_reporting::ContextualParseError; +use crate::gecko_bindings::bindings::Gecko_AppendPaletteValueHashEntry; +use crate::gecko_bindings::bindings::{Gecko_SetFontPaletteBase, Gecko_SetFontPaletteOverride}; +use crate::gecko_bindings::structs::gfx::FontPaletteValueSet; +use crate::gecko_bindings::structs::gfx::FontPaletteValueSet_PaletteValues_kDark; +use crate::gecko_bindings::structs::gfx::FontPaletteValueSet_PaletteValues_kLight; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::font_feature_values_rule::parse_family_name_list; +use crate::values::computed::font::FamilyName; +use crate::values::specified::Color as SpecifiedColor; +use crate::values::specified::NonNegativeInteger; +use crate::values::DashedIdent; +use cssparser::{ + AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, RuleBodyItemParser, + RuleBodyParser, SourceLocation, +}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{Comma, OneOrMoreSeparated}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +#[allow(missing_docs)] +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct FontPaletteOverrideColor { + index: NonNegativeInteger, + color: SpecifiedColor, +} + +impl Parse for FontPaletteOverrideColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontPaletteOverrideColor, ParseError<'i>> { + let index = NonNegativeInteger::parse(context, input)?; + let location = input.current_source_location(); + let color = SpecifiedColor::parse(context, input)?; + // Only absolute colors are accepted here. + if let SpecifiedColor::Absolute { .. } = color { + Ok(FontPaletteOverrideColor { index, color }) + } else { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for FontPaletteOverrideColor { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.index.to_css(dest)?; + dest.write_char(' ')?; + self.color.to_css(dest) + } +} + +impl OneOrMoreSeparated for FontPaletteOverrideColor { + type S = Comma; +} + +impl OneOrMoreSeparated for FamilyName { + type S = Comma; +} + +#[allow(missing_docs)] +#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +pub enum FontPaletteBase { + Light, + Dark, + Index(NonNegativeInteger), +} + +/// The [`@font-palette-values`][font-palette-values] at-rule. +/// +/// [font-palette-values]: https://drafts.csswg.org/css-fonts/#font-palette-values +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontPaletteValuesRule { + /// Palette name. + pub name: DashedIdent, + /// Font family list for @font-palette-values rule. + /// Family names cannot contain generic families. FamilyName + /// also accepts only non-generic names. + pub family_names: Vec<FamilyName>, + /// The base palette. + pub base_palette: Option<FontPaletteBase>, + /// The list of override colors. + pub override_colors: Vec<FontPaletteOverrideColor>, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl FontPaletteValuesRule { + /// Creates an empty FontPaletteValuesRule with given location and name. + fn new(name: DashedIdent, location: SourceLocation) -> Self { + FontPaletteValuesRule { + name, + family_names: vec![], + base_palette: None, + override_colors: vec![], + source_location: location, + } + } + + /// Parses a `FontPaletteValuesRule`. + pub fn parse( + context: &ParserContext, + input: &mut Parser, + name: DashedIdent, + location: SourceLocation, + ) -> Self { + let mut rule = FontPaletteValuesRule::new(name, location); + let mut parser = FontPaletteValuesDeclarationParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = + ContextualParseError::UnsupportedFontPaletteValuesDescriptor(slice, error); + context.log_css_error(location, error); + } + } + rule + } + + /// Prints inside of `@font-palette-values` block. + fn value_to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if !self.family_names.is_empty() { + dest.write_str("font-family: ")?; + self.family_names.to_css(dest)?; + dest.write_str("; ")?; + } + if let Some(base) = &self.base_palette { + dest.write_str("base-palette: ")?; + base.to_css(dest)?; + dest.write_str("; ")?; + } + if !self.override_colors.is_empty() { + dest.write_str("override-colors: ")?; + self.override_colors.to_css(dest)?; + dest.write_str("; ")?; + } + Ok(()) + } + + /// Convert to Gecko FontPaletteValueSet. + pub fn to_gecko_palette_value_set(&self, dest: *mut FontPaletteValueSet) { + for ref family in self.family_names.iter() { + let family = family.name.to_ascii_lowercase(); + let palette_values = unsafe { + Gecko_AppendPaletteValueHashEntry(dest, family.as_ptr(), self.name.0.as_ptr()) + }; + if let Some(base_palette) = &self.base_palette { + unsafe { + Gecko_SetFontPaletteBase( + palette_values, + match &base_palette { + FontPaletteBase::Light => FontPaletteValueSet_PaletteValues_kLight, + FontPaletteBase::Dark => FontPaletteValueSet_PaletteValues_kDark, + FontPaletteBase::Index(i) => i.0.value() as i32, + }, + ); + } + } + for c in &self.override_colors { + if let SpecifiedColor::Absolute(ref absolute) = c.color { + unsafe { + Gecko_SetFontPaletteOverride( + palette_values, + c.index.0.value(), + (&absolute.color) as *const _ as *mut _, + ); + } + } + } + } + } +} + +impl ToCssWithGuard for FontPaletteValuesRule { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@font-palette-values ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + self.value_to_css(&mut CssWriter::new(dest))?; + dest.write_char('}') + } +} + +/// Parser for declarations in `FontPaletteValuesRule`. +struct FontPaletteValuesDeclarationParser<'a> { + context: &'a ParserContext<'a>, + rule: &'a mut FontPaletteValuesRule, +} + +impl<'a, 'i> AtRuleParser<'i> for FontPaletteValuesDeclarationParser<'a> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'i> QualifiedRuleParser<'i> for FontPaletteValuesDeclarationParser<'a> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +fn parse_override_colors<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Vec<FontPaletteOverrideColor>, ParseError<'i>> { + input.parse_comma_separated(|i| FontPaletteOverrideColor::parse(context, i)) +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for FontPaletteValuesDeclarationParser<'a> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + "font-family" => { + self.rule.family_names = parse_family_name_list(self.context, input)? + }, + "base-palette" => { + self.rule.base_palette = Some(input.parse_entirely(|i| FontPaletteBase::parse(self.context, i))?) + }, + "override-colors" => { + self.rule.override_colors = parse_override_colors(self.context, input)? + }, + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } +} + +impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for FontPaletteValuesDeclarationParser<'a> +{ + fn parse_declarations(&self) -> bool { + true + } + fn parse_qualified(&self) -> bool { + false + } +} diff --git a/servo/components/style/stylesheets/import_rule.rs b/servo/components/style/stylesheets/import_rule.rs new file mode 100644 index 0000000000..e96134b436 --- /dev/null +++ b/servo/components/style/stylesheets/import_rule.rs @@ -0,0 +1,301 @@ +/* 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/. */ + +//! The [`@import`][import] at-rule. +//! +//! [import]: https://drafts.csswg.org/css-cascade-3/#at-import + +use crate::media_queries::MediaList; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{ + DeepCloneParams, DeepCloneWithLock, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, +}; +use crate::str::CssStringWriter; +use crate::stylesheets::{ + layer_rule::LayerName, supports_rule::SupportsCondition, CssRule, CssRuleType, + StylesheetInDocument, +}; +use crate::values::CssUrl; +use cssparser::{Parser, SourceLocation}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +/// A sheet that is held from an import rule. +#[cfg(feature = "gecko")] +#[derive(Debug)] +pub enum ImportSheet { + /// A bonafide stylesheet. + Sheet(crate::gecko::data::GeckoStyleSheet), + + /// An @import created while parsing off-main-thread, whose Gecko sheet has + /// yet to be created and attached. + Pending, + + /// An @import created with a false <supports-condition>, so will never be fetched. + Refused, +} + +#[cfg(feature = "gecko")] +impl ImportSheet { + /// Creates a new ImportSheet from a GeckoStyleSheet. + pub fn new(sheet: crate::gecko::data::GeckoStyleSheet) -> Self { + ImportSheet::Sheet(sheet) + } + + /// Creates a pending ImportSheet for a load that has not started yet. + pub fn new_pending() -> Self { + ImportSheet::Pending + } + + /// Creates a refused ImportSheet for a load that will not happen. + pub fn new_refused() -> Self { + ImportSheet::Refused + } + + /// Returns a reference to the GeckoStyleSheet in this ImportSheet, if it + /// exists. + pub fn as_sheet(&self) -> Option<&crate::gecko::data::GeckoStyleSheet> { + match *self { + ImportSheet::Sheet(ref s) => { + debug_assert!(!s.hack_is_null()); + if s.hack_is_null() { + return None; + } + Some(s) + }, + ImportSheet::Refused | ImportSheet::Pending => None, + } + } + + /// Returns the media list for this import rule. + pub fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + self.as_sheet().and_then(|s| s.media(guard)) + } + + /// Returns the rule list for this import rule. + pub fn rules<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a [CssRule] { + match self.as_sheet() { + Some(s) => s.rules(guard), + None => &[], + } + } +} + +#[cfg(feature = "gecko")] +impl DeepCloneWithLock for ImportSheet { + fn deep_clone_with_lock( + &self, + _lock: &SharedRwLock, + _guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + use crate::gecko::data::GeckoStyleSheet; + use crate::gecko_bindings::bindings; + match *self { + ImportSheet::Sheet(ref s) => { + let clone = unsafe { + bindings::Gecko_StyleSheet_Clone(s.raw() as *const _, params.reference_sheet) + }; + ImportSheet::Sheet(unsafe { GeckoStyleSheet::from_addrefed(clone) }) + }, + ImportSheet::Pending => ImportSheet::Pending, + ImportSheet::Refused => ImportSheet::Refused, + } + } +} + +/// A sheet that is held from an import rule. +#[cfg(feature = "servo")] +#[derive(Debug)] +pub struct ImportSheet(pub ::servo_arc::Arc<crate::stylesheets::Stylesheet>); + +#[cfg(feature = "servo")] +impl ImportSheet { + /// Returns the media list for this import rule. + pub fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + self.0.media(guard) + } + + /// Returns the rules for this import rule. + pub fn rules<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a [CssRule] { + self.0.rules() + } +} + +#[cfg(feature = "servo")] +impl DeepCloneWithLock for ImportSheet { + fn deep_clone_with_lock( + &self, + _lock: &SharedRwLock, + _guard: &SharedRwLockReadGuard, + _params: &DeepCloneParams, + ) -> Self { + use servo_arc::Arc; + + ImportSheet(Arc::new((&*self.0).clone())) + } +} + +/// The layer specified in an import rule (can be none, anonymous, or named). +#[derive(Debug, Clone)] +pub enum ImportLayer { + /// No layer specified + None, + + /// Anonymous layer (`layer`) + Anonymous, + + /// Named layer (`layer(name)`) + Named(LayerName), +} + +/// The supports condition in an import rule. +#[derive(Debug, Clone)] +pub struct ImportSupportsCondition { + /// The supports condition. + pub condition: SupportsCondition, + + /// If the import is enabled, from the result of the import condition. + pub enabled: bool, +} + +impl ToCss for ImportLayer { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + ImportLayer::None => Ok(()), + ImportLayer::Anonymous => dest.write_str("layer"), + ImportLayer::Named(ref name) => { + dest.write_str("layer(")?; + name.to_css(dest)?; + dest.write_char(')') + }, + } + } +} + +/// The [`@import`][import] at-rule. +/// +/// [import]: https://drafts.csswg.org/css-cascade-3/#at-import +#[derive(Debug)] +pub struct ImportRule { + /// The `<url>` this `@import` rule is loading. + pub url: CssUrl, + + /// The stylesheet is always present. However, in the case of gecko async + /// parsing, we don't actually have a Gecko sheet at first, and so the + /// ImportSheet just has stub behavior until it appears. + pub stylesheet: ImportSheet, + + /// A <supports-condition> for the rule. + pub supports: Option<ImportSupportsCondition>, + + /// A `layer()` function name. + pub layer: ImportLayer, + + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl ImportRule { + /// Parses the layer() / layer / supports() part of the import header, as per + /// https://drafts.csswg.org/css-cascade-5/#at-import: + /// + /// [ layer | layer(<layer-name>) ]? + /// [ supports([ <supports-condition> | <declaration> ]) ]? + /// + /// We do this here so that the import preloader can look at this without having to parse the + /// whole import rule or parse the media query list or what not. + pub fn parse_layer_and_supports<'i, 't>( + input: &mut Parser<'i, 't>, + context: &mut ParserContext, + ) -> (ImportLayer, Option<ImportSupportsCondition>) { + let layer = if input + .try_parse(|input| input.expect_ident_matching("layer")) + .is_ok() + { + ImportLayer::Anonymous + } else { + input + .try_parse(|input| { + input.expect_function_matching("layer")?; + input + .parse_nested_block(|input| LayerName::parse(context, input)) + .map(|name| ImportLayer::Named(name)) + }) + .ok() + .unwrap_or(ImportLayer::None) + }; + + let supports = if !static_prefs::pref!("layout.css.import-supports.enabled") { + None + } else { + input + .try_parse(SupportsCondition::parse_for_import) + .map(|condition| { + let enabled = context + .nest_for_rule(CssRuleType::Style, |context| condition.eval(context)); + ImportSupportsCondition { condition, enabled } + }) + .ok() + }; + + (layer, supports) + } +} + +impl ToShmem for ImportRule { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + Err(String::from( + "ToShmem failed for ImportRule: cannot handle imported style sheets", + )) + } +} + +impl DeepCloneWithLock for ImportRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + ImportRule { + url: self.url.clone(), + stylesheet: self.stylesheet.deep_clone_with_lock(lock, guard, params), + supports: self.supports.clone(), + layer: self.layer.clone(), + source_location: self.source_location.clone(), + } + } +} + +impl ToCssWithGuard for ImportRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@import ")?; + self.url.to_css(&mut CssWriter::new(dest))?; + + if !matches!(self.layer, ImportLayer::None) { + dest.write_char(' ')?; + self.layer.to_css(&mut CssWriter::new(dest))?; + } + + if let Some(ref supports) = self.supports { + dest.write_str(" supports(")?; + supports.condition.to_css(&mut CssWriter::new(dest))?; + dest.write_char(')')?; + } + + if let Some(media) = self.stylesheet.media(guard) { + if !media.is_empty() { + dest.write_char(' ')?; + media.to_css(&mut CssWriter::new(dest))?; + } + } + + dest.write_char(';') + } +} diff --git a/servo/components/style/stylesheets/keyframes_rule.rs b/servo/components/style/stylesheets/keyframes_rule.rs new file mode 100644 index 0000000000..6e5016080e --- /dev/null +++ b/servo/components/style/stylesheets/keyframes_rule.rs @@ -0,0 +1,691 @@ +/* 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/. */ + +//! Keyframes: https://drafts.csswg.org/css-animations/#keyframes + +use crate::error_reporting::ContextualParseError; +use crate::parser::ParserContext; +use crate::properties::longhands::animation_composition::single_value::SpecifiedValue as SpecifiedComposition; +use crate::properties::longhands::transition_timing_function::single_value::SpecifiedValue as SpecifiedTimingFunction; +use crate::properties::LonghandIdSet; +use crate::properties::{Importance, PropertyDeclaration}; +use crate::properties::{LonghandId, PropertyDeclarationBlock, PropertyId}; +use crate::properties::{PropertyDeclarationId, SourcePropertyDeclaration}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, SharedRwLock, SharedRwLockReadGuard}; +use crate::shared_lock::{Locked, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::rule_parser::VendorPrefix; +use crate::stylesheets::{CssRuleType, StylesheetContents}; +use crate::values::{serialize_percentage, KeyframesName}; +use cssparser::{ + parse_one_rule, AtRuleParser, CowRcStr, DeclarationParser, Parser, ParserInput, ParserState, + QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, Token, +}; +use servo_arc::Arc; +use std::borrow::Cow; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss}; + +/// A [`@keyframes`][keyframes] rule. +/// +/// [keyframes]: https://drafts.csswg.org/css-animations/#keyframes +#[derive(Debug, ToShmem)] +pub struct KeyframesRule { + /// The name of the current animation. + pub name: KeyframesName, + /// The keyframes specified for this CSS rule. + pub keyframes: Vec<Arc<Locked<Keyframe>>>, + /// Vendor prefix type the @keyframes has. + pub vendor_prefix: Option<VendorPrefix>, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for KeyframesRule { + // Serialization of KeyframesRule is not specced. + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@keyframes ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" {")?; + let iter = self.keyframes.iter(); + for lock in iter { + dest.write_str("\n")?; + let keyframe = lock.read_with(&guard); + keyframe.to_css(guard, dest)?; + } + dest.write_str("\n}") + } +} + +impl KeyframesRule { + /// Returns the index of the last keyframe that matches the given selector. + /// If the selector is not valid, or no keyframe is found, returns None. + /// + /// Related spec: + /// <https://drafts.csswg.org/css-animations-1/#interface-csskeyframesrule-findrule> + pub fn find_rule(&self, guard: &SharedRwLockReadGuard, selector: &str) -> Option<usize> { + let mut input = ParserInput::new(selector); + if let Ok(selector) = Parser::new(&mut input).parse_entirely(KeyframeSelector::parse) { + for (i, keyframe) in self.keyframes.iter().enumerate().rev() { + if keyframe.read_with(guard).selector == selector { + return Some(i); + } + } + } + None + } +} + +impl DeepCloneWithLock for KeyframesRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + KeyframesRule { + name: self.name.clone(), + keyframes: self + .keyframes + .iter() + .map(|x| { + Arc::new( + lock.wrap(x.read_with(guard).deep_clone_with_lock(lock, guard, params)), + ) + }) + .collect(), + vendor_prefix: self.vendor_prefix.clone(), + source_location: self.source_location.clone(), + } + } +} + +/// A number from 0 to 1, indicating the percentage of the animation when this +/// keyframe should run. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)] +pub struct KeyframePercentage(pub f32); + +impl ::std::cmp::Ord for KeyframePercentage { + #[inline] + fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { + // We know we have a number from 0 to 1, so unwrap() here is safe. + self.0.partial_cmp(&other.0).unwrap() + } +} + +impl ::std::cmp::Eq for KeyframePercentage {} + +impl ToCss for KeyframePercentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_percentage(self.0, dest) + } +} + +impl KeyframePercentage { + /// Trivially constructs a new `KeyframePercentage`. + #[inline] + pub fn new(value: f32) -> KeyframePercentage { + debug_assert!(value >= 0. && value <= 1.); + KeyframePercentage(value) + } + + fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<KeyframePercentage, ParseError<'i>> { + let token = input.next()?.clone(); + match token { + Token::Ident(ref identifier) if identifier.as_ref().eq_ignore_ascii_case("from") => { + Ok(KeyframePercentage::new(0.)) + }, + Token::Ident(ref identifier) if identifier.as_ref().eq_ignore_ascii_case("to") => { + Ok(KeyframePercentage::new(1.)) + }, + Token::Percentage { + unit_value: percentage, + .. + } if percentage >= 0. && percentage <= 1. => Ok(KeyframePercentage::new(percentage)), + _ => Err(input.new_unexpected_token_error(token)), + } + } +} + +/// A keyframes selector is a list of percentages or from/to symbols, which are +/// converted at parse time to percentages. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct KeyframeSelector(#[css(iterable)] Vec<KeyframePercentage>); + +impl KeyframeSelector { + /// Return the list of percentages this selector contains. + #[inline] + pub fn percentages(&self) -> &[KeyframePercentage] { + &self.0 + } + + /// A dummy public function so we can write a unit test for this. + pub fn new_for_unit_testing(percentages: Vec<KeyframePercentage>) -> KeyframeSelector { + KeyframeSelector(percentages) + } + + /// Parse a keyframe selector from CSS input. + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + input + .parse_comma_separated(KeyframePercentage::parse) + .map(KeyframeSelector) + } +} + +/// A keyframe. +#[derive(Debug, ToShmem)] +pub struct Keyframe { + /// The selector this keyframe was specified from. + pub selector: KeyframeSelector, + + /// The declaration block that was declared inside this keyframe. + /// + /// Note that `!important` rules in keyframes don't apply, but we keep this + /// `Arc` just for convenience. + pub block: Arc<Locked<PropertyDeclarationBlock>>, + + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for Keyframe { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + self.selector.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + self.block.read_with(guard).to_css(dest)?; + dest.write_str(" }")?; + Ok(()) + } +} + +impl Keyframe { + /// Parse a CSS keyframe. + pub fn parse<'i>( + css: &'i str, + parent_stylesheet_contents: &StylesheetContents, + lock: &SharedRwLock, + ) -> Result<Arc<Locked<Self>>, ParseError<'i>> { + let url_data = parent_stylesheet_contents.url_data.read(); + let namespaces = parent_stylesheet_contents.namespaces.read(); + let mut context = ParserContext::new( + parent_stylesheet_contents.origin, + &url_data, + Some(CssRuleType::Keyframe), + ParsingMode::DEFAULT, + parent_stylesheet_contents.quirks_mode, + Cow::Borrowed(&*namespaces), + None, + None, + ); + let mut input = ParserInput::new(css); + let mut input = Parser::new(&mut input); + + let mut declarations = SourcePropertyDeclaration::default(); + let mut rule_parser = KeyframeListParser { + context: &mut context, + shared_lock: &lock, + declarations: &mut declarations, + }; + parse_one_rule(&mut input, &mut rule_parser) + } +} + +impl DeepCloneWithLock for Keyframe { + /// Deep clones this Keyframe. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + _params: &DeepCloneParams, + ) -> Keyframe { + Keyframe { + selector: self.selector.clone(), + block: Arc::new(lock.wrap(self.block.read_with(guard).clone())), + source_location: self.source_location.clone(), + } + } +} + +/// A keyframes step value. This can be a synthetised keyframes animation, that +/// is, one autogenerated from the current computed values, or a list of +/// declarations to apply. +/// +/// TODO: Find a better name for this? +#[derive(Clone, Debug, MallocSizeOf)] +pub enum KeyframesStepValue { + /// A step formed by a declaration block specified by the CSS. + Declarations { + /// The declaration block per se. + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "XXX: Primary ref, measure if DMD says it's worthwhile" + )] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + block: Arc<Locked<PropertyDeclarationBlock>>, + }, + /// A synthetic step computed from the current computed values at the time + /// of the animation. + ComputedValues, +} + +/// A single step from a keyframe animation. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct KeyframesStep { + /// The percentage of the animation duration when this step starts. + pub start_percentage: KeyframePercentage, + /// Declarations that will determine the final style during the step, or + /// `ComputedValues` if this is an autogenerated step. + pub value: KeyframesStepValue, + /// Whether an animation-timing-function declaration exists in the list of + /// declarations. + /// + /// This is used to know when to override the keyframe animation style. + pub declared_timing_function: bool, + /// Whether an animation-composition declaration exists in the list of + /// declarations. + /// + /// This is used to know when to override the keyframe animation style. + pub declared_composition: bool, +} + +impl KeyframesStep { + #[inline] + fn new( + start_percentage: KeyframePercentage, + value: KeyframesStepValue, + guard: &SharedRwLockReadGuard, + ) -> Self { + let mut declared_timing_function = false; + let mut declared_composition = false; + if let KeyframesStepValue::Declarations { ref block } = value { + for prop_decl in block.read_with(guard).declarations().iter() { + match *prop_decl { + PropertyDeclaration::AnimationTimingFunction(..) => { + declared_timing_function = true; + }, + PropertyDeclaration::AnimationComposition(..) => { + declared_composition = true; + }, + _ => continue, + } + // Don't need to continue the loop if both are found. + if declared_timing_function && declared_composition { + break; + } + } + } + + KeyframesStep { + start_percentage, + value, + declared_timing_function, + declared_composition, + } + } + + /// Return specified PropertyDeclaration. + #[inline] + fn get_declared_property<'a>( + &'a self, + guard: &'a SharedRwLockReadGuard, + property: LonghandId, + ) -> Option<&'a PropertyDeclaration> { + match self.value { + KeyframesStepValue::Declarations { ref block } => { + let guard = block.read_with(guard); + let (declaration, _) = guard + .get(PropertyDeclarationId::Longhand(property)) + .unwrap(); + match *declaration { + PropertyDeclaration::CSSWideKeyword(..) => None, + // FIXME: Bug 1710735: Support css variable in @keyframes rule. + PropertyDeclaration::WithVariables(..) => None, + _ => Some(declaration), + } + }, + KeyframesStepValue::ComputedValues => { + panic!("Shouldn't happen to set this property in missing keyframes") + }, + } + } + + /// Return specified TransitionTimingFunction if this KeyframesSteps has + /// 'animation-timing-function'. + pub fn get_animation_timing_function( + &self, + guard: &SharedRwLockReadGuard, + ) -> Option<SpecifiedTimingFunction> { + if !self.declared_timing_function { + return None; + } + + self.get_declared_property(guard, LonghandId::AnimationTimingFunction) + .map(|decl| { + match *decl { + PropertyDeclaration::AnimationTimingFunction(ref value) => { + // Use the first value + value.0[0].clone() + }, + _ => unreachable!("Unexpected PropertyDeclaration"), + } + }) + } + + /// Return CompositeOperation if this KeyframesSteps has 'animation-composition'. + pub fn get_animation_composition( + &self, + guard: &SharedRwLockReadGuard, + ) -> Option<SpecifiedComposition> { + if !self.declared_composition { + return None; + } + + self.get_declared_property(guard, LonghandId::AnimationComposition) + .map(|decl| { + match *decl { + PropertyDeclaration::AnimationComposition(ref value) => { + // Use the first value + value.0[0].clone() + }, + _ => unreachable!("Unexpected PropertyDeclaration"), + } + }) + } +} + +/// This structure represents a list of animation steps computed from the list +/// of keyframes, in order. +/// +/// It only takes into account animable properties. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct KeyframesAnimation { + /// The difference steps of the animation. + pub steps: Vec<KeyframesStep>, + /// The properties that change in this animation. + pub properties_changed: LonghandIdSet, + /// Vendor prefix type the @keyframes has. + pub vendor_prefix: Option<VendorPrefix>, +} + +/// Get all the animated properties in a keyframes animation. +fn get_animated_properties( + keyframes: &[Arc<Locked<Keyframe>>], + guard: &SharedRwLockReadGuard, +) -> LonghandIdSet { + let mut ret = LonghandIdSet::new(); + // NB: declarations are already deduplicated, so we don't have to check for + // it here. + for keyframe in keyframes { + let keyframe = keyframe.read_with(&guard); + let block = keyframe.block.read_with(guard); + // CSS Animations spec clearly defines that properties with !important + // in keyframe rules are invalid and ignored, but it's still ambiguous + // whether we should drop the !important properties or retain the + // properties when they are set via CSSOM. So we assume there might + // be properties with !important in keyframe rules here. + // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824 + for declaration in block.normal_declaration_iter() { + let longhand_id = match declaration.id() { + PropertyDeclarationId::Longhand(id) => id, + _ => continue, + }; + + if longhand_id == LonghandId::Display { + continue; + } + + if !longhand_id.is_animatable() { + continue; + } + + ret.insert(longhand_id); + } + } + + ret +} + +impl KeyframesAnimation { + /// Create a keyframes animation from a given list of keyframes. + /// + /// This will return a keyframe animation with empty steps and + /// properties_changed if the list of keyframes is empty, or there are no + /// animated properties obtained from the keyframes. + /// + /// Otherwise, this will compute and sort the steps used for the animation, + /// and return the animation object. + pub fn from_keyframes( + keyframes: &[Arc<Locked<Keyframe>>], + vendor_prefix: Option<VendorPrefix>, + guard: &SharedRwLockReadGuard, + ) -> Self { + let mut result = KeyframesAnimation { + steps: vec![], + properties_changed: LonghandIdSet::new(), + vendor_prefix, + }; + + if keyframes.is_empty() { + return result; + } + + result.properties_changed = get_animated_properties(keyframes, guard); + if result.properties_changed.is_empty() { + return result; + } + + for keyframe in keyframes { + let keyframe = keyframe.read_with(&guard); + for percentage in keyframe.selector.0.iter() { + result.steps.push(KeyframesStep::new( + *percentage, + KeyframesStepValue::Declarations { + block: keyframe.block.clone(), + }, + guard, + )); + } + } + + // Sort by the start percentage, so we can easily find a frame. + result.steps.sort_by_key(|step| step.start_percentage); + + // Prepend autogenerated keyframes if appropriate. + if result.steps[0].start_percentage.0 != 0. { + result.steps.insert( + 0, + KeyframesStep::new( + KeyframePercentage::new(0.), + KeyframesStepValue::ComputedValues, + guard, + ), + ); + } + + if result.steps.last().unwrap().start_percentage.0 != 1. { + result.steps.push(KeyframesStep::new( + KeyframePercentage::new(1.), + KeyframesStepValue::ComputedValues, + guard, + )); + } + + result + } +} + +/// Parses a keyframes list, like: +/// 0%, 50% { +/// width: 50%; +/// } +/// +/// 40%, 60%, 100% { +/// width: 100%; +/// } +struct KeyframeListParser<'a, 'b> { + context: &'a mut ParserContext<'b>, + shared_lock: &'a SharedRwLock, + declarations: &'a mut SourcePropertyDeclaration, +} + +/// Parses a keyframe list from CSS input. +pub fn parse_keyframe_list<'a>( + context: &mut ParserContext<'a>, + input: &mut Parser, + shared_lock: &SharedRwLock, +) -> Vec<Arc<Locked<Keyframe>>> { + let mut declarations = SourcePropertyDeclaration::default(); + let mut parser = KeyframeListParser { + context, + shared_lock, + declarations: &mut declarations, + }; + RuleBodyParser::new(input, &mut parser) + .filter_map(Result::ok) + .collect() +} + +impl<'a, 'b, 'i> AtRuleParser<'i> for KeyframeListParser<'a, 'b> { + type Prelude = (); + type AtRule = Arc<Locked<Keyframe>>; + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for KeyframeListParser<'a, 'b> { + type Declaration = Arc<Locked<Keyframe>>; + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for KeyframeListParser<'a, 'b> { + type Prelude = KeyframeSelector; + type QualifiedRule = Arc<Locked<Keyframe>>; + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + let start_position = input.position(); + KeyframeSelector::parse(input).map_err(|e| { + let location = e.location; + let error = ContextualParseError::InvalidKeyframeRule( + input.slice_from(start_position), + e.clone(), + ); + self.context.log_css_error(location, error); + e + }) + } + + fn parse_block<'t>( + &mut self, + selector: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::QualifiedRule, ParseError<'i>> { + let mut block = PropertyDeclarationBlock::new(); + let declarations = &mut self.declarations; + self.context + .nest_for_rule(CssRuleType::Keyframe, |context| { + let mut parser = KeyframeDeclarationParser { + context: &context, + declarations, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + match declaration { + Ok(()) => { + block.extend(iter.parser.declarations.drain(), Importance::Normal); + }, + Err((error, slice)) => { + iter.parser.declarations.clear(); + let location = error.location; + let error = + ContextualParseError::UnsupportedKeyframePropertyDeclaration( + slice, error, + ); + context.log_css_error(location, error); + }, + } + // `parse_important` is not called here, `!important` is not allowed in keyframe blocks. + } + }); + Ok(Arc::new(self.shared_lock.wrap(Keyframe { + selector, + block: Arc::new(self.shared_lock.wrap(block)), + source_location: start.source_location(), + }))) + } +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, Arc<Locked<Keyframe>>, StyleParseErrorKind<'i>> + for KeyframeListParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + true + } + fn parse_declarations(&self) -> bool { + false + } +} + +struct KeyframeDeclarationParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + declarations: &'a mut SourcePropertyDeclaration, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for KeyframeDeclarationParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for KeyframeDeclarationParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for KeyframeDeclarationParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let id = match PropertyId::parse(&name, self.context) { + Ok(id) => id, + Err(()) => { + return Err(input.new_custom_error(StyleParseErrorKind::UnknownProperty(name))); + }, + }; + + // TODO(emilio): Shouldn't this use parse_entirely? + PropertyDeclaration::parse_into(self.declarations, id, self.context, input)?; + + // In case there is still unparsed text in the declaration, we should + // roll back. + input.expect_exhausted()?; + + Ok(()) + } +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for KeyframeDeclarationParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} diff --git a/servo/components/style/stylesheets/layer_rule.rs b/servo/components/style/stylesheets/layer_rule.rs new file mode 100644 index 0000000000..3ebe6bb34f --- /dev/null +++ b/servo/components/style/stylesheets/layer_rule.rs @@ -0,0 +1,228 @@ +/* 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 [`@layer`][layer] rule. +//! +//! [layer]: https://drafts.csswg.org/css-cascade-5/#layering + +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::values::AtomIdent; + +use super::CssRules; + +use cssparser::{Parser, SourceLocation, Token}; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// The order of a given layer. We use 16 bits so that we can pack LayerOrder +/// and CascadeLevel in a single 32-bit struct. If we need more bits we can go +/// back to packing CascadeLevel in a single byte as we did before. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)] +pub struct LayerOrder(u16); + +impl LayerOrder { + /// The order of the root layer. + pub const fn root() -> Self { + Self(std::u16::MAX - 1) + } + + /// The order of the style attribute layer. + pub const fn style_attribute() -> Self { + Self(std::u16::MAX) + } + + /// Returns whether this layer is for the style attribute, which behaves + /// differently in terms of !important, see + /// https://github.com/w3c/csswg-drafts/issues/6872 + /// + /// (This is a bit silly, mind-you, but it's needed so that revert-layer + /// behaves correctly). + #[inline] + pub fn is_style_attribute_layer(&self) -> bool { + *self == Self::style_attribute() + } + + /// The first cascade layer order. + pub const fn first() -> Self { + Self(0) + } + + /// Increment the cascade layer order. + #[inline] + pub fn inc(&mut self) { + if self.0 != std::u16::MAX - 1 { + self.0 += 1; + } + } +} + +/// A `<layer-name>`: https://drafts.csswg.org/css-cascade-5/#typedef-layer-name +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] +pub struct LayerName(pub SmallVec<[AtomIdent; 1]>); + +impl LayerName { + /// Returns an empty layer name (which isn't a valid final state, so caller + /// is responsible to fill up the name before use). + pub fn new_empty() -> Self { + Self(Default::default()) + } + + /// Returns a synthesized name for an anonymous layer. + pub fn new_anonymous() -> Self { + use std::sync::atomic::{AtomicUsize, Ordering}; + static NEXT_ANONYMOUS_LAYER_NAME: AtomicUsize = AtomicUsize::new(0); + + let mut name = SmallVec::new(); + let next_id = NEXT_ANONYMOUS_LAYER_NAME.fetch_add(1, Ordering::Relaxed); + // The parens don't _technically_ prevent conflicts with authors, as + // authors could write escaped parens as part of the identifier, I + // think, but highly reduces the possibility. + name.push(AtomIdent::from(&*format!("-moz-anon-layer({})", next_id))); + + LayerName(name) + } + + /// Returns the names of the layers. That is, for a layer like `foo.bar`, + /// it'd return [foo, bar]. + pub fn layer_names(&self) -> &[AtomIdent] { + &self.0 + } +} + +impl Parse for LayerName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = SmallVec::new(); + result.push(AtomIdent::from(&**input.expect_ident()?)); + loop { + let next_name = input.try_parse(|input| -> Result<AtomIdent, ParseError<'i>> { + match input.next_including_whitespace()? { + Token::Delim('.') => {}, + other => { + let t = other.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + } + + let name = match input.next_including_whitespace()? { + Token::Ident(ref ident) => ident, + other => { + let t = other.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + }; + + Ok(AtomIdent::from(&**name)) + }); + + match next_name { + Ok(name) => result.push(name), + Err(..) => break, + } + } + Ok(LayerName(result)) + } +} + +impl ToCss for LayerName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let mut first = true; + for name in self.0.iter() { + if !first { + dest.write_char('.')?; + } + first = false; + name.to_css(dest)?; + } + Ok(()) + } +} + +#[derive(Debug, ToShmem)] +/// A block `@layer <name>? { ... }` +/// https://drafts.csswg.org/css-cascade-5/#layer-block +pub struct LayerBlockRule { + /// The layer name, or `None` if anonymous. + pub name: Option<LayerName>, + /// The nested rules. + pub rules: Arc<Locked<CssRules>>, + /// The source position where this rule was found. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for LayerBlockRule { + fn to_css( + &self, + guard: &SharedRwLockReadGuard, + dest: &mut crate::str::CssStringWriter, + ) -> fmt::Result { + dest.write_str("@layer")?; + if let Some(ref name) = self.name { + dest.write_char(' ')?; + name.to_css(&mut CssWriter::new(dest))?; + } + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +impl DeepCloneWithLock for LayerBlockRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + Self { + name: self.name.clone(), + rules: Arc::new( + lock.wrap( + self.rules + .read_with(guard) + .deep_clone_with_lock(lock, guard, params), + ), + ), + source_location: self.source_location.clone(), + } + } +} + +/// A statement `@layer <name>, <name>, <name>;` +/// +/// https://drafts.csswg.org/css-cascade-5/#layer-empty +#[derive(Clone, Debug, ToShmem)] +pub struct LayerStatementRule { + /// The list of layers to sort. + pub names: Vec<LayerName>, + /// The source position where this rule was found. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for LayerStatementRule { + fn to_css( + &self, + _: &SharedRwLockReadGuard, + dest: &mut crate::str::CssStringWriter, + ) -> fmt::Result { + let mut writer = CssWriter::new(dest); + writer.write_str("@layer ")?; + let mut first = true; + for name in &*self.names { + if !first { + writer.write_str(", ")?; + } + first = false; + name.to_css(&mut writer)?; + } + writer.write_char(';') + } +} diff --git a/servo/components/style/stylesheets/loader.rs b/servo/components/style/stylesheets/loader.rs new file mode 100644 index 0000000000..f987cf9597 --- /dev/null +++ b/servo/components/style/stylesheets/loader.rs @@ -0,0 +1,31 @@ +/* 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/. */ + +//! The stylesheet loader is the abstraction used to trigger network requests +//! for `@import` rules. + +use crate::media_queries::MediaList; +use crate::parser::ParserContext; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::stylesheets::import_rule::{ImportLayer, ImportRule, ImportSupportsCondition}; +use crate::values::CssUrl; +use cssparser::SourceLocation; +use servo_arc::Arc; + +/// The stylesheet loader is the abstraction used to trigger network requests +/// for `@import` rules. +pub trait StylesheetLoader { + /// Request a stylesheet after parsing a given `@import` rule, and return + /// the constructed `@import` rule. + fn request_stylesheet( + &self, + url: CssUrl, + location: SourceLocation, + context: &ParserContext, + lock: &SharedRwLock, + media: Arc<Locked<MediaList>>, + supports: Option<ImportSupportsCondition>, + layer: ImportLayer, + ) -> Arc<Locked<ImportRule>>; +} diff --git a/servo/components/style/stylesheets/media_rule.rs b/servo/components/style/stylesheets/media_rule.rs new file mode 100644 index 0000000000..cde60a16bf --- /dev/null +++ b/servo/components/style/stylesheets/media_rule.rs @@ -0,0 +1,71 @@ +/* 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/. */ + +//! An [`@media`][media] rule. +//! +//! [media]: https://drafts.csswg.org/css-conditional/#at-ruledef-media + +use crate::media_queries::MediaList; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use cssparser::SourceLocation; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// An [`@media`][media] rule. +/// +/// [media]: https://drafts.csswg.org/css-conditional/#at-ruledef-media +#[derive(Debug, ToShmem)] +pub struct MediaRule { + /// The list of media queries used by this media rule. + pub media_queries: Arc<Locked<MediaList>>, + /// The nested rules to this media rule. + pub rules: Arc<Locked<CssRules>>, + /// The source position where this media rule was found. + pub source_location: SourceLocation, +} + +impl MediaRule { + /// 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) + } +} + +impl ToCssWithGuard for MediaRule { + // Serialization of MediaRule is not specced. + // https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSMediaRule + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@media ")?; + self.media_queries + .read_with(guard) + .to_css(&mut CssWriter::new(dest))?; + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +impl DeepCloneWithLock for MediaRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let media_queries = self.media_queries.read_with(guard); + let rules = self.rules.read_with(guard); + MediaRule { + media_queries: Arc::new(lock.wrap(media_queries.clone())), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + source_location: self.source_location.clone(), + } + } +} diff --git a/servo/components/style/stylesheets/mod.rs b/servo/components/style/stylesheets/mod.rs new file mode 100644 index 0000000000..800ebee12f --- /dev/null +++ b/servo/components/style/stylesheets/mod.rs @@ -0,0 +1,565 @@ +/* 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/. */ + +//! Style sheets and their CSS rules. + +mod cascading_at_rule; +pub mod container_rule; +mod counter_style_rule; +mod document_rule; +mod font_face_rule; +pub mod font_feature_values_rule; +pub mod font_palette_values_rule; +pub mod import_rule; +pub mod keyframes_rule; +pub mod layer_rule; +mod loader; +mod media_rule; +mod namespace_rule; +pub mod origin; +mod page_rule; +mod property_rule; +mod rule_list; +mod rule_parser; +mod rules_iterator; +mod style_rule; +mod stylesheet; +pub mod supports_rule; +pub mod viewport_rule; + +#[cfg(feature = "gecko")] +use crate::gecko_bindings::sugar::refptr::RefCounted; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::{bindings, structs}; +use crate::parser::ParserContext; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use cssparser::{parse_one_rule, Parser, ParserInput}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::borrow::Cow; +use std::fmt; +#[cfg(feature = "gecko")] +use std::mem::{self, ManuallyDrop}; +use style_traits::ParsingMode; +#[cfg(feature = "gecko")] +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +pub use self::container_rule::ContainerRule; +pub use self::counter_style_rule::CounterStyleRule; +pub use self::document_rule::DocumentRule; +pub use self::font_face_rule::FontFaceRule; +pub use self::font_feature_values_rule::FontFeatureValuesRule; +pub use self::font_palette_values_rule::FontPaletteValuesRule; +pub use self::import_rule::ImportRule; +pub use self::keyframes_rule::KeyframesRule; +pub use self::layer_rule::{LayerBlockRule, LayerStatementRule}; +pub use self::loader::StylesheetLoader; +pub use self::media_rule::MediaRule; +pub use self::namespace_rule::NamespaceRule; +pub use self::origin::{Origin, OriginSet, OriginSetIterator, PerOrigin, PerOriginIter}; +pub use self::page_rule::{PageRule, PageSelector, PageSelectors}; +pub use self::property_rule::PropertyRule; +pub use self::rule_list::{CssRules, CssRulesHelpers}; +pub use self::rule_parser::{InsertRuleContext, State, TopLevelRuleParser}; +pub use self::rules_iterator::{AllRules, EffectiveRules}; +pub use self::rules_iterator::{ + EffectiveRulesIterator, NestedRuleIterationCondition, RulesIterator, +}; +pub use self::style_rule::StyleRule; +pub use self::stylesheet::{AllowImportRules, SanitizationData, SanitizationKind}; +pub use self::stylesheet::{DocumentStyleSheet, Namespaces, Stylesheet}; +pub use self::stylesheet::{StylesheetContents, StylesheetInDocument, UserAgentStylesheets}; +pub use self::supports_rule::SupportsRule; +pub use self::viewport_rule::ViewportRule; + +/// The CORS mode used for a CSS load. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +pub enum CorsMode { + /// No CORS mode, so cross-origin loads can be done. + None, + /// Anonymous CORS request. + Anonymous, +} + +/// Extra data that the backend may need to resolve url values. +/// +/// If the usize's lowest bit is 0, then this is a strong reference to a +/// structs::URLExtraData object. +/// +/// Otherwise, shifting the usize's bits the right by one gives the +/// UserAgentStyleSheetID value corresponding to the style sheet whose +/// URLExtraData this is, which is stored in URLExtraData_sShared. We don't +/// hold a strong reference to that object from here, but we rely on that +/// array's objects being held alive until shutdown. +/// +/// We use this packed representation rather than an enum so that +/// `from_ptr_ref` can work. +#[cfg(feature = "gecko")] +#[derive(PartialEq)] +#[repr(C)] +pub struct UrlExtraData(usize); + +/// Extra data that the backend may need to resolve url values. +#[cfg(not(feature = "gecko"))] +pub type UrlExtraData = ::servo_url::ServoUrl; + +#[cfg(feature = "gecko")] +impl Clone for UrlExtraData { + fn clone(&self) -> UrlExtraData { + UrlExtraData::new(self.ptr()) + } +} + +#[cfg(feature = "gecko")] +impl Drop for UrlExtraData { + fn drop(&mut self) { + // No need to release when we have an index into URLExtraData_sShared. + if self.0 & 1 == 0 { + unsafe { + self.as_ref().release(); + } + } + } +} + +#[cfg(feature = "gecko")] +impl ToShmem for UrlExtraData { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + if self.0 & 1 == 0 { + let shared_extra_datas = unsafe { &structs::URLExtraData_sShared }; + let self_ptr = self.as_ref() as *const _ as *mut _; + let sheet_id = shared_extra_datas + .iter() + .position(|r| r.mRawPtr == self_ptr); + let sheet_id = match sheet_id { + Some(id) => id, + None => { + return Err(String::from( + "ToShmem failed for UrlExtraData: expected sheet's URLExtraData to be in \ + URLExtraData::sShared", + )); + }, + }; + Ok(ManuallyDrop::new(UrlExtraData((sheet_id << 1) | 1))) + } else { + Ok(ManuallyDrop::new(UrlExtraData(self.0))) + } + } +} + +#[cfg(feature = "gecko")] +impl UrlExtraData { + /// Create a new UrlExtraData wrapping a pointer to the specified Gecko + /// URLExtraData object. + pub fn new(ptr: *mut structs::URLExtraData) -> UrlExtraData { + unsafe { + (*ptr).addref(); + } + UrlExtraData(ptr as usize) + } + + /// True if this URL scheme is chrome. + #[inline] + pub fn chrome_rules_enabled(&self) -> bool { + self.as_ref().mChromeRulesEnabled + } + + /// Create a reference to this `UrlExtraData` from a reference to pointer. + /// + /// The pointer must be valid and non null. + /// + /// This method doesn't touch refcount. + #[inline] + pub unsafe fn from_ptr_ref(ptr: &*mut structs::URLExtraData) -> &Self { + mem::transmute(ptr) + } + + /// Returns a pointer to the Gecko URLExtraData object. + pub fn ptr(&self) -> *mut structs::URLExtraData { + if self.0 & 1 == 0 { + self.0 as *mut structs::URLExtraData + } else { + unsafe { + let sheet_id = self.0 >> 1; + structs::URLExtraData_sShared[sheet_id].mRawPtr + } + } + } + + fn as_ref(&self) -> &structs::URLExtraData { + unsafe { &*(self.ptr() as *const structs::URLExtraData) } + } +} + +#[cfg(feature = "gecko")] +impl fmt::Debug for UrlExtraData { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + macro_rules! define_debug_struct { + ($struct_name:ident, $gecko_class:ident, $debug_fn:ident) => { + struct $struct_name(*mut structs::$gecko_class); + impl fmt::Debug for $struct_name { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + let mut spec = nsCString::new(); + unsafe { + bindings::$debug_fn(self.0, &mut spec); + } + spec.fmt(formatter) + } + } + }; + } + + define_debug_struct!(DebugURI, nsIURI, Gecko_nsIURI_Debug); + define_debug_struct!( + DebugReferrerInfo, + nsIReferrerInfo, + Gecko_nsIReferrerInfo_Debug + ); + + formatter + .debug_struct("URLExtraData") + .field("chrome_rules_enabled", &self.chrome_rules_enabled()) + .field("base", &DebugURI(self.as_ref().mBaseURI.raw())) + .field( + "referrer", + &DebugReferrerInfo(self.as_ref().mReferrerInfo.raw()), + ) + .finish() + } +} + +// XXX We probably need to figure out whether we should mark Eq here. +// It is currently marked so because properties::UnparsedValue wants Eq. +#[cfg(feature = "gecko")] +impl Eq for UrlExtraData {} + +/// A CSS rule. +/// +/// TODO(emilio): Lots of spec links should be around. +#[derive(Clone, Debug, ToShmem)] +#[allow(missing_docs)] +pub enum CssRule { + // No Charset here, CSSCharsetRule has been removed from CSSOM + // https://drafts.csswg.org/cssom/#changes-from-5-december-2013 + Namespace(Arc<NamespaceRule>), + Import(Arc<Locked<ImportRule>>), + Style(Arc<Locked<StyleRule>>), + Media(Arc<MediaRule>), + Container(Arc<ContainerRule>), + FontFace(Arc<Locked<FontFaceRule>>), + FontFeatureValues(Arc<FontFeatureValuesRule>), + FontPaletteValues(Arc<FontPaletteValuesRule>), + CounterStyle(Arc<Locked<CounterStyleRule>>), + Viewport(Arc<ViewportRule>), + Keyframes(Arc<Locked<KeyframesRule>>), + Supports(Arc<SupportsRule>), + Page(Arc<Locked<PageRule>>), + Property(Arc<PropertyRule>), + Document(Arc<DocumentRule>), + LayerBlock(Arc<LayerBlockRule>), + LayerStatement(Arc<LayerStatementRule>), +} + +impl CssRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + match *self { + // Not all fields are currently fully measured. Extra measurement + // may be added later. + CssRule::Namespace(_) => 0, + + // We don't need to measure ImportRule::stylesheet because we measure + // it on the C++ side in the child list of the ServoStyleSheet. + CssRule::Import(_) => 0, + + CssRule::Style(ref lock) => { + lock.unconditional_shallow_size_of(ops) + lock.read_with(guard).size_of(guard, ops) + }, + CssRule::Media(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + CssRule::Container(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + CssRule::FontFace(_) => 0, + CssRule::FontFeatureValues(_) => 0, + CssRule::FontPaletteValues(_) => 0, + CssRule::CounterStyle(_) => 0, + CssRule::Viewport(_) => 0, + CssRule::Keyframes(_) => 0, + CssRule::Supports(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + CssRule::Page(ref lock) => { + lock.unconditional_shallow_size_of(ops) + lock.read_with(guard).size_of(guard, ops) + }, + CssRule::Property(ref rule) => { + rule.unconditional_shallow_size_of(ops) + rule.size_of(guard, ops) + }, + CssRule::Document(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + // TODO(emilio): Add memory reporting for these rules. + CssRule::LayerBlock(_) | CssRule::LayerStatement(_) => 0, + } + } +} + +/// https://drafts.csswg.org/cssom-1/#dom-cssrule-type +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, PartialEq)] +#[repr(u8)] +pub enum CssRuleType { + // https://drafts.csswg.org/cssom/#the-cssrule-interface + Style = 1, + // Charset = 2, // Historical + Import = 3, + Media = 4, + FontFace = 5, + Page = 6, + // https://drafts.csswg.org/css-animations-1/#interface-cssrule-idl + Keyframes = 7, + Keyframe = 8, + // https://drafts.csswg.org/cssom/#the-cssrule-interface + // Margin = 9, // Not implemented yet. + Namespace = 10, + // https://drafts.csswg.org/css-counter-styles-3/#extentions-to-cssrule-interface + CounterStyle = 11, + // https://drafts.csswg.org/css-conditional-3/#extentions-to-cssrule-interface + Supports = 12, + // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#extentions-to-cssrule-interface + Document = 13, + // https://drafts.csswg.org/css-fonts/#om-fontfeaturevalues + FontFeatureValues = 14, + // https://drafts.csswg.org/css-device-adapt/#css-rule-interface + Viewport = 15, + // After viewport, all rules should return 0 from the API, but we still need + // a constant somewhere. + LayerBlock = 16, + LayerStatement = 17, + Container = 18, + FontPaletteValues = 19, + // 20 is an arbitrary number to use for Property. + Property = 20, +} + +impl CssRuleType { + /// Returns a bit that identifies this rule type. + #[inline] + pub const fn bit(self) -> u32 { + 1 << self as u32 + } +} + +/// Set of rule types. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct CssRuleTypes(u32); + +impl From<CssRuleType> for CssRuleTypes { + fn from(ty: CssRuleType) -> Self { + Self(ty.bit()) + } +} + +impl CssRuleTypes { + /// Returns whether the rule is in the current set. + #[inline] + pub fn contains(self, ty: CssRuleType) -> bool { + self.0 & ty.bit() != 0 + } + + /// Returns all the rules specified in the set. + pub fn bits(self) -> u32 { + self.0 + } + + /// Inserts a rule type into the set. + #[inline] + pub fn insert(&mut self, ty: CssRuleType) { + self.0 |= ty.bit() + } +} + +#[allow(missing_docs)] +pub enum RulesMutateError { + Syntax, + IndexSize, + HierarchyRequest, + InvalidState, +} + +impl CssRule { + /// Returns the CSSOM rule type of this rule. + pub fn rule_type(&self) -> CssRuleType { + match *self { + CssRule::Style(_) => CssRuleType::Style, + CssRule::Import(_) => CssRuleType::Import, + CssRule::Media(_) => CssRuleType::Media, + CssRule::FontFace(_) => CssRuleType::FontFace, + CssRule::FontFeatureValues(_) => CssRuleType::FontFeatureValues, + CssRule::FontPaletteValues(_) => CssRuleType::FontPaletteValues, + CssRule::CounterStyle(_) => CssRuleType::CounterStyle, + CssRule::Keyframes(_) => CssRuleType::Keyframes, + CssRule::Namespace(_) => CssRuleType::Namespace, + CssRule::Viewport(_) => CssRuleType::Viewport, + CssRule::Supports(_) => CssRuleType::Supports, + CssRule::Page(_) => CssRuleType::Page, + CssRule::Property(_) => CssRuleType::Property, + CssRule::Document(_) => CssRuleType::Document, + CssRule::LayerBlock(_) => CssRuleType::LayerBlock, + CssRule::LayerStatement(_) => CssRuleType::LayerStatement, + CssRule::Container(_) => CssRuleType::Container, + } + } + + /// Parse a CSS rule. + /// + /// Returns a parsed CSS rule and the final state of the parser. + /// + /// Input state is None for a nested rule + pub fn parse( + css: &str, + insert_rule_context: InsertRuleContext, + parent_stylesheet_contents: &StylesheetContents, + shared_lock: &SharedRwLock, + state: State, + loader: Option<&dyn StylesheetLoader>, + allow_import_rules: AllowImportRules, + ) -> Result<Self, RulesMutateError> { + let url_data = parent_stylesheet_contents.url_data.read(); + let namespaces = parent_stylesheet_contents.namespaces.read(); + let context = ParserContext::new( + parent_stylesheet_contents.origin, + &url_data, + None, + ParsingMode::DEFAULT, + parent_stylesheet_contents.quirks_mode, + Cow::Borrowed(&*namespaces), + None, + None, + ); + + let mut input = ParserInput::new(css); + let mut input = Parser::new(&mut input); + + // nested rules are in the body state + let mut rule_parser = TopLevelRuleParser { + context, + shared_lock: &shared_lock, + loader, + state, + dom_error: None, + insert_rule_context: Some(insert_rule_context), + allow_import_rules, + declaration_parser_state: Default::default(), + rules: Default::default(), + }; + + match parse_one_rule(&mut input, &mut rule_parser) { + Ok(_) => Ok(rule_parser.rules.pop().unwrap()), + Err(_) => Err(rule_parser.dom_error.unwrap_or(RulesMutateError::Syntax)), + } + } +} + +impl DeepCloneWithLock for CssRule { + /// Deep clones this CssRule. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> CssRule { + match *self { + CssRule::Namespace(ref arc) => CssRule::Namespace(arc.clone()), + CssRule::Import(ref arc) => { + let rule = arc + .read_with(guard) + .deep_clone_with_lock(lock, guard, params); + CssRule::Import(Arc::new(lock.wrap(rule))) + }, + CssRule::Style(ref arc) => { + let rule = arc.read_with(guard); + CssRule::Style(Arc::new( + lock.wrap(rule.deep_clone_with_lock(lock, guard, params)), + )) + }, + CssRule::Container(ref arc) => { + CssRule::Container(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::Media(ref arc) => { + CssRule::Media(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::FontFace(ref arc) => { + let rule = arc.read_with(guard); + CssRule::FontFace(Arc::new(lock.wrap(rule.clone()))) + }, + CssRule::FontFeatureValues(ref arc) => CssRule::FontFeatureValues(arc.clone()), + CssRule::FontPaletteValues(ref arc) => CssRule::FontPaletteValues(arc.clone()), + CssRule::CounterStyle(ref arc) => { + let rule = arc.read_with(guard); + CssRule::CounterStyle(Arc::new(lock.wrap(rule.clone()))) + }, + CssRule::Viewport(ref arc) => CssRule::Viewport(arc.clone()), + CssRule::Keyframes(ref arc) => { + let rule = arc.read_with(guard); + CssRule::Keyframes(Arc::new( + lock.wrap(rule.deep_clone_with_lock(lock, guard, params)), + )) + }, + CssRule::Supports(ref arc) => { + CssRule::Supports(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::Page(ref arc) => { + let rule = arc.read_with(guard); + CssRule::Page(Arc::new( + lock.wrap(rule.deep_clone_with_lock(lock, guard, params)), + )) + }, + CssRule::Property(ref arc) => { + // @property rules are immutable, so we don't need any of the `Locked` + // shenanigans, actually, and can just share the rule. + CssRule::Property(arc.clone()) + }, + CssRule::Document(ref arc) => { + CssRule::Document(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::LayerStatement(ref arc) => CssRule::LayerStatement(arc.clone()), + CssRule::LayerBlock(ref arc) => { + CssRule::LayerBlock(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + } + } +} + +impl ToCssWithGuard for CssRule { + // https://drafts.csswg.org/cssom/#serialize-a-css-rule + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + match *self { + CssRule::Namespace(ref rule) => rule.to_css(guard, dest), + CssRule::Import(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Style(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::FontFace(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::FontFeatureValues(ref rule) => rule.to_css(guard, dest), + CssRule::FontPaletteValues(ref rule) => rule.to_css(guard, dest), + CssRule::CounterStyle(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Viewport(ref rule) => rule.to_css(guard, dest), + CssRule::Keyframes(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Media(ref rule) => rule.to_css(guard, dest), + CssRule::Supports(ref rule) => rule.to_css(guard, dest), + CssRule::Page(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Property(ref rule) => rule.to_css(guard, dest), + CssRule::Document(ref rule) => rule.to_css(guard, dest), + CssRule::LayerBlock(ref rule) => rule.to_css(guard, dest), + CssRule::LayerStatement(ref rule) => rule.to_css(guard, dest), + CssRule::Container(ref rule) => rule.to_css(guard, dest), + } + } +} diff --git a/servo/components/style/stylesheets/namespace_rule.rs b/servo/components/style/stylesheets/namespace_rule.rs new file mode 100644 index 0000000000..ad980b70a8 --- /dev/null +++ b/servo/components/style/stylesheets/namespace_rule.rs @@ -0,0 +1,43 @@ +/* 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/. */ + +//! The `@namespace` at-rule. + +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::{Namespace, Prefix}; +use cssparser::SourceLocation; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A `@namespace` rule. +#[derive(Clone, Debug, PartialEq, ToShmem)] +#[allow(missing_docs)] +pub struct NamespaceRule { + /// The namespace prefix, and `None` if it's the default Namespace + pub prefix: Option<Prefix>, + /// The actual namespace url. + pub url: Namespace, + /// The source location this rule was found at. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for NamespaceRule { + // https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSNamespaceRule + fn to_css( + &self, + _guard: &SharedRwLockReadGuard, + dest_str: &mut CssStringWriter, + ) -> fmt::Result { + let mut dest = CssWriter::new(dest_str); + dest.write_str("@namespace ")?; + if let Some(ref prefix) = self.prefix { + prefix.to_css(&mut dest)?; + dest.write_char(' ')?; + } + dest.write_str("url(")?; + self.url.to_string().to_css(&mut dest)?; + dest.write_str(");") + } +} diff --git a/servo/components/style/stylesheets/origin.rs b/servo/components/style/stylesheets/origin.rs new file mode 100644 index 0000000000..6f8f97b661 --- /dev/null +++ b/servo/components/style/stylesheets/origin.rs @@ -0,0 +1,247 @@ +/* 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/. */ + +//! [CSS cascade origins](https://drafts.csswg.org/css-cascade/#cascading-origins). + +use std::marker::PhantomData; +use std::ops::BitOrAssign; + +/// Each style rule has an origin, which determines where it enters the cascade. +/// +/// <https://drafts.csswg.org/css-cascade/#cascading-origins> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem, PartialOrd, Ord)] +#[repr(u8)] +pub enum Origin { + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user-agent> + UserAgent = 0x1, + + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user> + User = 0x2, + + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-author> + Author = 0x4, +} + +impl Origin { + /// Returns an origin that goes in order for `index`. + /// + /// This is used for iterating across origins. + fn from_index(index: i8) -> Option<Self> { + Some(match index { + 0 => Origin::Author, + 1 => Origin::User, + 2 => Origin::UserAgent, + _ => return None, + }) + } + + fn to_index(self) -> i8 { + match self { + Origin::Author => 0, + Origin::User => 1, + Origin::UserAgent => 2, + } + } + + /// Returns an iterator from this origin, towards all the less specific + /// origins. So for `UserAgent`, it'd iterate through all origins. + #[inline] + pub fn following_including(self) -> OriginSetIterator { + OriginSetIterator { + set: OriginSet::ORIGIN_USER | OriginSet::ORIGIN_AUTHOR | OriginSet::ORIGIN_USER_AGENT, + cur: self.to_index(), + rev: true, + } + } +} + +bitflags! { + /// A set of origins. This is equivalent to Gecko's OriginFlags. + #[derive(Clone, Copy, PartialEq, MallocSizeOf)] + pub struct OriginSet: u8 { + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user-agent> + const ORIGIN_USER_AGENT = Origin::UserAgent as u8; + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user> + const ORIGIN_USER = Origin::User as u8; + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-author> + const ORIGIN_AUTHOR = Origin::Author as u8; + } +} + +impl OriginSet { + /// Returns an iterator over the origins present in this `OriginSet`. + /// + /// See the `OriginSet` documentation for information about the order + /// origins are iterated. + pub fn iter(&self) -> OriginSetIterator { + OriginSetIterator { + set: *self, + cur: 0, + rev: false, + } + } +} + +impl From<Origin> for OriginSet { + fn from(origin: Origin) -> Self { + Self::from_bits_retain(origin as u8) + } +} + +impl BitOrAssign<Origin> for OriginSet { + fn bitor_assign(&mut self, origin: Origin) { + *self |= OriginSet::from(origin); + } +} + +/// Iterates over the origins present in an `OriginSet`, in order from +/// highest priority (author) to lower (user agent). +#[derive(Clone)] +pub struct OriginSetIterator { + set: OriginSet, + cur: i8, + rev: bool, +} + +impl Iterator for OriginSetIterator { + type Item = Origin; + + fn next(&mut self) -> Option<Origin> { + loop { + let origin = Origin::from_index(self.cur)?; + + if self.rev { + self.cur -= 1; + } else { + self.cur += 1; + } + + if self.set.contains(origin.into()) { + return Some(origin); + } + } + } +} + +/// An object that stores a `T` for each origin of the CSS cascade. +#[derive(Debug, Default, MallocSizeOf)] +pub struct PerOrigin<T> { + /// Data for `Origin::UserAgent`. + pub user_agent: T, + + /// Data for `Origin::User`. + pub user: T, + + /// Data for `Origin::Author`. + pub author: T, +} + +impl<T> PerOrigin<T> { + /// Returns a reference to the per-origin data for the specified origin. + #[inline] + pub fn borrow_for_origin(&self, origin: &Origin) -> &T { + match *origin { + Origin::UserAgent => &self.user_agent, + Origin::User => &self.user, + Origin::Author => &self.author, + } + } + + /// Returns a mutable reference to the per-origin data for the specified + /// origin. + #[inline] + pub fn borrow_mut_for_origin(&mut self, origin: &Origin) -> &mut T { + match *origin { + Origin::UserAgent => &mut self.user_agent, + Origin::User => &mut self.user, + Origin::Author => &mut self.author, + } + } + + /// Iterates over references to per-origin extra style data, from highest + /// level (author) to lowest (user agent). + pub fn iter_origins(&self) -> PerOriginIter<T> { + PerOriginIter { + data: &self, + cur: 0, + rev: false, + } + } + + /// Iterates over references to per-origin extra style data, from lowest + /// level (user agent) to highest (author). + pub fn iter_origins_rev(&self) -> PerOriginIter<T> { + PerOriginIter { + data: &self, + cur: 2, + rev: true, + } + } + + /// Iterates over mutable references to per-origin extra style data, from + /// highest level (author) to lowest (user agent). + pub fn iter_mut_origins(&mut self) -> PerOriginIterMut<T> { + PerOriginIterMut { + data: self, + cur: 0, + _marker: PhantomData, + } + } +} + +/// Iterator over `PerOrigin<T>`, from highest level (author) to lowest +/// (user agent). +/// +/// We rely on this specific order for correctly looking up @font-face, +/// @counter-style and @keyframes rules. +pub struct PerOriginIter<'a, T: 'a> { + data: &'a PerOrigin<T>, + cur: i8, + rev: bool, +} + +impl<'a, T> Iterator for PerOriginIter<'a, T> +where + T: 'a, +{ + type Item = (&'a T, Origin); + + fn next(&mut self) -> Option<Self::Item> { + let origin = Origin::from_index(self.cur)?; + + self.cur += if self.rev { -1 } else { 1 }; + + Some((self.data.borrow_for_origin(&origin), origin)) + } +} + +/// Like `PerOriginIter<T>`, but iterates over mutable references to the +/// per-origin data. +/// +/// We must use unsafe code here since it's not possible for the borrow +/// checker to know that we are safely returning a different reference +/// each time from `next()`. +pub struct PerOriginIterMut<'a, T: 'a> { + data: *mut PerOrigin<T>, + cur: i8, + _marker: PhantomData<&'a mut PerOrigin<T>>, +} + +impl<'a, T> Iterator for PerOriginIterMut<'a, T> +where + T: 'a, +{ + type Item = (&'a mut T, Origin); + + fn next(&mut self) -> Option<Self::Item> { + let origin = Origin::from_index(self.cur)?; + + self.cur += 1; + + Some(( + unsafe { (*self.data).borrow_mut_for_origin(&origin) }, + origin, + )) + } +} diff --git a/servo/components/style/stylesheets/page_rule.rs b/servo/components/style/stylesheets/page_rule.rs new file mode 100644 index 0000000000..5cd2458aa2 --- /dev/null +++ b/servo/components/style/stylesheets/page_rule.rs @@ -0,0 +1,145 @@ +/* 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::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::{AtomIdent, CustomIdent}; +use cssparser::{Parser, SourceLocation}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// Type of a single [`@page`][page selector] +/// +/// We do not support pseudo selectors yet. +/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct PageSelector(pub AtomIdent); + +impl PageSelector { + /// Checks if the ident matches a page-name's ident. + /// + /// This does not currently take pseudo selectors into account. + #[inline] + pub fn ident_matches(&self, other: &CustomIdent) -> bool { + self.0 .0 == other.0 + } +} + +impl Parse for PageSelector { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let s = input.expect_ident()?; + Ok(PageSelector(AtomIdent::from(&**s))) + } +} + +/// 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, + /// 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.block.unconditional_shallow_size_of(ops) + + self.block.read_with(guard).size_of(ops) + + self.selectors.size_of(ops) + } +} + +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 { + dest.write_str("@page ")?; + if !self.selectors.is_empty() { + self.selectors.to_css(&mut CssWriter::new(dest))?; + dest.write_char(' ')?; + } + dest.write_str("{ ")?; + let declaration_block = self.block.read_with(guard); + declaration_block.to_css(dest)?; + if !declaration_block.declarations().is_empty() { + dest.write_char(' ')?; + } + dest.write_char('}') + } +} + +impl DeepCloneWithLock for PageRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + _params: &DeepCloneParams, + ) -> Self { + PageRule { + selectors: self.selectors.clone(), + block: Arc::new(lock.wrap(self.block.read_with(&guard).clone())), + source_location: self.source_location.clone(), + } + } +} diff --git a/servo/components/style/stylesheets/property_rule.rs b/servo/components/style/stylesheets/property_rule.rs new file mode 100644 index 0000000000..1d1c1c982e --- /dev/null +++ b/servo/components/style/stylesheets/property_rule.rs @@ -0,0 +1,5 @@ +/* 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/. */ + +pub use crate::properties_and_values::rule::PropertyRuleData as PropertyRule; diff --git a/servo/components/style/stylesheets/rule_list.rs b/servo/components/style/stylesheets/rule_list.rs new file mode 100644 index 0000000000..ab747565ff --- /dev/null +++ b/servo/components/style/stylesheets/rule_list.rs @@ -0,0 +1,198 @@ +/* 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 list of CSS rules. + +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::loader::StylesheetLoader; +use crate::stylesheets::rule_parser::{InsertRuleContext, State}; +use crate::stylesheets::stylesheet::StylesheetContents; +use crate::stylesheets::{AllowImportRules, CssRule, RulesMutateError}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocShallowSizeOf, MallocSizeOfOps}; +use servo_arc::Arc; +use std::fmt::{self, Write}; + +/// A list of CSS rules. +#[derive(Debug, ToShmem)] +pub struct CssRules(pub Vec<CssRule>); + +impl CssRules { + /// Whether this CSS rules is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl DeepCloneWithLock for CssRules { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + CssRules( + self.0 + .iter() + .map(|x| x.deep_clone_with_lock(lock, guard, params)) + .collect(), + ) + } +} + +impl CssRules { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.0.shallow_size_of(ops); + for rule in self.0.iter() { + n += rule.size_of(guard, ops); + } + n + } + + /// Trivially construct a new set of CSS rules. + pub fn new(rules: Vec<CssRule>, shared_lock: &SharedRwLock) -> Arc<Locked<CssRules>> { + Arc::new(shared_lock.wrap(CssRules(rules))) + } + + /// Returns whether all the rules in this list are namespace or import + /// rules. + fn only_ns_or_import(&self) -> bool { + self.0.iter().all(|r| match *r { + CssRule::Namespace(..) | CssRule::Import(..) => true, + _ => false, + }) + } + + /// <https://drafts.csswg.org/cssom/#remove-a-css-rule> + pub fn remove_rule(&mut self, index: usize) -> Result<(), RulesMutateError> { + // Step 1, 2 + if index >= self.0.len() { + return Err(RulesMutateError::IndexSize); + } + + { + // Step 3 + let ref rule = self.0[index]; + + // Step 4 + if let CssRule::Namespace(..) = *rule { + if !self.only_ns_or_import() { + return Err(RulesMutateError::InvalidState); + } + } + } + + // Step 5, 6 + self.0.remove(index); + Ok(()) + } + + /// Serializes this CSSRules to CSS text as a block of rules. + /// + /// This should be speced into CSSOM spec at some point. See + /// <https://github.com/w3c/csswg-drafts/issues/1985> + pub fn to_css_block( + &self, + guard: &SharedRwLockReadGuard, + dest: &mut CssStringWriter, + ) -> fmt::Result { + dest.write_str(" {")?; + self.to_css_block_without_opening(guard, dest) + } + + /// As above, but without the opening curly bracket. That's needed for nesting. + pub fn to_css_block_without_opening( + &self, + guard: &SharedRwLockReadGuard, + dest: &mut CssStringWriter, + ) -> fmt::Result { + for rule in self.0.iter() { + dest.write_str("\n ")?; + rule.to_css(guard, dest)?; + } + dest.write_str("\n}") + } +} + +/// A trait to implement helpers for `Arc<Locked<CssRules>>`. +pub trait CssRulesHelpers { + /// <https://drafts.csswg.org/cssom/#insert-a-css-rule> + /// + /// Written in this funky way because parsing an @import rule may cause us + /// to clone a stylesheet from the same document due to caching in the CSS + /// loader. + /// + /// TODO(emilio): We could also pass the write guard down into the loader + /// instead, but that seems overkill. + fn insert_rule( + &self, + lock: &SharedRwLock, + rule: &str, + parent_stylesheet_contents: &StylesheetContents, + index: usize, + nested: bool, + loader: Option<&dyn StylesheetLoader>, + allow_import_rules: AllowImportRules, + ) -> Result<CssRule, RulesMutateError>; +} + +impl CssRulesHelpers for Locked<CssRules> { + fn insert_rule( + &self, + lock: &SharedRwLock, + rule: &str, + parent_stylesheet_contents: &StylesheetContents, + index: usize, + nested: bool, + loader: Option<&dyn StylesheetLoader>, + allow_import_rules: AllowImportRules, + ) -> Result<CssRule, RulesMutateError> { + let new_rule = { + let read_guard = lock.read(); + let rules = self.read_with(&read_guard); + + // Step 1, 2 + if index > rules.0.len() { + return Err(RulesMutateError::IndexSize); + } + + // Computes the parser state at the given index + let insert_rule_context = InsertRuleContext { + rule_list: &rules.0, + index, + }; + + let state = if nested { + State::Body + } else if index == 0 { + State::Start + } else { + insert_rule_context.max_rule_state_at_index(index - 1) + }; + + // Steps 3, 4, 5, 6 + CssRule::parse( + &rule, + insert_rule_context, + parent_stylesheet_contents, + lock, + state, + loader, + allow_import_rules, + )? + }; + + { + let mut write_guard = lock.write(); + let rules = self.write_with(&mut write_guard); + rules.0.insert(index, new_rule.clone()); + } + + Ok(new_rule) + } +} diff --git a/servo/components/style/stylesheets/rule_parser.rs b/servo/components/style/stylesheets/rule_parser.rs new file mode 100644 index 0000000000..de95c898fd --- /dev/null +++ b/servo/components/style/stylesheets/rule_parser.rs @@ -0,0 +1,879 @@ +/* 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/. */ + +//! Parsing of the stylesheet contents. + +use crate::counter_style::{parse_counter_style_body, parse_counter_style_name_definition}; +use crate::custom_properties::parse_name as parse_custom_property_name; +use crate::error_reporting::ContextualParseError; +use crate::font_face::parse_font_face_block; +use crate::media_queries::MediaList; +use crate::parser::{Parse, ParserContext}; +use crate::properties::declaration_block::{ + parse_property_declaration_list, DeclarationParserState, PropertyDeclarationBlock, +}; +use crate::properties_and_values::rule::{parse_property_block, PropertyRuleName}; +use crate::selector_parser::{SelectorImpl, SelectorParser}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::str::starts_with_ignore_ascii_case; +use crate::stylesheets::container_rule::{ContainerCondition, ContainerRule}; +use crate::stylesheets::document_rule::DocumentCondition; +use crate::stylesheets::font_feature_values_rule::parse_family_name_list; +use crate::stylesheets::import_rule::{ImportLayer, ImportRule, ImportSupportsCondition}; +use crate::stylesheets::keyframes_rule::parse_keyframe_list; +use crate::stylesheets::layer_rule::{LayerBlockRule, LayerName, LayerStatementRule}; +use crate::stylesheets::supports_rule::SupportsCondition; +use crate::stylesheets::{ + viewport_rule, AllowImportRules, CorsMode, CssRule, CssRuleType, CssRules, DocumentRule, + FontFeatureValuesRule, FontPaletteValuesRule, KeyframesRule, MediaRule, NamespaceRule, + PageRule, PageSelectors, RulesMutateError, StyleRule, StylesheetLoader, SupportsRule, + ViewportRule, +}; +use crate::values::computed::font::FamilyName; +use crate::values::{CssUrl, CustomIdent, DashedIdent, KeyframesName}; +use crate::{Atom, Namespace, Prefix}; +use cssparser::{ + AtRuleParser, BasicParseError, BasicParseErrorKind, CowRcStr, DeclarationParser, Parser, + ParserState, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, + SourcePosition, +}; +use selectors::SelectorList; +use servo_arc::Arc; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// The information we need particularly to do CSSOM insertRule stuff. +pub struct InsertRuleContext<'a> { + /// The rule list we're about to insert into. + pub rule_list: &'a [CssRule], + /// The index we're about to get inserted at. + pub index: usize, +} + +impl<'a> InsertRuleContext<'a> { + /// Returns the max rule state allowable for insertion at a given index in + /// the rule list. + pub fn max_rule_state_at_index(&self, index: usize) -> State { + let rule = match self.rule_list.get(index) { + Some(rule) => rule, + None => return State::Body, + }; + match rule { + CssRule::Import(..) => State::Imports, + CssRule::Namespace(..) => State::Namespaces, + CssRule::LayerStatement(..) => { + // If there are @import / @namespace after this layer, then + // we're in the early-layers phase, otherwise we're in the body + // and everything is fair game. + let next_non_layer_statement_rule = self.rule_list[index + 1..] + .iter() + .find(|r| !matches!(*r, CssRule::LayerStatement(..))); + if let Some(non_layer) = next_non_layer_statement_rule { + if matches!(*non_layer, CssRule::Import(..) | CssRule::Namespace(..)) { + return State::EarlyLayers; + } + } + State::Body + }, + _ => State::Body, + } + } +} + +/// The parser for the top-level rules in a stylesheet. +pub struct TopLevelRuleParser<'a, 'i> { + /// A reference to the lock we need to use to create rules. + pub shared_lock: &'a SharedRwLock, + /// A reference to a stylesheet loader if applicable, for `@import` rules. + pub loader: Option<&'a dyn StylesheetLoader>, + /// The top-level parser context. + pub context: ParserContext<'a>, + /// The current state of the parser. + pub state: State, + /// Whether we have tried to parse was invalid due to being in the wrong + /// place (e.g. an @import rule was found while in the `Body` state). Reset + /// to `false` when `take_had_hierarchy_error` is called. + pub dom_error: Option<RulesMutateError>, + /// The info we need insert a rule in a list. + pub insert_rule_context: Option<InsertRuleContext<'a>>, + /// Whether @import rules will be allowed. + pub allow_import_rules: AllowImportRules, + /// Parser state for declaration blocks in either nested rules or style rules. + pub declaration_parser_state: DeclarationParserState<'i>, + /// The rules we've parsed so far. + pub rules: Vec<CssRule>, +} + +impl<'a, 'i> TopLevelRuleParser<'a, 'i> { + fn nested<'b>(&'b mut self) -> NestedRuleParser<'b, 'a, 'i> { + NestedRuleParser { + shared_lock: self.shared_lock, + context: &mut self.context, + declaration_parser_state: &mut self.declaration_parser_state, + rules: &mut self.rules, + } + } + + /// Returns the current state of the parser. + pub fn state(&self) -> State { + self.state + } + + /// Checks whether we can parse a rule that would transition us to + /// `new_state`. + /// + /// This is usually a simple branch, but we may need more bookkeeping if + /// doing `insertRule` from CSSOM. + fn check_state(&mut self, new_state: State) -> bool { + if self.state > new_state { + self.dom_error = Some(RulesMutateError::HierarchyRequest); + return false; + } + + let ctx = match self.insert_rule_context { + Some(ref ctx) => ctx, + None => return true, + }; + + let max_rule_state = ctx.max_rule_state_at_index(ctx.index); + if new_state > max_rule_state { + self.dom_error = Some(RulesMutateError::HierarchyRequest); + return false; + } + + // If there's anything that isn't a namespace rule (or import rule, but + // we checked that already at the beginning), reject with a + // StateError. + if new_state == State::Namespaces && + ctx.rule_list[ctx.index..] + .iter() + .any(|r| !matches!(*r, CssRule::Namespace(..))) + { + self.dom_error = Some(RulesMutateError::InvalidState); + return false; + } + + true + } +} + +/// The current state of the parser. +#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +pub enum State { + /// We haven't started parsing rules. + Start = 1, + /// We're parsing early `@layer` statement rules. + EarlyLayers = 2, + /// We're parsing `@import` and early `@layer` statement rules. + Imports = 3, + /// We're parsing `@namespace` rules. + Namespaces = 4, + /// We're parsing the main body of the stylesheet. + Body = 5, +} + +#[derive(Clone, Debug, MallocSizeOf, ToShmem)] +/// Vendor prefix. +pub enum VendorPrefix { + /// -moz prefix. + Moz, + /// -webkit prefix. + WebKit, +} + +/// A rule prelude for at-rule with block. +pub enum AtRulePrelude { + /// A @font-face rule prelude. + FontFace, + /// A @font-feature-values rule prelude, with its FamilyName list. + FontFeatureValues(Vec<FamilyName>), + /// A @font-palette-values rule prelude, with its identifier. + FontPaletteValues(DashedIdent), + /// A @counter-style rule prelude, with its counter style name. + CounterStyle(CustomIdent), + /// A @media rule prelude, with its media queries. + Media(Arc<Locked<MediaList>>), + /// A @container rule prelude. + Container(Arc<ContainerCondition>), + /// An @supports rule, with its conditional + Supports(SupportsCondition), + /// A @viewport rule prelude. + Viewport, + /// A @keyframes rule, with its animation name and vendor prefix if exists. + Keyframes(KeyframesName, Option<VendorPrefix>), + /// A @page rule prelude, with its page name if it exists. + Page(PageSelectors), + /// A @property rule prelude. + Property(PropertyRuleName), + /// A @document rule, with its conditional. + Document(DocumentCondition), + /// A @import rule prelude. + Import( + CssUrl, + Arc<Locked<MediaList>>, + Option<ImportSupportsCondition>, + ImportLayer, + ), + /// A @namespace rule prelude. + Namespace(Option<Prefix>, Namespace), + /// A @layer rule prelude. + Layer(Vec<LayerName>), +} + +impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a, 'i> { + type Prelude = AtRulePrelude; + type AtRule = SourcePosition; + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<AtRulePrelude, ParseError<'i>> { + match_ignore_ascii_case! { &*name, + "import" => { + if !self.check_state(State::Imports) { + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedImportRule)) + } + + if let AllowImportRules::No = self.allow_import_rules { + return Err(input.new_custom_error(StyleParseErrorKind::DisallowedImportRule)) + } + + // FIXME(emilio): We should always be able to have a loader + // around! See bug 1533783. + if self.loader.is_none() { + error!("Saw @import rule, but no way to trigger the load"); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedImportRule)) + } + + let url_string = input.expect_url_or_string()?.as_ref().to_owned(); + let url = CssUrl::parse_from_string(url_string, &self.context, CorsMode::None); + + let (layer, supports) = ImportRule::parse_layer_and_supports(input, &mut self.context); + + let media = MediaList::parse(&self.context, input); + let media = Arc::new(self.shared_lock.wrap(media)); + + return Ok(AtRulePrelude::Import(url, media, supports, layer)); + }, + "namespace" => { + if !self.check_state(State::Namespaces) { + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedNamespaceRule)) + } + + let prefix = input.try_parse(|i| i.expect_ident_cloned()) + .map(|s| Prefix::from(s.as_ref())).ok(); + let maybe_namespace = match input.expect_url_or_string() { + Ok(url_or_string) => url_or_string, + Err(BasicParseError { kind: BasicParseErrorKind::UnexpectedToken(t), location }) => { + return Err(location.new_custom_error(StyleParseErrorKind::UnexpectedTokenWithinNamespace(t))) + } + Err(e) => return Err(e.into()), + }; + let url = Namespace::from(maybe_namespace.as_ref()); + return Ok(AtRulePrelude::Namespace(prefix, url)); + }, + // @charset is removed by rust-cssparser if it’s the first rule in the stylesheet + // anything left is invalid. + "charset" => { + self.dom_error = Some(RulesMutateError::HierarchyRequest); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedCharsetRule)) + }, + "layer" => { + let state_to_check = if self.state <= State::EarlyLayers { + // The real state depends on whether there's a block or not. + // We don't know that yet, but the parse_block check deals + // with that. + State::EarlyLayers + } else { + State::Body + }; + if !self.check_state(state_to_check) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }, + _ => { + // All other rules have blocks, so we do this check early in + // parse_block instead. + } + } + + AtRuleParser::parse_prelude(&mut self.nested(), name, input) + } + + #[inline] + fn parse_block<'t>( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::AtRule, ParseError<'i>> { + if !self.check_state(State::Body) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + AtRuleParser::parse_block(&mut self.nested(), prelude, start, input)?; + self.state = State::Body; + Ok(start.position()) + } + + #[inline] + fn rule_without_block( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + ) -> Result<Self::AtRule, ()> { + match prelude { + AtRulePrelude::Import(url, media, supports, layer) => { + let loader = self + .loader + .expect("Expected a stylesheet loader for @import"); + + let import_rule = loader.request_stylesheet( + url, + start.source_location(), + &self.context, + &self.shared_lock, + media, + supports, + layer, + ); + + self.state = State::Imports; + self.rules.push(CssRule::Import(import_rule)) + }, + AtRulePrelude::Namespace(prefix, url) => { + let namespaces = self.context.namespaces.to_mut(); + let prefix = if let Some(prefix) = prefix { + namespaces.prefixes.insert(prefix.clone(), url.clone()); + Some(prefix) + } else { + namespaces.default = Some(url.clone()); + None + }; + + self.state = State::Namespaces; + self.rules.push(CssRule::Namespace(Arc::new(NamespaceRule { + prefix, + url, + source_location: start.source_location(), + }))); + }, + AtRulePrelude::Layer(..) => { + AtRuleParser::rule_without_block(&mut self.nested(), prelude, start)?; + if self.state <= State::EarlyLayers { + self.state = State::EarlyLayers; + } else { + self.state = State::Body; + } + }, + _ => AtRuleParser::rule_without_block(&mut self.nested(), prelude, start)?, + }; + + Ok(start.position()) + } +} + +impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'i> { + type Prelude = SelectorList<SelectorImpl>; + type QualifiedRule = SourcePosition; + type Error = StyleParseErrorKind<'i>; + + #[inline] + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + if !self.check_state(State::Body) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + QualifiedRuleParser::parse_prelude(&mut self.nested(), input) + } + + #[inline] + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::QualifiedRule, ParseError<'i>> { + QualifiedRuleParser::parse_block(&mut self.nested(), prelude, start, input)?; + self.state = State::Body; + Ok(start.position()) + } +} + +struct NestedRuleParser<'a, 'b: 'a, 'i> { + shared_lock: &'a SharedRwLock, + context: &'a mut ParserContext<'b>, + declaration_parser_state: &'a mut DeclarationParserState<'i>, + rules: &'a mut Vec<CssRule>, +} + +struct NestedParseResult { + rules: Vec<CssRule>, + declarations: PropertyDeclarationBlock, +} + +impl NestedParseResult { + fn into_rules( + mut self, + shared_lock: &SharedRwLock, + source_location: SourceLocation, + ) -> Arc<Locked<CssRules>> { + lazy_static! { + static ref AMPERSAND: SelectorList<SelectorImpl> = { + let list = SelectorList::ampersand(); + list.0 + .iter() + .for_each(|selector| selector.mark_as_intentionally_leaked()); + list + }; + }; + + if !self.declarations.is_empty() { + self.rules.insert( + 0, + CssRule::Style(Arc::new(shared_lock.wrap(StyleRule { + selectors: AMPERSAND.clone(), + block: Arc::new(shared_lock.wrap(self.declarations)), + rules: None, + source_location, + }))), + ) + } + + CssRules::new(self.rules, shared_lock) + } +} + +impl<'a, 'b, 'i> NestedRuleParser<'a, 'b, 'i> { + /// When nesting is disabled, we prevent parsing at rules and qualified rules inside style + /// rules. + fn allow_at_and_qualified_rules(&self) -> bool { + if !self.context.rule_types.contains(CssRuleType::Style) { + return true; + } + static_prefs::pref!("layout.css.nesting.enabled") + } + + fn nest_for_rule<R>(&mut self, rule_type: CssRuleType, cb: impl FnOnce(&mut Self) -> R) -> R { + let old_rule_types = self.context.rule_types; + self.context.rule_types.insert(rule_type); + let r = cb(self); + self.context.rule_types = old_rule_types; + r + } + + fn parse_nested( + &mut self, + input: &mut Parser<'i, '_>, + rule_type: CssRuleType, + selectors: Option<&SelectorList<SelectorImpl>>, + ) -> NestedParseResult { + self.nest_for_rule(rule_type, |parser| { + let parse_declarations = parser.parse_declarations(); + let mut old_declaration_state = std::mem::take(parser.declaration_parser_state); + let mut rules = std::mem::take(parser.rules); + let mut iter = RuleBodyParser::new(input, parser); + while let Some(result) = iter.next() { + match result { + Ok(()) => {}, + Err((error, slice)) => { + if parse_declarations { + iter.parser.declaration_parser_state.did_error( + iter.parser.context, + error, + slice, + ); + } else { + let location = error.location; + let error = ContextualParseError::InvalidRule(slice, error); + iter.parser.context.log_css_error(location, error); + } + }, + } + } + let declarations = if parse_declarations { + parser + .declaration_parser_state + .report_errors_if_needed(parser.context, selectors); + parser.declaration_parser_state.take_declarations() + } else { + PropertyDeclarationBlock::default() + }; + debug_assert!( + !parser.declaration_parser_state.has_parsed_declarations(), + "Parsed but didn't consume declarations" + ); + std::mem::swap(parser.declaration_parser_state, &mut old_declaration_state); + std::mem::swap(parser.rules, &mut rules); + NestedParseResult { + rules, + declarations, + } + }) + } +} + +impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'b, 'i> { + type Prelude = AtRulePrelude; + type AtRule = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + if !self.allow_at_and_qualified_rules() { + return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))); + } + Ok(match_ignore_ascii_case! { &*name, + "media" => { + let media_queries = MediaList::parse(self.context, input); + let arc = Arc::new(self.shared_lock.wrap(media_queries)); + AtRulePrelude::Media(arc) + }, + "supports" => { + let cond = SupportsCondition::parse(input)?; + AtRulePrelude::Supports(cond) + }, + "font-face" => { + AtRulePrelude::FontFace + }, + "container" if static_prefs::pref!("layout.css.container-queries.enabled") => { + let condition = Arc::new(ContainerCondition::parse(self.context, input)?); + AtRulePrelude::Container(condition) + }, + "layer" => { + let names = input.try_parse(|input| { + input.parse_comma_separated(|input| { + LayerName::parse(self.context, input) + }) + }).unwrap_or_default(); + AtRulePrelude::Layer(names) + }, + "font-feature-values" if cfg!(feature = "gecko") => { + let family_names = parse_family_name_list(self.context, input)?; + AtRulePrelude::FontFeatureValues(family_names) + }, + "font-palette-values" if static_prefs::pref!("layout.css.font-palette.enabled") => { + let name = DashedIdent::parse(self.context, input)?; + AtRulePrelude::FontPaletteValues(name) + }, + "counter-style" if cfg!(feature = "gecko") => { + let name = parse_counter_style_name_definition(input)?; + AtRulePrelude::CounterStyle(name) + }, + "viewport" if viewport_rule::enabled() => { + AtRulePrelude::Viewport + }, + "keyframes" | "-webkit-keyframes" | "-moz-keyframes" => { + let prefix = if starts_with_ignore_ascii_case(&*name, "-webkit-") { + Some(VendorPrefix::WebKit) + } else if starts_with_ignore_ascii_case(&*name, "-moz-") { + Some(VendorPrefix::Moz) + } else { + None + }; + if cfg!(feature = "servo") && + prefix.as_ref().map_or(false, |p| matches!(*p, VendorPrefix::Moz)) { + // Servo should not support @-moz-keyframes. + return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))) + } + let name = KeyframesName::parse(self.context, input)?; + AtRulePrelude::Keyframes(name, prefix) + }, + "page" if cfg!(feature = "gecko") => { + AtRulePrelude::Page( + input.try_parse(|i| PageSelectors::parse(self.context, i)).unwrap_or_default() + ) + }, + "property" if static_prefs::pref!("layout.css.properties-and-values.enabled") => { + let name = input.expect_ident_cloned()?; + let name = parse_custom_property_name(&name).map_err(|_| { + input.new_custom_error(StyleParseErrorKind::UnexpectedIdent(name.clone())) + })?; + AtRulePrelude::Property(PropertyRuleName(Arc::new(Atom::from(name)))) + }, + "-moz-document" if cfg!(feature = "gecko") => { + let cond = DocumentCondition::parse(self.context, input)?; + AtRulePrelude::Document(cond) + }, + _ => return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))) + }) + } + + fn parse_block<'t>( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let rule = match prelude { + AtRulePrelude::FontFace => self.nest_for_rule(CssRuleType::FontFace, |p| { + CssRule::FontFace(Arc::new(p.shared_lock.wrap( + parse_font_face_block(&p.context, input, start.source_location()).into(), + ))) + }), + AtRulePrelude::FontFeatureValues(family_names) => { + self.nest_for_rule(CssRuleType::FontFeatureValues, |p| { + CssRule::FontFeatureValues(Arc::new(FontFeatureValuesRule::parse( + &p.context, + input, + family_names, + start.source_location(), + ))) + }) + }, + AtRulePrelude::FontPaletteValues(name) => { + self.nest_for_rule(CssRuleType::FontPaletteValues, |p| { + CssRule::FontPaletteValues(Arc::new(FontPaletteValuesRule::parse( + &p.context, + input, + name, + start.source_location(), + ))) + }) + }, + AtRulePrelude::CounterStyle(name) => { + let body = self.nest_for_rule(CssRuleType::CounterStyle, |p| { + parse_counter_style_body(name, &p.context, input, start.source_location()) + })?; + CssRule::CounterStyle(Arc::new(self.shared_lock.wrap(body))) + }, + AtRulePrelude::Media(media_queries) => { + let source_location = start.source_location(); + CssRule::Media(Arc::new(MediaRule { + media_queries, + rules: self + .parse_nested(input, CssRuleType::Media, None) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Supports(condition) => { + let enabled = + self.nest_for_rule(CssRuleType::Style, |p| condition.eval(&p.context)); + let source_location = start.source_location(); + CssRule::Supports(Arc::new(SupportsRule { + condition, + rules: self + .parse_nested(input, CssRuleType::Supports, None) + .into_rules(self.shared_lock, source_location), + enabled, + source_location, + })) + }, + AtRulePrelude::Viewport => { + let body = self.nest_for_rule(CssRuleType::Viewport, |p| { + ViewportRule::parse(&p.context, input) + })?; + CssRule::Viewport(Arc::new(body)) + }, + AtRulePrelude::Keyframes(name, vendor_prefix) => { + self.nest_for_rule(CssRuleType::Keyframe, |p| { + CssRule::Keyframes(Arc::new(p.shared_lock.wrap(KeyframesRule { + name, + keyframes: parse_keyframe_list(&mut p.context, input, p.shared_lock), + vendor_prefix, + source_location: start.source_location(), + }))) + }) + }, + AtRulePrelude::Page(selectors) => { + let declarations = self.nest_for_rule(CssRuleType::Page, |p| { + // TODO: Support nesting in @page rules? + parse_property_declaration_list(&p.context, input, None) + }); + CssRule::Page(Arc::new(self.shared_lock.wrap(PageRule { + selectors, + block: Arc::new(self.shared_lock.wrap(declarations)), + source_location: start.source_location(), + }))) + }, + AtRulePrelude::Property(name) => self.nest_for_rule(CssRuleType::Property, |p| { + CssRule::Property(Arc::new(parse_property_block( + &p.context, + input, + name, + start.source_location(), + ))) + }), + AtRulePrelude::Document(condition) => { + if !cfg!(feature = "gecko") { + unreachable!() + } + let source_location = start.source_location(); + CssRule::Document(Arc::new(DocumentRule { + condition, + rules: self + .parse_nested(input, CssRuleType::Document, None) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Container(condition) => { + let source_location = start.source_location(); + CssRule::Container(Arc::new(ContainerRule { + condition, + rules: self + .parse_nested(input, CssRuleType::Container, None) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Layer(names) => { + let name = match names.len() { + 0 | 1 => names.into_iter().next(), + _ => return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), + }; + let source_location = start.source_location(); + CssRule::LayerBlock(Arc::new(LayerBlockRule { + name, + rules: self + .parse_nested(input, CssRuleType::LayerBlock, None) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Import(..) | AtRulePrelude::Namespace(..) => { + // These rules don't have blocks. + return Err(input.new_unexpected_token_error(cssparser::Token::CurlyBracketBlock)); + }, + }; + self.rules.push(rule); + Ok(()) + } + + #[inline] + fn rule_without_block( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + ) -> Result<(), ()> { + let rule = match prelude { + AtRulePrelude::Layer(names) => { + if names.is_empty() { + return Err(()); + } + CssRule::LayerStatement(Arc::new(LayerStatementRule { + names, + source_location: start.source_location(), + })) + }, + _ => return Err(()), + }; + self.rules.push(rule); + Ok(()) + } +} + +#[inline(never)] +fn check_for_useless_selector( + input: &mut Parser, + context: &ParserContext, + selectors: &SelectorList<SelectorImpl>, +) { + use cssparser::ToCss; + + 'selector_loop: for selector in selectors.0.iter() { + let mut current = selector.iter(); + loop { + let mut found_host = false; + let mut found_non_host = false; + for component in &mut current { + if component.is_host() { + found_host = true; + } else { + found_non_host = true; + } + if found_host && found_non_host { + let location = input.current_source_location(); + context.log_css_error( + location, + ContextualParseError::NeverMatchingHostSelector(selector.to_css_string()), + ); + continue 'selector_loop; + } + } + if current.next_sequence().is_none() { + break; + } + } + } +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'b, 'i> { + type Prelude = SelectorList<SelectorImpl>; + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + let selector_parser = SelectorParser { + stylesheet_origin: self.context.stylesheet_origin, + namespaces: &self.context.namespaces, + url_data: self.context.url_data, + for_supports_rule: false, + }; + let selectors = SelectorList::parse(&selector_parser, input)?; + if self.context.error_reporting_enabled() { + check_for_useless_selector(input, &self.context, &selectors); + } + Ok(selectors) + } + + fn parse_block<'t>( + &mut self, + selectors: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let result = self.parse_nested(input, CssRuleType::Style, Some(&selectors)); + let block = Arc::new(self.shared_lock.wrap(result.declarations)); + self.rules + .push(CssRule::Style(Arc::new(self.shared_lock.wrap(StyleRule { + selectors, + block, + rules: if result.rules.is_empty() { + None + } else { + Some(CssRules::new(result.rules, self.shared_lock)) + }, + source_location: start.source_location(), + })))); + Ok(()) + } +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for NestedRuleParser<'a, 'b, 'i> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + self.declaration_parser_state + .parse_value(self.context, name, input) + } +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for NestedRuleParser<'a, 'b, 'i> +{ + fn parse_qualified(&self) -> bool { + self.allow_at_and_qualified_rules() + } + + /// If nesting is disabled, we can't get there for a non-style-rule. If it's enabled, we parse + /// raw declarations there. + fn parse_declarations(&self) -> bool { + self.context.rule_types.contains(CssRuleType::Style) + } +} diff --git a/servo/components/style/stylesheets/rules_iterator.rs b/servo/components/style/stylesheets/rules_iterator.rs new file mode 100644 index 0000000000..950bcd238e --- /dev/null +++ b/servo/components/style/stylesheets/rules_iterator.rs @@ -0,0 +1,327 @@ +/* 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/. */ + +//! An iterator over a list of rules. + +use crate::context::QuirksMode; +use crate::media_queries::Device; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{CssRule, DocumentRule, ImportRule, MediaRule, SupportsRule}; +use smallvec::SmallVec; +use std::slice; + +/// An iterator over a list of rules. +pub struct RulesIterator<'a, 'b, C> +where + 'b: 'a, + C: NestedRuleIterationCondition + 'static, +{ + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'b>, + stack: SmallVec<[slice::Iter<'a, CssRule>; 3]>, + _phantom: ::std::marker::PhantomData<C>, +} + +impl<'a, 'b, C> RulesIterator<'a, 'b, C> +where + 'b: 'a, + C: NestedRuleIterationCondition + 'static, +{ + /// Creates a new `RulesIterator` to iterate over `rules`. + pub fn new( + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'b>, + rules: slice::Iter<'a, CssRule>, + ) -> Self { + let mut stack = SmallVec::new(); + stack.push(rules); + Self { + device, + quirks_mode, + guard, + stack, + _phantom: ::std::marker::PhantomData, + } + } + + /// Skips all the remaining children of the last nested rule processed. + pub fn skip_children(&mut self) { + self.stack.pop(); + } + + /// Returns the children of `rule`, and whether `rule` is effective. + pub fn children( + rule: &'a CssRule, + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'_>, + effective: &mut bool, + ) -> Option<slice::Iter<'a, CssRule>> { + *effective = true; + match *rule { + CssRule::Namespace(_) | + CssRule::FontFace(_) | + CssRule::CounterStyle(_) | + CssRule::Viewport(_) | + CssRule::Keyframes(_) | + CssRule::Page(_) | + CssRule::Property(_) | + CssRule::LayerStatement(_) | + CssRule::FontFeatureValues(_) | + CssRule::FontPaletteValues(_) => None, + CssRule::Style(ref style_rule) => { + let style_rule = style_rule.read_with(guard); + style_rule + .rules + .as_ref() + .map(|r| r.read_with(guard).0.iter()) + }, + CssRule::Import(ref import_rule) => { + let import_rule = import_rule.read_with(guard); + if !C::process_import(guard, device, quirks_mode, import_rule) { + *effective = false; + return None; + } + Some(import_rule.stylesheet.rules(guard).iter()) + }, + CssRule::Document(ref doc_rule) => { + if !C::process_document(guard, device, quirks_mode, doc_rule) { + *effective = false; + return None; + } + Some(doc_rule.rules.read_with(guard).0.iter()) + }, + CssRule::Container(ref container_rule) => { + Some(container_rule.rules.read_with(guard).0.iter()) + }, + CssRule::Media(ref media_rule) => { + if !C::process_media(guard, device, quirks_mode, media_rule) { + *effective = false; + return None; + } + Some(media_rule.rules.read_with(guard).0.iter()) + }, + CssRule::Supports(ref supports_rule) => { + if !C::process_supports(guard, device, quirks_mode, supports_rule) { + *effective = false; + return None; + } + Some(supports_rule.rules.read_with(guard).0.iter()) + }, + CssRule::LayerBlock(ref layer_rule) => Some(layer_rule.rules.read_with(guard).0.iter()), + } + } +} + +impl<'a, 'b, C> Iterator for RulesIterator<'a, 'b, C> +where + 'b: 'a, + C: NestedRuleIterationCondition + 'static, +{ + type Item = &'a CssRule; + + fn next(&mut self) -> Option<Self::Item> { + while !self.stack.is_empty() { + let rule = { + let nested_iter = self.stack.last_mut().unwrap(); + match nested_iter.next() { + Some(r) => r, + None => { + self.stack.pop(); + continue; + }, + } + }; + + let mut effective = true; + let children = Self::children( + rule, + self.device, + self.quirks_mode, + self.guard, + &mut effective, + ); + if !effective { + continue; + } + + if let Some(children) = children { + // NOTE: It's important that `children` gets pushed even if + // empty, so that `skip_children()` works as expected. + self.stack.push(children); + } + + return Some(rule); + } + + None + } +} + +/// RulesIterator. +pub trait NestedRuleIterationCondition { + /// Whether we should process the nested rules in a given `@import` rule. + fn process_import( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &ImportRule, + ) -> bool; + + /// Whether we should process the nested rules in a given `@media` rule. + fn process_media( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &MediaRule, + ) -> bool; + + /// Whether we should process the nested rules in a given `@-moz-document` + /// rule. + fn process_document( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &DocumentRule, + ) -> bool; + + /// Whether we should process the nested rules in a given `@supports` rule. + fn process_supports( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &SupportsRule, + ) -> bool; +} + +/// A struct that represents the condition that a rule applies to the document. +pub struct EffectiveRules; + +impl EffectiveRules { + /// Returns whether a given rule is effective. + pub fn is_effective( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &CssRule, + ) -> bool { + match *rule { + CssRule::Import(ref import_rule) => { + let import_rule = import_rule.read_with(guard); + Self::process_import(guard, device, quirks_mode, import_rule) + }, + CssRule::Document(ref doc_rule) => { + Self::process_document(guard, device, quirks_mode, doc_rule) + }, + CssRule::Media(ref media_rule) => { + Self::process_media(guard, device, quirks_mode, media_rule) + }, + CssRule::Supports(ref supports_rule) => { + Self::process_supports(guard, device, quirks_mode, supports_rule) + }, + _ => true, + } + } +} + +impl NestedRuleIterationCondition for EffectiveRules { + fn process_import( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &ImportRule, + ) -> bool { + match rule.stylesheet.media(guard) { + Some(m) => m.evaluate(device, quirks_mode), + None => true, + } + } + + fn process_media( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &MediaRule, + ) -> bool { + rule.media_queries + .read_with(guard) + .evaluate(device, quirks_mode) + } + + fn process_document( + _: &SharedRwLockReadGuard, + device: &Device, + _: QuirksMode, + rule: &DocumentRule, + ) -> bool { + rule.condition.evaluate(device) + } + + fn process_supports( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + rule: &SupportsRule, + ) -> bool { + rule.enabled + } +} + +/// A filter that processes all the rules in a rule list. +pub struct AllRules; + +impl NestedRuleIterationCondition for AllRules { + fn process_import( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &ImportRule, + ) -> bool { + true + } + + fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool { + true + } + + fn process_document( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &DocumentRule, + ) -> bool { + true + } + + fn process_supports( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &SupportsRule, + ) -> bool { + true + } +} + +/// An iterator over all the effective rules of a stylesheet. +/// +/// NOTE: This iterator recurses into `@import` rules. +pub type EffectiveRulesIterator<'a, 'b> = RulesIterator<'a, 'b, EffectiveRules>; + +impl<'a, 'b> EffectiveRulesIterator<'a, 'b> { + /// Returns an iterator over the effective children of a rule, even if + /// `rule` itself is not effective. + pub fn effective_children( + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'b>, + rule: &'a CssRule, + ) -> Self { + let children = + RulesIterator::<AllRules>::children(rule, device, quirks_mode, guard, &mut false); + EffectiveRulesIterator::new(device, quirks_mode, guard, children.unwrap_or([].iter())) + } +} diff --git a/servo/components/style/stylesheets/style_rule.rs b/servo/components/style/stylesheets/style_rule.rs new file mode 100644 index 0000000000..d5a22d6fc2 --- /dev/null +++ b/servo/components/style/stylesheets/style_rule.rs @@ -0,0 +1,104 @@ +/* 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 style rule. + +use crate::properties::PropertyDeclarationBlock; +use crate::selector_parser::SelectorImpl; +use crate::shared_lock::{ + DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, +}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use cssparser::SourceLocation; +#[cfg(feature = "gecko")] +use malloc_size_of::MallocUnconditionalShallowSizeOf; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use selectors::SelectorList; +use servo_arc::Arc; +use std::fmt::{self, Write}; + +/// A style rule, with selectors and declarations. +#[derive(Debug, ToShmem)] +pub struct StyleRule { + /// The list of selectors in this rule. + pub selectors: SelectorList<SelectorImpl>, + /// The declaration block with the properties it contains. + pub block: Arc<Locked<PropertyDeclarationBlock>>, + /// The nested rules to this style rule. Only non-`None` when nesting is enabled. + pub rules: Option<Arc<Locked<CssRules>>>, + /// The location in the sheet where it was found. + pub source_location: SourceLocation, +} + +impl DeepCloneWithLock for StyleRule { + /// Deep clones this StyleRule. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> StyleRule { + StyleRule { + selectors: self.selectors.clone(), + block: Arc::new(lock.wrap(self.block.read_with(guard).clone())), + rules: self.rules.as_ref().map(|rules| { + let rules = rules.read_with(guard); + Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))) + }), + source_location: self.source_location.clone(), + } + } +} + +impl StyleRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + n += self.selectors.0.size_of(ops); + n += self.block.unconditional_shallow_size_of(ops) + + self.block.read_with(guard).size_of(ops); + if let Some(ref rules) = self.rules { + n += rules.unconditional_shallow_size_of(ops) + + rules.read_with(guard).size_of(guard, ops) + } + n + } +} + +impl ToCssWithGuard for StyleRule { + /// https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSStyleRule + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + use cssparser::ToCss; + // Step 1 + self.selectors.to_css(dest)?; + dest.write_str(" {")?; + + // Step 2 + let declaration_block = self.block.read_with(guard); + let has_declarations = !declaration_block.declarations().is_empty(); + + // Step 3 + if let Some(ref rules) = self.rules { + let rules = rules.read_with(guard); + // Step 6 (here because it's more convenient) + if !rules.is_empty() { + if has_declarations { + dest.write_str("\n ")?; + declaration_block.to_css(dest)?; + } + return rules.to_css_block_without_opening(guard, dest); + } + } + + // Steps 4 & 5 + if has_declarations { + dest.write_char(' ')?; + declaration_block.to_css(dest)?; + } + dest.write_str(" }") + } +} diff --git a/servo/components/style/stylesheets/stylesheet.rs b/servo/components/style/stylesheets/stylesheet.rs new file mode 100644 index 0000000000..097cf7f393 --- /dev/null +++ b/servo/components/style/stylesheets/stylesheet.rs @@ -0,0 +1,595 @@ +/* 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/. */ + +use crate::context::QuirksMode; +use crate::error_reporting::{ContextualParseError, ParseErrorReporter}; +use crate::media_queries::{Device, MediaList}; +use crate::parser::ParserContext; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard}; +use crate::stylesheets::loader::StylesheetLoader; +use crate::stylesheets::rule_parser::{State, TopLevelRuleParser}; +use crate::stylesheets::rules_iterator::{EffectiveRules, EffectiveRulesIterator}; +use crate::stylesheets::rules_iterator::{NestedRuleIterationCondition, RulesIterator}; +use crate::stylesheets::{CssRule, CssRules, Origin, UrlExtraData}; +use crate::use_counters::UseCounters; +use crate::{Namespace, Prefix}; +use cssparser::{Parser, ParserInput, StyleSheetParser}; +use fxhash::FxHashMap; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use parking_lot::RwLock; +use servo_arc::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use style_traits::ParsingMode; + +/// This structure holds the user-agent and user stylesheets. +pub struct UserAgentStylesheets { + /// The lock used for user-agent stylesheets. + pub shared_lock: SharedRwLock, + /// The user or user agent stylesheets. + pub user_or_user_agent_stylesheets: Vec<DocumentStyleSheet>, + /// The quirks mode stylesheet. + pub quirks_mode_stylesheet: DocumentStyleSheet, +} + +/// A set of namespaces applying to a given stylesheet. +/// +/// The namespace id is used in gecko +#[derive(Clone, Debug, Default, MallocSizeOf)] +#[allow(missing_docs)] +pub struct Namespaces { + pub default: Option<Namespace>, + pub prefixes: FxHashMap<Prefix, Namespace>, +} + +/// The contents of a given stylesheet. This effectively maps to a +/// StyleSheetInner in Gecko. +#[derive(Debug)] +pub struct StylesheetContents { + /// List of rules in the order they were found (important for + /// cascading order) + pub rules: Arc<Locked<CssRules>>, + /// The origin of this stylesheet. + pub origin: Origin, + /// The url data this stylesheet should use. + pub url_data: RwLock<UrlExtraData>, + /// The namespaces that apply to this stylesheet. + pub namespaces: RwLock<Namespaces>, + /// The quirks mode of this stylesheet. + pub quirks_mode: QuirksMode, + /// This stylesheet's source map URL. + pub source_map_url: RwLock<Option<String>>, + /// This stylesheet's source URL. + pub source_url: RwLock<Option<String>>, + + /// We don't want to allow construction outside of this file, to guarantee + /// that all contents are created with Arc<>. + _forbid_construction: (), +} + +impl StylesheetContents { + /// Parse a given CSS string, with a given url-data, origin, and + /// quirks mode. + pub fn from_str( + css: &str, + url_data: UrlExtraData, + origin: Origin, + shared_lock: &SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + line_number_offset: u32, + use_counters: Option<&UseCounters>, + allow_import_rules: AllowImportRules, + sanitization_data: Option<&mut SanitizationData>, + ) -> Arc<Self> { + let (namespaces, rules, source_map_url, source_url) = Stylesheet::parse_rules( + css, + &url_data, + origin, + &shared_lock, + stylesheet_loader, + error_reporter, + quirks_mode, + line_number_offset, + use_counters, + allow_import_rules, + sanitization_data, + ); + + Arc::new(Self { + rules: CssRules::new(rules, &shared_lock), + origin, + url_data: RwLock::new(url_data), + namespaces: RwLock::new(namespaces), + quirks_mode, + source_map_url: RwLock::new(source_map_url), + source_url: RwLock::new(source_url), + _forbid_construction: (), + }) + } + + /// Creates a new StylesheetContents with the specified pre-parsed rules, + /// origin, URL data, and quirks mode. + /// + /// Since the rules have already been parsed, and the intention is that + /// this function is used for read only User Agent style sheets, an empty + /// namespace map is used, and the source map and source URLs are set to + /// None. + /// + /// An empty namespace map should be fine, as it is only used for parsing, + /// not serialization of existing selectors. Since UA sheets are read only, + /// we should never need the namespace map. + pub fn from_shared_data( + rules: Arc<Locked<CssRules>>, + origin: Origin, + url_data: UrlExtraData, + quirks_mode: QuirksMode, + ) -> Arc<Self> { + debug_assert!(rules.is_static()); + Arc::new(Self { + rules, + origin, + url_data: RwLock::new(url_data), + namespaces: RwLock::new(Namespaces::default()), + quirks_mode, + source_map_url: RwLock::new(None), + source_url: RwLock::new(None), + _forbid_construction: (), + }) + } + + /// Returns a reference to the list of rules. + #[inline] + pub fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] { + &self.rules.read_with(guard).0 + } + + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + if self.rules.is_static() { + return 0; + } + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + } +} + +impl DeepCloneWithLock for StylesheetContents { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + // Make a deep clone of the rules, using the new lock. + let rules = self + .rules + .read_with(guard) + .deep_clone_with_lock(lock, guard, params); + + Self { + rules: Arc::new(lock.wrap(rules)), + quirks_mode: self.quirks_mode, + origin: self.origin, + url_data: RwLock::new((*self.url_data.read()).clone()), + namespaces: RwLock::new((*self.namespaces.read()).clone()), + source_map_url: RwLock::new((*self.source_map_url.read()).clone()), + source_url: RwLock::new((*self.source_url.read()).clone()), + _forbid_construction: (), + } + } +} + +/// The structure servo uses to represent a stylesheet. +#[derive(Debug)] +pub struct Stylesheet { + /// The contents of this stylesheet. + pub contents: Arc<StylesheetContents>, + /// The lock used for objects inside this stylesheet + pub shared_lock: SharedRwLock, + /// List of media associated with the Stylesheet. + pub media: Arc<Locked<MediaList>>, + /// Whether this stylesheet should be disabled. + pub disabled: AtomicBool, +} + +macro_rules! rule_filter { + ($( $method: ident($variant:ident => $rule_type: ident), )+) => { + $( + #[allow(missing_docs)] + fn $method<F>(&self, device: &Device, guard: &SharedRwLockReadGuard, mut f: F) + where F: FnMut(&crate::stylesheets::$rule_type), + { + use crate::stylesheets::CssRule; + + for rule in self.effective_rules(device, guard) { + if let CssRule::$variant(ref rule) = *rule { + f(&rule) + } + } + } + )+ + } +} + +/// A trait to represent a given stylesheet in a document. +pub trait StylesheetInDocument: ::std::fmt::Debug { + /// Get whether this stylesheet is enabled. + fn enabled(&self) -> bool; + + /// Get the media associated with this stylesheet. + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList>; + + /// Returns a reference to the list of rules in this stylesheet. + fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] { + self.contents().rules(guard) + } + + /// Returns a reference to the contents of the stylesheet. + fn contents(&self) -> &StylesheetContents; + + /// Return an iterator using the condition `C`. + #[inline] + fn iter_rules<'a, 'b, C>( + &'a self, + device: &'a Device, + guard: &'a SharedRwLockReadGuard<'b>, + ) -> RulesIterator<'a, 'b, C> + where + C: NestedRuleIterationCondition, + { + let contents = self.contents(); + RulesIterator::new( + device, + contents.quirks_mode, + guard, + contents.rules(guard).iter(), + ) + } + + /// Returns whether the style-sheet applies for the current device. + fn is_effective_for_device(&self, device: &Device, guard: &SharedRwLockReadGuard) -> bool { + match self.media(guard) { + Some(medialist) => medialist.evaluate(device, self.contents().quirks_mode), + None => true, + } + } + + /// Return an iterator over the effective rules within the style-sheet, as + /// according to the supplied `Device`. + #[inline] + fn effective_rules<'a, 'b>( + &'a self, + device: &'a Device, + guard: &'a SharedRwLockReadGuard<'b>, + ) -> EffectiveRulesIterator<'a, 'b> { + self.iter_rules::<EffectiveRules>(device, guard) + } + + rule_filter! { + effective_viewport_rules(Viewport => ViewportRule), + } +} + +impl StylesheetInDocument for Stylesheet { + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + Some(self.media.read_with(guard)) + } + + fn enabled(&self) -> bool { + !self.disabled() + } + + #[inline] + fn contents(&self) -> &StylesheetContents { + &self.contents + } +} + +/// A simple wrapper over an `Arc<Stylesheet>`, with pointer comparison, and +/// suitable for its use in a `StylesheetSet`. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct DocumentStyleSheet( + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] pub Arc<Stylesheet>, +); + +impl PartialEq for DocumentStyleSheet { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl StylesheetInDocument for DocumentStyleSheet { + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + self.0.media(guard) + } + + fn enabled(&self) -> bool { + self.0.enabled() + } + + #[inline] + fn contents(&self) -> &StylesheetContents { + self.0.contents() + } +} + +/// The kind of sanitization to use when parsing a stylesheet. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SanitizationKind { + /// Perform no sanitization. + None, + /// Allow only @font-face, style rules, and @namespace. + Standard, + /// Allow everything but conditional rules. + NoConditionalRules, +} + +/// Whether @import rules are allowed. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AllowImportRules { + /// @import rules will be parsed. + Yes, + /// @import rules will not be parsed. + No, +} + +impl SanitizationKind { + fn allows(self, rule: &CssRule) -> bool { + debug_assert_ne!(self, SanitizationKind::None); + // NOTE(emilio): If this becomes more complex (not filtering just by + // top-level rules), we should thread all the data through nested rules + // and such. But this doesn't seem necessary at the moment. + let is_standard = matches!(self, SanitizationKind::Standard); + match *rule { + CssRule::Document(..) | + CssRule::Media(..) | + CssRule::Supports(..) | + CssRule::Import(..) | + CssRule::Container(..) | + // TODO(emilio): Perhaps Layer should not be always sanitized? But + // we sanitize @media and co, so this seems safer for now. + CssRule::LayerStatement(..) | + CssRule::LayerBlock(..) => false, + + CssRule::FontFace(..) | CssRule::Namespace(..) | CssRule::Style(..) => true, + + CssRule::Keyframes(..) | + CssRule::Page(..) | + CssRule::Property(..) | + CssRule::FontFeatureValues(..) | + CssRule::FontPaletteValues(..) | + CssRule::Viewport(..) | + CssRule::CounterStyle(..) => !is_standard, + } + } +} + +/// A struct to hold the data relevant to style sheet sanitization. +#[derive(Debug)] +pub struct SanitizationData { + kind: SanitizationKind, + output: String, +} + +impl SanitizationData { + /// Create a new input for sanitization. + #[inline] + pub fn new(kind: SanitizationKind) -> Option<Self> { + if matches!(kind, SanitizationKind::None) { + return None; + } + Some(Self { + kind, + output: String::new(), + }) + } + + /// Take the sanitized output. + #[inline] + pub fn take(self) -> String { + self.output + } +} + +impl Stylesheet { + /// Updates an empty stylesheet from a given string of text. + pub fn update_from_str( + existing: &Stylesheet, + css: &str, + url_data: UrlExtraData, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + line_number_offset: u32, + allow_import_rules: AllowImportRules, + ) { + // FIXME: Consider adding use counters to Servo? + let (namespaces, rules, source_map_url, source_url) = Self::parse_rules( + css, + &url_data, + existing.contents.origin, + &existing.shared_lock, + stylesheet_loader, + error_reporter, + existing.contents.quirks_mode, + line_number_offset, + /* use_counters = */ None, + allow_import_rules, + /* sanitization_data = */ None, + ); + + *existing.contents.url_data.write() = url_data; + *existing.contents.namespaces.write() = namespaces; + + // Acquire the lock *after* parsing, to minimize the exclusive section. + let mut guard = existing.shared_lock.write(); + *existing.contents.rules.write_with(&mut guard) = CssRules(rules); + *existing.contents.source_map_url.write() = source_map_url; + *existing.contents.source_url.write() = source_url; + } + + fn parse_rules( + css: &str, + url_data: &UrlExtraData, + origin: Origin, + shared_lock: &SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + line_number_offset: u32, + use_counters: Option<&UseCounters>, + allow_import_rules: AllowImportRules, + mut sanitization_data: Option<&mut SanitizationData>, + ) -> (Namespaces, Vec<CssRule>, Option<String>, Option<String>) { + let mut input = ParserInput::new_with_line_number_offset(css, line_number_offset); + let mut input = Parser::new(&mut input); + + let context = ParserContext::new( + origin, + url_data, + None, + ParsingMode::DEFAULT, + quirks_mode, + /* namespaces = */ Default::default(), + error_reporter, + use_counters, + ); + + let mut rule_parser = TopLevelRuleParser { + shared_lock, + loader: stylesheet_loader, + context, + state: State::Start, + dom_error: None, + insert_rule_context: None, + allow_import_rules, + declaration_parser_state: Default::default(), + rules: Vec::new(), + }; + + { + let mut iter = StyleSheetParser::new(&mut input, &mut rule_parser); + while let Some(result) = iter.next() { + match result { + Ok(rule_start) => { + // TODO(emilio, nesting): sanitize nested CSS rules, probably? + if let Some(ref mut data) = sanitization_data { + if let Some(ref rule) = iter.parser.rules.last() { + if !data.kind.allows(rule) { + iter.parser.rules.pop(); + continue; + } + } + let end = iter.input.position().byte_index(); + data.output.push_str(&css[rule_start.byte_index()..end]); + } + }, + Err((error, slice)) => { + let location = error.location; + let error = ContextualParseError::InvalidRule(slice, error); + iter.parser.context.log_css_error(location, error); + }, + } + } + } + + let source_map_url = input.current_source_map_url().map(String::from); + let source_url = input.current_source_url().map(String::from); + ( + rule_parser.context.namespaces.into_owned(), + rule_parser.rules, + source_map_url, + source_url, + ) + } + + /// Creates an empty stylesheet and parses it with a given base url, origin + /// and media. + /// + /// Effectively creates a new stylesheet and forwards the hard work to + /// `Stylesheet::update_from_str`. + pub fn from_str( + css: &str, + url_data: UrlExtraData, + origin: Origin, + media: Arc<Locked<MediaList>>, + shared_lock: SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + line_number_offset: u32, + allow_import_rules: AllowImportRules, + ) -> Self { + // FIXME: Consider adding use counters to Servo? + let contents = StylesheetContents::from_str( + css, + url_data, + origin, + &shared_lock, + stylesheet_loader, + error_reporter, + quirks_mode, + line_number_offset, + /* use_counters = */ None, + allow_import_rules, + /* sanitized_output = */ None, + ); + + Stylesheet { + contents, + shared_lock, + media, + disabled: AtomicBool::new(false), + } + } + + /// Returns whether the stylesheet has been explicitly disabled through the + /// CSSOM. + pub fn disabled(&self) -> bool { + self.disabled.load(Ordering::SeqCst) + } + + /// Records that the stylesheet has been explicitly disabled through the + /// CSSOM. + /// + /// Returns whether the the call resulted in a change in disabled state. + /// + /// Disabled stylesheets remain in the document, but their rules are not + /// added to the Stylist. + pub fn set_disabled(&self, disabled: bool) -> bool { + self.disabled.swap(disabled, Ordering::SeqCst) != disabled + } +} + +#[cfg(feature = "servo")] +impl Clone for Stylesheet { + fn clone(&self) -> Self { + // Create a new lock for our clone. + let lock = self.shared_lock.clone(); + let guard = self.shared_lock.read(); + + // Make a deep clone of the media, using the new lock. + let media = self.media.read_with(&guard).clone(); + let media = Arc::new(lock.wrap(media)); + let contents = Arc::new(self.contents.deep_clone_with_lock( + &lock, + &guard, + &DeepCloneParams, + )); + + Stylesheet { + contents, + media, + shared_lock: lock, + disabled: AtomicBool::new(self.disabled.load(Ordering::SeqCst)), + } + } +} diff --git a/servo/components/style/stylesheets/supports_rule.rs b/servo/components/style/stylesheets/supports_rule.rs new file mode 100644 index 0000000000..365785b088 --- /dev/null +++ b/servo/components/style/stylesheets/supports_rule.rs @@ -0,0 +1,436 @@ +/* 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/. */ + +//! [@supports rules](https://drafts.csswg.org/css-conditional-3/#at-supports) + +use crate::font_face::{FontFaceSourceFormatKeyword, FontFaceSourceTechFlags}; +use crate::parser::ParserContext; +use crate::properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration}; +use crate::selector_parser::{SelectorImpl, SelectorParser}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::{CssRuleType, CssRules}; +use cssparser::parse_important; +use cssparser::{Delimiter, Parser, SourceLocation, Token}; +use cssparser::{ParseError as CssParseError, ParserInput}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use selectors::parser::{Selector, SelectorParseErrorKind}; +use servo_arc::Arc; +use std::ffi::{CStr, CString}; +use std::fmt::{self, Write}; +use std::str; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// An [`@supports`][supports] rule. +/// +/// [supports]: https://drafts.csswg.org/css-conditional-3/#at-supports +#[derive(Debug, ToShmem)] +pub struct SupportsRule { + /// The parsed condition + pub condition: SupportsCondition, + /// Child rules + pub rules: Arc<Locked<CssRules>>, + /// The result of evaluating the condition + pub enabled: bool, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl SupportsRule { + /// 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) + } +} + +impl ToCssWithGuard for SupportsRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@supports ")?; + self.condition.to_css(&mut CssWriter::new(dest))?; + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +impl DeepCloneWithLock for SupportsRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(guard); + SupportsRule { + condition: self.condition.clone(), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + enabled: self.enabled, + source_location: self.source_location.clone(), + } + } +} + +/// An @supports condition +/// +/// <https://drafts.csswg.org/css-conditional-3/#at-supports> +#[derive(Clone, Debug, ToShmem)] +pub enum SupportsCondition { + /// `not (condition)` + Not(Box<SupportsCondition>), + /// `(condition)` + Parenthesized(Box<SupportsCondition>), + /// `(condition) and (condition) and (condition) ..` + And(Vec<SupportsCondition>), + /// `(condition) or (condition) or (condition) ..` + Or(Vec<SupportsCondition>), + /// `property-ident: value` (value can be any tokens) + Declaration(Declaration), + /// A `selector()` function. + Selector(RawSelector), + /// `-moz-bool-pref("pref-name")` + /// Since we need to pass it through FFI to get the pref value, + /// we store it as CString directly. + MozBoolPref(CString), + /// `font-format(<font-format>)` + FontFormat(FontFaceSourceFormatKeyword), + /// `font-tech(<font-tech>)` + FontTech(FontFaceSourceTechFlags), + /// `(any tokens)` or `func(any tokens)` + FutureSyntax(String), +} + +impl SupportsCondition { + /// Parse a condition + /// + /// <https://drafts.csswg.org/css-conditional/#supports_condition> + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() { + let inner = SupportsCondition::parse_in_parens(input)?; + return Ok(SupportsCondition::Not(Box::new(inner))); + } + + let in_parens = SupportsCondition::parse_in_parens(input)?; + + let location = input.current_source_location(); + let (keyword, wrapper) = match input.next() { + // End of input + Err(..) => return Ok(in_parens), + Ok(&Token::Ident(ref ident)) => { + match_ignore_ascii_case! { &ident, + "and" => ("and", SupportsCondition::And as fn(_) -> _), + "or" => ("or", SupportsCondition::Or as fn(_) -> _), + _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + } + }, + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + }; + + let mut conditions = Vec::with_capacity(2); + conditions.push(in_parens); + loop { + conditions.push(SupportsCondition::parse_in_parens(input)?); + if input + .try_parse(|input| input.expect_ident_matching(keyword)) + .is_err() + { + // Did not find the expected keyword. + // If we found some other token, it will be rejected by + // `Parser::parse_entirely` somewhere up the stack. + return Ok(wrapper(conditions)); + } + } + } + + /// Parses a functional supports condition. + fn parse_functional<'i, 't>( + function: &str, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + match_ignore_ascii_case! { function, + // Although this is an internal syntax, it is not necessary + // to check parsing context as far as we accept any + // unexpected token as future syntax, and evaluate it to + // false when not in chrome / ua sheet. + // See https://drafts.csswg.org/css-conditional-3/#general_enclosed + "-moz-bool-pref" => { + let name = { + let name = input.expect_string()?; + CString::new(name.as_bytes()) + }.map_err(|_| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?; + Ok(SupportsCondition::MozBoolPref(name)) + }, + "selector" => { + let pos = input.position(); + consume_any_value(input)?; + Ok(SupportsCondition::Selector(RawSelector( + input.slice_from(pos).to_owned() + ))) + }, + "font-format" if static_prefs::pref!("layout.css.font-tech.enabled") => { + let kw = FontFaceSourceFormatKeyword::parse(input)?; + Ok(SupportsCondition::FontFormat(kw)) + }, + "font-tech" if static_prefs::pref!("layout.css.font-tech.enabled") => { + let flag = FontFaceSourceTechFlags::parse_one(input)?; + Ok(SupportsCondition::FontTech(flag)) + }, + _ => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + } + + /// Parses an `@import` condition as per + /// https://drafts.csswg.org/css-cascade-5/#typedef-import-conditions + pub fn parse_for_import<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("supports")?; + input.parse_nested_block(parse_condition_or_declaration) + } + + /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens> + fn parse_in_parens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + // Whitespace is normally taken care of in `Parser::next`, but we want to not include it in + // `pos` for the SupportsCondition::FutureSyntax cases. + input.skip_whitespace(); + let pos = input.position(); + let location = input.current_source_location(); + match *input.next()? { + Token::ParenthesisBlock => { + let nested = input + .try_parse(|input| input.parse_nested_block(parse_condition_or_declaration)); + if let Ok(nested) = nested { + return Ok(Self::Parenthesized(Box::new(nested))); + } + }, + Token::Function(ref ident) => { + let ident = ident.clone(); + let nested = input.try_parse(|input| { + input.parse_nested_block(|input| { + SupportsCondition::parse_functional(&ident, input) + }) + }); + if nested.is_ok() { + return nested; + } + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + input.parse_nested_block(consume_any_value)?; + Ok(SupportsCondition::FutureSyntax( + input.slice_from(pos).to_owned(), + )) + } + + /// Evaluate a supports condition + pub fn eval(&self, cx: &ParserContext) -> bool { + match *self { + SupportsCondition::Not(ref cond) => !cond.eval(cx), + SupportsCondition::Parenthesized(ref cond) => cond.eval(cx), + SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx)), + SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx)), + SupportsCondition::Declaration(ref decl) => decl.eval(cx), + SupportsCondition::MozBoolPref(ref name) => eval_moz_bool_pref(name, cx), + SupportsCondition::Selector(ref selector) => selector.eval(cx), + SupportsCondition::FontFormat(ref format) => eval_font_format(format), + SupportsCondition::FontTech(ref tech) => eval_font_tech(tech), + SupportsCondition::FutureSyntax(_) => false, + } + } +} + +#[cfg(feature = "gecko")] +fn eval_moz_bool_pref(name: &CStr, cx: &ParserContext) -> bool { + use crate::gecko_bindings::bindings; + if !cx.in_ua_or_chrome_sheet() { + return false; + } + unsafe { bindings::Gecko_GetBoolPrefValue(name.as_ptr()) } +} + +fn eval_font_format(kw: &FontFaceSourceFormatKeyword) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsFontFormatSupported(*kw) } +} + +fn eval_font_tech(flag: &FontFaceSourceTechFlags) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsFontTechSupported(*flag) } +} + +#[cfg(feature = "servo")] +fn eval_moz_bool_pref(_: &CStr, _: &ParserContext) -> bool { + false +} + +/// supports_condition | declaration +/// <https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext> +pub fn parse_condition_or_declaration<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<SupportsCondition, ParseError<'i>> { + if let Ok(condition) = input.try_parse(SupportsCondition::parse) { + Ok(condition) + } else { + Declaration::parse(input).map(SupportsCondition::Declaration) + } +} + +impl ToCss for SupportsCondition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + SupportsCondition::Not(ref cond) => { + dest.write_str("not ")?; + cond.to_css(dest) + }, + SupportsCondition::Parenthesized(ref cond) => { + dest.write_char('(')?; + cond.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::And(ref vec) => { + let mut first = true; + for cond in vec { + if !first { + dest.write_str(" and ")?; + } + first = false; + cond.to_css(dest)?; + } + Ok(()) + }, + SupportsCondition::Or(ref vec) => { + let mut first = true; + for cond in vec { + if !first { + dest.write_str(" or ")?; + } + first = false; + cond.to_css(dest)?; + } + Ok(()) + }, + SupportsCondition::Declaration(ref decl) => decl.to_css(dest), + SupportsCondition::Selector(ref selector) => { + dest.write_str("selector(")?; + selector.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::MozBoolPref(ref name) => { + dest.write_str("-moz-bool-pref(")?; + let name = + str::from_utf8(name.as_bytes()).expect("Should be parsed from valid UTF-8"); + name.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::FontFormat(ref kw) => { + dest.write_str("font-format(")?; + kw.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::FontTech(ref flag) => { + dest.write_str("font-tech(")?; + flag.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::FutureSyntax(ref s) => dest.write_str(&s), + } + } +} + +#[derive(Clone, Debug, ToShmem)] +/// A possibly-invalid CSS selector. +pub struct RawSelector(pub String); + +impl ToCss for RawSelector { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.0) + } +} + +impl RawSelector { + /// Tries to evaluate a `selector()` function. + pub fn eval(&self, context: &ParserContext) -> bool { + let mut input = ParserInput::new(&self.0); + let mut input = Parser::new(&mut input); + input + .parse_entirely(|input| -> Result<(), CssParseError<()>> { + let parser = SelectorParser { + namespaces: &context.namespaces, + stylesheet_origin: context.stylesheet_origin, + url_data: context.url_data, + for_supports_rule: true, + }; + + Selector::<SelectorImpl>::parse(&parser, input) + .map_err(|_| input.new_custom_error(()))?; + + Ok(()) + }) + .is_ok() + } +} + +#[derive(Clone, Debug, ToShmem)] +/// A possibly-invalid property declaration +pub struct Declaration(pub String); + +impl ToCss for Declaration { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.0) + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value> +fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + input.expect_no_error_token().map_err(|err| err.into()) +} + +impl Declaration { + /// Parse a declaration + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Declaration, ParseError<'i>> { + let pos = input.position(); + input.expect_ident()?; + input.expect_colon()?; + consume_any_value(input)?; + Ok(Declaration(input.slice_from(pos).to_owned())) + } + + /// Determine if a declaration parses + /// + /// <https://drafts.csswg.org/css-conditional-3/#support-definition> + pub fn eval(&self, context: &ParserContext) -> bool { + debug_assert!(context.rule_types().contains(CssRuleType::Style)); + + let mut input = ParserInput::new(&self.0); + let mut input = Parser::new(&mut input); + input + .parse_entirely(|input| -> Result<(), CssParseError<()>> { + let prop = input.expect_ident_cloned().unwrap(); + input.expect_colon().unwrap(); + + let id = + PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?; + + let mut declarations = SourcePropertyDeclaration::default(); + input.parse_until_before(Delimiter::Bang, |input| { + PropertyDeclaration::parse_into(&mut declarations, id, &context, input) + .map_err(|_| input.new_custom_error(())) + })?; + let _ = input.try_parse(parse_important); + Ok(()) + }) + .is_ok() + } +} diff --git a/servo/components/style/stylesheets/viewport_rule.rs b/servo/components/style/stylesheets/viewport_rule.rs new file mode 100644 index 0000000000..08fc788789 --- /dev/null +++ b/servo/components/style/stylesheets/viewport_rule.rs @@ -0,0 +1,810 @@ +/* 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/. */ + +//! The [`@viewport`][at] at-rule and [`meta`][meta] element. +//! +//! [at]: https://drafts.csswg.org/css-device-adapt/#atviewport-rule +//! [meta]: https://drafts.csswg.org/css-device-adapt/#viewport-meta + +use crate::context::QuirksMode; +use crate::error_reporting::ContextualParseError; +use crate::media_queries::Device; +use crate::parser::{Parse, ParserContext}; +use crate::properties::StyleBuilder; +use crate::rule_cache::RuleCacheConditions; +use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::cascading_at_rule::DescriptorDeclaration; +use crate::stylesheets::container_rule::ContainerSizeQuery; +use crate::stylesheets::{Origin, StylesheetInDocument}; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::length::LengthPercentageOrAuto; +use crate::values::generics::NonNegative; +use crate::values::specified::{self, NoCalcLength}; +use crate::values::specified::{NonNegativeLengthPercentageOrAuto, ViewportPercentageLength}; +use app_units::Au; +use cssparser::{ + parse_important, AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, + RuleBodyItemParser, RuleBodyParser, +}; +use euclid::Size2D; +use selectors::parser::SelectorParseErrorKind; +use std::borrow::Cow; +use std::fmt::{self, Write}; +use std::iter::Enumerate; +use std::str::Chars; +use style_traits::viewport::{Orientation, UserZoom, ViewportConstraints, Zoom}; +use style_traits::{CssWriter, ParseError, PinchZoomFactor, StyleParseErrorKind, ToCss}; + +/// Whether parsing and processing of `@viewport` rules is enabled. +#[cfg(feature = "servo")] +pub fn enabled() -> bool { + use servo_config::pref; + pref!(layout.viewport.enabled) +} + +/// Whether parsing and processing of `@viewport` rules is enabled. +#[cfg(not(feature = "servo"))] +pub fn enabled() -> bool { + false // Gecko doesn't support @viewport. +} + +macro_rules! declare_viewport_descriptor { + ( $( $variant_name: expr => $variant: ident($data: ident), )+ ) => { + declare_viewport_descriptor_inner!([] [ $( $variant_name => $variant($data), )+ ] 0); + }; +} + +macro_rules! declare_viewport_descriptor_inner { + ( + [ $( $assigned_variant_name: expr => + $assigned_variant: ident($assigned_data: ident) = $assigned_discriminant: expr, )* ] + [ + $next_variant_name: expr => $next_variant: ident($next_data: ident), + $( $variant_name: expr => $variant: ident($data: ident), )* + ] + $next_discriminant: expr + ) => { + declare_viewport_descriptor_inner! { + [ + $( $assigned_variant_name => $assigned_variant($assigned_data) = $assigned_discriminant, )* + $next_variant_name => $next_variant($next_data) = $next_discriminant, + ] + [ $( $variant_name => $variant($data), )* ] + $next_discriminant + 1 + } + }; + + ( + [ $( $assigned_variant_name: expr => + $assigned_variant: ident($assigned_data: ident) = $assigned_discriminant: expr, )* ] + [ ] + $number_of_variants: expr + ) => { + #[derive(Clone, Debug, PartialEq, ToShmem)] + #[cfg_attr(feature = "servo", derive(MallocSizeOf))] + #[allow(missing_docs)] + pub enum ViewportDescriptor { + $( + $assigned_variant($assigned_data), + )+ + } + + const VIEWPORT_DESCRIPTOR_VARIANTS: usize = $number_of_variants; + + impl ViewportDescriptor { + #[allow(missing_docs)] + pub fn discriminant_value(&self) -> usize { + match *self { + $( + ViewportDescriptor::$assigned_variant(..) => $assigned_discriminant, + )* + } + } + } + + impl ToCss for ViewportDescriptor { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + $( + ViewportDescriptor::$assigned_variant(ref val) => { + dest.write_str($assigned_variant_name)?; + dest.write_str(": ")?; + val.to_css(dest)?; + }, + )* + } + dest.write_char(';') + } + } + }; +} + +declare_viewport_descriptor! { + "min-width" => MinWidth(ViewportLength), + "max-width" => MaxWidth(ViewportLength), + + "min-height" => MinHeight(ViewportLength), + "max-height" => MaxHeight(ViewportLength), + + "zoom" => Zoom(Zoom), + "min-zoom" => MinZoom(Zoom), + "max-zoom" => MaxZoom(Zoom), + + "user-zoom" => UserZoom(UserZoom), + "orientation" => Orientation(Orientation), +} + +trait FromMeta: Sized { + fn from_meta(value: &str) -> Option<Self>; +} + +/// ViewportLength is a length | percentage | auto | extend-to-zoom +/// See: +/// * http://dev.w3.org/csswg/css-device-adapt/#min-max-width-desc +/// * http://dev.w3.org/csswg/css-device-adapt/#extend-to-zoom +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub enum ViewportLength { + Specified(NonNegativeLengthPercentageOrAuto), + ExtendToZoom, +} + +impl FromMeta for ViewportLength { + fn from_meta(value: &str) -> Option<ViewportLength> { + macro_rules! specified { + ($value:expr) => { + ViewportLength::Specified(LengthPercentageOrAuto::LengthPercentage(NonNegative( + specified::LengthPercentage::Length($value), + ))) + }; + } + + Some(match value { + v if v.eq_ignore_ascii_case("device-width") => specified!( + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(100.)) + ), + v if v.eq_ignore_ascii_case("device-height") => specified!( + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(100.)) + ), + _ => match value.parse::<f32>() { + Ok(n) if n >= 0. => specified!(NoCalcLength::from_px(n.max(1.).min(10000.))), + Ok(_) => return None, + Err(_) => specified!(NoCalcLength::from_px(1.)), + }, + }) + } +} + +impl ViewportLength { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // we explicitly do not accept 'extend-to-zoom', since it is a UA + // internal value for <META> viewport translation + NonNegativeLengthPercentageOrAuto::parse(context, input).map(ViewportLength::Specified) + } +} + +impl FromMeta for Zoom { + fn from_meta(value: &str) -> Option<Zoom> { + Some(match value { + v if v.eq_ignore_ascii_case("yes") => Zoom::Number(1.), + v if v.eq_ignore_ascii_case("no") => Zoom::Number(0.1), + v if v.eq_ignore_ascii_case("device-width") => Zoom::Number(10.), + v if v.eq_ignore_ascii_case("device-height") => Zoom::Number(10.), + _ => match value.parse::<f32>() { + Ok(n) if n >= 0. => Zoom::Number(n.max(0.1).min(10.)), + Ok(_) => return None, + Err(_) => Zoom::Number(0.1), + }, + }) + } +} + +impl FromMeta for UserZoom { + fn from_meta(value: &str) -> Option<UserZoom> { + Some(match value { + v if v.eq_ignore_ascii_case("yes") => UserZoom::Zoom, + v if v.eq_ignore_ascii_case("no") => UserZoom::Fixed, + v if v.eq_ignore_ascii_case("device-width") => UserZoom::Zoom, + v if v.eq_ignore_ascii_case("device-height") => UserZoom::Zoom, + _ => match value.parse::<f32>() { + Ok(n) if n >= 1. || n <= -1. => UserZoom::Zoom, + _ => UserZoom::Fixed, + }, + }) + } +} + +struct ViewportRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, +} + +#[allow(missing_docs)] +pub type ViewportDescriptorDeclaration = DescriptorDeclaration<ViewportDescriptor>; + +fn parse_shorthand<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<(ViewportLength, ViewportLength), ParseError<'i>> { + let min = ViewportLength::parse(context, input)?; + match input.try_parse(|i| ViewportLength::parse(context, i)) { + Err(_) => Ok((min.clone(), min)), + Ok(max) => Ok((min, max)), + } +} + +type ViewportDeclarations = Vec<ViewportDescriptorDeclaration>; + +impl<'a, 'b, 'i> AtRuleParser<'i> for ViewportRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = ViewportDeclarations; + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for ViewportRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = ViewportDeclarations; + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for ViewportRuleParser<'a, 'b> { + type Declaration = Vec<ViewportDescriptorDeclaration>; + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Vec<ViewportDescriptorDeclaration>, ParseError<'i>> { + macro_rules! declaration { + ($declaration:ident($parse:expr)) => { + declaration!($declaration { + value: $parse(input)?, + important: input.try_parse(parse_important).is_ok(), + }) + }; + ($declaration:ident { value: $value:expr, important: $important:expr, }) => { + ViewportDescriptorDeclaration::new( + self.context.stylesheet_origin, + ViewportDescriptor::$declaration($value), + $important, + ) + }; + } + + macro_rules! ok { + ($declaration:ident($parse:expr)) => { + Ok(vec![declaration!($declaration($parse))]) + }; + (shorthand -> [$min:ident, $max:ident]) => {{ + let shorthand = parse_shorthand(self.context, input)?; + let important = input.try_parse(parse_important).is_ok(); + + Ok(vec![ + declaration!($min { + value: shorthand.0, + important: important, + }), + declaration!($max { + value: shorthand.1, + important: important, + }), + ]) + }}; + } + + match_ignore_ascii_case! { &*name, + "min-width" => ok!(MinWidth(|i| ViewportLength::parse(self.context, i))), + "max-width" => ok!(MaxWidth(|i| ViewportLength::parse(self.context, i))), + "width" => ok!(shorthand -> [MinWidth, MaxWidth]), + "min-height" => ok!(MinHeight(|i| ViewportLength::parse(self.context, i))), + "max-height" => ok!(MaxHeight(|i| ViewportLength::parse(self.context, i))), + "height" => ok!(shorthand -> [MinHeight, MaxHeight]), + "zoom" => ok!(Zoom(Zoom::parse)), + "min-zoom" => ok!(MinZoom(Zoom::parse)), + "max-zoom" => ok!(MaxZoom(Zoom::parse)), + "user-zoom" => ok!(UserZoom(UserZoom::parse)), + "orientation" => ok!(Orientation(Orientation::parse)), + _ => Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + } +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, ViewportDeclarations, StyleParseErrorKind<'i>> + for ViewportRuleParser<'a, 'b> +{ + fn parse_declarations(&self) -> bool { + true + } + fn parse_qualified(&self) -> bool { + false + } +} + +/// A `@viewport` rule. +#[derive(Clone, Debug, PartialEq, ToShmem)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct ViewportRule { + /// The declarations contained in this @viewport rule. + pub declarations: Vec<ViewportDescriptorDeclaration>, +} + +/// Whitespace as defined by DEVICE-ADAPT § 9.2 +// TODO: should we just use whitespace as defined by HTML5? +const WHITESPACE: &'static [char] = &['\t', '\n', '\r', ' ']; + +/// Separators as defined by DEVICE-ADAPT § 9.2 +// need to use \x2c instead of ',' due to test-tidy +const SEPARATOR: &'static [char] = &['\x2c', ';']; + +#[inline] +fn is_whitespace_separator_or_equals(c: &char) -> bool { + WHITESPACE.contains(c) || SEPARATOR.contains(c) || *c == '=' +} + +impl ViewportRule { + /// Parse a single @viewport rule. + /// + /// TODO(emilio): This could use the `Parse` trait now. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut parser = ViewportRuleParser { context }; + + let mut cascade = Cascade::new(); + let mut parser = RuleBodyParser::new(input, &mut parser); + while let Some(result) = parser.next() { + match result { + Ok(declarations) => { + for declarations in declarations { + cascade.add(Cow::Owned(declarations)) + } + }, + Err((error, slice)) => { + let location = error.location; + let error = ContextualParseError::UnsupportedViewportDescriptorDeclaration( + slice, error, + ); + context.log_css_error(location, error); + }, + } + } + Ok(ViewportRule { + declarations: cascade.finish(), + }) + } +} + +impl ViewportRule { + #[allow(missing_docs)] + pub fn from_meta(content: &str) -> Option<ViewportRule> { + let mut declarations = vec![None; VIEWPORT_DESCRIPTOR_VARIANTS]; + macro_rules! push_descriptor { + ($descriptor:ident($value:expr)) => {{ + let descriptor = ViewportDescriptor::$descriptor($value); + let discriminant = descriptor.discriminant_value(); + declarations[discriminant] = Some(ViewportDescriptorDeclaration::new( + Origin::Author, + descriptor, + false, + )); + }}; + } + + let mut has_width = false; + let mut has_height = false; + let mut has_zoom = false; + + let mut iter = content.chars().enumerate(); + + macro_rules! start_of_name { + ($iter:ident) => { + $iter + .by_ref() + .skip_while(|&(_, c)| is_whitespace_separator_or_equals(&c)) + .next() + }; + } + + while let Some((start, _)) = start_of_name!(iter) { + let property = ViewportRule::parse_meta_property(content, &mut iter, start); + + if let Some((name, value)) = property { + macro_rules! push { + ($descriptor:ident($translate:path)) => { + if let Some(value) = $translate(value) { + push_descriptor!($descriptor(value)); + } + }; + } + + match name { + n if n.eq_ignore_ascii_case("width") => { + if let Some(value) = ViewportLength::from_meta(value) { + push_descriptor!(MinWidth(ViewportLength::ExtendToZoom)); + push_descriptor!(MaxWidth(value)); + has_width = true; + } + }, + n if n.eq_ignore_ascii_case("height") => { + if let Some(value) = ViewportLength::from_meta(value) { + push_descriptor!(MinHeight(ViewportLength::ExtendToZoom)); + push_descriptor!(MaxHeight(value)); + has_height = true; + } + }, + n if n.eq_ignore_ascii_case("initial-scale") => { + if let Some(value) = Zoom::from_meta(value) { + push_descriptor!(Zoom(value)); + has_zoom = true; + } + }, + n if n.eq_ignore_ascii_case("minimum-scale") => push!(MinZoom(Zoom::from_meta)), + n if n.eq_ignore_ascii_case("maximum-scale") => push!(MaxZoom(Zoom::from_meta)), + n if n.eq_ignore_ascii_case("user-scalable") => { + push!(UserZoom(UserZoom::from_meta)) + }, + _ => {}, + } + } + } + + // DEVICE-ADAPT § 9.4 - The 'width' and 'height' properties + // http://dev.w3.org/csswg/css-device-adapt/#width-and-height-properties + if !has_width && has_zoom { + if has_height { + push_descriptor!(MinWidth(ViewportLength::Specified( + LengthPercentageOrAuto::Auto + ))); + push_descriptor!(MaxWidth(ViewportLength::Specified( + LengthPercentageOrAuto::Auto + ))); + } else { + push_descriptor!(MinWidth(ViewportLength::ExtendToZoom)); + push_descriptor!(MaxWidth(ViewportLength::ExtendToZoom)); + } + } + + let declarations: Vec<_> = declarations.into_iter().filter_map(|entry| entry).collect(); + if !declarations.is_empty() { + Some(ViewportRule { declarations }) + } else { + None + } + } + + fn parse_meta_property<'a>( + content: &'a str, + iter: &mut Enumerate<Chars<'a>>, + start: usize, + ) -> Option<(&'a str, &'a str)> { + fn end_of_token(iter: &mut Enumerate<Chars>) -> Option<(usize, char)> { + iter.by_ref() + .skip_while(|&(_, c)| !is_whitespace_separator_or_equals(&c)) + .next() + } + + fn skip_whitespace(iter: &mut Enumerate<Chars>) -> Option<(usize, char)> { + iter.by_ref() + .skip_while(|&(_, c)| WHITESPACE.contains(&c)) + .next() + } + + // <name> <whitespace>* '=' + let end = match end_of_token(iter) { + Some((end, c)) if WHITESPACE.contains(&c) => match skip_whitespace(iter) { + Some((_, c)) if c == '=' => end, + _ => return None, + }, + Some((end, c)) if c == '=' => end, + _ => return None, + }; + let name = &content[start..end]; + + // <whitespace>* <value> + let start = match skip_whitespace(iter) { + Some((start, c)) if !SEPARATOR.contains(&c) => start, + _ => return None, + }; + let value = match end_of_token(iter) { + Some((end, _)) => &content[start..end], + _ => &content[start..], + }; + + Some((name, value)) + } +} + +impl ToCssWithGuard for ViewportRule { + // Serialization of ViewportRule is not specced. + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@viewport { ")?; + let mut iter = self.declarations.iter(); + iter.next().unwrap().to_css(&mut CssWriter::new(dest))?; + for declaration in iter { + dest.write_char(' ')?; + declaration.to_css(&mut CssWriter::new(dest))?; + } + dest.write_str(" }") + } +} + +#[allow(missing_docs)] +pub struct Cascade { + declarations: Vec<Option<(usize, ViewportDescriptorDeclaration)>>, + count_so_far: usize, +} + +#[allow(missing_docs)] +impl Cascade { + pub fn new() -> Self { + Cascade { + declarations: vec![None; VIEWPORT_DESCRIPTOR_VARIANTS], + count_so_far: 0, + } + } + + pub fn from_stylesheets<'a, I, S>( + stylesheets: I, + guards: &StylesheetGuards, + device: &Device, + ) -> Self + where + I: Iterator<Item = (&'a S, Origin)>, + S: StylesheetInDocument + 'static, + { + let mut cascade = Self::new(); + for (stylesheet, origin) in stylesheets { + stylesheet.effective_viewport_rules(device, guards.for_origin(origin), |rule| { + for declaration in &rule.declarations { + cascade.add(Cow::Borrowed(declaration)) + } + }) + } + cascade + } + + pub fn add(&mut self, declaration: Cow<ViewportDescriptorDeclaration>) { + let descriptor = declaration.descriptor.discriminant_value(); + + match self.declarations[descriptor] { + Some((ref mut order_of_appearance, ref mut entry_declaration)) => { + if declaration.higher_or_equal_precendence(entry_declaration) { + *entry_declaration = declaration.into_owned(); + *order_of_appearance = self.count_so_far; + } + }, + ref mut entry @ None => { + *entry = Some((self.count_so_far, declaration.into_owned())); + }, + } + self.count_so_far += 1; + } + + pub fn finish(mut self) -> Vec<ViewportDescriptorDeclaration> { + // sort the descriptors by order of appearance + self.declarations + .sort_by_key(|entry| entry.as_ref().map(|&(index, _)| index)); + self.declarations + .into_iter() + .filter_map(|entry| entry.map(|(_, decl)| decl)) + .collect() + } +} + +/// Just a helper trait to be able to implement methods on ViewportConstraints. +pub trait MaybeNew { + /// Create a ViewportConstraints from a viewport size and a `@viewport` + /// rule. + fn maybe_new( + device: &Device, + rule: &ViewportRule, + quirks_mode: QuirksMode, + ) -> Option<ViewportConstraints>; +} + +impl MaybeNew for ViewportConstraints { + fn maybe_new( + device: &Device, + rule: &ViewportRule, + quirks_mode: QuirksMode, + ) -> Option<ViewportConstraints> { + use std::cmp; + + if rule.declarations.is_empty() { + return None; + } + + let mut min_width = None; + let mut max_width = None; + + let mut min_height = None; + let mut max_height = None; + + let mut initial_zoom = None; + let mut min_zoom = None; + let mut max_zoom = None; + + let mut user_zoom = UserZoom::Zoom; + let mut orientation = Orientation::Auto; + + // collapse the list of declarations into descriptor values + for declaration in &rule.declarations { + match declaration.descriptor { + ViewportDescriptor::MinWidth(ref value) => min_width = Some(value), + ViewportDescriptor::MaxWidth(ref value) => max_width = Some(value), + + ViewportDescriptor::MinHeight(ref value) => min_height = Some(value), + ViewportDescriptor::MaxHeight(ref value) => max_height = Some(value), + + ViewportDescriptor::Zoom(value) => initial_zoom = value.to_f32(), + ViewportDescriptor::MinZoom(value) => min_zoom = value.to_f32(), + ViewportDescriptor::MaxZoom(value) => max_zoom = value.to_f32(), + + ViewportDescriptor::UserZoom(value) => user_zoom = value, + ViewportDescriptor::Orientation(value) => orientation = value, + } + } + + // TODO: return `None` if all descriptors are either absent or initial value + + macro_rules! choose { + ($op:ident, $opta:expr, $optb:expr) => { + match ($opta, $optb) { + (None, None) => None, + (a, None) => a, + (None, b) => b, + (Some(a), Some(b)) => Some(a.$op(b)), + } + }; + } + macro_rules! min { + ($opta:expr, $optb:expr) => { + choose!(min, $opta, $optb) + }; + } + macro_rules! max { + ($opta:expr, $optb:expr) => { + choose!(max, $opta, $optb) + }; + } + + // DEVICE-ADAPT § 6.2.1 Resolve min-zoom and max-zoom values + if min_zoom.is_some() && max_zoom.is_some() { + max_zoom = Some(min_zoom.unwrap().max(max_zoom.unwrap())) + } + + // DEVICE-ADAPT § 6.2.2 Constrain zoom value to the [min-zoom, max-zoom] range + if initial_zoom.is_some() { + initial_zoom = max!(min_zoom, min!(max_zoom, initial_zoom)); + } + + // DEVICE-ADAPT § 6.2.3 Resolve non-auto lengths to pixel lengths + let initial_viewport = device.au_viewport_size(); + + let mut conditions = RuleCacheConditions::default(); + let context = Context::new( + // Note: DEVICE-ADAPT § 5. states that relative length values are + // resolved against initial values + StyleBuilder::for_inheritance(device, None, None), + quirks_mode, + &mut conditions, + ContainerSizeQuery::none(), + ); + + // DEVICE-ADAPT § 9.3 Resolving 'extend-to-zoom' + let extend_width; + let extend_height; + if let Some(extend_zoom) = max!(initial_zoom, max_zoom) { + let scale_factor = 1. / extend_zoom; + extend_width = Some(initial_viewport.width.scale_by(scale_factor)); + extend_height = Some(initial_viewport.height.scale_by(scale_factor)); + } else { + extend_width = None; + extend_height = None; + } + + macro_rules! to_pixel_length { + ($value:ident, $dimension:ident, $extend_to:ident => $auto_extend_to:expr) => { + if let Some($value) = $value { + match *$value { + ViewportLength::Specified(ref length) => match *length { + LengthPercentageOrAuto::Auto => None, + LengthPercentageOrAuto::LengthPercentage(ref lop) => Some( + lop.to_computed_value(&context) + .to_used_value(initial_viewport.$dimension), + ), + }, + ViewportLength::ExtendToZoom => { + // $extend_to will be 'None' if 'extend-to-zoom' is 'auto' + match ($extend_to, $auto_extend_to) { + (None, None) => None, + (a, None) => a, + (None, b) => b, + (a, b) => cmp::max(a, b), + } + }, + } + } else { + None + } + }; + } + + // DEVICE-ADAPT § 9.3 states that max-descriptors need to be resolved + // before min-descriptors. + // http://dev.w3.org/csswg/css-device-adapt/#resolve-extend-to-zoom + let max_width = to_pixel_length!(max_width, width, extend_width => None); + let max_height = to_pixel_length!(max_height, height, extend_height => None); + + let min_width = to_pixel_length!(min_width, width, extend_width => max_width); + let min_height = to_pixel_length!(min_height, height, extend_height => max_height); + + // DEVICE-ADAPT § 6.2.4 Resolve initial width and height from min/max descriptors + macro_rules! resolve { + ($min:ident, $max:ident, $initial:expr) => { + if $min.is_some() || $max.is_some() { + let max = match $max { + Some(max) => cmp::min(max, $initial), + None => $initial, + }; + + Some(match $min { + Some(min) => cmp::max(min, max), + None => max, + }) + } else { + None + } + }; + } + + let width = resolve!(min_width, max_width, initial_viewport.width); + let height = resolve!(min_height, max_height, initial_viewport.height); + + // DEVICE-ADAPT § 6.2.5 Resolve width value + let width = if width.is_none() && height.is_none() { + Some(initial_viewport.width) + } else { + width + }; + + let width = width.unwrap_or_else(|| match initial_viewport.height { + Au(0) => initial_viewport.width, + initial_height => { + let ratio = initial_viewport.width.to_f32_px() / initial_height.to_f32_px(); + Au::from_f32_px(height.unwrap().to_f32_px() * ratio) + }, + }); + + // DEVICE-ADAPT § 6.2.6 Resolve height value + let height = height.unwrap_or_else(|| match initial_viewport.width { + Au(0) => initial_viewport.height, + initial_width => { + let ratio = initial_viewport.height.to_f32_px() / initial_width.to_f32_px(); + Au::from_f32_px(width.to_f32_px() * ratio) + }, + }); + + Some(ViewportConstraints { + size: Size2D::new(width.to_f32_px(), height.to_f32_px()), + + // TODO: compute a zoom factor for 'auto' as suggested by DEVICE-ADAPT § 10. + initial_zoom: PinchZoomFactor::new(initial_zoom.unwrap_or(1.)), + min_zoom: min_zoom.map(PinchZoomFactor::new), + max_zoom: max_zoom.map(PinchZoomFactor::new), + + user_zoom, + orientation, + }) + } +} |