use crate as css;
use crate::css_properties::custom::EnvironmentVariable;
use crate::css_values::ident::{DashedIdent, Ident};
use crate::{Parser, PrintErr, Printer, Result};
use bun_alloc::ArenaPtr;
pub use crate::Error;
use crate::css_values::length::Length;
use crate::css_values::number::{CSSIntegerFns, CSSNumberFns};
use crate::css_values::ratio::Ratio;
use crate::css_values::resolution::Resolution;
type CSSNumber = f32;
type CSSInteger = i32;
pub trait QueryCondition: Sized + ToCss {
type Feature;
fn parse_feature(input: &mut Parser) -> Result<Self>;
fn parse_feature_with_options(
input: &mut Parser,
_options: &css::ParserOptions,
) -> Result<Self> {
Self::parse_feature(input)
}
#[expect(clippy::boxed_local)]
fn create_negation(condition: Box<Self>) -> Self {
Self::create_negation_in(*condition, ArenaPtr::global())
}
fn create_negation_in(condition: Self, _alloc: ArenaPtr) -> Self {
Self::create_negation(Box::new(condition))
}
fn create_operation(operator: Operator, conditions: Vec<Self>) -> Self {
let mut v = Vec::new_in(ArenaPtr::global());
v.extend(conditions);
Self::create_operation_in(operator, v)
}
fn create_operation_in(operator: Operator, conditions: Vec<Self, ArenaPtr>) -> Self {
let mut v = Vec::with_capacity(conditions.len());
v.extend(conditions);
Self::create_operation(operator, v)
}
fn parse_style_query(input: &mut Parser) -> Result<Self>;
fn parse_style_query_with_options(
input: &mut Parser,
_options: &css::ParserOptions,
) -> Result<Self> {
Self::parse_style_query(input)
}
fn needs_parens(
&self,
parent_operator: Option<Operator>,
targets: &css::targets::Targets,
) -> bool;
fn as_feature(&self) -> Option<&Self::Feature>;
fn as_not(&self) -> Option<&Self>;
fn as_operation(&self) -> Option<(Operator, &[Self])>;
fn feature_to_css(f: &Self::Feature, dest: &mut Printer) -> core::result::Result<(), PrintErr>;
fn extra_to_css(&self, _dest: &mut Printer) -> core::result::Result<(), PrintErr> {
unreachable!("QueryCondition: no extra variants")
}
fn condition_to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
if let Some(f) = self.as_feature() {
return Self::feature_to_css(f, dest);
}
if let Some(c) = self.as_not() {
dest.write_str("not ")?;
let needs = c.needs_parens(None, &dest.targets);
return to_css_with_parens_if_needed(c, dest, needs);
}
if let Some((op, conds)) = self.as_operation() {
return operation_to_css(op, conds, dest);
}
self.extra_to_css(dest)
}
}
pub use crate::generics::ToCss;
#[derive(Debug, Clone, PartialEq)]
pub struct MediaList {
pub media_queries: Vec<MediaQuery, ArenaPtr>,
}
impl Default for MediaList {
fn default() -> Self {
Self {
media_queries: Vec::new_in(ArenaPtr::global()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MediaQuery {
pub qualifier: Option<Qualifier>,
pub media_type: MediaType,
pub condition: Option<MediaCondition>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, css::DefineEnumProperty)]
pub enum Operator {
And,
Or,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, css::DefineEnumProperty)]
pub enum Qualifier {
Only,
Not,
}
#[derive(Debug, Copy, Clone)]
pub enum MediaType {
All,
Print,
Screen,
Custom(*const [u8]),
}
impl PartialEq for MediaType {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::All, Self::All) => true,
(Self::Print, Self::Print) => true,
(Self::Screen, Self::Screen) => true,
(Self::Custom(a), Self::Custom(b)) => unsafe { **a == **b },
_ => false,
}
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueryConditionFlags: u8 {
const ALLOW_OR = 1 << 0;
const ALLOW_STYLE = 1 << 1;
}
}
impl QueryConditionFlags {
#[inline]
pub(crate) fn allow_or(self) -> bool {
self.contains(Self::ALLOW_OR)
}
#[inline]
pub(crate) fn allow_style(self) -> bool {
self.contains(Self::ALLOW_STYLE)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MediaCondition {
Feature(Box<MediaFeature, ArenaPtr>),
Not(Box<MediaCondition, ArenaPtr>),
Operation {
operator: Operator,
conditions: Vec<MediaCondition, ArenaPtr>,
},
}
pub(crate) type MediaFeature = QueryFeature<MediaFeatureId>;
#[derive(Debug, Clone)]
pub enum MediaFeatureName<FeatureId: FeatureIdTrait> {
Standard(FeatureId),
Custom(DashedIdent),
Unknown(Ident),
}
impl<FeatureId: FeatureIdTrait> PartialEq for MediaFeatureName<FeatureId> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Standard(a), Self::Standard(b)) => a == b,
(Self::Custom(a), Self::Custom(b)) => a.v() == b.v(),
(Self::Unknown(a), Self::Unknown(b)) => unsafe { *a.v == *b.v },
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum QueryFeature<FeatureId: FeatureIdTrait> {
Plain {
name: MediaFeatureName<FeatureId>,
value: MediaFeatureValue,
},
Boolean { name: MediaFeatureName<FeatureId> },
Range {
name: MediaFeatureName<FeatureId>,
operator: MediaFeatureComparison,
value: MediaFeatureValue,
},
Interval {
name: MediaFeatureName<FeatureId>,
start: MediaFeatureValue,
start_operator: MediaFeatureComparison,
end: MediaFeatureValue,
end_operator: MediaFeatureComparison,
},
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::IntoStaticStr)]
pub enum MediaFeatureComparison {
#[strum(serialize = "=")]
Equal = 1,
#[strum(serialize = ">")]
GreaterThan = 2,
#[strum(serialize = ">=")]
GreaterThanEqual = 4,
#[strum(serialize = "<")]
LessThan = 8,
#[strum(serialize = "<=")]
LessThanEqual = 16,
}
#[derive(Clone)]
pub enum MediaFeatureValue {
Length(Length),
Number(CSSNumber),
Integer(CSSInteger),
Boolean(bool),
Resolution(Resolution),
Ratio(Ratio),
Ident(Ident),
Env(EnvironmentVariable),
}
impl core::fmt::Debug for MediaFeatureValue {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use MediaFeatureValue as V;
match self {
V::Length(_) => f.write_str("Length(..)"),
V::Number(n) => write!(f, "Number({n})"),
V::Integer(i) => write!(f, "Integer({i})"),
V::Boolean(b) => write!(f, "Boolean({b})"),
V::Resolution(r) => write!(f, "Resolution({r:?})"),
V::Ratio(r) => write!(f, "Ratio({r:?})"),
V::Ident(i) => write!(f, "Ident({i:?})"),
V::Env(_) => f.write_str("Env(..)"),
}
}
}
impl PartialEq for MediaFeatureValue {
fn eq(&self, other: &Self) -> bool {
use MediaFeatureValue as V;
match (self, other) {
(V::Length(a), V::Length(b)) => a == b,
(V::Number(a), V::Number(b)) => a == b,
(V::Integer(a), V::Integer(b)) => a == b,
(V::Boolean(a), V::Boolean(b)) => a == b,
(V::Resolution(a), V::Resolution(b)) => a == b,
(V::Ratio(a), V::Ratio(b)) => a == b,
(V::Ident(a), V::Ident(b)) => a.v() == b.v(),
(V::Env(a), V::Env(b)) => {
use crate::generics::CssEql as _;
a.eql(b)
}
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MediaFeatureType {
Length,
Number,
Integer,
Boolean,
Resolution,
Ratio,
Ident,
Unknown,
}
impl MediaFeatureType {
pub fn allows_ranges(self) -> bool {
use MediaFeatureType as T;
matches!(
self,
T::Length | T::Number | T::Integer | T::Resolution | T::Ratio | T::Unknown
)
}
}
pub trait FeatureIdTrait: Copy + PartialEq + Eq {
fn value_type(&self) -> MediaFeatureType;
fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr>;
fn from_str(s: &[u8]) -> Option<Self>;
fn to_css_with_prefix(
&self,
prefix: &str,
dest: &mut Printer,
) -> core::result::Result<(), PrintErr> {
dest.write_str(prefix)?;
self.to_css(dest)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::IntoStaticStr)]
pub enum MediaFeatureId {
#[strum(serialize = "width")]
Width,
#[strum(serialize = "height")]
Height,
#[strum(serialize = "aspect-ratio")]
AspectRatio,
#[strum(serialize = "orientation")]
Orientation,
#[strum(serialize = "overflow-block")]
OverflowBlock,
#[strum(serialize = "overflow-inline")]
OverflowInline,
#[strum(serialize = "horizontal-viewport-segments")]
HorizontalViewportSegments,
#[strum(serialize = "vertical-viewport-segments")]
VerticalViewportSegments,
#[strum(serialize = "display-mode")]
DisplayMode,
#[strum(serialize = "resolution")]
Resolution,
#[strum(serialize = "scan")]
Scan,
#[strum(serialize = "grid")]
Grid,
#[strum(serialize = "update")]
Update,
#[strum(serialize = "environment-blending")]
EnvironmentBlending,
#[strum(serialize = "color")]
Color,
#[strum(serialize = "color-index")]
ColorIndex,
#[strum(serialize = "monochrome")]
Monochrome,
#[strum(serialize = "color-gamut")]
ColorGamut,
#[strum(serialize = "dynamic-range")]
DynamicRange,
#[strum(serialize = "inverted-colors")]
InvertedColors,
#[strum(serialize = "pointer")]
Pointer,
#[strum(serialize = "hover")]
Hover,
#[strum(serialize = "any-pointer")]
AnyPointer,
#[strum(serialize = "any-hover")]
AnyHover,
#[strum(serialize = "nav-controls")]
NavControls,
#[strum(serialize = "video-color-gamut")]
VideoColorGamut,
#[strum(serialize = "video-dynamic-range")]
VideoDynamicRange,
#[strum(serialize = "scripting")]
Scripting,
#[strum(serialize = "prefers-reduced-motion")]
PrefersReducedMotion,
#[strum(serialize = "prefers-reduced-transparency")]
PrefersReducedTransparency,
#[strum(serialize = "prefers-contrast")]
PrefersContrast,
#[strum(serialize = "forced-colors")]
ForcedColors,
#[strum(serialize = "prefers-color-scheme")]
PrefersColorScheme,
#[strum(serialize = "prefers-reduced-data")]
PrefersReducedData,
#[strum(serialize = "device-width")]
DeviceWidth,
#[strum(serialize = "device-height")]
DeviceHeight,
#[strum(serialize = "device-aspect-ratio")]
DeviceAspectRatio,
#[strum(serialize = "-webkit-device-pixel-ratio")]
WebkitDevicePixelRatio,
#[strum(serialize = "-moz-device-pixel-ratio")]
MozDevicePixelRatio,
}
impl MediaFeatureId {
pub(crate) fn value_type(self) -> MediaFeatureType {
use MediaFeatureId::*;
use MediaFeatureType as T;
match self {
Width | Height | DeviceWidth | DeviceHeight => T::Length,
AspectRatio | DeviceAspectRatio => T::Ratio,
Orientation
| OverflowBlock
| OverflowInline
| DisplayMode
| Scan
| Update
| EnvironmentBlending
| ColorGamut
| DynamicRange
| InvertedColors
| Pointer
| Hover
| AnyPointer
| AnyHover
| NavControls
| VideoColorGamut
| VideoDynamicRange
| Scripting
| PrefersReducedMotion
| PrefersReducedTransparency
| PrefersContrast
| ForcedColors
| PrefersColorScheme
| PrefersReducedData => T::Ident,
HorizontalViewportSegments
| VerticalViewportSegments
| Color
| ColorIndex
| Monochrome => T::Integer,
Resolution => T::Resolution,
Grid => T::Boolean,
WebkitDevicePixelRatio | MozDevicePixelRatio => T::Number,
}
}
}
impl FeatureIdTrait for MediaFeatureId {
fn value_type(&self) -> MediaFeatureType {
MediaFeatureId::value_type(*self)
}
fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
dest.write_str(<&'static str>::from(*self))
}
fn to_css_with_prefix(
&self,
prefix: &str,
dest: &mut Printer,
) -> core::result::Result<(), PrintErr> {
match self {
MediaFeatureId::WebkitDevicePixelRatio => {
dest.write_str("-webkit-")?;
dest.write_str(prefix)?;
dest.write_str("device-pixel-ratio")
}
_ => {
dest.write_str(prefix)?;
FeatureIdTrait::to_css(self, dest)
}
}
}
fn from_str(s: &[u8]) -> Option<Self> {
use MediaFeatureId::*;
crate::match_ignore_ascii_case! { s, {
b"width" => Some(Width),
b"height" => Some(Height),
b"aspect-ratio" => Some(AspectRatio),
b"orientation" => Some(Orientation),
b"overflow-block" => Some(OverflowBlock),
b"overflow-inline" => Some(OverflowInline),
b"horizontal-viewport-segments" => Some(HorizontalViewportSegments),
b"vertical-viewport-segments" => Some(VerticalViewportSegments),
b"display-mode" => Some(DisplayMode),
b"resolution" => Some(Resolution),
b"scan" => Some(Scan),
b"grid" => Some(Grid),
b"update" => Some(Update),
b"environment-blending" => Some(EnvironmentBlending),
b"color" => Some(Color),
b"color-index" => Some(ColorIndex),
b"monochrome" => Some(Monochrome),
b"color-gamut" => Some(ColorGamut),
b"dynamic-range" => Some(DynamicRange),
b"inverted-colors" => Some(InvertedColors),
b"pointer" => Some(Pointer),
b"hover" => Some(Hover),
b"any-pointer" => Some(AnyPointer),
b"any-hover" => Some(AnyHover),
b"nav-controls" => Some(NavControls),
b"video-color-gamut" => Some(VideoColorGamut),
b"video-dynamic-range" => Some(VideoDynamicRange),
b"scripting" => Some(Scripting),
b"prefers-reduced-motion" => Some(PrefersReducedMotion),
b"prefers-reduced-transparency" => Some(PrefersReducedTransparency),
b"prefers-contrast" => Some(PrefersContrast),
b"forced-colors" => Some(ForcedColors),
b"prefers-color-scheme" => Some(PrefersColorScheme),
b"prefers-reduced-data" => Some(PrefersReducedData),
b"device-width" => Some(DeviceWidth),
b"device-height" => Some(DeviceHeight),
b"device-aspect-ratio" => Some(DeviceAspectRatio),
b"-webkit-device-pixel-ratio" => Some(WebkitDevicePixelRatio),
b"-moz-device-pixel-ratio" => Some(MozDevicePixelRatio),
_ => None,
}}
}
}
impl MediaList {
pub fn always_matches(&self) -> bool {
self.media_queries.is_empty() || self.media_queries.iter().all(MediaQuery::always_matches)
}
pub fn never_matches(&self) -> bool {
!self.media_queries.is_empty() && self.media_queries.iter().all(MediaQuery::never_matches)
}
pub fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
if self.media_queries.is_empty() {
return dest.write_str("not all");
}
dest.write_comma_separated(&self.media_queries, |d, q| q.to_css(d))
}
}
impl crate::generic::ToCss for MediaList {
#[inline]
fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
MediaList::to_css(self, dest)
}
}
impl MediaQuery {
pub fn always_matches(&self) -> bool {
self.qualifier.is_none()
&& matches!(self.media_type, MediaType::All)
&& self.condition.is_none()
}
pub fn never_matches(&self) -> bool {
matches!(self.qualifier, Some(Qualifier::Not))
&& matches!(self.media_type, MediaType::All)
&& self.condition.is_none()
}
pub fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
if let Some(qual) = self.qualifier {
qual.to_css(dest)?;
dest.write_char(b' ')?;
}
match &self.media_type {
MediaType::All => {
if self.qualifier.is_some() || self.condition.is_none() {
dest.write_str("all")?;
}
}
MediaType::Print => dest.write_str("print")?,
MediaType::Screen => dest.write_str("screen")?,
MediaType::Custom(desc) => {
dest.write_str(unsafe { crate::arena_str(*desc) })?;
}
}
let Some(condition) = &self.condition else {
return Ok(());
};
let needs_parens = if !matches!(self.media_type, MediaType::All) || self.qualifier.is_some()
{
dest.write_str(" and ")?;
matches!(
condition,
MediaCondition::Operation { operator, .. } if *operator != Operator::And
)
} else {
false
};
to_css_with_parens_if_needed(condition, dest, needs_parens)
}
}
pub(crate) fn to_css_with_parens_if_needed<T: ToCss + ?Sized>(
v: &T,
dest: &mut Printer,
needs_parens: bool,
) -> core::result::Result<(), PrintErr> {
if needs_parens {
dest.write_char(b'(')?;
}
v.to_css(dest)?;
if needs_parens {
dest.write_char(b')')?;
}
Ok(())
}
pub(crate) fn operation_to_css<C: QueryCondition>(
operator: Operator,
conditions: &[C],
dest: &mut Printer,
) -> core::result::Result<(), PrintErr> {
let first = &conditions[0];
to_css_with_parens_if_needed(
first,
dest,
first.needs_parens(Some(operator), &dest.targets),
)?;
if conditions.len() == 1 {
return Ok(());
}
for item in &conditions[1..] {
dest.write_char(b' ')?;
operator.to_css(dest)?;
dest.write_char(b' ')?;
to_css_with_parens_if_needed(item, dest, item.needs_parens(Some(operator), &dest.targets))?;
}
Ok(())
}
impl ToCss for MediaCondition {
fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
self.condition_to_css(dest)
}
}
impl QueryCondition for MediaCondition {
type Feature = MediaFeature;
fn as_feature(&self) -> Option<&MediaFeature> {
if let Self::Feature(f) = self {
Some(&**f)
} else {
None
}
}
fn as_not(&self) -> Option<&Self> {
if let Self::Not(c) = self {
Some(c)
} else {
None
}
}
fn as_operation(&self) -> Option<(Operator, &[Self])> {
if let Self::Operation {
operator,
conditions,
} = self
{
Some((*operator, conditions))
} else {
None
}
}
fn feature_to_css(f: &MediaFeature, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
f.to_css(dest)
}
fn parse_feature(input: &mut Parser) -> Result<Self> {
let feature = MediaFeature::parse(input)?;
Ok(MediaCondition::Feature(Box::new_in(
feature,
ArenaPtr::new(input.arena()),
)))
}
fn parse_feature_with_options(
input: &mut Parser,
options: &css::ParserOptions,
) -> Result<Self> {
let feature = MediaFeature::parse_with_options(input, options)?;
Ok(MediaCondition::Feature(Box::new_in(
feature,
ArenaPtr::new(input.arena()),
)))
}
fn create_negation_in(condition: Self, alloc: ArenaPtr) -> Self {
MediaCondition::Not(Box::new_in(condition, alloc))
}
fn create_operation_in(operator: Operator, conditions: Vec<Self, ArenaPtr>) -> Self {
MediaCondition::Operation {
operator,
conditions,
}
}
fn parse_style_query(input: &mut Parser) -> Result<Self> {
Err(input.new_error_for_next_token())
}
fn needs_parens(
&self,
parent_operator: Option<Operator>,
targets: &css::targets::Targets,
) -> bool {
match self {
MediaCondition::Not(_) => true,
MediaCondition::Operation { operator, .. } => Some(*operator) != parent_operator,
MediaCondition::Feature(f) => f.needs_parens(parent_operator, targets),
}
}
}
impl<FeatureId: FeatureIdTrait> QueryFeature<FeatureId> {
pub(crate) fn needs_parens(
&self,
parent_operator: Option<Operator>,
targets: &css::targets::Targets,
) -> bool {
parent_operator != Some(Operator::And)
&& matches!(self, QueryFeature::Interval { .. })
&& targets.should_compile_same(css::compat::Feature::MediaIntervalSyntax)
}
pub(crate) fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
dest.write_char(b'(')?;
match self {
QueryFeature::Boolean { name } => {
name.to_css(dest)?;
}
QueryFeature::Plain { name, value } => {
name.to_css(dest)?;
dest.delim(b':', false)?;
value.to_css(dest)?;
}
QueryFeature::Range {
name,
operator,
value,
} => {
if dest
.targets
.should_compile_same(css::compat::Feature::MediaRangeSyntax)
{
return write_min_max(*operator, name, value, dest);
}
name.to_css(dest)?;
operator.to_css(dest)?;
value.to_css(dest)?;
}
QueryFeature::Interval {
name,
start,
start_operator,
end,
end_operator,
} => {
if dest
.targets
.should_compile_same(css::compat::Feature::MediaIntervalSyntax)
{
write_min_max(start_operator.opposite(), name, start, dest)?;
dest.write_str(" and (")?;
return write_min_max(*end_operator, name, end, dest);
}
start.to_css(dest)?;
start_operator.to_css(dest)?;
name.to_css(dest)?;
end_operator.to_css(dest)?;
end.to_css(dest)?;
}
}
dest.write_char(b')')
}
}
impl<FeatureId: FeatureIdTrait> MediaFeatureName<FeatureId> {
pub(crate) fn value_type(&self) -> MediaFeatureType {
match self {
MediaFeatureName::Standard(standard) => standard.value_type(),
_ => MediaFeatureType::Unknown,
}
}
pub(crate) fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
match self {
MediaFeatureName::Standard(v) => v.to_css(dest),
MediaFeatureName::Custom(d) => d.to_css(dest),
MediaFeatureName::Unknown(v) => v.to_css(dest),
}
}
pub(crate) fn to_css_with_prefix(
&self,
prefix: &str,
dest: &mut Printer,
) -> core::result::Result<(), PrintErr> {
match self {
MediaFeatureName::Standard(v) => v.to_css_with_prefix(prefix, dest),
MediaFeatureName::Custom(d) => {
dest.write_str(prefix)?;
d.to_css(dest)
}
MediaFeatureName::Unknown(v) => {
dest.write_str(prefix)?;
v.to_css(dest)
}
}
}
pub(crate) fn parse(input: &mut Parser) -> Result<(Self, Option<MediaFeatureComparison>)> {
use bun_core::strings;
let ident = input.expect_ident_cloned()?;
if strings::starts_with(ident, b"--") {
return Ok((MediaFeatureName::Custom(DashedIdent { v: ident }), None));
}
let mut name: &[u8] = ident;
let is_webkit = strings::starts_with_case_insensitive_ascii(name, b"-webkit-");
if is_webkit {
name = &name[8..];
}
let comparator: Option<MediaFeatureComparison> =
if strings::starts_with_case_insensitive_ascii(name, b"min-") {
name = &name[4..];
Some(MediaFeatureComparison::GreaterThanEqual)
} else if strings::starts_with_case_insensitive_ascii(name, b"max-") {
name = &name[4..];
Some(MediaFeatureComparison::LessThanEqual)
} else {
None
};
let mut webkit_buf: [u8; 64] = [0; 64];
let final_name: &[u8] = if is_webkit {
let len = 8 + name.len();
if len <= webkit_buf.len() {
webkit_buf[..8].copy_from_slice(b"-webkit-");
webkit_buf[8..len].copy_from_slice(name);
&webkit_buf[..len]
} else {
b""
}
} else {
name
};
if !final_name.is_empty() {
if let Some(standard) = FeatureId::from_str(final_name) {
return Ok((MediaFeatureName::Standard(standard), comparator));
}
}
Ok((MediaFeatureName::Unknown(Ident { v: ident }), None))
}
}
impl MediaFeatureComparison {
pub(crate) fn to_css(self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
match self {
MediaFeatureComparison::Equal => dest.delim(b'-', true),
MediaFeatureComparison::GreaterThan => dest.delim(b'>', true),
MediaFeatureComparison::GreaterThanEqual => {
dest.whitespace()?;
dest.write_str(">=")?;
dest.whitespace()
}
MediaFeatureComparison::LessThan => dest.delim(b'<', true),
MediaFeatureComparison::LessThanEqual => {
dest.whitespace()?;
dest.write_str("<=")?;
dest.whitespace()
}
}
}
pub(crate) fn opposite(self) -> Self {
match self {
MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
MediaFeatureComparison::Equal => MediaFeatureComparison::Equal,
}
}
}
impl MediaFeatureValue {
pub(crate) fn to_css(&self, dest: &mut Printer) -> core::result::Result<(), PrintErr> {
match self {
MediaFeatureValue::Length(len) => len.to_css(dest),
MediaFeatureValue::Number(num) => css::to_css::float32(*num, dest),
MediaFeatureValue::Integer(int) => css::to_css::integer(*int, dest),
MediaFeatureValue::Boolean(b) => {
if *b {
dest.write_char(b'1')
} else {
dest.write_char(b'0')
}
}
MediaFeatureValue::Resolution(res) => res.to_css(dest),
MediaFeatureValue::Ratio(ratio) => ratio.to_css(dest),
MediaFeatureValue::Ident(id) => id.to_css(dest),
MediaFeatureValue::Env(env) => env.to_css(dest, false),
}
}
pub(crate) fn add_f32(self, other: f32) -> MediaFeatureValue {
match self {
MediaFeatureValue::Length(len) => MediaFeatureValue::Length(len.add(Length::px(other))),
MediaFeatureValue::Number(num) => MediaFeatureValue::Number(num + other),
MediaFeatureValue::Integer(num) => {
MediaFeatureValue::Integer(num + if other.is_sign_positive() { 1 } else { -1 })
}
MediaFeatureValue::Boolean(v) => MediaFeatureValue::Boolean(v),
MediaFeatureValue::Resolution(res) => MediaFeatureValue::Resolution(res.add_f32(other)),
MediaFeatureValue::Ratio(ratio) => MediaFeatureValue::Ratio(ratio.add_f32(other)),
MediaFeatureValue::Ident(id) => MediaFeatureValue::Ident(id),
MediaFeatureValue::Env(env) => MediaFeatureValue::Env(env), }
}
pub(crate) fn value_type(&self) -> MediaFeatureType {
use MediaFeatureValue as V;
match self {
V::Length(_) => MediaFeatureType::Length,
V::Number(_) => MediaFeatureType::Number,
V::Integer(_) => MediaFeatureType::Integer,
V::Boolean(_) => MediaFeatureType::Boolean,
V::Resolution(_) => MediaFeatureType::Resolution,
V::Ratio(_) => MediaFeatureType::Ratio,
V::Ident(_) => MediaFeatureType::Ident,
V::Env(_) => MediaFeatureType::Unknown,
}
}
pub(crate) fn check_type(&self, expected_type: MediaFeatureType) -> bool {
let vt = self.value_type();
if expected_type == MediaFeatureType::Unknown || vt == MediaFeatureType::Unknown {
return true;
}
expected_type == vt
}
pub(crate) fn parse(
input: &mut Parser,
expected_type: MediaFeatureType,
options: &css::ParserOptions,
) -> Result<MediaFeatureValue> {
if let Ok(value) = input.try_parse(|i| MediaFeatureValue::parse_known(i, expected_type)) {
return Ok(value);
}
MediaFeatureValue::parse_unknown(input, options)
}
pub(crate) fn parse_known(
input: &mut Parser,
expected_type: MediaFeatureType,
) -> Result<MediaFeatureValue> {
Ok(match expected_type {
MediaFeatureType::Boolean => {
let value = CSSIntegerFns::parse(input)?;
if value != 0 && value != 1 {
return Err(input.new_custom_error(css::ParserError::invalid_value));
}
MediaFeatureValue::Boolean(value == 1)
}
MediaFeatureType::Number => MediaFeatureValue::Number(CSSNumberFns::parse(input)?),
MediaFeatureType::Integer => MediaFeatureValue::Integer(CSSIntegerFns::parse(input)?),
MediaFeatureType::Length => MediaFeatureValue::Length(Length::parse(input)?),
MediaFeatureType::Resolution => {
MediaFeatureValue::Resolution(Resolution::parse(input)?)
}
MediaFeatureType::Ratio => MediaFeatureValue::Ratio(Ratio::parse(input)?),
MediaFeatureType::Ident => MediaFeatureValue::Ident(Ident::parse(input)?),
MediaFeatureType::Unknown => {
return Err(input.new_custom_error(css::ParserError::invalid_value));
}
})
}
pub(crate) fn parse_unknown(
input: &mut Parser,
options: &css::ParserOptions,
) -> Result<MediaFeatureValue> {
if let Ok(ratio) = input.try_parse(Ratio::parse_required) {
return Ok(MediaFeatureValue::Ratio(ratio));
}
if let Ok(num) = input.try_parse(CSSNumberFns::parse) {
return Ok(MediaFeatureValue::Number(num));
}
if let Ok(res) = input.try_parse(Length::parse) {
return Ok(MediaFeatureValue::Length(res));
}
if let Ok(res) = input.try_parse(Resolution::parse) {
return Ok(MediaFeatureValue::Resolution(res));
}
if let Ok(env) = input.try_parse(|i| EnvironmentVariable::parse(i, options, 0)) {
return Ok(MediaFeatureValue::Env(env));
}
let ident = Ident::parse(input)?;
Ok(MediaFeatureValue::Ident(ident))
}
}
fn write_min_max<FeatureId: FeatureIdTrait>(
operator: MediaFeatureComparison,
name: &MediaFeatureName<FeatureId>,
value: &MediaFeatureValue,
dest: &mut Printer,
) -> core::result::Result<(), PrintErr> {
let prefix = match operator {
MediaFeatureComparison::GreaterThan | MediaFeatureComparison::GreaterThanEqual => {
Some("min-")
}
MediaFeatureComparison::LessThan | MediaFeatureComparison::LessThanEqual => Some("max-"),
MediaFeatureComparison::Equal => None,
};
if let Some(p) = prefix {
name.to_css_with_prefix(p, dest)?;
} else {
name.to_css(dest)?;
}
dest.delim(b':', false)?;
let adjusted: Option<MediaFeatureValue> = match operator {
MediaFeatureComparison::GreaterThan => Some(value.clone().add_f32(0.001)),
MediaFeatureComparison::LessThan => Some(value.clone().add_f32(-0.001)),
_ => None,
};
if let Some(val) = adjusted {
val.to_css(dest)?;
} else {
value.to_css(dest)?;
}
dest.write_char(b')')
}
impl MediaList {
pub fn deep_clone(&self, bump: &bun_alloc::Arena) -> Self {
let mut media_queries =
Vec::with_capacity_in(self.media_queries.len(), ArenaPtr::new(bump));
media_queries.extend(self.media_queries.iter().map(|q| q.deep_clone(bump)));
Self { media_queries }
}
#[inline]
pub fn clone_in(&self, bump: &bun_alloc::Arena) -> Self {
self.deep_clone(bump)
}
#[inline]
pub fn clone_with_import_records(
&self,
bump: &bun_alloc::Arena,
_import_records: &mut Vec<bun_ast::ImportRecord>,
) -> Self {
self.deep_clone(bump)
}
#[inline]
pub fn eql(&self, other: &Self) -> bool {
self == other
}
}
impl MediaQuery {
pub fn deep_clone(&self, bump: &bun_alloc::Arena) -> Self {
Self {
qualifier: self.qualifier,
media_type: self.media_type.deep_clone(bump),
condition: self.condition.as_ref().map(|c| c.deep_clone(bump)),
}
}
}
impl MediaType {
#[inline]
pub(crate) fn deep_clone(&self, _bump: &bun_alloc::Arena) -> Self {
*self
}
}
impl MediaCondition {
pub(crate) fn deep_clone(&self, bump: &bun_alloc::Arena) -> Self {
let alloc = ArenaPtr::new(bump);
match self {
MediaCondition::Feature(f) => {
MediaCondition::Feature(Box::new_in(f.deep_clone(bump), alloc))
}
MediaCondition::Not(c) => MediaCondition::Not(Box::new_in(c.deep_clone(bump), alloc)),
MediaCondition::Operation {
operator,
conditions,
} => MediaCondition::Operation {
operator: *operator,
conditions: {
let mut out = Vec::with_capacity_in(conditions.len(), alloc);
out.extend(conditions.iter().map(|c| c.deep_clone(bump)));
out
},
},
}
}
}
impl<FeatureId: FeatureIdTrait> MediaFeatureName<FeatureId> {
#[inline]
pub(crate) fn deep_clone(&self, _bump: &bun_alloc::Arena) -> Self {
self.clone()
}
}
impl<FeatureId: FeatureIdTrait> QueryFeature<FeatureId> {
pub(crate) fn deep_clone(&self, bump: &bun_alloc::Arena) -> Self {
match self {
QueryFeature::Plain { name, value } => QueryFeature::Plain {
name: name.deep_clone(bump),
value: value.deep_clone(bump),
},
QueryFeature::Boolean { name } => QueryFeature::Boolean {
name: name.deep_clone(bump),
},
QueryFeature::Range {
name,
operator,
value,
} => QueryFeature::Range {
name: name.deep_clone(bump),
operator: *operator,
value: value.deep_clone(bump),
},
QueryFeature::Interval {
name,
start,
start_operator,
end,
end_operator,
} => QueryFeature::Interval {
name: name.deep_clone(bump),
start: start.deep_clone(bump),
start_operator: *start_operator,
end: end.deep_clone(bump),
end_operator: *end_operator,
},
}
}
}
impl MediaFeatureValue {
pub(crate) fn deep_clone(&self, bump: &bun_alloc::Arena) -> Self {
use MediaFeatureValue as V;
match self {
V::Length(l) => V::Length(l.clone()),
V::Number(n) => V::Number(*n),
V::Integer(i) => V::Integer(*i),
V::Boolean(b) => V::Boolean(*b),
V::Resolution(r) => V::Resolution(*r),
V::Ratio(r) => V::Ratio(*r),
V::Ident(i) => V::Ident(i.deep_clone(bump)),
V::Env(e) => {
use crate::generics::DeepClone as _;
V::Env(e.deep_clone(bump))
}
}
}
}
impl MediaType {
pub(crate) fn parse(input: &mut Parser) -> Result<MediaType> {
let name = input.expect_ident()?;
Ok(MediaType::from_str(name))
}
pub(crate) fn from_str(name: &[u8]) -> MediaType {
use bun_core::eql_case_insensitive_ascii_check_length as eq;
if eq(name, b"all") {
return MediaType::All;
}
if eq(name, b"print") {
return MediaType::Print;
}
if eq(name, b"screen") {
return MediaType::Screen;
}
MediaType::Custom(std::ptr::from_ref::<[u8]>(name))
}
}
impl MediaList {
pub fn parse(input: &mut Parser, options: &css::ParserOptions) -> Result<MediaList> {
let mut media_queries: Vec<MediaQuery, ArenaPtr> =
Vec::new_in(ArenaPtr::new(input.arena()));
loop {
match input
.parse_until_before(css::Delimiters::COMMA, |i| MediaQuery::parse(i, options))
{
Ok(mq) => media_queries.push(mq),
Err(e) => {
if matches!(
e.kind,
css::error::ParserErrorKind::basic(css::BasicParseErrorKind::end_of_input)
) {
break;
}
return Err(e);
}
}
match input.next() {
Ok(tok) => {
if !matches!(tok, css::Token::Comma) {
unreachable!(
"expected a comma after parsing a MediaQuery — bug in CSS parser"
);
}
}
Err(_) => break,
}
}
Ok(MediaList { media_queries })
}
}
impl MediaQuery {
pub fn parse(input: &mut Parser, options: &css::ParserOptions) -> Result<MediaQuery> {
let (qualifier, explicit_media_type) = input
.try_parse(|i| -> Result<(Option<Qualifier>, Option<MediaType>)> {
let qualifier = i.try_parse(Qualifier::parse).ok();
let media_type = MediaType::parse(i)?;
Ok((qualifier, Some(media_type)))
})
.unwrap_or((None, None));
let condition = if explicit_media_type.is_none() {
Some(MediaCondition::parse_with_flags(
input,
QueryConditionFlags::ALLOW_OR,
options,
)?)
} else if input.try_parse(|i| i.expect_ident_matching(b"and")).is_ok() {
Some(MediaCondition::parse_with_flags(
input,
QueryConditionFlags::empty(),
options,
)?)
} else {
None
};
let media_type = explicit_media_type.unwrap_or(MediaType::All);
Ok(MediaQuery {
qualifier,
media_type,
condition,
})
}
}
impl MediaCondition {
#[inline]
pub(crate) fn parse_with_flags(
input: &mut Parser,
flags: QueryConditionFlags,
options: &css::ParserOptions,
) -> Result<Self> {
parse_query_condition_with_options::<MediaCondition>(input, flags, options)
}
}
#[inline]
pub fn parse_query_condition<C: QueryCondition>(
input: &mut Parser,
flags: QueryConditionFlags,
) -> Result<C> {
parse_query_condition_with_options::<C>(input, flags, &css::ParserOptions::default(None))
}
pub fn parse_query_condition_with_options<C: QueryCondition>(
input: &mut Parser,
flags: QueryConditionFlags,
options: &css::ParserOptions,
) -> Result<C> {
use bun_core::strings;
let location = input.current_source_location();
let (is_negation, is_style) = 'brk: {
let tok = input.next()?.clone();
match &tok {
css::Token::OpenParen => break 'brk (false, false),
css::Token::Ident(ident) => {
if strings::eql_case_insensitive_ascii_check_length(ident, b"not") {
break 'brk (true, false);
}
}
css::Token::Function(f) => {
if flags.allow_style()
&& strings::eql_case_insensitive_ascii_check_length(f, b"style")
{
break 'brk (false, true);
}
}
_ => {}
}
return Err(location.new_unexpected_token_error(tok));
};
let alloc = ArenaPtr::new(input.arena());
let first_condition: C = match (is_negation, is_style) {
(true, false) => {
let inner_condition = parse_parens_or_function::<C>(input, flags, options)?;
return Ok(C::create_negation_in(inner_condition, alloc));
}
(true, true) => {
let inner_condition = C::parse_style_query_with_options(input, options)?;
return Ok(C::create_negation_in(inner_condition, alloc));
}
(false, false) => parse_paren_block::<C>(input, flags, options)?,
(false, true) => C::parse_style_query_with_options(input, options)?,
};
let operator: Operator = match input.try_parse(Operator::parse) {
Ok(op) => op,
Err(_) => return Ok(first_condition),
};
if !flags.allow_or() && operator == Operator::Or {
return Err(location.new_unexpected_token_error(css::Token::Ident(b"or")));
}
let mut conditions: Vec<C, ArenaPtr> = Vec::new_in(alloc);
conditions.push(first_condition);
conditions.push(parse_parens_or_function::<C>(input, flags, options)?);
let delim: &[u8] = match operator {
Operator::And => b"and",
Operator::Or => b"or",
};
loop {
if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
return Ok(C::create_operation_in(operator, conditions));
}
conditions.push(parse_parens_or_function::<C>(input, flags, options)?);
}
}
pub fn parse_parens_or_function<C: QueryCondition>(
input: &mut Parser,
flags: QueryConditionFlags,
options: &css::ParserOptions,
) -> Result<C> {
use bun_core::strings;
let location = input.current_source_location();
let t = input.next()?.clone();
match &t {
css::Token::OpenParen => return parse_paren_block::<C>(input, flags, options),
css::Token::Function(f) => {
if flags.allow_style() && strings::eql_case_insensitive_ascii_check_length(f, b"style")
{
return C::parse_style_query_with_options(input, options);
}
}
_ => {}
}
Err(location.new_unexpected_token_error(t))
}
fn parse_paren_block<C: QueryCondition>(
input: &mut Parser,
flags: QueryConditionFlags,
options: &css::ParserOptions,
) -> Result<C> {
input.parse_nested_block(|i| {
if let Ok(inner) =
i.try_parse(|i2| parse_query_condition_with_options::<C>(i2, flags, options))
{
return Ok(inner);
}
C::parse_feature_with_options(i, options)
})
}
impl<FeatureId: FeatureIdTrait> QueryFeature<FeatureId> {
#[inline]
pub(crate) fn parse(input: &mut Parser) -> Result<Self> {
Self::parse_with_options(input, &css::ParserOptions::default(None))
}
pub(crate) fn parse_with_options(
input: &mut Parser,
options: &css::ParserOptions,
) -> Result<Self> {
match input.try_parse(|i| Self::parse_name_first(i, options)) {
Ok(res) => Ok(res),
Err(e) => {
if matches!(
e.kind,
css::error::ParserErrorKind::custom(css::ParserError::invalid_media_query)
) {
return Err(e);
}
Self::parse_value_first(input, options)
}
}
}
pub(crate) fn parse_name_first(
input: &mut Parser,
options: &css::ParserOptions,
) -> Result<Self> {
let (name, legacy_op) = MediaFeatureName::<FeatureId>::parse(input)?;
let operator = match input.try_parse(|i| consume_operation_or_colon(i, true)) {
Ok(operator) => operator,
Err(_) => return Ok(QueryFeature::Boolean { name }),
};
if operator.is_some() && legacy_op.is_some() {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
let value = MediaFeatureValue::parse(input, name.value_type(), options)?;
if !value.check_type(name.value_type()) {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
if let Some(op) = operator.or(legacy_op) {
if !name.value_type().allows_ranges() {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
Ok(QueryFeature::Range {
name,
operator: op,
value,
})
} else {
Ok(QueryFeature::Plain { name, value })
}
}
pub(crate) fn parse_value_first(
input: &mut Parser,
options: &css::ParserOptions,
) -> Result<Self> {
let start = input.state();
let name: MediaFeatureName<FeatureId> = loop {
if let Ok((name, legacy_op)) = input.try_parse(MediaFeatureName::<FeatureId>::parse) {
if legacy_op.is_some() {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
break name;
}
if input.next().is_err() {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
};
input.reset(&start);
let value = MediaFeatureValue::parse(input, name.value_type(), options)?;
let operator = consume_operation_or_colon(input, false)?;
{
let (feature_name, _blah) = MediaFeatureName::<FeatureId>::parse(input)?;
debug_assert!(feature_name == name);
}
if !name.value_type().allows_ranges() || !value.check_type(name.value_type()) {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
if let Ok(end_operator_) = input.try_parse(|i| consume_operation_or_colon(i, false)) {
let start_operator = operator.unwrap();
let end_operator = end_operator_.unwrap();
const GT: u8 = MediaFeatureComparison::GreaterThan as u8;
const GTE: u8 = MediaFeatureComparison::GreaterThanEqual as u8;
const LT: u8 = MediaFeatureComparison::LessThan as u8;
const LTE: u8 = MediaFeatureComparison::LessThanEqual as u8;
let check_val: u8 = (start_operator as u8) | (end_operator as u8);
#[allow(clippy::eq_op)]
match check_val {
v if v == (GT | GT)
|| v == (GT | GTE)
|| v == (GTE | GTE)
|| v == (LT | LT)
|| v == (LT | LTE)
|| v == (LTE | LTE) => {}
_ => {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
}
let end_value = MediaFeatureValue::parse(input, name.value_type(), options)?;
if !end_value.check_type(name.value_type()) {
return Err(input.new_custom_error(css::ParserError::invalid_media_query));
}
Ok(QueryFeature::Interval {
name,
start: value,
start_operator,
end: end_value,
end_operator,
})
} else {
let final_operator = operator.unwrap().opposite();
Ok(QueryFeature::Range {
name,
operator: final_operator,
value,
})
}
}
}
fn consume_operation_or_colon(
input: &mut Parser,
allow_colon: bool,
) -> Result<Option<MediaFeatureComparison>> {
let location = input.current_source_location();
let first_delim: u32 = {
let loc = input.current_source_location();
let next_token = input.next()?.clone();
match next_token {
css::Token::Colon if allow_colon => return Ok(None),
css::Token::Delim(oper) => oper,
_ => return Err(loc.new_unexpected_token_error(next_token)),
}
};
match first_delim {
d if d == u32::from(b'=') => Ok(Some(MediaFeatureComparison::Equal)),
d if d == u32::from(b'>') => {
if input.try_parse(|i| i.expect_delim(b'=')).is_ok() {
return Ok(Some(MediaFeatureComparison::GreaterThanEqual));
}
Ok(Some(MediaFeatureComparison::GreaterThan))
}
d if d == u32::from(b'<') => {
if input.try_parse(|i| i.expect_delim(b'=')).is_ok() {
return Ok(Some(MediaFeatureComparison::LessThanEqual));
}
Ok(Some(MediaFeatureComparison::LessThan))
}
_ => Err(location.new_unexpected_token_error(css::Token::Delim(first_delim))),
}
}