mirror of
https://github.com/servo/servo.git
synced 2025-08-04 13:10:20 +01:00
style: Add parsing for <general-enclosed> in queries conditions
See https://drafts.csswg.org/mediaqueries-5/#typedef-general-enclosed Differential Revision: https://phabricator.services.mozilla.com/D158662
This commit is contained in:
parent
a298c296e4
commit
bee44a5259
7 changed files with 164 additions and 30 deletions
|
@ -11,6 +11,7 @@ use crate::context::QuirksMode;
|
||||||
use crate::error_reporting::ContextualParseError;
|
use crate::error_reporting::ContextualParseError;
|
||||||
use crate::parser::ParserContext;
|
use crate::parser::ParserContext;
|
||||||
use crate::values::computed;
|
use crate::values::computed;
|
||||||
|
use crate::queries::condition::KleeneValue;
|
||||||
use cssparser::{Delimiter, Parser};
|
use cssparser::{Delimiter, Parser};
|
||||||
use cssparser::{ParserInput, Token};
|
use cssparser::{ParserInput, Token};
|
||||||
|
|
||||||
|
@ -84,13 +85,27 @@ impl MediaList {
|
||||||
let media_match = mq.media_type.matches(device.media_type());
|
let media_match = mq.media_type.matches(device.media_type());
|
||||||
|
|
||||||
// Check if the media condition match.
|
// Check if the media condition match.
|
||||||
let query_match =
|
let query_match = match media_match {
|
||||||
media_match && mq.condition.as_ref().map_or(true, |c| c.matches(context));
|
true => mq.condition.as_ref().map_or(KleeneValue::True, |c| c.matches(context)),
|
||||||
|
false => KleeneValue::False,
|
||||||
|
};
|
||||||
|
|
||||||
// Apply the logical NOT qualifier to the result
|
// Apply the logical NOT qualifier to the result
|
||||||
match mq.qualifier {
|
match mq.qualifier {
|
||||||
Some(Qualifier::Not) => !query_match,
|
Some(Qualifier::Not) => {
|
||||||
_ => query_match,
|
match query_match {
|
||||||
|
KleeneValue::False => true,
|
||||||
|
KleeneValue::True => false,
|
||||||
|
KleeneValue::Unknown => false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
match query_match {
|
||||||
|
KleeneValue::False => false,
|
||||||
|
KleeneValue::True => true,
|
||||||
|
KleeneValue::Unknown => false,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,6 +13,7 @@ use crate::values::computed;
|
||||||
use cssparser::{Parser, Token};
|
use cssparser::{Parser, Token};
|
||||||
use std::fmt::{self, Write};
|
use std::fmt::{self, Write};
|
||||||
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
|
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
|
||||||
|
use core::ops::Not;
|
||||||
|
|
||||||
/// A binary `and` or `or` operator.
|
/// A binary `and` or `or` operator.
|
||||||
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
|
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
|
||||||
|
@ -29,6 +30,29 @@ enum AllowOr {
|
||||||
No,
|
No,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)]
|
||||||
|
pub enum KleeneValue {
|
||||||
|
/// True
|
||||||
|
True,
|
||||||
|
/// False
|
||||||
|
False,
|
||||||
|
/// Either true or false, but we’re not sure which yet.
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Not for KleeneValue {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn not(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::True => Self::False,
|
||||||
|
Self::False => Self::True,
|
||||||
|
Self::Unknown => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a condition.
|
/// Represents a condition.
|
||||||
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
|
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
|
||||||
pub enum QueryCondition {
|
pub enum QueryCondition {
|
||||||
|
@ -40,6 +64,8 @@ pub enum QueryCondition {
|
||||||
Operation(Box<[QueryCondition]>, Operator),
|
Operation(Box<[QueryCondition]>, Operator),
|
||||||
/// A condition wrapped in parenthesis.
|
/// A condition wrapped in parenthesis.
|
||||||
InParens(Box<QueryCondition>),
|
InParens(Box<QueryCondition>),
|
||||||
|
/// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ]
|
||||||
|
GeneralEnclosed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToCss for QueryCondition {
|
impl ToCss for QueryCondition {
|
||||||
|
@ -71,10 +97,17 @@ impl ToCss for QueryCondition {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
|
QueryCondition::GeneralEnclosed(ref s) => dest.write_str(&s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
|
||||||
|
fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
|
||||||
|
input.expect_no_error_token().map_err(|err| err.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TODO: style() case needs to be handled.
|
||||||
impl QueryCondition {
|
impl QueryCondition {
|
||||||
/// Parse a single condition.
|
/// Parse a single condition.
|
||||||
pub fn parse<'i, 't>(
|
pub fn parse<'i, 't>(
|
||||||
|
@ -82,7 +115,19 @@ impl QueryCondition {
|
||||||
input: &mut Parser<'i, 't>,
|
input: &mut Parser<'i, 't>,
|
||||||
feature_type: FeatureType,
|
feature_type: FeatureType,
|
||||||
) -> Result<Self, ParseError<'i>> {
|
) -> Result<Self, ParseError<'i>> {
|
||||||
Self::parse_internal(context, input, feature_type, AllowOr::Yes)
|
input.skip_whitespace();
|
||||||
|
let state = input.state();
|
||||||
|
let start = input.position();
|
||||||
|
match *input.next()? {
|
||||||
|
Token::Function(_) => {
|
||||||
|
input.parse_nested_block(consume_any_value)?;
|
||||||
|
return Ok(QueryCondition::GeneralEnclosed(input.slice_from(start).to_owned()));
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
input.reset(&state);
|
||||||
|
Self::parse_internal(context, input, feature_type, AllowOr::Yes)
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit<F>(&self, visitor: &mut F)
|
fn visit<F>(&self, visitor: &mut F)
|
||||||
|
@ -92,6 +137,7 @@ impl QueryCondition {
|
||||||
visitor(self);
|
visitor(self);
|
||||||
match *self {
|
match *self {
|
||||||
Self::Feature(..) => {},
|
Self::Feature(..) => {},
|
||||||
|
Self::GeneralEnclosed(..) => {},
|
||||||
Self::Not(ref cond) => cond.visit(visitor),
|
Self::Not(ref cond) => cond.visit(visitor),
|
||||||
Self::Operation(ref conds, _op) => {
|
Self::Operation(ref conds, _op) => {
|
||||||
for cond in conds.iter() {
|
for cond in conds.iter() {
|
||||||
|
@ -132,21 +178,12 @@ impl QueryCondition {
|
||||||
allow_or: AllowOr,
|
allow_or: AllowOr,
|
||||||
) -> Result<Self, ParseError<'i>> {
|
) -> Result<Self, ParseError<'i>> {
|
||||||
let location = input.current_source_location();
|
let location = input.current_source_location();
|
||||||
|
if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() {
|
||||||
// FIXME(emilio): This can be cleaner with nll.
|
|
||||||
let is_negation = match *input.next()? {
|
|
||||||
Token::ParenthesisBlock => false,
|
|
||||||
Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => true,
|
|
||||||
ref t => return Err(location.new_unexpected_token_error(t.clone())),
|
|
||||||
};
|
|
||||||
|
|
||||||
if is_negation {
|
|
||||||
let inner_condition = Self::parse_in_parens(context, input, feature_type)?;
|
let inner_condition = Self::parse_in_parens(context, input, feature_type)?;
|
||||||
return Ok(QueryCondition::Not(Box::new(inner_condition)));
|
return Ok(QueryCondition::Not(Box::new(inner_condition)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParenthesisBlock.
|
let first_condition = Self::parse_in_parens(context, input, feature_type)?;
|
||||||
let first_condition = Self::parse_paren_block(context, input, feature_type)?;
|
|
||||||
let operator = match input.try_parse(Operator::parse) {
|
let operator = match input.try_parse(Operator::parse) {
|
||||||
Ok(op) => op,
|
Ok(op) => op,
|
||||||
Err(..) => return Ok(first_condition),
|
Err(..) => return Ok(first_condition),
|
||||||
|
@ -192,28 +229,87 @@ impl QueryCondition {
|
||||||
input: &mut Parser<'i, 't>,
|
input: &mut Parser<'i, 't>,
|
||||||
feature_type: FeatureType,
|
feature_type: FeatureType,
|
||||||
) -> Result<Self, ParseError<'i>> {
|
) -> Result<Self, ParseError<'i>> {
|
||||||
|
let start = input.position();
|
||||||
input.parse_nested_block(|input| {
|
input.parse_nested_block(|input| {
|
||||||
// Base case.
|
// Base case.
|
||||||
if let Ok(inner) = input.try_parse(|i| Self::parse(context, i, feature_type)) {
|
if let Ok(inner) = input.try_parse(|i| Self::parse_internal(context, i, feature_type, AllowOr::Yes)) {
|
||||||
return Ok(QueryCondition::InParens(Box::new(inner)));
|
return Ok(QueryCondition::InParens(Box::new(inner)));
|
||||||
}
|
}
|
||||||
let expr =
|
if let Ok(expr) = QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type) {
|
||||||
QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type)?;
|
return Ok(QueryCondition::Feature(expr));
|
||||||
Ok(QueryCondition::Feature(expr))
|
}
|
||||||
|
|
||||||
|
consume_any_value(input)?;
|
||||||
|
Ok(QueryCondition::GeneralEnclosed(input.slice_from(start).to_owned()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this condition matches the device and quirks mode.
|
/// Whether this condition matches the device and quirks mode.
|
||||||
pub fn matches(&self, context: &computed::Context) -> bool {
|
/// https://drafts.csswg.org/mediaqueries/#evaluating
|
||||||
|
/// https://drafts.csswg.org/mediaqueries/#typedef-general-enclosed
|
||||||
|
/// Kleene 3-valued logic is adopted here due to the introduction of
|
||||||
|
/// <general-enclosed>.
|
||||||
|
pub fn matches(&self, context: &computed::Context) -> KleeneValue {
|
||||||
match *self {
|
match *self {
|
||||||
QueryCondition::Feature(ref f) => f.matches(context),
|
QueryCondition::Feature(ref f) => {
|
||||||
|
match f.matches(context) {
|
||||||
|
true => KleeneValue::True,
|
||||||
|
false => KleeneValue::False,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown,
|
||||||
QueryCondition::InParens(ref c) => c.matches(context),
|
QueryCondition::InParens(ref c) => c.matches(context),
|
||||||
QueryCondition::Not(ref c) => !c.matches(context),
|
QueryCondition::Not(ref c) => {
|
||||||
|
!c.matches(context)
|
||||||
|
},
|
||||||
QueryCondition::Operation(ref conditions, op) => {
|
QueryCondition::Operation(ref conditions, op) => {
|
||||||
let mut iter = conditions.iter();
|
let mut iter = conditions.iter();
|
||||||
match op {
|
match op {
|
||||||
Operator::And => iter.all(|c| c.matches(context)),
|
Operator::And => {
|
||||||
Operator::Or => iter.any(|c| c.matches(context)),
|
if conditions.is_empty() {
|
||||||
|
return KleeneValue::True;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = iter.next().as_ref().map_or( KleeneValue::True, |c| -> KleeneValue {c.matches(context)});
|
||||||
|
if result == KleeneValue::False {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
while let Some(c) = iter.next() {
|
||||||
|
match c.matches(context) {
|
||||||
|
KleeneValue::False => {
|
||||||
|
return KleeneValue::False;
|
||||||
|
},
|
||||||
|
KleeneValue::Unknown => {
|
||||||
|
result = KleeneValue::Unknown;
|
||||||
|
},
|
||||||
|
KleeneValue::True => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Operator::Or => {
|
||||||
|
if conditions.is_empty() {
|
||||||
|
return KleeneValue::False;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = iter.next().as_ref().map_or( KleeneValue::False, |c| -> KleeneValue {c.matches(context)});
|
||||||
|
if result == KleeneValue::True {
|
||||||
|
return KleeneValue::True;
|
||||||
|
}
|
||||||
|
while let Some(c) = iter.next() {
|
||||||
|
match c.matches(context) {
|
||||||
|
KleeneValue::True => {
|
||||||
|
return KleeneValue::True;
|
||||||
|
},
|
||||||
|
KleeneValue::Unknown => {
|
||||||
|
result = KleeneValue::Unknown;
|
||||||
|
},
|
||||||
|
KleeneValue::False => {},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ use cssparser::{Parser, Token};
|
||||||
use std::cmp::{Ordering, PartialOrd};
|
use std::cmp::{Ordering, PartialOrd};
|
||||||
use std::fmt::{self, Write};
|
use std::fmt::{self, Write};
|
||||||
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
|
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
|
||||||
|
use crate::error_reporting::ContextualParseError;
|
||||||
|
|
||||||
/// Whether we're parsing a media or container query feature.
|
/// Whether we're parsing a media or container query feature.
|
||||||
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
|
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
|
||||||
|
@ -502,11 +503,13 @@ impl QueryFeatureExpression {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a feature expression where we've already consumed the parenthesis.
|
/// Parse a feature expression where we've already consumed the parenthesis.
|
||||||
|
/// For unknown features, we keeps the warning even though they might be parsed "correctly" because of <general-enclosed>
|
||||||
pub fn parse_in_parenthesis_block<'i, 't>(
|
pub fn parse_in_parenthesis_block<'i, 't>(
|
||||||
context: &ParserContext,
|
context: &ParserContext,
|
||||||
input: &mut Parser<'i, 't>,
|
input: &mut Parser<'i, 't>,
|
||||||
feature_type: FeatureType,
|
feature_type: FeatureType,
|
||||||
) -> Result<Self, ParseError<'i>> {
|
) -> Result<Self, ParseError<'i>> {
|
||||||
|
let start_position = input.position();
|
||||||
let (feature_index, range) =
|
let (feature_index, range) =
|
||||||
match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
|
match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
@ -514,6 +517,12 @@ impl QueryFeatureExpression {
|
||||||
if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
|
if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
|
||||||
return Ok(expr);
|
return Ok(expr);
|
||||||
}
|
}
|
||||||
|
let location = e.location;
|
||||||
|
let error = ContextualParseError::UnsupportedRule(
|
||||||
|
input.slice_from(start_position),
|
||||||
|
e.clone(),
|
||||||
|
);
|
||||||
|
context.log_css_error(location, error);
|
||||||
return Err(e);
|
return Err(e);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//! [mq]: https://drafts.csswg.org/mediaqueries/
|
//! [mq]: https://drafts.csswg.org/mediaqueries/
|
||||||
//! [cq]: https://drafts.csswg.org/css-contain-3/#container-rule
|
//! [cq]: https://drafts.csswg.org/css-contain-3/#container-rule
|
||||||
|
|
||||||
mod condition;
|
pub mod condition;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod feature;
|
pub mod feature;
|
||||||
|
|
|
@ -15,6 +15,7 @@ use crate::properties::ComputedValues;
|
||||||
use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription};
|
use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription};
|
||||||
use crate::queries::values::Orientation;
|
use crate::queries::values::Orientation;
|
||||||
use crate::queries::{FeatureType, QueryCondition};
|
use crate::queries::{FeatureType, QueryCondition};
|
||||||
|
use crate::queries::condition::KleeneValue;
|
||||||
use crate::shared_lock::{
|
use crate::shared_lock::{
|
||||||
DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
|
DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard,
|
||||||
};
|
};
|
||||||
|
@ -226,7 +227,7 @@ impl ContainerCondition {
|
||||||
device: &Device,
|
device: &Device,
|
||||||
element: E,
|
element: E,
|
||||||
invalidation_flags: &mut ComputedValueFlags,
|
invalidation_flags: &mut ComputedValueFlags,
|
||||||
) -> bool
|
) -> KleeneValue
|
||||||
where
|
where
|
||||||
E: TElement,
|
E: TElement,
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,6 +19,7 @@ use crate::invalidation::stylesheets::RuleChangeKind;
|
||||||
use crate::media_queries::Device;
|
use crate::media_queries::Device;
|
||||||
use crate::properties::{self, CascadeMode, ComputedValues};
|
use crate::properties::{self, CascadeMode, ComputedValues};
|
||||||
use crate::properties::{AnimationDeclarations, PropertyDeclarationBlock};
|
use crate::properties::{AnimationDeclarations, PropertyDeclarationBlock};
|
||||||
|
use crate::queries::condition::KleeneValue;
|
||||||
use crate::rule_cache::{RuleCache, RuleCacheConditions};
|
use crate::rule_cache::{RuleCache, RuleCacheConditions};
|
||||||
use crate::rule_collector::{containing_shadow_ignoring_svg_use, RuleCollector};
|
use crate::rule_collector::{containing_shadow_ignoring_svg_use, RuleCollector};
|
||||||
use crate::rule_tree::{CascadeLevel, RuleTree, StrongRuleNode, StyleSource};
|
use crate::rule_tree::{CascadeLevel, RuleTree, StrongRuleNode, StyleSource};
|
||||||
|
@ -2380,7 +2381,13 @@ impl CascadeData {
|
||||||
None => return true,
|
None => return true,
|
||||||
Some(ref c) => c,
|
Some(ref c) => c,
|
||||||
};
|
};
|
||||||
if !condition.matches(stylist.device(), element, &mut context.extra_data.cascade_input_flags) {
|
let result = match !condition.matches(stylist.device(), element, &mut context.extra_data.cascade_input_flags) {
|
||||||
|
KleeneValue::True => true,
|
||||||
|
KleeneValue::False => false,
|
||||||
|
KleeneValue::Unknown => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if result {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
id = condition_ref.parent;
|
id = condition_ref.parent;
|
||||||
|
|
|
@ -9,6 +9,7 @@ use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI};
|
||||||
use crate::media_queries::Device;
|
use crate::media_queries::Device;
|
||||||
use crate::parser::{Parse, ParserContext};
|
use crate::parser::{Parse, ParserContext};
|
||||||
use crate::queries::{FeatureType, QueryCondition};
|
use crate::queries::{FeatureType, QueryCondition};
|
||||||
|
use crate::queries::condition::KleeneValue;
|
||||||
use crate::values::computed::{self, ToComputedValue};
|
use crate::values::computed::{self, ToComputedValue};
|
||||||
use crate::values::specified::{Length, NoCalcLength, ViewportPercentageLength};
|
use crate::values::specified::{Length, NoCalcLength, ViewportPercentageLength};
|
||||||
use app_units::Au;
|
use app_units::Au;
|
||||||
|
@ -57,10 +58,15 @@ impl SourceSizeList {
|
||||||
/// Evaluate this <source-size-list> to get the final viewport length.
|
/// Evaluate this <source-size-list> to get the final viewport length.
|
||||||
pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> Au {
|
pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> Au {
|
||||||
computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
|
computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
|
||||||
let matching_source_size = self
|
let matching_source_size = self
|
||||||
.source_sizes
|
.source_sizes
|
||||||
.iter()
|
.iter()
|
||||||
.find(|source_size| source_size.condition.matches(context));
|
.find(|source_size| match source_size.condition.matches(context) {
|
||||||
|
KleeneValue::Unknown => false,
|
||||||
|
KleeneValue::False => false,
|
||||||
|
KleeneValue::True => true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
match matching_source_size {
|
match matching_source_size {
|
||||||
Some(source_size) => source_size.value.to_computed_value(context),
|
Some(source_size) => source_size.value.to_computed_value(context),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue