servo/components/style/stylesheets/rule_parser.rs
Tom Tromey 546ecaeee9 Use cssparser's new_with_line_number_offset
cssparser provides a way to set the initial line number on a
ParserInput.  This patch changes servo to use this facility, rather than
reimplement the same functionality itself.
2017-08-28 12:49:30 -06:00

577 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 http://mozilla.org/MPL/2.0/. */
//! Parsing of the stylesheet contents.
use {Namespace, Prefix};
use computed_values::font_family::FamilyName;
use counter_style::{parse_counter_style_body, parse_counter_style_name};
use cssparser::{AtRuleParser, AtRuleType, Parser, QualifiedRuleParser, RuleListParser};
use cssparser::{CowRcStr, SourceLocation, BasicParseError};
use error_reporting::{ContextualParseError, ParseErrorReporter};
use font_face::parse_font_face_block;
use media_queries::{parse_media_query_list, MediaList};
use parser::{Parse, ParserContext, ParserErrorContext};
use properties::parse_property_declaration_list;
use selector_parser::{SelectorImpl, SelectorParser};
use selectors::SelectorList;
use selectors::parser::SelectorParseError;
use servo_arc::Arc;
use shared_lock::{Locked, SharedRwLock};
use str::starts_with_ignore_ascii_case;
use style_traits::{StyleParseError, ParseError};
use stylesheets::{CssRule, CssRules, CssRuleType, Origin, StylesheetLoader};
use stylesheets::{DocumentRule, FontFeatureValuesRule, KeyframesRule, MediaRule};
use stylesheets::{NamespaceRule, PageRule, StyleRule, SupportsRule, ViewportRule};
use stylesheets::document_rule::DocumentCondition;
use stylesheets::font_feature_values_rule::parse_family_name_list;
use stylesheets::keyframes_rule::parse_keyframe_list;
use stylesheets::stylesheet::Namespaces;
use stylesheets::supports_rule::SupportsCondition;
use stylesheets::viewport_rule;
use values::CustomIdent;
use values::KeyframesName;
use values::specified::url::SpecifiedUrl;
/// The parser for the top-level rules in a stylesheet.
pub struct TopLevelRuleParser<'a, R: 'a> {
/// The origin of the stylesheet we're parsing.
pub stylesheet_origin: Origin,
/// 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 StylesheetLoader>,
/// The top-level parser context.
///
/// This won't contain any namespaces, and only nested parsers created with
/// `ParserContext::new_with_rule_type` will.
pub context: ParserContext<'a>,
/// The context required for reporting parse errors.
pub error_context: ParserErrorContext<'a, R>,
/// 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 had_hierarchy_error: bool,
/// The namespace map we use for parsing. Needs to start as `Some()`, and
/// will be taken out after parsing namespace rules, and that reference will
/// be moved to `ParserContext`.
pub namespaces: &'a mut Namespaces,
}
impl<'b, R> TopLevelRuleParser<'b, R> {
fn nested<'a: 'b>(&'a self) -> NestedRuleParser<'a, 'b, R> {
NestedRuleParser {
stylesheet_origin: self.stylesheet_origin,
shared_lock: self.shared_lock,
context: &self.context,
error_context: &self.error_context,
namespaces: &self.namespaces,
}
}
/// Returns the current state of the parser.
pub fn state(&self) -> State {
self.state
}
/// Returns whether we previously tried to parse a rule that was invalid
/// due to being in the wrong place (e.g. an @import rule was found after
/// a regular style rule). The state of this flag is reset when this
/// function is called.
pub fn take_had_hierarchy_error(&mut self) -> bool {
let had_hierarchy_error = self.had_hierarchy_error;
self.had_hierarchy_error = false;
had_hierarchy_error
}
}
/// 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 `@import` rules.
Imports = 2,
/// We're parsing `@namespace` rules.
Namespaces = 3,
/// We're parsing the main body of the stylesheet.
Body = 4,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "servo", derive(HeapSizeOf))]
/// Vendor prefix.
pub enum VendorPrefix {
/// -moz prefix.
Moz,
/// -webkit prefix.
WebKit,
}
/// A rule prelude for a given at-rule.
pub enum AtRulePrelude {
/// A @font-face rule prelude.
FontFace(SourceLocation),
/// A @font-feature-values rule prelude, with its FamilyName list.
FontFeatureValues(Vec<FamilyName>, SourceLocation),
/// A @counter-style rule prelude, with its counter style name.
CounterStyle(CustomIdent),
/// A @media rule prelude, with its media queries.
Media(Arc<Locked<MediaList>>, SourceLocation),
/// An @supports rule, with its conditional
Supports(SupportsCondition, SourceLocation),
/// A @viewport rule prelude.
Viewport,
/// A @keyframes rule, with its animation name and vendor prefix if exists.
Keyframes(KeyframesName, Option<VendorPrefix>, SourceLocation),
/// A @page rule prelude.
Page(SourceLocation),
/// A @document rule, with its conditional.
Document(DocumentCondition, SourceLocation),
}
#[cfg(feature = "gecko")]
fn register_namespace(ns: &Namespace) -> Result<i32, ()> {
use gecko_bindings::bindings;
let id = unsafe { bindings::Gecko_RegisterNamespace(ns.0.as_ptr()) };
if id == -1 {
Err(())
} else {
Ok(id)
}
}
#[cfg(feature = "servo")]
fn register_namespace(_: &Namespace) -> Result<(), ()> {
Ok(()) // servo doesn't use namespace ids
}
impl<'a, 'i, R: ParseErrorReporter> AtRuleParser<'i> for TopLevelRuleParser<'a, R> {
type Prelude = AtRulePrelude;
type AtRule = CssRule;
type Error = SelectorParseError<'i, StyleParseError<'i>>;
fn parse_prelude<'t>(
&mut self,
name: CowRcStr<'i>,
input: &mut Parser<'i, 't>
) -> Result<AtRuleType<AtRulePrelude, CssRule>, ParseError<'i>> {
let location = get_location_with_offset(input.current_source_location());
match_ignore_ascii_case! { &*name,
"import" => {
if self.state > State::Imports {
// "@import must be before any rule but @charset"
self.had_hierarchy_error = true;
return Err(StyleParseError::UnexpectedImportRule.into())
}
let url_string = input.expect_url_or_string()?.as_ref().to_owned();
let specified_url = SpecifiedUrl::parse_from_string(url_string, &self.context)?;
let media = parse_media_query_list(&self.context, input);
let media = Arc::new(self.shared_lock.wrap(media));
let loader =
self.loader.expect("Expected a stylesheet loader for @import");
let import_rule = loader.request_stylesheet(
specified_url,
location,
&self.context,
&self.shared_lock,
media,
);
self.state = State::Imports;
return Ok(AtRuleType::WithoutBlock(CssRule::Import(import_rule)))
},
"namespace" => {
if self.state > State::Namespaces {
// "@namespace must be before any rule but @charset and @import"
self.had_hierarchy_error = true;
return Err(StyleParseError::UnexpectedNamespaceRule.into())
}
let prefix_result = input.try(|i| i.expect_ident_cloned());
let maybe_namespace = match input.expect_url_or_string() {
Ok(url_or_string) => url_or_string,
Err(BasicParseError::UnexpectedToken(t)) =>
return Err(StyleParseError::UnexpectedTokenWithinNamespace(t).into()),
Err(e) => return Err(e.into()),
};
let url = Namespace::from(maybe_namespace.as_ref());
let id = register_namespace(&url)
.map_err(|()| StyleParseError::UnspecifiedError)?;
let opt_prefix = if let Ok(prefix) = prefix_result {
let prefix = Prefix::from(prefix.as_ref());
self.namespaces
.prefixes
.insert(prefix.clone(), (url.clone(), id));
Some(prefix)
} else {
self.namespaces.default = Some((url.clone(), id));
None
};
self.state = State::Namespaces;
return Ok(AtRuleType::WithoutBlock(CssRule::Namespace(Arc::new(
self.shared_lock.wrap(NamespaceRule {
prefix: opt_prefix,
url: url,
source_location: location,
})
))))
},
// @charset is removed by rust-cssparser if its the first rule in the stylesheet
// anything left is invalid.
"charset" => {
self.had_hierarchy_error = true;
return Err(StyleParseError::UnexpectedCharsetRule.into())
}
_ => {}
}
AtRuleParser::parse_prelude(&mut self.nested(), name, input)
}
#[inline]
fn parse_block<'t>(
&mut self,
prelude: AtRulePrelude,
input: &mut Parser<'i, 't>
) -> Result<CssRule, ParseError<'i>> {
AtRuleParser::parse_block(&mut self.nested(), prelude, input)
.map(|rule| { self.state = State::Body; rule })
}
}
pub struct QualifiedRuleParserPrelude {
selectors: SelectorList<SelectorImpl>,
source_location: SourceLocation,
}
impl<'a, 'i, R: ParseErrorReporter> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, R> {
type Prelude = QualifiedRuleParserPrelude;
type QualifiedRule = CssRule;
type Error = SelectorParseError<'i, StyleParseError<'i>>;
#[inline]
fn parse_prelude<'t>(
&mut self,
input: &mut Parser<'i, 't>,
) -> Result<QualifiedRuleParserPrelude, ParseError<'i>> {
QualifiedRuleParser::parse_prelude(&mut self.nested(), input)
}
#[inline]
fn parse_block<'t>(
&mut self,
prelude: QualifiedRuleParserPrelude,
input: &mut Parser<'i, 't>
) -> Result<CssRule, ParseError<'i>> {
QualifiedRuleParser::parse_block(&mut self.nested(), prelude, input)
.map(|result| { self.state = State::Body; result })
}
}
#[derive(Clone)] // shallow, relatively cheap .clone
struct NestedRuleParser<'a, 'b: 'a, R: 'b> {
stylesheet_origin: Origin,
shared_lock: &'a SharedRwLock,
context: &'a ParserContext<'b>,
error_context: &'a ParserErrorContext<'b, R>,
namespaces: &'a Namespaces,
}
impl<'a, 'b, R: ParseErrorReporter> NestedRuleParser<'a, 'b, R> {
fn parse_nested_rules(
&mut self,
input: &mut Parser,
rule_type: CssRuleType
) -> Arc<Locked<CssRules>> {
let context =
ParserContext::new_with_rule_type(self.context, rule_type, self.namespaces);
let nested_parser = NestedRuleParser {
stylesheet_origin: self.stylesheet_origin,
shared_lock: self.shared_lock,
context: &context,
error_context: &self.error_context,
namespaces: self.namespaces,
};
let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser);
let mut rules = Vec::new();
while let Some(result) = iter.next() {
match result {
Ok(rule) => rules.push(rule),
Err(err) => {
let error = ContextualParseError::UnsupportedRule(err.slice, err.error);
self.context.log_css_error(self.error_context, err.location, error);
}
}
}
CssRules::new(rules, self.shared_lock)
}
}
impl<'a, 'b, 'i, R: ParseErrorReporter> AtRuleParser<'i> for NestedRuleParser<'a, 'b, R> {
type Prelude = AtRulePrelude;
type AtRule = CssRule;
type Error = SelectorParseError<'i, StyleParseError<'i>>;
fn parse_prelude<'t>(
&mut self,
name: CowRcStr<'i>,
input: &mut Parser<'i, 't>
) -> Result<AtRuleType<AtRulePrelude, CssRule>, ParseError<'i>> {
let location = get_location_with_offset(input.current_source_location());
match_ignore_ascii_case! { &*name,
"media" => {
let media_queries = parse_media_query_list(self.context, input);
let arc = Arc::new(self.shared_lock.wrap(media_queries));
Ok(AtRuleType::WithBlock(AtRulePrelude::Media(arc, location)))
},
"supports" => {
let cond = SupportsCondition::parse(input)?;
Ok(AtRuleType::WithBlock(AtRulePrelude::Supports(cond, location)))
},
"font-face" => {
Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace(location)))
},
"font-feature-values" => {
if !cfg!(feature = "gecko") {
// Support for this rule is not fully implemented in Servo yet.
return Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
}
let family_names = parse_family_name_list(self.context, input)?;
Ok(AtRuleType::WithBlock(AtRulePrelude::FontFeatureValues(family_names, location)))
},
"counter-style" => {
if !cfg!(feature = "gecko") {
// Support for this rule is not fully implemented in Servo yet.
return Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
}
let name = parse_counter_style_name(input)?;
// ASCII-case-insensitive matches for "decimal" and "disc".
// The name is already lower-cased by `parse_counter_style_name`
// so we can use == here.
if name.0 == atom!("decimal") || name.0 == atom!("disc") {
return Err(StyleParseError::UnspecifiedError.into())
}
Ok(AtRuleType::WithBlock(AtRulePrelude::CounterStyle(name)))
},
"viewport" => {
if viewport_rule::enabled() {
Ok(AtRuleType::WithBlock(AtRulePrelude::Viewport))
} else {
Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
}
},
"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(StyleParseError::UnsupportedAtRule(name.clone()).into())
}
let name = KeyframesName::parse(self.context, input)?;
Ok(AtRuleType::WithBlock(AtRulePrelude::Keyframes(name, prefix, location)))
},
"page" => {
if cfg!(feature = "gecko") {
Ok(AtRuleType::WithBlock(AtRulePrelude::Page(location)))
} else {
Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
}
},
"-moz-document" => {
if cfg!(feature = "gecko") {
let cond = DocumentCondition::parse(self.context, input)?;
Ok(AtRuleType::WithBlock(AtRulePrelude::Document(cond, location)))
} else {
Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
}
},
_ => Err(StyleParseError::UnsupportedAtRule(name.clone()).into())
}
}
fn parse_block<'t>(
&mut self,
prelude: AtRulePrelude,
input: &mut Parser<'i, 't>
) -> Result<CssRule, ParseError<'i>> {
match prelude {
AtRulePrelude::FontFace(location) => {
let context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::FontFace,
self.namespaces,
);
Ok(CssRule::FontFace(Arc::new(self.shared_lock.wrap(
parse_font_face_block(&context, self.error_context, input, location).into()))))
}
AtRulePrelude::FontFeatureValues(family_names, location) => {
let context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::FontFeatureValues,
self.namespaces,
);
Ok(CssRule::FontFeatureValues(Arc::new(self.shared_lock.wrap(
FontFeatureValuesRule::parse(&context, self.error_context, input, family_names, location)))))
}
AtRulePrelude::CounterStyle(name) => {
let context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::CounterStyle,
self.namespaces,
);
Ok(CssRule::CounterStyle(Arc::new(self.shared_lock.wrap(
parse_counter_style_body(name, &context, self.error_context, input)?.into()))))
}
AtRulePrelude::Media(media_queries, location) => {
Ok(CssRule::Media(Arc::new(self.shared_lock.wrap(MediaRule {
media_queries: media_queries,
rules: self.parse_nested_rules(input, CssRuleType::Media),
source_location: location,
}))))
}
AtRulePrelude::Supports(cond, location) => {
let eval_context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::Style,
self.namespaces,
);
let enabled = cond.eval(&eval_context);
Ok(CssRule::Supports(Arc::new(self.shared_lock.wrap(SupportsRule {
condition: cond,
rules: self.parse_nested_rules(input, CssRuleType::Supports),
enabled: enabled,
source_location: location,
}))))
}
AtRulePrelude::Viewport => {
let context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::Viewport,
self.namespaces,
);
Ok(CssRule::Viewport(Arc::new(self.shared_lock.wrap(
ViewportRule::parse(&context, self.error_context, input)?))))
}
AtRulePrelude::Keyframes(name, prefix, location) => {
let context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::Keyframes,
self.namespaces,
);
Ok(CssRule::Keyframes(Arc::new(self.shared_lock.wrap(KeyframesRule {
name: name,
keyframes: parse_keyframe_list(&context, self.error_context, input, self.shared_lock),
vendor_prefix: prefix,
source_location: location,
}))))
}
AtRulePrelude::Page(location) => {
let context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::Page,
self.namespaces,
);
let declarations = parse_property_declaration_list(&context, self.error_context, input);
Ok(CssRule::Page(Arc::new(self.shared_lock.wrap(PageRule {
block: Arc::new(self.shared_lock.wrap(declarations)),
source_location: location,
}))))
}
AtRulePrelude::Document(cond, location) => {
if cfg!(feature = "gecko") {
Ok(CssRule::Document(Arc::new(self.shared_lock.wrap(DocumentRule {
condition: cond,
rules: self.parse_nested_rules(input, CssRuleType::Document),
source_location: location,
}))))
} else {
unreachable!()
}
}
}
}
}
impl<'a, 'b, 'i, R: ParseErrorReporter> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'b, R> {
type Prelude = QualifiedRuleParserPrelude;
type QualifiedRule = CssRule;
type Error = SelectorParseError<'i, StyleParseError<'i>>;
fn parse_prelude<'t>(
&mut self,
input: &mut Parser<'i, 't>
) -> Result<QualifiedRuleParserPrelude, ParseError<'i>> {
let selector_parser = SelectorParser {
stylesheet_origin: self.stylesheet_origin,
namespaces: self.namespaces,
url_data: Some(self.context.url_data),
};
let location = get_location_with_offset(input.current_source_location());
let selectors = SelectorList::parse(&selector_parser, input)?;
Ok(QualifiedRuleParserPrelude {
selectors: selectors,
source_location: location,
})
}
fn parse_block<'t>(
&mut self,
prelude: QualifiedRuleParserPrelude,
input: &mut Parser<'i, 't>
) -> Result<CssRule, ParseError<'i>> {
let context =
ParserContext::new_with_rule_type(
self.context,
CssRuleType::Style,
self.namespaces,
);
let declarations = parse_property_declaration_list(&context, self.error_context, input);
Ok(CssRule::Style(Arc::new(self.shared_lock.wrap(StyleRule {
selectors: prelude.selectors,
block: Arc::new(self.shared_lock.wrap(declarations)),
source_location: prelude.source_location,
}))))
}
}
/// Adjust a location's column to accommodate DevTools.
fn get_location_with_offset(location: SourceLocation) -> SourceLocation {
SourceLocation {
line: location.line,
// Column offsets are not yet supported, but Gecko devtools expect 1-based columns.
column: location.column + 1,
}
}