blob: 0c873357221f1abf17c350bd1a075584236980f1 [file] [log] [blame] [edit]
//! CSS properties related to box sizing.
use crate::compat::Feature;
use crate::context::PropertyHandlerContext;
use crate::declaration::DeclarationList;
use crate::error::{ParserError, PrinterError};
use crate::logical::PropertyCategory;
use crate::macros::{enum_property, property_bitflags};
use crate::printer::Printer;
use crate::properties::{Property, PropertyId};
use crate::traits::{IsCompatible, Parse, PropertyHandler, ToCss};
use crate::values::length::LengthPercentage;
use crate::values::ratio::Ratio;
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::Visit;
use cssparser::*;
#[cfg(feature = "serde")]
use crate::serialization::*;
// https://drafts.csswg.org/css-sizing-3/#specifying-sizes
// https://www.w3.org/TR/css-sizing-4/#sizing-values
/// A value for the [preferred size properties](https://drafts.csswg.org/css-sizing-3/#preferred-size-properties),
/// i.e. `width` and `height.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum Size {
/// The `auto` keyword.
Auto,
/// An explicit length or percentage.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
LengthPercentage(LengthPercentage),
/// The `min-content` keyword.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
MinContent(VendorPrefix),
/// The `max-content` keyword.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
MaxContent(VendorPrefix),
/// The `fit-content` keyword.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
FitContent(VendorPrefix),
/// The `fit-content()` function.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
FitContentFunction(LengthPercentage),
/// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
Stretch(VendorPrefix),
/// The `contain` keyword.
Contain,
}
impl<'i> Parse<'i> for Size {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let res = input.try_parse(|input| {
let ident = input.expect_ident()?;
Ok(match_ignore_ascii_case! { &*ident,
"auto" => Size::Auto,
"min-content" => Size::MinContent(VendorPrefix::None),
"-webkit-min-content" => Size::MinContent(VendorPrefix::WebKit),
"-moz-min-content" => Size::MinContent(VendorPrefix::Moz),
"max-content" => Size::MaxContent(VendorPrefix::None),
"-webkit-max-content" => Size::MaxContent(VendorPrefix::WebKit),
"-moz-max-content" => Size::MaxContent(VendorPrefix::Moz),
"stretch" => Size::Stretch(VendorPrefix::None),
"-webkit-fill-available" => Size::Stretch(VendorPrefix::WebKit),
"-moz-available" => Size::Stretch(VendorPrefix::Moz),
"fit-content" => Size::FitContent(VendorPrefix::None),
"-webkit-fit-content" => Size::FitContent(VendorPrefix::WebKit),
"-moz-fit-content" => Size::FitContent(VendorPrefix::Moz),
"contain" => Size::Contain,
_ => return Err(input.new_custom_error(ParserError::InvalidValue))
})
});
if res.is_ok() {
return res;
}
if let Ok(res) = input.try_parse(parse_fit_content) {
return Ok(Size::FitContentFunction(res));
}
let lp = input.try_parse(LengthPercentage::parse)?;
Ok(Size::LengthPercentage(lp))
}
}
impl ToCss for Size {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use Size::*;
match self {
Auto => dest.write_str("auto"),
Contain => dest.write_str("contain"),
MinContent(vp) => {
vp.to_css(dest)?;
dest.write_str("min-content")
}
MaxContent(vp) => {
vp.to_css(dest)?;
dest.write_str("max-content")
}
FitContent(vp) => {
vp.to_css(dest)?;
dest.write_str("fit-content")
}
Stretch(vp) => match *vp {
VendorPrefix::None => dest.write_str("stretch"),
VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
VendorPrefix::Moz => dest.write_str("-moz-available"),
_ => unreachable!(),
},
FitContentFunction(l) => {
dest.write_str("fit-content(")?;
l.to_css(dest)?;
dest.write_str(")")
}
LengthPercentage(l) => l.to_css(dest),
}
}
}
impl IsCompatible for Size {
fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
use Size::*;
match self {
LengthPercentage(l) => l.is_compatible(browsers),
MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
FitContentFunction(l) => {
Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
}
Stretch(vp) => match *vp {
VendorPrefix::None => Feature::StretchSize,
VendorPrefix::WebKit => Feature::WebkitFillAvailableSize,
VendorPrefix::Moz => Feature::MozAvailableSize,
_ => return false,
}
.is_compatible(browsers),
Contain => false, // ??? no data in mdn
Auto => true,
}
}
}
/// A value for the [minimum](https://drafts.csswg.org/css-sizing-3/#min-size-properties)
/// and [maximum](https://drafts.csswg.org/css-sizing-3/#max-size-properties) size properties,
/// e.g. `min-width` and `max-height`.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum MaxSize {
/// The `none` keyword.
None,
/// An explicit length or percentage.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
LengthPercentage(LengthPercentage),
/// The `min-content` keyword.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
MinContent(VendorPrefix),
/// The `max-content` keyword.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
MaxContent(VendorPrefix),
/// The `fit-content` keyword.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
FitContent(VendorPrefix),
/// The `fit-content()` function.
#[cfg_attr(feature = "serde", serde(with = "ValueWrapper::<LengthPercentage>"))]
FitContentFunction(LengthPercentage),
/// The `stretch` keyword, or the `-webkit-fill-available` or `-moz-available` prefixed keywords.
#[cfg_attr(feature = "serde", serde(with = "PrefixWrapper"))]
Stretch(VendorPrefix),
/// The `contain` keyword.
Contain,
}
impl<'i> Parse<'i> for MaxSize {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let res = input.try_parse(|input| {
let ident = input.expect_ident()?;
Ok(match_ignore_ascii_case! { &*ident,
"none" => MaxSize::None,
"min-content" => MaxSize::MinContent(VendorPrefix::None),
"-webkit-min-content" => MaxSize::MinContent(VendorPrefix::WebKit),
"-moz-min-content" => MaxSize::MinContent(VendorPrefix::Moz),
"max-content" => MaxSize::MaxContent(VendorPrefix::None),
"-webkit-max-content" => MaxSize::MaxContent(VendorPrefix::WebKit),
"-moz-max-content" => MaxSize::MaxContent(VendorPrefix::Moz),
"stretch" => MaxSize::Stretch(VendorPrefix::None),
"-webkit-fill-available" => MaxSize::Stretch(VendorPrefix::WebKit),
"-moz-available" => MaxSize::Stretch(VendorPrefix::Moz),
"fit-content" => MaxSize::FitContent(VendorPrefix::None),
"-webkit-fit-content" => MaxSize::FitContent(VendorPrefix::WebKit),
"-moz-fit-content" => MaxSize::FitContent(VendorPrefix::Moz),
"contain" => MaxSize::Contain,
_ => return Err(input.new_custom_error(ParserError::InvalidValue))
})
});
if res.is_ok() {
return res;
}
if let Ok(res) = input.try_parse(parse_fit_content) {
return Ok(MaxSize::FitContentFunction(res));
}
let lp = input.try_parse(LengthPercentage::parse)?;
Ok(MaxSize::LengthPercentage(lp))
}
}
impl ToCss for MaxSize {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
use MaxSize::*;
match self {
None => dest.write_str("none"),
Contain => dest.write_str("contain"),
MinContent(vp) => {
vp.to_css(dest)?;
dest.write_str("min-content")
}
MaxContent(vp) => {
vp.to_css(dest)?;
dest.write_str("max-content")
}
FitContent(vp) => {
vp.to_css(dest)?;
dest.write_str("fit-content")
}
Stretch(vp) => match *vp {
VendorPrefix::None => dest.write_str("stretch"),
VendorPrefix::WebKit => dest.write_str("-webkit-fill-available"),
VendorPrefix::Moz => dest.write_str("-moz-available"),
_ => unreachable!(),
},
FitContentFunction(l) => {
dest.write_str("fit-content(")?;
l.to_css(dest)?;
dest.write_str(")")
}
LengthPercentage(l) => l.to_css(dest),
}
}
}
impl IsCompatible for MaxSize {
fn is_compatible(&self, browsers: crate::targets::Browsers) -> bool {
use MaxSize::*;
match self {
LengthPercentage(l) => l.is_compatible(browsers),
MinContent(..) => Feature::MinContentSize.is_compatible(browsers),
MaxContent(..) => Feature::MaxContentSize.is_compatible(browsers),
FitContent(..) => Feature::FitContentSize.is_compatible(browsers),
FitContentFunction(l) => {
Feature::FitContentFunctionSize.is_compatible(browsers) && l.is_compatible(browsers)
}
Stretch(vp) => match *vp {
VendorPrefix::None => Feature::StretchSize,
VendorPrefix::WebKit => Feature::WebkitFillAvailableSize,
VendorPrefix::Moz => Feature::MozAvailableSize,
_ => return false,
}
.is_compatible(browsers),
Contain => false, // ??? no data in mdn
None => true,
}
}
}
fn parse_fit_content<'i, 't>(
input: &mut Parser<'i, 't>,
) -> Result<LengthPercentage, ParseError<'i, ParserError<'i>>> {
input.expect_function_matching("fit-content")?;
input.parse_nested_block(|input| LengthPercentage::parse(input))
}
enum_property! {
/// A value for the [box-sizing](https://drafts.csswg.org/css-sizing-3/#box-sizing) property.
pub enum BoxSizing {
/// Exclude the margin/border/padding from the width and height.
ContentBox,
/// Include the padding and border (but not the margin) in the width and height.
BorderBox,
}
}
/// A value for the [aspect-ratio](https://drafts.csswg.org/css-sizing-4/#aspect-ratio) property.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct AspectRatio {
/// The `auto` keyword.
pub auto: bool,
/// A preferred aspect ratio for the box, specified as width / height.
pub ratio: Option<Ratio>,
}
impl<'i> Parse<'i> for AspectRatio {
fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let location = input.current_source_location();
let mut auto = input.try_parse(|i| i.expect_ident_matching("auto"));
let ratio = input.try_parse(Ratio::parse);
if auto.is_err() {
auto = input.try_parse(|i| i.expect_ident_matching("auto"));
}
if auto.is_err() && ratio.is_err() {
return Err(location.new_custom_error(ParserError::InvalidValue));
}
Ok(AspectRatio {
auto: auto.is_ok(),
ratio: ratio.ok(),
})
}
}
impl ToCss for AspectRatio {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
if self.auto {
dest.write_str("auto")?;
}
if let Some(ratio) = &self.ratio {
if self.auto {
dest.write_char(' ')?;
}
ratio.to_css(dest)?;
}
Ok(())
}
}
property_bitflags! {
#[derive(Default)]
struct SizeProperty: u16 {
const Width = 1 << 0;
const Height = 1 << 1;
const MinWidth = 1 << 2;
const MinHeight = 1 << 3;
const MaxWidth = 1 << 4;
const MaxHeight = 1 << 5;
const BlockSize = 1 << 6;
const InlineSize = 1 << 7;
const MinBlockSize = 1 << 8;
const MinInlineSize = 1 << 9;
const MaxBlockSize = 1 << 10;
const MaxInlineSize = 1 << 11;
}
}
#[derive(Default)]
pub(crate) struct SizeHandler {
width: Option<Size>,
height: Option<Size>,
min_width: Option<Size>,
min_height: Option<Size>,
max_width: Option<MaxSize>,
max_height: Option<MaxSize>,
block_size: Option<Size>,
inline_size: Option<Size>,
min_block_size: Option<Size>,
min_inline_size: Option<Size>,
max_block_size: Option<MaxSize>,
max_inline_size: Option<MaxSize>,
has_any: bool,
flushed_properties: SizeProperty,
category: PropertyCategory,
}
impl<'i> PropertyHandler<'i> for SizeHandler {
fn handle_property(
&mut self,
property: &Property<'i>,
dest: &mut DeclarationList<'i>,
context: &mut PropertyHandlerContext<'i, '_>,
) -> bool {
let logical_supported = !context.should_compile_logical(Feature::LogicalSize);
macro_rules! property {
($prop: ident, $val: ident, $category: ident) => {{
// If the category changes betweet logical and physical,
// or if the value contains syntax that isn't supported across all targets,
// preserve the previous value as a fallback.
if PropertyCategory::$category != self.category || (self.$prop.is_some() && matches!(context.targets.browsers, Some(targets) if !$val.is_compatible(targets))) {
self.flush(dest, context);
}
self.$prop = Some($val.clone());
self.category = PropertyCategory::$category;
self.has_any = true;
}};
}
match property {
Property::Width(v) => property!(width, v, Physical),
Property::Height(v) => property!(height, v, Physical),
Property::MinWidth(v) => property!(min_width, v, Physical),
Property::MinHeight(v) => property!(min_height, v, Physical),
Property::MaxWidth(v) => property!(max_width, v, Physical),
Property::MaxHeight(v) => property!(max_height, v, Physical),
Property::BlockSize(size) => property!(block_size, size, Logical),
Property::MinBlockSize(size) => property!(min_block_size, size, Logical),
Property::MaxBlockSize(size) => property!(max_block_size, size, Logical),
Property::InlineSize(size) => property!(inline_size, size, Logical),
Property::MinInlineSize(size) => property!(min_inline_size, size, Logical),
Property::MaxInlineSize(size) => property!(max_inline_size, size, Logical),
Property::Unparsed(unparsed) => {
self.flush(dest, context);
macro_rules! logical_unparsed {
($physical: ident) => {
if logical_supported {
self
.flushed_properties
.insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
dest.push(property.clone());
} else {
dest.push(Property::Unparsed(
unparsed.with_property_id(PropertyId::$physical),
));
self.flushed_properties.insert(SizeProperty::$physical);
}
};
}
match &unparsed.property_id {
PropertyId::Width
| PropertyId::Height
| PropertyId::MinWidth
| PropertyId::MaxWidth
| PropertyId::MinHeight
| PropertyId::MaxHeight => {
self
.flushed_properties
.insert(SizeProperty::try_from(&unparsed.property_id).unwrap());
dest.push(property.clone());
}
PropertyId::BlockSize => logical_unparsed!(Height),
PropertyId::MinBlockSize => logical_unparsed!(MinHeight),
PropertyId::MaxBlockSize => logical_unparsed!(MaxHeight),
PropertyId::InlineSize => logical_unparsed!(Width),
PropertyId::MinInlineSize => logical_unparsed!(MinWidth),
PropertyId::MaxInlineSize => logical_unparsed!(MaxWidth),
_ => return false,
}
}
_ => return false,
}
true
}
fn finalize(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
self.flush(dest, context);
self.flushed_properties = SizeProperty::empty();
}
}
impl SizeHandler {
fn flush<'i>(&mut self, dest: &mut DeclarationList, context: &mut PropertyHandlerContext<'i, '_>) {
if !self.has_any {
return;
}
self.has_any = false;
let logical_supported = !context.should_compile_logical(Feature::LogicalSize);
macro_rules! prefix {
($prop: ident, $size: ident, $feature: ident) => {
if !self.flushed_properties.contains(SizeProperty::$prop) {
let prefixes =
context.targets.prefixes(VendorPrefix::None, crate::prefixes::Feature::$feature) - VendorPrefix::None;
for prefix in prefixes {
dest.push(Property::$prop($size::$feature(prefix)));
}
}
};
}
macro_rules! property {
($prop: ident, $val: ident, $size: ident) => {{
if let Some(val) = std::mem::take(&mut self.$val) {
match val {
$size::Stretch(VendorPrefix::None) => prefix!($prop, $size, Stretch),
$size::MinContent(VendorPrefix::None) => prefix!($prop, $size, MinContent),
$size::MaxContent(VendorPrefix::None) => prefix!($prop, $size, MaxContent),
$size::FitContent(VendorPrefix::None) => prefix!($prop, $size, FitContent),
_ => {}
}
dest.push(Property::$prop(val.clone()));
self.flushed_properties.insert(SizeProperty::$prop);
}
}};
}
macro_rules! logical {
($prop: ident, $val: ident, $physical: ident, $size: ident) => {
if logical_supported {
property!($prop, $val, $size);
} else {
property!($physical, $val, $size);
}
};
}
property!(Width, width, Size);
property!(MinWidth, min_width, Size);
property!(MaxWidth, max_width, MaxSize);
property!(Height, height, Size);
property!(MinHeight, min_height, Size);
property!(MaxHeight, max_height, MaxSize);
logical!(BlockSize, block_size, Height, Size);
logical!(MinBlockSize, min_block_size, MinHeight, Size);
logical!(MaxBlockSize, max_block_size, MaxHeight, MaxSize);
logical!(InlineSize, inline_size, Width, Size);
logical!(MinInlineSize, min_inline_size, MinWidth, Size);
logical!(MaxInlineSize, max_inline_size, MaxWidth, MaxSize);
}
}