summaryrefslogtreecommitdiffstats
path: root/servo/components/style_traits/values.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--servo/components/style_traits/values.rs615
1 files changed, 615 insertions, 0 deletions
diff --git a/servo/components/style_traits/values.rs b/servo/components/style_traits/values.rs
new file mode 100644
index 0000000000..96bb23651f
--- /dev/null
+++ b/servo/components/style_traits/values.rs
@@ -0,0 +1,615 @@
+/* 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/. */
+
+//! Helper types and traits for the handling of CSS values.
+
+use app_units::Au;
+use cssparser::ToCss as CssparserToCss;
+use cssparser::{serialize_string, ParseError, Parser, Token, UnicodeRange};
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+
+/// Serialises a value according to its CSS representation.
+///
+/// This trait is implemented for `str` and its friends, serialising the string
+/// contents as a CSS quoted string.
+///
+/// This trait is derivable with `#[derive(ToCss)]`, with the following behaviour:
+/// * unit variants get serialised as the `snake-case` representation
+/// of their name;
+/// * unit variants whose name starts with "Moz" or "Webkit" are prepended
+/// with a "-";
+/// * if `#[css(comma)]` is found on a variant, its fields are separated by
+/// commas, otherwise, by spaces;
+/// * if `#[css(function)]` is found on a variant, the variant name gets
+/// serialised like unit variants and its fields are surrounded by parentheses;
+/// * if `#[css(iterable)]` is found on a function variant, that variant needs
+/// to have a single member, and that member needs to be iterable. The
+/// iterable will be serialized as the arguments for the function;
+/// * an iterable field can also be annotated with `#[css(if_empty = "foo")]`
+/// to print `"foo"` if the iterator is empty;
+/// * if `#[css(dimension)]` is found on a variant, that variant needs
+/// to have a single member. The variant would be serialized as a CSS
+/// dimension token, like: <member><identifier>;
+/// * if `#[css(skip)]` is found on a field, the `ToCss` call for that field
+/// is skipped;
+/// * if `#[css(skip_if = "function")]` is found on a field, the `ToCss` call
+/// for that field is skipped if `function` returns true. This function is
+/// provided the field as an argument;
+/// * if `#[css(contextual_skip_if = "function")]` is found on a field, the
+/// `ToCss` call for that field is skipped if `function` returns true. This
+/// function is given all the fields in the current struct or variant as an
+/// argument;
+/// * `#[css(represents_keyword)]` can be used on bool fields in order to
+/// serialize the field name if the field is true, or nothing otherwise. It
+/// also collects those keywords for `SpecifiedValueInfo`.
+/// * `#[css(bitflags(single="", mixed="", validate="", overlapping_bits)]` can
+/// be used to derive parse / serialize / etc on bitflags. The rules for parsing
+/// bitflags are the following:
+///
+/// * `single` flags can only appear on their own. It's common that bitflags
+/// properties at least have one such value like `none` or `auto`.
+/// * `mixed` properties can appear mixed together, but not along any other
+/// flag that shares a bit with itself. For example, if you have three
+/// bitflags like:
+///
+/// FOO = 1 << 0;
+/// BAR = 1 << 1;
+/// BAZ = 1 << 2;
+/// BAZZ = BAR | BAZ;
+///
+/// Then the following combinations won't be valid:
+///
+/// * foo foo: (every flag shares a bit with itself)
+/// * bar bazz: (bazz shares a bit with bar)
+///
+/// But `bar baz` will be valid, as they don't share bits, and so would
+/// `foo` with any other flag, or `bazz` on its own.
+/// * `overlapping_bits` enables some tracking during serialization of mixed
+/// flags to avoid serializing variants that can subsume other variants.
+/// In the example above, you could do:
+/// mixed="foo,bazz,bar,baz", overlapping_bits
+/// to ensure that if bazz is serialized, bar and baz aren't, even though
+/// their bits are set. Note that the serialization order is canonical,
+/// and thus depends on the order you specify the flags in.
+///
+/// * finally, one can put `#[css(derive_debug)]` on the whole type, to
+/// implement `Debug` by a single call to `ToCss::to_css`.
+pub trait ToCss {
+ /// Serialize `self` in CSS syntax, writing to `dest`.
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write;
+
+ /// Serialize `self` in CSS syntax and return a string.
+ ///
+ /// (This is a convenience wrapper for `to_css` and probably should not be overridden.)
+ #[inline]
+ fn to_css_string(&self) -> String {
+ let mut s = String::new();
+ self.to_css(&mut CssWriter::new(&mut s)).unwrap();
+ s
+ }
+}
+
+impl<'a, T> ToCss for &'a T
+where
+ T: ToCss + ?Sized,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ (*self).to_css(dest)
+ }
+}
+
+impl ToCss for crate::owned_str::OwnedStr {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_string(self, dest)
+ }
+}
+
+impl ToCss for str {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_string(self, dest)
+ }
+}
+
+impl ToCss for String {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_string(self, dest)
+ }
+}
+
+impl<T> ToCss for Option<T>
+where
+ T: ToCss,
+{
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.as_ref().map_or(Ok(()), |value| value.to_css(dest))
+ }
+}
+
+impl ToCss for () {
+ #[inline]
+ fn to_css<W>(&self, _: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ Ok(())
+ }
+}
+
+/// A writer tailored for serialising CSS.
+///
+/// Coupled with SequenceWriter, this allows callers to transparently handle
+/// things like comma-separated values etc.
+pub struct CssWriter<'w, W: 'w> {
+ inner: &'w mut W,
+ prefix: Option<&'static str>,
+}
+
+impl<'w, W> CssWriter<'w, W>
+where
+ W: Write,
+{
+ /// Creates a new `CssWriter`.
+ #[inline]
+ pub fn new(inner: &'w mut W) -> Self {
+ Self {
+ inner,
+ prefix: Some(""),
+ }
+ }
+}
+
+impl<'w, W> Write for CssWriter<'w, W>
+where
+ W: Write,
+{
+ #[inline]
+ fn write_str(&mut self, s: &str) -> fmt::Result {
+ if s.is_empty() {
+ return Ok(());
+ }
+ if let Some(prefix) = self.prefix.take() {
+ // We are going to write things, but first we need to write
+ // the prefix that was set by `SequenceWriter::item`.
+ if !prefix.is_empty() {
+ self.inner.write_str(prefix)?;
+ }
+ }
+ self.inner.write_str(s)
+ }
+
+ #[inline]
+ fn write_char(&mut self, c: char) -> fmt::Result {
+ if let Some(prefix) = self.prefix.take() {
+ // See comment in `write_str`.
+ if !prefix.is_empty() {
+ self.inner.write_str(prefix)?;
+ }
+ }
+ self.inner.write_char(c)
+ }
+}
+
+/// Convenience wrapper to serialise CSS values separated by a given string.
+pub struct SequenceWriter<'a, 'b: 'a, W: 'b> {
+ inner: &'a mut CssWriter<'b, W>,
+ separator: &'static str,
+}
+
+impl<'a, 'b, W> SequenceWriter<'a, 'b, W>
+where
+ W: Write + 'b,
+{
+ /// Create a new sequence writer.
+ #[inline]
+ pub fn new(inner: &'a mut CssWriter<'b, W>, separator: &'static str) -> Self {
+ if inner.prefix.is_none() {
+ // See comment in `item`.
+ inner.prefix = Some("");
+ }
+ Self { inner, separator }
+ }
+
+ #[inline]
+ fn write_item<F>(&mut self, f: F) -> fmt::Result
+ where
+ F: FnOnce(&mut CssWriter<'b, W>) -> fmt::Result,
+ {
+ // Separate non-generic functions so that this code is not repeated
+ // in every monomorphization with a different type `F` or `W`.
+ // https://github.com/servo/servo/issues/26713
+ fn before(
+ prefix: &mut Option<&'static str>,
+ separator: &'static str,
+ ) -> Option<&'static str> {
+ let old_prefix = *prefix;
+ if old_prefix.is_none() {
+ // If there is no prefix in the inner writer, a previous
+ // call to this method produced output, which means we need
+ // to write the separator next time we produce output again.
+ *prefix = Some(separator);
+ }
+ old_prefix
+ }
+ fn after(
+ old_prefix: Option<&'static str>,
+ prefix: &mut Option<&'static str>,
+ separator: &'static str,
+ ) {
+ match (old_prefix, *prefix) {
+ (_, None) => {
+ // This call produced output and cleaned up after itself.
+ },
+ (None, Some(p)) => {
+ // Some previous call to `item` produced output,
+ // but this one did not, prefix should be the same as
+ // the one we set.
+ debug_assert_eq!(separator, p);
+ // We clean up here even though it's not necessary just
+ // to be able to do all these assertion checks.
+ *prefix = None;
+ },
+ (Some(old), Some(new)) => {
+ // No previous call to `item` produced output, and this one
+ // either.
+ debug_assert_eq!(old, new);
+ },
+ }
+ }
+
+ let old_prefix = before(&mut self.inner.prefix, self.separator);
+ f(self.inner)?;
+ after(old_prefix, &mut self.inner.prefix, self.separator);
+ Ok(())
+ }
+
+ /// Serialises a CSS value, writing any separator as necessary.
+ ///
+ /// The separator is never written before any `item` produces any output,
+ /// and is written in subsequent calls only if the `item` produces some
+ /// output on its own again. This lets us handle `Option<T>` fields by
+ /// just not printing anything on `None`.
+ #[inline]
+ pub fn item<T>(&mut self, item: &T) -> fmt::Result
+ where
+ T: ToCss,
+ {
+ self.write_item(|inner| item.to_css(inner))
+ }
+
+ /// Writes a string as-is (i.e. not escaped or wrapped in quotes)
+ /// with any separator as necessary.
+ ///
+ /// See SequenceWriter::item.
+ #[inline]
+ pub fn raw_item(&mut self, item: &str) -> fmt::Result {
+ self.write_item(|inner| inner.write_str(item))
+ }
+}
+
+/// Type used as the associated type in the `OneOrMoreSeparated` trait on a
+/// type to indicate that a serialized list of elements of this type is
+/// separated by commas.
+pub struct Comma;
+
+/// Type used as the associated type in the `OneOrMoreSeparated` trait on a
+/// type to indicate that a serialized list of elements of this type is
+/// separated by spaces.
+pub struct Space;
+
+/// Type used as the associated type in the `OneOrMoreSeparated` trait on a
+/// type to indicate that a serialized list of elements of this type is
+/// separated by commas, but spaces without commas are also allowed when
+/// parsing.
+pub struct CommaWithSpace;
+
+/// A trait satisfied by the types corresponding to separators.
+pub trait Separator {
+ /// The separator string that the satisfying separator type corresponds to.
+ fn separator() -> &'static str;
+
+ /// Parses a sequence of values separated by this separator.
+ ///
+ /// The given closure is called repeatedly for each item in the sequence.
+ ///
+ /// Successful results are accumulated in a vector.
+ ///
+ /// This method returns `Err(_)` the first time a closure does or if
+ /// the separators aren't correct.
+ fn parse<'i, 't, F, T, E>(
+ parser: &mut Parser<'i, 't>,
+ parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>;
+}
+
+impl Separator for Comma {
+ fn separator() -> &'static str {
+ ", "
+ }
+
+ fn parse<'i, 't, F, T, E>(
+ input: &mut Parser<'i, 't>,
+ parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>,
+ {
+ input.parse_comma_separated(parse_one)
+ }
+}
+
+impl Separator for Space {
+ fn separator() -> &'static str {
+ " "
+ }
+
+ fn parse<'i, 't, F, T, E>(
+ input: &mut Parser<'i, 't>,
+ mut parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>,
+ {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ let mut results = vec![parse_one(input)?];
+ loop {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ if let Ok(item) = input.try(&mut parse_one) {
+ results.push(item);
+ } else {
+ return Ok(results);
+ }
+ }
+ }
+}
+
+impl Separator for CommaWithSpace {
+ fn separator() -> &'static str {
+ ", "
+ }
+
+ fn parse<'i, 't, F, T, E>(
+ input: &mut Parser<'i, 't>,
+ mut parse_one: F,
+ ) -> Result<Vec<T>, ParseError<'i, E>>
+ where
+ F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>,
+ {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ let mut results = vec![parse_one(input)?];
+ loop {
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ let comma_location = input.current_source_location();
+ let comma = input.try(|i| i.expect_comma()).is_ok();
+ input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less.
+ if let Ok(item) = input.try(&mut parse_one) {
+ results.push(item);
+ } else if comma {
+ return Err(comma_location.new_unexpected_token_error(Token::Comma));
+ } else {
+ break;
+ }
+ }
+ Ok(results)
+ }
+}
+
+/// Marker trait on T to automatically implement ToCss for Vec<T> when T's are
+/// separated by some delimiter `delim`.
+pub trait OneOrMoreSeparated {
+ /// Associated type indicating which separator is used.
+ type S: Separator;
+}
+
+impl OneOrMoreSeparated for UnicodeRange {
+ type S = Comma;
+}
+
+impl<T> ToCss for Vec<T>
+where
+ T: ToCss + OneOrMoreSeparated,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let mut iter = self.iter();
+ iter.next().unwrap().to_css(dest)?;
+ for item in iter {
+ dest.write_str(<T as OneOrMoreSeparated>::S::separator())?;
+ item.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl<T> ToCss for Box<T>
+where
+ T: ?Sized + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ (**self).to_css(dest)
+ }
+}
+
+impl<T> ToCss for Arc<T>
+where
+ T: ?Sized + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ (**self).to_css(dest)
+ }
+}
+
+impl ToCss for Au {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.to_f64_px().to_css(dest)?;
+ dest.write_str("px")
+ }
+}
+
+macro_rules! impl_to_css_for_predefined_type {
+ ($name: ty) => {
+ impl<'a> ToCss for $name {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ ::cssparser::ToCss::to_css(self, dest)
+ }
+ }
+ };
+}
+
+impl_to_css_for_predefined_type!(f32);
+impl_to_css_for_predefined_type!(i8);
+impl_to_css_for_predefined_type!(i32);
+impl_to_css_for_predefined_type!(u16);
+impl_to_css_for_predefined_type!(u32);
+impl_to_css_for_predefined_type!(::cssparser::Token<'a>);
+impl_to_css_for_predefined_type!(::cssparser::RGBA);
+impl_to_css_for_predefined_type!(::cssparser::Color);
+impl_to_css_for_predefined_type!(::cssparser::UnicodeRange);
+
+/// Define an enum type with unit variants that each correspond to a CSS keyword.
+macro_rules! define_css_keyword_enum {
+ (pub enum $name:ident { $($variant:ident = $css:expr,)+ }) => {
+ #[allow(missing_docs)]
+ #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+ #[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)]
+ pub enum $name {
+ $($variant),+
+ }
+
+ impl $name {
+ /// Parse this property from a CSS input stream.
+ pub fn parse<'i, 't>(input: &mut ::cssparser::Parser<'i, 't>)
+ -> Result<$name, $crate::ParseError<'i>> {
+ use cssparser::Token;
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Ident(ref ident) => {
+ Self::from_ident(ident).map_err(|()| {
+ location.new_unexpected_token_error(
+ Token::Ident(ident.clone()),
+ )
+ })
+ }
+ ref token => {
+ Err(location.new_unexpected_token_error(token.clone()))
+ }
+ }
+ }
+
+ /// Parse this property from an already-tokenized identifier.
+ pub fn from_ident(ident: &str) -> Result<$name, ()> {
+ match_ignore_ascii_case! { ident,
+ $($css => Ok($name::$variant),)+
+ _ => Err(())
+ }
+ }
+ }
+
+ impl $crate::ToCss for $name {
+ fn to_css<W>(
+ &self,
+ dest: &mut $crate::CssWriter<W>,
+ ) -> ::std::fmt::Result
+ where
+ W: ::std::fmt::Write,
+ {
+ match *self {
+ $( $name::$variant => ::std::fmt::Write::write_str(dest, $css) ),+
+ }
+ }
+ }
+ };
+}
+
+/// Helper types for the handling of specified values.
+pub mod specified {
+ use crate::ParsingMode;
+
+ /// Whether to allow negative lengths or not.
+ #[repr(u8)]
+ #[derive(
+ Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, PartialOrd, Serialize, ToShmem,
+ )]
+ pub enum AllowedNumericType {
+ /// Allow all kind of numeric values.
+ All,
+ /// Allow only non-negative numeric values.
+ NonNegative,
+ /// Allow only numeric values greater or equal to 1.0.
+ AtLeastOne,
+ /// Allow only numeric values from 0 to 1.0.
+ ZeroToOne,
+ }
+
+ impl Default for AllowedNumericType {
+ #[inline]
+ fn default() -> Self {
+ AllowedNumericType::All
+ }
+ }
+
+ impl AllowedNumericType {
+ /// Whether the value fits the rules of this numeric type.
+ #[inline]
+ pub fn is_ok(&self, parsing_mode: ParsingMode, val: f32) -> bool {
+ if parsing_mode.allows_all_numeric_values() {
+ return true;
+ }
+ match *self {
+ AllowedNumericType::All => true,
+ AllowedNumericType::NonNegative => val >= 0.0,
+ AllowedNumericType::AtLeastOne => val >= 1.0,
+ AllowedNumericType::ZeroToOne => val >= 0.0 && val <= 1.0,
+ }
+ }
+
+ /// Clamp the value following the rules of this numeric type.
+ #[inline]
+ pub fn clamp(&self, val: f32) -> f32 {
+ match *self {
+ AllowedNumericType::All => val,
+ AllowedNumericType::NonNegative => val.max(0.),
+ AllowedNumericType::AtLeastOne => val.max(1.),
+ AllowedNumericType::ZeroToOne => val.max(0.).min(1.),
+ }
+ }
+ }
+}