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:
CanadaHonk 2023-04-13 09:02:30 +00:00
Родитель e75f4068fb
Коммит 4d4ecbb77c
10 изменённых файлов: 241 добавлений и 19 удалений

Просмотреть файл

@ -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 wont,
@ -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>