diff options
Diffstat (limited to 'servo/components/style/properties_and_values/syntax')
3 files changed, 586 insertions, 0 deletions
diff --git a/servo/components/style/properties_and_values/syntax/ascii.rs b/servo/components/style/properties_and_values/syntax/ascii.rs new file mode 100644 index 0000000000..e1a1b08535 --- /dev/null +++ b/servo/components/style/properties_and_values/syntax/ascii.rs @@ -0,0 +1,60 @@ +/* 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/. */ + +/// Trims ASCII whitespace characters from a slice, and returns the trimmed input. +pub fn trim_ascii_whitespace(input: &str) -> &str { + if input.is_empty() { + return input; + } + + let mut start = 0; + { + let mut iter = input.as_bytes().iter(); + loop { + let byte = match iter.next() { + Some(b) => b, + None => return "", + }; + + if !byte.is_ascii_whitespace() { + break; + } + start += 1; + } + } + + let mut end = input.len(); + assert!(start < end); + { + let mut iter = input.as_bytes()[start..].iter().rev(); + loop { + let byte = match iter.next() { + Some(b) => b, + None => { + debug_assert!(false, "We should have caught this in the loop above!"); + return ""; + }, + }; + + if !byte.is_ascii_whitespace() { + break; + } + end -= 1; + } + } + + &input[start..end] +} + +#[test] +fn trim_ascii_whitespace_test() { + fn test(i: &str, o: &str) { + assert_eq!(trim_ascii_whitespace(i), o) + } + + test("", ""); + test(" ", ""); + test(" a b c ", "a b c"); + test(" \t \t \ta b c \t \t \t \t", "a b c"); +} diff --git a/servo/components/style/properties_and_values/syntax/data_type.rs b/servo/components/style/properties_and_values/syntax/data_type.rs new file mode 100644 index 0000000000..be331e2222 --- /dev/null +++ b/servo/components/style/properties_and_values/syntax/data_type.rs @@ -0,0 +1,134 @@ +/* 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/. */ + +//! Used for parsing and serializing component names from the syntax string. + +use super::{Component, ComponentName, Multiplier}; +use std::fmt::{self, Debug, Write}; +use style_traits::{CssWriter, ToCss}; + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#supported-names> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)] +pub enum DataType { + /// Any valid `<length>` value + Length, + /// `<number>` values + Number, + /// Any valid <percentage> value + Percentage, + /// Any valid `<length>` or `<percentage>` value, any valid `<calc()>` expression combining + /// `<length>` and `<percentage>` components. + LengthPercentage, + /// Any valid `<color>` value + Color, + /// Any valid `<image>` value + Image, + /// Any valid `<url>` value + Url, + /// Any valid `<integer>` value + Integer, + /// Any valid `<angle>` value + Angle, + /// Any valid `<time>` value + Time, + /// Any valid `<resolution>` value + Resolution, + /// Any valid `<transform-function>` value + TransformFunction, + /// Any valid `<custom-ident>` value + CustomIdent, + /// A list of valid `<transform-function>` values. Note that "<transform-list>" is a pre-multiplied + /// data type name equivalent to "<transform-function>+" + TransformList, + /// Any valid `<string>` value + /// + /// <https://github.com/w3c/css-houdini-drafts/issues/1103> + String, +} + +impl DataType { + /// Converts a component name from a pre-multiplied data type to its un-pre-multiplied equivalent. + /// + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#pre-multiplied-data-type-name> + pub fn unpremultiply(&self) -> Option<Component> { + match *self { + DataType::TransformList => Some(Component { + name: ComponentName::DataType(DataType::TransformFunction), + multiplier: Some(Multiplier::Space), + }), + _ => None, + } + } + + /// Parses a syntax component name. + pub fn from_str(ty: &str) -> Option<Self> { + Some(match ty.as_bytes() { + b"length" => DataType::Length, + b"number" => DataType::Number, + b"percentage" => DataType::Percentage, + b"length-percentage" => DataType::LengthPercentage, + b"color" => DataType::Color, + b"image" => DataType::Image, + b"url" => DataType::Url, + b"integer" => DataType::Integer, + b"angle" => DataType::Angle, + b"time" => DataType::Time, + b"resolution" => DataType::Resolution, + b"transform-function" => DataType::TransformFunction, + b"custom-ident" => DataType::CustomIdent, + b"transform-list" => DataType::TransformList, + b"string" => DataType::String, + _ => return None, + }) + } + + /// Returns true if this data type requires deferring computation to properly + /// resolve font-dependent lengths. + pub fn may_reference_font_relative_length(&self) -> bool { + match self { + DataType::Length | + DataType::LengthPercentage | + DataType::TransformFunction | + DataType::TransformList => true, + DataType::Number | + DataType::Percentage | + DataType::Color | + DataType::Image | + DataType::Url | + DataType::Integer | + DataType::Angle | + DataType::Time | + DataType::Resolution | + DataType::CustomIdent | + DataType::String => false, + } + } +} + +impl ToCss for DataType { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_char('<')?; + dest.write_str(match *self { + DataType::Length => "length", + DataType::Number => "number", + DataType::Percentage => "percentage", + DataType::LengthPercentage => "length-percentage", + DataType::Color => "color", + DataType::Image => "image", + DataType::Url => "url", + DataType::Integer => "integer", + DataType::Angle => "angle", + DataType::Time => "time", + DataType::Resolution => "resolution", + DataType::TransformFunction => "transform-function", + DataType::CustomIdent => "custom-ident", + DataType::TransformList => "transform-list", + DataType::String => "string", + })?; + dest.write_char('>') + } +} diff --git a/servo/components/style/properties_and_values/syntax/mod.rs b/servo/components/style/properties_and_values/syntax/mod.rs new file mode 100644 index 0000000000..404c8caa7b --- /dev/null +++ b/servo/components/style/properties_and_values/syntax/mod.rs @@ -0,0 +1,392 @@ +/* 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/. */ + +//! Used for parsing and serializing the [`@property`] syntax string. +//! +//! <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax> + +use std::fmt::{self, Debug}; +use std::{borrow::Cow, fmt::Write}; + +use crate::parser::{Parse, ParserContext}; +use crate::values::CustomIdent; +use cssparser::{Parser as CSSParser, ParserInput as CSSParserInput}; +use style_traits::{ + CssWriter, ParseError as StyleParseError, PropertySyntaxParseError as ParseError, + StyleParseErrorKind, ToCss, +}; + +use self::data_type::DataType; + +mod ascii; +pub mod data_type; + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax> +#[derive(Debug, Clone, Default, MallocSizeOf, PartialEq)] +pub struct Descriptor { + /// The parsed components, if any. + /// TODO: Could be a Box<[]> if that supported const construction. + pub components: Vec<Component>, + /// The specified css syntax, if any. + specified: Option<Box<str>>, +} + +impl Descriptor { + /// Returns the universal descriptor. + pub const fn universal() -> Self { + Self { + components: Vec::new(), + specified: None, + } + } + + /// Returns whether this is the universal syntax descriptor. + #[inline] + pub fn is_universal(&self) -> bool { + self.components.is_empty() + } + + /// Returns the specified string, if any. + #[inline] + pub fn specified_string(&self) -> Option<&str> { + self.specified.as_deref() + } + + /// Parse a syntax descriptor. + /// https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-definition + pub fn from_str(css: &str, save_specified: bool) -> Result<Self, ParseError> { + // 1. Strip leading and trailing ASCII whitespace from string. + let input = ascii::trim_ascii_whitespace(css); + + // 2. If string's length is 0, return failure. + if input.is_empty() { + return Err(ParseError::EmptyInput); + } + + let specified = if save_specified { + Some(Box::from(css)) + } else { + None + }; + + // 3. If string's length is 1, and the only code point in string is U+002A + // ASTERISK (*), return the universal syntax descriptor. + if input.len() == 1 && input.as_bytes()[0] == b'*' { + return Ok(Self { + components: Default::default(), + specified, + }); + } + + // 4. Let stream be an input stream created from the code points of string, + // preprocessed as specified in [css-syntax-3]. Let descriptor be an + // initially empty list of syntax components. + // + // NOTE(emilio): Instead of preprocessing we cheat and treat new-lines and + // nulls in the parser specially. + let mut components = vec![]; + { + let mut parser = Parser::new(input, &mut components); + // 5. Repeatedly consume the next input code point from stream. + parser.parse()?; + } + Ok(Self { components, specified }) + } + + /// Returns true if the syntax permits the value to be computed as a length. + pub fn may_compute_length(&self) -> bool { + for component in self.components.iter() { + match &component.name { + ComponentName::DataType(ref t) => { + if matches!(t, DataType::Length | DataType::LengthPercentage) { + return true; + } + }, + ComponentName::Ident(_) => (), + }; + } + false + } + + /// Returns true if the syntax requires deferring computation to properly + /// resolve font-dependent lengths. + pub fn may_reference_font_relative_length(&self) -> bool { + for component in self.components.iter() { + match &component.name { + ComponentName::DataType(ref t) => { + if t.may_reference_font_relative_length() { + return true; + } + }, + ComponentName::Ident(_) => (), + }; + } + false + } +} + +impl ToCss for Descriptor { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if let Some(ref specified) = self.specified { + return specified.to_css(dest); + } + + if self.is_universal() { + return dest.write_char('*'); + } + + let mut first = true; + for component in &*self.components { + if !first { + dest.write_str(" | ")?; + } + component.to_css(dest)?; + first = false; + } + + Ok(()) + } +} + +impl Parse for Descriptor { + /// Parse a syntax descriptor. + fn parse<'i>( + _: &ParserContext, + parser: &mut CSSParser<'i, '_>, + ) -> Result<Self, StyleParseError<'i>> { + let input = parser.expect_string()?; + Descriptor::from_str(input.as_ref(), /* save_specified = */ true) + .map_err(|err| parser.new_custom_error(StyleParseErrorKind::PropertySyntaxField(err))) + } +} + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#multipliers> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue)] +pub enum Multiplier { + /// Indicates a space-separated list. + Space, + /// Indicates a comma-separated list. + Comma, +} + +impl ToCss for Multiplier { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_char(match *self { + Multiplier::Space => '+', + Multiplier::Comma => '#', + }) + } +} + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component> +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct Component { + name: ComponentName, + multiplier: Option<Multiplier>, +} + +impl Component { + /// Returns the component's name. + #[inline] + pub fn name(&self) -> &ComponentName { + &self.name + } + + /// Returns the component's multiplier, if one exists. + #[inline] + pub fn multiplier(&self) -> Option<Multiplier> { + self.multiplier + } + + /// If the component is premultiplied, return the un-premultiplied component. + #[inline] + pub fn unpremultiplied(&self) -> Cow<Self> { + match self.name.unpremultiply() { + Some(component) => { + debug_assert!( + self.multiplier.is_none(), + "Shouldn't have parsed a multiplier for a pre-multiplied data type name", + ); + Cow::Owned(component) + }, + None => Cow::Borrowed(self), + } + } +} + +impl ToCss for Component { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.name().to_css(dest)?; + self.multiplier().to_css(dest) + } +} + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component-name> +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)] +pub enum ComponentName { + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#data-type-name> + DataType(DataType), + /// <https://drafts.csswg.org/css-values-4/#custom-idents> + Ident(CustomIdent), +} + +impl ComponentName { + fn unpremultiply(&self) -> Option<Component> { + match *self { + ComponentName::DataType(ref t) => t.unpremultiply(), + ComponentName::Ident(..) => None, + } + } + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#pre-multiplied-data-type-name> + fn is_pre_multiplied(&self) -> bool { + self.unpremultiply().is_some() + } +} + +struct Parser<'a> { + input: &'a str, + position: usize, + output: &'a mut Vec<Component>, +} + +/// <https://drafts.csswg.org/css-syntax-3/#letter> +fn is_letter(byte: u8) -> bool { + match byte { + b'A'..=b'Z' | b'a'..=b'z' => true, + _ => false, + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#non-ascii-code-point> +fn is_non_ascii(byte: u8) -> bool { + byte >= 0x80 +} + +/// <https://drafts.csswg.org/css-syntax-3/#name-start-code-point> +fn is_name_start(byte: u8) -> bool { + is_letter(byte) || is_non_ascii(byte) || byte == b'_' +} + +impl<'a> Parser<'a> { + fn new(input: &'a str, output: &'a mut Vec<Component>) -> Self { + Self { + input, + position: 0, + output, + } + } + + fn peek(&self) -> Option<u8> { + self.input.as_bytes().get(self.position).cloned() + } + + fn parse(&mut self) -> Result<(), ParseError> { + // 5. Repeatedly consume the next input code point from stream: + loop { + let component = self.parse_component()?; + self.output.push(component); + self.skip_whitespace(); + + let byte = match self.peek() { + None => return Ok(()), + Some(b) => b, + }; + + if byte != b'|' { + return Err(ParseError::ExpectedPipeBetweenComponents); + } + + self.position += 1; + } + } + + fn skip_whitespace(&mut self) { + loop { + match self.peek() { + Some(c) if c.is_ascii_whitespace() => self.position += 1, + _ => return, + } + } + } + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-data-type-name> + fn parse_data_type_name(&mut self) -> Result<DataType, ParseError> { + let start = self.position; + loop { + let byte = match self.peek() { + Some(b) => b, + None => return Err(ParseError::UnclosedDataTypeName), + }; + if byte != b'>' { + self.position += 1; + continue; + } + let ty = match DataType::from_str(&self.input[start..self.position]) { + Some(ty) => ty, + None => return Err(ParseError::UnknownDataTypeName), + }; + self.position += 1; + return Ok(ty); + } + } + + fn parse_name(&mut self) -> Result<ComponentName, ParseError> { + let b = match self.peek() { + Some(b) => b, + None => return Err(ParseError::UnexpectedEOF), + }; + + if b == b'<' { + self.position += 1; + return Ok(ComponentName::DataType(self.parse_data_type_name()?)); + } + + if b != b'\\' && !is_name_start(b) { + return Err(ParseError::InvalidNameStart); + } + + let input = &self.input[self.position..]; + let mut input = CSSParserInput::new(input); + let mut input = CSSParser::new(&mut input); + let name = match CustomIdent::parse(&mut input, &[]) { + Ok(name) => name, + Err(_) => return Err(ParseError::InvalidName), + }; + self.position += input.position().byte_index(); + return Ok(ComponentName::Ident(name)); + } + + fn parse_multiplier(&mut self) -> Option<Multiplier> { + let multiplier = match self.peek()? { + b'+' => Multiplier::Space, + b'#' => Multiplier::Comma, + _ => return None, + }; + self.position += 1; + Some(multiplier) + } + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-component> + fn parse_component(&mut self) -> Result<Component, ParseError> { + // Consume as much whitespace as possible from stream. + self.skip_whitespace(); + let name = self.parse_name()?; + let multiplier = if name.is_pre_multiplied() { + None + } else { + self.parse_multiplier() + }; + Ok(Component { name, multiplier }) + } +} |