зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1427715 - Implement supports() syntax for @import rules r=emilio
Implemented supports conditions using supports() in @import rules as per CSS Cascading and Inheritance Level 4. Locked behind new pref, layout.css.import-supports.enabled, only enabled in nightlies in this patch. Also added new WPT tests for @import supports() generally. Spec: https://drafts.csswg.org/css-cascade-4/#conditional-import WPT tests: https://wpt.fyi/results/css/css-cascade/import-conditions.html Differential Revision: https://phabricator.services.mozilla.com/D172622
This commit is contained in:
Родитель
e75f4068fb
Коммит
4d4ecbb77c
|
@ -9312,6 +9312,13 @@
|
|||
mirror: always
|
||||
rust: true
|
||||
|
||||
# Whether supports() conditions in @import is enabled
|
||||
- name: layout.css.import-supports.enabled
|
||||
type: RelaxedAtomicBool
|
||||
value: @IS_NIGHTLY_BUILD@
|
||||
mirror: always
|
||||
rust: true
|
||||
|
||||
# Whether frame visibility tracking is enabled globally.
|
||||
- name: layout.framevisibility.enabled
|
||||
type: bool
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::media_queries::MediaList;
|
|||
use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock};
|
||||
use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
|
||||
use crate::str::CssStringWriter;
|
||||
use crate::stylesheets::supports_rule::SupportsCondition;
|
||||
use crate::stylesheets::layer_rule::LayerName;
|
||||
use crate::stylesheets::{CssRule, StylesheetInDocument};
|
||||
use crate::values::CssUrl;
|
||||
|
@ -24,9 +25,13 @@ use to_shmem::{self, SharedMemoryBuilder, ToShmem};
|
|||
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")]
|
||||
|
@ -41,6 +46,11 @@ impl ImportSheet {
|
|||
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> {
|
||||
|
@ -52,6 +62,7 @@ impl ImportSheet {
|
|||
}
|
||||
Some(s)
|
||||
},
|
||||
ImportSheet::Refused |
|
||||
ImportSheet::Pending => None,
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +99,7 @@ impl DeepCloneWithLock for ImportSheet {
|
|||
ImportSheet::Sheet(unsafe { GeckoStyleSheet::from_addrefed(clone) })
|
||||
},
|
||||
ImportSheet::Pending => ImportSheet::Pending,
|
||||
ImportSheet::Refused => ImportSheet::Refused,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,6 +143,16 @@ pub struct ImportLayer {
|
|||
pub name: Option<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
|
||||
|
@ -160,6 +182,9 @@ pub struct ImportRule {
|
|||
/// 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: Option<ImportLayer>,
|
||||
|
||||
|
@ -185,6 +210,7 @@ impl DeepCloneWithLock for ImportRule {
|
|||
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(),
|
||||
}
|
||||
|
@ -196,6 +222,12 @@ impl ToCssWithGuard for ImportRule {
|
|||
dest.write_str("@import ")?;
|
||||
self.url.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(' ')?;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
use crate::media_queries::MediaList;
|
||||
use crate::parser::ParserContext;
|
||||
use crate::shared_lock::{Locked, SharedRwLock};
|
||||
use crate::stylesheets::import_rule::{ImportLayer, ImportRule};
|
||||
use crate::stylesheets::import_rule::{ImportLayer, ImportSupportsCondition, ImportRule};
|
||||
use crate::values::CssUrl;
|
||||
use cssparser::SourceLocation;
|
||||
use servo_arc::Arc;
|
||||
|
@ -25,6 +25,7 @@ pub trait StylesheetLoader {
|
|||
context: &ParserContext,
|
||||
lock: &SharedRwLock,
|
||||
media: Arc<Locked<MediaList>>,
|
||||
supports: Option<ImportSupportsCondition>,
|
||||
layer: Option<ImportLayer>,
|
||||
) -> Arc<Locked<ImportRule>>;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ 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;
|
||||
use crate::stylesheets::import_rule::{ImportLayer, ImportSupportsCondition};
|
||||
use crate::stylesheets::keyframes_rule::parse_keyframe_list;
|
||||
use crate::stylesheets::layer_rule::{LayerBlockRule, LayerName, LayerStatementRule};
|
||||
use crate::stylesheets::stylesheet::Namespaces;
|
||||
|
@ -204,7 +204,7 @@ pub enum AtRulePrelude {
|
|||
/// A @document rule, with its conditional.
|
||||
Document(DocumentCondition),
|
||||
/// A @import rule prelude.
|
||||
Import(CssUrl, Arc<Locked<MediaList>>, Option<ImportLayer>),
|
||||
Import(CssUrl, Arc<Locked<MediaList>>, Option<ImportSupportsCondition>, Option<ImportLayer>),
|
||||
/// A @namespace rule prelude.
|
||||
Namespace(Option<Prefix>, Namespace),
|
||||
/// A @layer rule prelude.
|
||||
|
@ -241,6 +241,24 @@ impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a> {
|
|||
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 supports = if !static_prefs::pref!("layout.css.import-supports.enabled") {
|
||||
None
|
||||
} else {
|
||||
input.try_parse(SupportsCondition::parse_for_import).map(|condition| {
|
||||
let eval_context = ParserContext::new_with_rule_type(
|
||||
&self.context,
|
||||
CssRuleType::Style,
|
||||
self.namespaces,
|
||||
);
|
||||
|
||||
let enabled = condition.eval(&eval_context, self.namespaces);
|
||||
ImportSupportsCondition {
|
||||
condition,
|
||||
enabled
|
||||
}
|
||||
}).ok()
|
||||
};
|
||||
|
||||
let layer = if !static_prefs::pref!("layout.css.cascade-layers.enabled") {
|
||||
None
|
||||
} else if input.try_parse(|input| input.expect_ident_matching("layer")).is_ok() {
|
||||
|
@ -261,7 +279,7 @@ impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a> {
|
|||
let media = MediaList::parse(&self.context, input);
|
||||
let media = Arc::new(self.shared_lock.wrap(media));
|
||||
|
||||
return Ok(AtRulePrelude::Import(url, media, layer));
|
||||
return Ok(AtRulePrelude::Import(url, media, supports, layer));
|
||||
},
|
||||
"namespace" => {
|
||||
if !self.check_state(State::Namespaces) {
|
||||
|
@ -330,7 +348,7 @@ impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a> {
|
|||
start: &ParserState,
|
||||
) -> Result<Self::AtRule, ()> {
|
||||
let rule = match prelude {
|
||||
AtRulePrelude::Import(url, media, layer) => {
|
||||
AtRulePrelude::Import(url, media, supports, layer) => {
|
||||
let loader = self
|
||||
.loader
|
||||
.expect("Expected a stylesheet loader for @import");
|
||||
|
@ -341,6 +359,7 @@ impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a> {
|
|||
&self.context,
|
||||
&self.shared_lock,
|
||||
media,
|
||||
supports,
|
||||
layer,
|
||||
);
|
||||
|
||||
|
|
|
@ -202,8 +202,8 @@ impl SupportsCondition {
|
|||
Token::ParenthesisBlock => {
|
||||
let nested = input
|
||||
.try_parse(|input| input.parse_nested_block(parse_condition_or_declaration));
|
||||
if nested.is_ok() {
|
||||
return nested;
|
||||
if let Ok(nested) = nested {
|
||||
return Ok(Self::Parenthesized(Box::new(nested)));
|
||||
}
|
||||
},
|
||||
Token::Function(ref ident) => {
|
||||
|
@ -272,7 +272,7 @@ 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(SupportsCondition::Parenthesized(Box::new(condition)))
|
||||
Ok(condition)
|
||||
} else {
|
||||
Declaration::parse(input).map(SupportsCondition::Declaration)
|
||||
}
|
||||
|
@ -316,9 +316,7 @@ impl ToCss for SupportsCondition {
|
|||
Ok(())
|
||||
},
|
||||
SupportsCondition::Declaration(ref decl) => {
|
||||
dest.write_char('(')?;
|
||||
decl.to_css(dest)?;
|
||||
dest.write_char(')')
|
||||
decl.to_css(dest)
|
||||
},
|
||||
SupportsCondition::Selector(ref selector) => {
|
||||
dest.write_str("selector(")?;
|
||||
|
|
|
@ -19,7 +19,7 @@ use style::global_style_data::GLOBAL_STYLE_DATA;
|
|||
use style::media_queries::MediaList;
|
||||
use style::parser::ParserContext;
|
||||
use style::shared_lock::{Locked, SharedRwLock};
|
||||
use style::stylesheets::import_rule::{ImportLayer, ImportSheet};
|
||||
use style::stylesheets::import_rule::{ImportLayer, ImportSupportsCondition, ImportSheet};
|
||||
use style::stylesheets::AllowImportRules;
|
||||
use style::stylesheets::{ImportRule, Origin, StylesheetLoader as StyleStylesheetLoader};
|
||||
use style::stylesheets::{StylesheetContents, UrlExtraData};
|
||||
|
@ -52,8 +52,20 @@ impl StyleStylesheetLoader for StylesheetLoader {
|
|||
_context: &ParserContext,
|
||||
lock: &SharedRwLock,
|
||||
media: Arc<Locked<MediaList>>,
|
||||
supports: Option<ImportSupportsCondition>,
|
||||
layer: Option<ImportLayer>,
|
||||
) -> Arc<Locked<ImportRule>> {
|
||||
// Ensure the supports conditions for this @import are true, if not, refuse to load
|
||||
if !supports.as_ref().map_or(true, |s| s.enabled) {
|
||||
return Arc::new(lock.wrap(ImportRule {
|
||||
url,
|
||||
stylesheet: ImportSheet::new_refused(),
|
||||
supports,
|
||||
layer,
|
||||
source_location,
|
||||
}));
|
||||
}
|
||||
|
||||
// After we get this raw pointer ImportRule will be moved into a lock and Arc
|
||||
// and so the Arc<Url> pointer inside will also move,
|
||||
// but the Url it points to or the allocating backing the String inside that Url won’t,
|
||||
|
@ -72,6 +84,7 @@ impl StyleStylesheetLoader for StylesheetLoader {
|
|||
Arc::new(lock.wrap(ImportRule {
|
||||
url,
|
||||
stylesheet,
|
||||
supports,
|
||||
layer,
|
||||
source_location,
|
||||
}))
|
||||
|
@ -161,12 +174,25 @@ impl StyleStylesheetLoader for AsyncStylesheetParser {
|
|||
_context: &ParserContext,
|
||||
lock: &SharedRwLock,
|
||||
media: Arc<Locked<MediaList>>,
|
||||
supports: Option<ImportSupportsCondition>,
|
||||
layer: Option<ImportLayer>,
|
||||
) -> Arc<Locked<ImportRule>> {
|
||||
// Ensure the supports conditions for this @import are true, if not, refuse to load
|
||||
if !supports.as_ref().map_or(true, |s| s.enabled) {
|
||||
return Arc::new(lock.wrap(ImportRule {
|
||||
url: url.clone(),
|
||||
stylesheet: ImportSheet::new_refused(),
|
||||
supports,
|
||||
layer,
|
||||
source_location,
|
||||
}));
|
||||
}
|
||||
|
||||
let stylesheet = ImportSheet::new_pending();
|
||||
let rule = Arc::new(lock.wrap(ImportRule {
|
||||
url: url.clone(),
|
||||
stylesheet,
|
||||
supports,
|
||||
layer,
|
||||
source_location,
|
||||
}));
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
prefs: [layout.css.import-supports.enabled:true]
|
|
@ -1,6 +0,0 @@
|
|||
[import-conditions.html]
|
||||
[supports(display:block) is a valid import condition]
|
||||
expected: FAIL
|
||||
|
||||
[supports(display:block) (width >= 0px) is a valid import condition]
|
||||
expected: FAIL
|
|
@ -13,6 +13,34 @@
|
|||
importCondition: "supports(display:block)",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports((display:flex))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports((display:block) and (display:flex))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports((display:block) and (foo:bar))",
|
||||
matches: false
|
||||
},
|
||||
{
|
||||
importCondition: "supports((display:block) or (display:flex))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports((display:block) or (foo:bar))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(not (display: flex))",
|
||||
matches: false
|
||||
},
|
||||
{
|
||||
importCondition: "supports(display: block !important)",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(foo:bar)",
|
||||
matches: false
|
||||
|
@ -28,7 +56,49 @@
|
|||
{
|
||||
importCondition: "(width >= 0px) supports(display:block)",
|
||||
matches: false
|
||||
}
|
||||
},
|
||||
|
||||
// selector()
|
||||
{
|
||||
importCondition: "supports(selector(a))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(selector(p a))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(selector(p > a))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(selector(p + a))",
|
||||
matches: true
|
||||
},
|
||||
|
||||
// font-tech()
|
||||
{
|
||||
importCondition: "supports(font-tech(color-COLRv1))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(font-tech(invalid))",
|
||||
matches: false
|
||||
},
|
||||
|
||||
// font-format()
|
||||
{
|
||||
importCondition: "supports(font-format(opentype))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(font-format(woff))",
|
||||
matches: true
|
||||
},
|
||||
{
|
||||
importCondition: "supports(font-format(invalid))",
|
||||
matches: false
|
||||
},
|
||||
];
|
||||
let target = document.getElementById("target");
|
||||
for (let testCase of testCases) {
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>@import rule with supports parsing / serialization</title>
|
||||
<link rel="author" href="mailto:oj@oojmed.com">
|
||||
<link rel="help" href="https://drafts.csswg.org/css-cascade-4/#at-import">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script>
|
||||
function setupSheet(rule) {
|
||||
const style = document.createElement("style");
|
||||
document.head.append(style);
|
||||
const {sheet} = style;
|
||||
const {cssRules} = sheet;
|
||||
|
||||
assert_equals(cssRules.length, 0, "Sheet should have no rules");
|
||||
sheet.insertRule(rule);
|
||||
assert_equals(cssRules.length, 1, "Sheet should have 1 rule");
|
||||
|
||||
return {sheet, cssRules};
|
||||
}
|
||||
|
||||
function test_valid_supports_import(rule, serialized) {
|
||||
if (serialized === undefined)
|
||||
serialized = rule;
|
||||
|
||||
test(function() {
|
||||
const {sheet, cssRules} = setupSheet(rule);
|
||||
|
||||
const serialization = cssRules[0].cssText;
|
||||
assert_equals(serialization, serialized, 'serialization should be canonical');
|
||||
|
||||
sheet.deleteRule(0);
|
||||
assert_equals(cssRules.length, 0, 'Sheet should have no rule');
|
||||
sheet.insertRule(serialization);
|
||||
assert_equals(cssRules.length, 1, 'Sheet should have 1 rule');
|
||||
|
||||
assert_equals(cssRules[0].cssText, serialization, 'serialization should round-trip');
|
||||
}, rule + ' should be a valid supports() import rule');
|
||||
}
|
||||
|
||||
function test_invalid_supports_import(rule) {
|
||||
test(function() {
|
||||
const {sheet, cssRules} = setupSheet(rule);
|
||||
|
||||
sheet.deleteRule(0);
|
||||
assert_equals(cssRules.length, 0, 'Sheet should have no rule');
|
||||
}, rule + ' should still be a valid import rule with an invalid supports() declaration');
|
||||
}
|
||||
|
||||
test_valid_supports_import('@import url("nonexist.css") supports();');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(display:block);');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports((display:flex));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(not (display: flex));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports((display: flex) and (display: block));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports((display: flex) or (display: block));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports((display: flex) or (foo: bar));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(display: block !important);');
|
||||
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(selector(a));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(selector(p a));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(selector(p > a));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(selector(p + a));');
|
||||
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(font-tech(color-colrv1));');
|
||||
test_valid_supports_import('@import url("nonexist.css") supports(font-format(opentype));');
|
||||
|
||||
test_valid_supports_import('@import url(nonexist.css) supports(display:block);',
|
||||
'@import url("nonexist.css") supports(display:block);');
|
||||
|
||||
test_valid_supports_import('@import "nonexist.css" supports(display:block);',
|
||||
'@import url("nonexist.css") supports(display:block);');
|
||||
|
||||
test_invalid_supports_import('@import url("nonexist.css") supports;');
|
||||
</script>
|
Загрузка…
Ссылка в новой задаче